diff --git a/UI/Window/RenderConfigWindow.cs b/UI/Window/RenderConfigWindow.cs index 33005b8..19a73b9 100644 --- a/UI/Window/RenderConfigWindow.cs +++ b/UI/Window/RenderConfigWindow.cs @@ -1,15 +1,53 @@ -using ADOFAIRenderer.Localization; +using System; +using System.Collections.Generic; +using System.IO; +using ADOFAIRenderer.API; +using ADOFAIRenderer.Core; +using ADOFAIRenderer.Data; +using ADOFAIRenderer.Localization; +using ADOFAIRenderer.Util; +using ADOFAIRenderer.Util.FFmpeg; +using MelonLoader; +using O5Kit.Control; using O5Kit.Core; using O5Kit.Factory; using UnityEngine; +using UnityEngine.UI; namespace ADOFAIRenderer.UI.Window; internal static class RenderConfigWindow { private static O5Window? _configWindow; - - // 윈도우 만드는 메서드... + + // Controls cache for validation & sync + private static O5InputField? _renderFpsInput; + private static O5InputField? _videoFpsInput; + private static O5InputField? _renderWidthInput; + private static O5InputField? _renderHeightInput; + private static O5InputField? _delayAfterEndInput; + private static O5Toggle? _showJudgementToggle; + + private static O5Dropdown? _codecDropdown; + private static O5Dropdown? _rateControlDropdown; + private static O5InputField? _bitrateInput; + private static O5InputField? _maxBitrateInput; + private static O5InputField? _qpInput; + private static O5InputField? _keyframeIntervalInput; + private static O5Toggle? _forceSoftwareToggle; + + private static O5Toggle? _renderAudioToggle; + private static O5Dropdown? _sampleRateDropdown; + private static O5Dropdown? _channelsDropdown; + private static O5InputField? _audioBitrateInput; + + private static O5InputField? _outputDirInput; + + private static O5Dropdown? _fileExtensionDropdown; + private static O5InputField? _randomSeedInput; + private static O5Toggle? _processAudioToggle; + private static O5Toggle? _nonTileToggle; + public static void CreateWindow() { if (_configWindow == null) @@ -18,7 +56,7 @@ internal static class RenderConfigWindow { Id = "RendererMod_RenderConfig", Title = LocalizationManager.Get("ui.render_config.window_title"), - Size = new Vector2(580f, 420f), + Size = new Vector2(580f, 480f), Draggable = true, Resizable = true, ShowCloseButton = true @@ -28,20 +66,687 @@ internal static class RenderConfigWindow { win.Rect.gameObject.SetActive(false); }; - + BuildUI(_configWindow.Content); } + var config = RenderManager.Instance.Configuration ??= new RenderConfig(); + ApplyConfigToUI(config); + SetWindowActive(true); + _configWindow.BringToFront(); } private static void BuildUI(RectTransform content) { - var (viewport, scrollContent, scroller) = - O5Factory.ScrollView(RendererUIManager.Context, content, spacing: 10f); + var ctx = RendererUIManager.Context; + var (viewport, scrollContent, scroller) = + O5Factory.ScrollView(ctx, content, spacing: 10f, padding: 8f); + + var config = RenderManager.Instance.Configuration ??= new RenderConfig(); + EnsureDefaultConfig(config); + + // 1. RenderFPS & VideoFPS + var (row1Left, row1Right) = CreateTwoColumnRow(scrollContent); + _renderFpsInput = CreateInputField( + ctx, row1Left, + LocalizationManager.Get("ui.render_config.render_fps"), + "60", config.RenderFPS.ToString("0.##"), + "input_render_fps", + TMPro.TMP_InputField.CharacterValidation.Decimal, + val => { if (double.TryParse(val, out var d) && d > 0) config.RenderFPS = d; } + ); + _videoFpsInput = CreateInputField( + ctx, row1Right, + LocalizationManager.Get("ui.render_config.video_fps"), + "60", config.VideoFPS.ToString("0.##"), + "input_video_fps", + TMPro.TMP_InputField.CharacterValidation.Decimal, + val => { if (double.TryParse(val, out var d) && d > 0) config.VideoFPS = d; } + ); + + // 2. RenderWidth & RenderHeight + var (row2Left, row2Right) = CreateTwoColumnRow(scrollContent); + _renderWidthInput = CreateInputField( + ctx, row2Left, + LocalizationManager.Get("ui.render_config.render_width"), + "1920", config.RenderWidth.ToString(), + "input_render_width", + TMPro.TMP_InputField.CharacterValidation.Integer, + val => { if (int.TryParse(val, out var i) && i > 0) config.RenderWidth = i; } + ); + _renderHeightInput = CreateInputField( + ctx, row2Right, + LocalizationManager.Get("ui.render_config.render_height"), + "1080", config.RenderHeight.ToString(), + "input_render_height", + TMPro.TMP_InputField.CharacterValidation.Integer, + val => { if (int.TryParse(val, out var i) && i > 0) config.RenderHeight = i; } + ); + + // 3. Delay After Level End & Show Judgement + var (row3Left, row3Right) = CreateTwoColumnRow(scrollContent); + _delayAfterEndInput = CreateInputField( + ctx, row3Left, + LocalizationManager.Get("ui.render_config.delay_after_end"), + "1.0", config.DelayAfterEnd.ToString("0.##"), + "input_delay_after_end", + TMPro.TMP_InputField.CharacterValidation.Decimal, + val => { if (double.TryParse(val, out var d) && d >= 0) config.DelayAfterEnd = d; } + ); + _showJudgementToggle = CreateToggleField( + ctx, row3Right, + LocalizationManager.Get("ui.render_config.show_judgement"), + false, config.ShowJudgementText, + LocalizationManager.Get("ui.render_config.show_judgement"), + "toggle_show_judgement", + val => config.ShowJudgementText = val + ); + + // 4. Video Encoding Config (Collapsible Card) + var (_, videoContent) = O5Factory.Card( + ctx, scrollContent, + LocalizationManager.Get("ui.render_config.video_encoding"), + activeValue: true, + onActiveChanged: null, + onDeleteClick: null, + showDeleteButton: false, + showActiveToggle: false + ); + + var (vRow1Left, vRow1Right) = CreateTwoColumnRow(videoContent); + var codecs = (Codec[])Enum.GetValues(typeof(Codec)); + _codecDropdown = CreateDropdownField( + ctx, vRow1Left, + LocalizationManager.Get("ui.render_config.codec"), + Codec.H264, config.VideoConfig.Codec, + codecs, c => c.ToString(), + "dd_video_codec", + c => config.VideoConfig.Codec = c + ); + + var rateModes = (RateControlMode[])Enum.GetValues(typeof(RateControlMode)); + _rateControlDropdown = CreateDropdownField( + ctx, vRow1Right, + LocalizationManager.Get("ui.render_config.rate_control"), + RateControlMode.CBR, config.VideoConfig.RateControlMode, + rateModes, m => m.ToString(), + "dd_rate_control", + m => config.VideoConfig.RateControlMode = m + ); + + var (vRow2Left, vRow2Right) = CreateTwoColumnRow(videoContent); + _bitrateInput = CreateInputField( + ctx, vRow2Left, + LocalizationManager.Get("ui.render_config.bitrate"), + "30000", config.VideoConfig.Bitrate.ToString(), + "input_bitrate", + TMPro.TMP_InputField.CharacterValidation.Integer, + val => { if (int.TryParse(val, out var b) && b > 0) config.VideoConfig.Bitrate = b; } + ); + _maxBitrateInput = CreateInputField( + ctx, vRow2Right, + LocalizationManager.Get("ui.render_config.max_bitrate"), + "50000", config.VideoConfig.MaxBitrate.ToString(), + "input_max_bitrate", + TMPro.TMP_InputField.CharacterValidation.Integer, + val => { if (int.TryParse(val, out var mb) && mb > 0) config.VideoConfig.MaxBitrate = mb; } + ); + + var (vRow3Left, vRow3Right) = CreateTwoColumnRow(videoContent); + _qpInput = CreateInputField( + ctx, vRow3Left, + LocalizationManager.Get("ui.render_config.qp"), + "18", config.VideoConfig.QP.ToString(), + "input_qp", + TMPro.TMP_InputField.CharacterValidation.Integer, + val => { if (int.TryParse(val, out var qp) && qp >= 0) config.VideoConfig.QP = qp; } + ); + _keyframeIntervalInput = CreateInputField( + ctx, vRow3Right, + LocalizationManager.Get("ui.render_config.keyframe_interval"), + "2.0", config.VideoConfig.KeyframeInterval.ToString("0.##"), + "input_keyframe_interval", + TMPro.TMP_InputField.CharacterValidation.Decimal, + val => { if (double.TryParse(val, out var kf) && kf > 0) config.VideoConfig.KeyframeInterval = kf; } + ); + + _forceSoftwareToggle = O5Factory.Toggle( + ctx, videoContent, + false, config.VideoConfig.ForceSoftwareEncoder, + val => config.VideoConfig.ForceSoftwareEncoder = val, + LocalizationManager.Get("ui.render_config.force_software"), + "toggle_force_software" + ); + + // 5. Audio Encoding Config (Collapsible Card) + var (_, audioContent) = O5Factory.Card( + ctx, scrollContent, + LocalizationManager.Get("ui.render_config.audio_encoding"), + activeValue: true, + onActiveChanged: null, + onDeleteClick: null, + showDeleteButton: false, + showActiveToggle: false + ); + + var (aRow1Left, aRow1Right) = CreateTwoColumnRow(audioContent); + _renderAudioToggle = CreateToggleField( + ctx, aRow1Left, + LocalizationManager.Get("ui.render_config.render_audio"), + true, config.AudioConfig.RenderAudio, + LocalizationManager.Get("ui.render_config.render_audio"), + "toggle_render_audio", + val => config.AudioConfig.RenderAudio = val + ); + + var sampleRates = new[] { 44100, 48000 }; + _sampleRateDropdown = CreateDropdownField( + ctx, aRow1Right, + LocalizationManager.Get("ui.render_config.sample_rate"), + 44100, config.AudioConfig.AudioSampleRate, + sampleRates, r => $"{r} Hz", + "dd_sample_rate", + r => config.AudioConfig.AudioSampleRate = r + ); + + var (aRow2Left, aRow2Right) = CreateTwoColumnRow(audioContent); + var channels = new[] { 1, 2 }; + _channelsDropdown = CreateDropdownField( + ctx, aRow2Left, + LocalizationManager.Get("ui.render_config.channels"), + 2, config.AudioConfig.AudioChannels, + channels, c => c == 1 ? "1 (Mono)" : "2 (Stereo)", + "dd_channels", + c => config.AudioConfig.AudioChannels = c + ); + _audioBitrateInput = CreateInputField( + ctx, aRow2Right, + LocalizationManager.Get("ui.render_config.audio_bitrate"), + "256", (config.AudioConfig.AudioBitrate / 1000).ToString(), + "input_audio_bitrate", + TMPro.TMP_InputField.CharacterValidation.Integer, + val => { if (int.TryParse(val, out var ab) && ab > 0) config.AudioConfig.AudioBitrate = ab * 1000; } + ); + + // 6. Global Settings (Collapsible Card) + var (_, globalContent) = O5Factory.Card( + ctx, scrollContent, + LocalizationManager.Get("ui.render_config.global_settings"), + activeValue: true, + onActiveChanged: null, + onDeleteClick: null, + showDeleteButton: false, + showActiveToggle: false + ); + + _outputDirInput = CreateFullWidthInputField( + ctx, globalContent, + LocalizationManager.Get("ui.render_config.output_dir"), + GlobalSettings.DefaultOutputDirectory, + GlobalSettings.EffectiveOutputDirectory, + "input_output_dir", + val => + { + var trimmed = val?.Trim() ?? string.Empty; + if (string.IsNullOrWhiteSpace(trimmed) || + string.Equals(trimmed, GlobalSettings.DefaultOutputDirectory, StringComparison.OrdinalIgnoreCase)) + { + GlobalSettings.Current.OutputDirectory = null; + } + else + { + GlobalSettings.Current.OutputDirectory = trimmed; + } + } + ); + + var (gBtnLeft, gBtnRight) = CreateTwoColumnRow(globalContent); + O5Factory.Button( + ctx, gBtnLeft, + () => + { + var initial = string.IsNullOrWhiteSpace(_outputDirInput.Value) + ? GlobalSettings.DefaultOutputDirectory + : _outputDirInput.Value; + FolderDialogUtil.OpenFolderBrowser(initial, selected => + { + if (!string.IsNullOrWhiteSpace(selected)) + { + _outputDirInput.Set(selected); + GlobalSettings.Current.OutputDirectory = selected; + GlobalSettings.Save(); + } + }); + }, + LocalizationManager.Get("ui.render_config.browse"), + "btn_browse_output" + ); + + O5Factory.Button( + ctx, gBtnRight, + () => + { + _outputDirInput.Set(GlobalSettings.DefaultOutputDirectory); + GlobalSettings.Current.OutputDirectory = null; + GlobalSettings.Save(); + }, + LocalizationManager.Get("ui.render_config.reset_default"), + "btn_reset_output" + ); + + // 7. Advanced Settings (Collapsible Card) + var (_, advContent) = O5Factory.Card( + ctx, scrollContent, + LocalizationManager.Get("ui.render_config.advanced_settings"), + activeValue: true, + onActiveChanged: null, + onDeleteClick: null, + showDeleteButton: false, + showActiveToggle: false + ); + + var (adRow1Left, adRow1Right) = CreateTwoColumnRow(advContent); + var extensions = new[] { "mkv", "mp4", "mov", "webm", "avi" }; + _fileExtensionDropdown = CreateDropdownField( + ctx, adRow1Left, + LocalizationManager.Get("ui.render_config.file_extension"), + "mkv", config.AdvancedConfig.FileExtension, + extensions, e => e, + "dd_adv_file_extension", + e => { if (!string.IsNullOrEmpty(e)) config.AdvancedConfig.FileExtension = e; } + ); + + _randomSeedInput = CreateInputField( + ctx, adRow1Right, + LocalizationManager.Get("ui.render_config.random_seed"), + "", config.AdvancedConfig.RandomSeed?.ToString() ?? "", + "input_random_seed", + TMPro.TMP_InputField.CharacterValidation.Integer, + val => + { + if (int.TryParse(val, out var s)) config.AdvancedConfig.RandomSeed = s; + else if (string.IsNullOrWhiteSpace(val)) config.AdvancedConfig.RandomSeed = null; + } + ); + + var (adRow2Left, adRow2Right) = CreateTwoColumnRow(advContent); + _processAudioToggle = CreateToggleField( + ctx, adRow2Left, + LocalizationManager.Get("ui.render_config.process_audio"), + true, config.AdvancedConfig.ProcessAudio, + LocalizationManager.Get("ui.render_config.process_audio"), + "toggle_process_audio", + val => config.AdvancedConfig.ProcessAudio = val + ); + _nonTileToggle = CreateToggleField( + ctx, adRow2Right, + LocalizationManager.Get("ui.render_config.non_tile"), + false, config.AdvancedConfig.NonTile, + LocalizationManager.Get("ui.render_config.non_tile"), + "toggle_non_tile", + val => config.AdvancedConfig.NonTile = val + ); + + // 8. Go Render Button + O5Factory.Button( + ctx, scrollContent, + TryGoRender, + LocalizationManager.Get("ui.render_config.go_render"), + "btn_go_render", + height: 48f + ); + } + + private static void EnsureDefaultConfig(RenderConfig config) + { + if (config.RenderWidth <= 0) config.RenderWidth = Screen.width > 0 ? Screen.width : 1920; + if (config.RenderHeight <= 0) config.RenderHeight = Screen.height > 0 ? Screen.height : 1080; + if (config.RenderFPS <= 0) config.RenderFPS = 60.0; + if (config.VideoFPS <= 0) config.VideoFPS = 60.0; + if (config.DelayAfterEnd < 0) config.DelayAfterEnd = 1.0; + } + + private static void ApplyConfigToUI(RenderConfig config) + { + EnsureDefaultConfig(config); + + _renderFpsInput?.Set(config.RenderFPS.ToString("0.##"), false); + _videoFpsInput?.Set(config.VideoFPS.ToString("0.##"), false); + _renderWidthInput?.Set(config.RenderWidth.ToString(), false); + _renderHeightInput?.Set(config.RenderHeight.ToString(), false); + _delayAfterEndInput?.Set(config.DelayAfterEnd.ToString("0.##"), false); + _showJudgementToggle?.Set(config.ShowJudgementText, false); + + _codecDropdown?.Set(config.VideoConfig.Codec, false); + _rateControlDropdown?.Set(config.VideoConfig.RateControlMode, false); + _bitrateInput?.Set(config.VideoConfig.Bitrate.ToString(), false); + _maxBitrateInput?.Set(config.VideoConfig.MaxBitrate.ToString(), false); + _qpInput?.Set(config.VideoConfig.QP.ToString(), false); + _keyframeIntervalInput?.Set(config.VideoConfig.KeyframeInterval.ToString("0.##"), false); + _forceSoftwareToggle?.Set(config.VideoConfig.ForceSoftwareEncoder, false); + + _renderAudioToggle?.Set(config.AudioConfig.RenderAudio, false); + _sampleRateDropdown?.Set(config.AudioConfig.AudioSampleRate, false); + _channelsDropdown?.Set(config.AudioConfig.AudioChannels, false); + _audioBitrateInput?.Set((config.AudioConfig.AudioBitrate / 1000).ToString(), false); + + _outputDirInput?.Set(GlobalSettings.EffectiveOutputDirectory, false); + + _fileExtensionDropdown?.Set(config.AdvancedConfig.FileExtension, false); + _randomSeedInput?.Set(config.AdvancedConfig.RandomSeed?.ToString() ?? "", false); + _processAudioToggle?.Set(config.AdvancedConfig.ProcessAudio, false); + _nonTileToggle?.Set(config.AdvancedConfig.NonTile, false); + } + + private static void TryGoRender() + { + var config = RenderManager.Instance.Configuration; + + // Validate RenderFPS + if (!double.TryParse(_renderFpsInput?.Value, out var renderFps) || renderFps <= 0) + { + ShowValidationError("Render FPS must be a positive number."); + return; + } + config.RenderFPS = renderFps; + + // Validate VideoFPS + if (!double.TryParse(_videoFpsInput?.Value, out var videoFps) || videoFps <= 0) + { + ShowValidationError("Video FPS must be a positive number."); + return; + } + config.VideoFPS = videoFps; + + // Validate RenderWidth + if (!int.TryParse(_renderWidthInput?.Value, out var renderWidth) || renderWidth <= 0) + { + ShowValidationError("Render Width must be an integer greater than 0."); + return; + } + config.RenderWidth = renderWidth; + + // Validate RenderHeight + if (!int.TryParse(_renderHeightInput?.Value, out var renderHeight) || renderHeight <= 0) + { + ShowValidationError("Render Height must be an integer greater than 0."); + return; + } + config.RenderHeight = renderHeight; + + // Validate DelayAfterEnd + if (!double.TryParse(_delayAfterEndInput?.Value, out var delay) || delay < 0) + { + ShowValidationError("Delay After End must be a non-negative number."); + return; + } + config.DelayAfterEnd = delay; + + // Sync Video Config + if (_bitrateInput != null && int.TryParse(_bitrateInput.Value, out var bitrate) && bitrate > 0) + { + config.VideoConfig.Bitrate = bitrate; + } + if (_maxBitrateInput != null && int.TryParse(_maxBitrateInput.Value, out var maxBitrate) && maxBitrate > 0) + { + config.VideoConfig.MaxBitrate = maxBitrate; + } + if (_qpInput != null && int.TryParse(_qpInput.Value, out var qp) && qp >= 0) + { + config.VideoConfig.QP = qp; + } + if (_keyframeIntervalInput != null && double.TryParse(_keyframeIntervalInput.Value, out var kf) && kf > 0) + { + config.VideoConfig.KeyframeInterval = kf; + } + + // Sync Audio Config + if (_audioBitrateInput != null && int.TryParse(_audioBitrateInput.Value, out var ab) && ab > 0) + { + config.AudioConfig.AudioBitrate = ab * 1000; + } + + // Sync Advanced Config + if (_randomSeedInput != null) + { + if (int.TryParse(_randomSeedInput.Value, out var seed)) + { + config.AdvancedConfig.RandomSeed = seed; + } + else if (string.IsNullOrWhiteSpace(_randomSeedInput.Value)) + { + config.AdvancedConfig.RandomSeed = null; + } + } + + // Sync Output Directory + if (_outputDirInput != null) + { + var path = _outputDirInput.Value.Trim(); + if (string.IsNullOrWhiteSpace(path) || + string.Equals(path, GlobalSettings.DefaultOutputDirectory, StringComparison.OrdinalIgnoreCase)) + { + GlobalSettings.Current.OutputDirectory = null; + } + else + { + try + { + var fullPath = Path.GetFullPath(path); + if (!Directory.Exists(fullPath)) + { + Directory.CreateDirectory(fullPath); + } + GlobalSettings.Current.OutputDirectory = fullPath; + } + catch (Exception ex) + { + ShowValidationError($"Output directory path is invalid:\n{ex.Message}"); + return; + } + } + } + + GlobalSettings.Save(); + SetWindowActive(false); + + new RenderSession().Start(config, RenderManager.Instance.levelPath); + } + + private static void ShowValidationError(string message) + { + Melon.Logger.Error(message); + CommonWarningDialogUI.ShowDialog( + LocalizationManager.Get("ui.render_config.validation_title"), + message, + new DialogButton("OK", () => { }) + ); + } + + private static (RectTransform left, RectTransform right) CreateTwoColumnRow(Transform parent, float spacing = 8f) + { + var rowObj = new GameObject("TwoColRow"); + rowObj.transform.SetParent(parent, false); + + var rowRect = rowObj.AddComponent(); + + var hLayout = rowObj.AddComponent(); + hLayout.spacing = spacing; + hLayout.childControlWidth = true; + hLayout.childControlHeight = true; + hLayout.childForceExpandWidth = true; + hLayout.childForceExpandHeight = false; + + var col1Obj = new GameObject("Col1"); + col1Obj.transform.SetParent(rowObj.transform, false); + var col1Rect = col1Obj.AddComponent(); + var col1Layout = col1Obj.AddComponent(); + col1Layout.spacing = 3f; + col1Layout.childControlWidth = true; + col1Layout.childControlHeight = true; + col1Layout.childForceExpandWidth = true; + col1Layout.childForceExpandHeight = false; + + var col2Obj = new GameObject("Col2"); + col2Obj.transform.SetParent(rowObj.transform, false); + var col2Rect = col2Obj.AddComponent(); + var col2Layout = col2Obj.AddComponent(); + col2Layout.spacing = 3f; + col2Layout.childControlWidth = true; + col2Layout.childControlHeight = true; + col2Layout.childForceExpandWidth = true; + col2Layout.childForceExpandHeight = false; + + return (col1Rect, col2Rect); + } + + private static O5InputField CreateInputField( + O5Context ctx, + Transform colParent, + string labelText, + string? defaultValue, + string value, + string id, + TMPro.TMP_InputField.CharacterValidation characterValidation = TMPro.TMP_InputField.CharacterValidation.None, + Action? onChanged = null) + { + var label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true); + label.text = labelText; + label.color = ctx.Theme.TextDim; - // TODO: RenderFPS double 입력칸, VideoFPS double 입력칸, RenderWidth int (>0) 입력칸, RenderHeight int (>0) 입력칸, Video Encoding Config, Audio Encoding Config, Delay After Level End double (>=0) 입력칸, Show Judgement Toggle, Global Settings, Advanced Settings, Go Render 버튼 - // NOTE: 한 가로줄에 두개씩 + // ReSharper disable once InconsistentNaming + var labelLE = label.gameObject.AddComponent(); + labelLE.preferredHeight = 18f; + labelLE.minHeight = 18f; + + var inputField = O5Factory.Input( + ctx: ctx, + parent: colParent, + defaultValue: defaultValue, + value: value, + onChanged: onChanged, + placeholder: labelText, + icon: null, + id: id + ); + + inputField.InputField.characterValidation = characterValidation; + return inputField; + } + + private static O5InputField CreateFullWidthInputField( + O5Context ctx, + Transform parent, + string labelText, + string? defaultValue, + string value, + string id, + Action? onChanged = null) + { + var container = new GameObject($"FullWidth_{id}"); + container.transform.SetParent(parent, false); + + var containerRect = container.AddComponent(); + var layout = container.AddComponent(); + layout.spacing = 3f; + layout.childControlWidth = true; + layout.childControlHeight = true; + layout.childForceExpandWidth = true; + layout.childForceExpandHeight = false; + + var label = O5Factory.ControlText(ctx, containerRect, 13f, noPad: true); + label.text = labelText; + label.color = ctx.Theme.TextDim; + + // ReSharper disable once InconsistentNaming + var labelLE = label.gameObject.AddComponent(); + labelLE.preferredHeight = 18f; + labelLE.minHeight = 18f; + + var input = O5Factory.Input( + ctx: ctx, + parent: containerRect, + defaultValue: defaultValue, + value: value, + onChanged: onChanged, + placeholder: labelText, + icon: null, + id: id + ); + + return input; + } + + private static O5Toggle CreateToggleField( + O5Context ctx, + Transform colParent, + string labelText, + bool? defaultValue, + bool value, + string toggleText, + string id, + Action? onChanged = null) + { + var label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true); + label.text = labelText; + label.color = ctx.Theme.TextDim; + + // ReSharper disable once InconsistentNaming + var labelLE = label.gameObject.AddComponent(); + labelLE.preferredHeight = 18f; + labelLE.minHeight = 18f; + + return O5Factory.Toggle( + ctx: ctx, + parent: colParent, + defaultValue: defaultValue, + value: value, + onChanged: onChanged, + text: toggleText, + id: id + ); + } + + private static O5Dropdown CreateDropdownField( + O5Context ctx, + Transform colParent, + string labelText, + T defaultValue, + T value, + IReadOnlyList values, + Func display, + string id, + Action? onChanged = null) + { + var label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true); + label.text = labelText; + label.color = ctx.Theme.TextDim; + + // ReSharper disable once InconsistentNaming + var labelLE = label.gameObject.AddComponent(); + labelLE.preferredHeight = 18f; + labelLE.minHeight = 18f; + + var ddContainer = new GameObject($"DD_{id}"); + ddContainer.transform.SetParent(colParent, false); + var ddRect = ddContainer.AddComponent(); + + // ReSharper disable once InconsistentNaming + var ddLE = ddContainer.AddComponent(); + ddLE.preferredHeight = 50f; + ddLE.minHeight = 50f; + + return O5Factory.DropDown( + ctx: ctx, + parent: ddContainer.transform, + defaultValue: defaultValue, + value: value, + values: values, + display: display, + onChanged: val => { if (val != null) onChanged?.Invoke(val); }, + id: id + ); } private static void SetWindowActive(bool enabled)