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) { _configWindow = RendererUIManager.Instance.WindowManager.Create(new O5WindowOptions { Id = "RendererMod_RenderConfig", Title = LocalizationManager.Get("ui.render_config.window_title"), Size = new Vector2(580f, 480f), Draggable = true, Resizable = true, ShowCloseButton = true }); _configWindow.CloseRequested += win => { 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 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-3. Top section: 좌/우 컨테이너 1개씩으로 3행을 쌓는다 (중앙선 1개) var (topLeft, topRight) = CreateTwoColumnPanel(scrollContent, spacing: 8f, columnSpacing: 10f); _renderFpsInput = CreateInputField( ctx, CreateFieldSlot(topLeft), 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; } ); _renderWidthInput = CreateInputField( ctx, CreateFieldSlot(topLeft), 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; } ); _delayAfterEndInput = CreateInputField( ctx, CreateFieldSlot(topLeft), 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; } ); _videoFpsInput = CreateInputField( ctx, CreateFieldSlot(topRight), 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; } ); _renderHeightInput = CreateInputField( ctx, CreateFieldSlot(topRight), 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; } ); _showJudgementToggle = CreateToggleField( ctx, CreateFieldSlot(topRight), 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 (videoLeft, videoRight) = CreateTwoColumnPanel(videoContent); var codecs = (Codec[])Enum.GetValues(typeof(Codec)); _codecDropdown = CreateDropdownField( ctx, CreateFieldSlot(videoLeft), LocalizationManager.Get("ui.render_config.codec"), Codec.H264, config.VideoConfig.Codec, codecs, c => c.ToString(), "dd_video_codec", c => config.VideoConfig.Codec = c ); _bitrateInput = CreateInputField( ctx, CreateFieldSlot(videoLeft), 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; } ); _qpInput = CreateInputField( ctx, CreateFieldSlot(videoLeft), 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; } ); var rateModes = (RateControlMode[])Enum.GetValues(typeof(RateControlMode)); _rateControlDropdown = CreateDropdownField( ctx, CreateFieldSlot(videoRight), LocalizationManager.Get("ui.render_config.rate_control"), RateControlMode.CBR, config.VideoConfig.RateControlMode, rateModes, m => m.ToString(), "dd_rate_control", m => config.VideoConfig.RateControlMode = m ); _maxBitrateInput = CreateInputField( ctx, CreateFieldSlot(videoRight), 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; } ); _keyframeIntervalInput = CreateInputField( ctx, CreateFieldSlot(videoRight), 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 (audioLeft, audioRight) = CreateTwoColumnPanel(audioContent); _renderAudioToggle = CreateToggleField( ctx, CreateFieldSlot(audioLeft), 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 channels = new[] { 1, 2 }; _channelsDropdown = CreateDropdownField( ctx, CreateFieldSlot(audioLeft), 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 ); var sampleRates = new[] { 44100, 48000 }; _sampleRateDropdown = CreateDropdownField( ctx, CreateFieldSlot(audioRight), LocalizationManager.Get("ui.render_config.sample_rate"), 44100, config.AudioConfig.AudioSampleRate, sampleRates, r => $"{r} Hz", "dd_sample_rate", r => config.AudioConfig.AudioSampleRate = r ); _audioBitrateInput = CreateInputField( ctx, CreateFieldSlot(audioRight), 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 (globalLeft, globalRight) = CreateTwoColumnPanel(globalContent); O5Factory.Button( ctx, globalLeft, () => { 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, globalRight, () => { _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 (advLeft, advRight) = CreateTwoColumnPanel(advContent); var extensions = new[] { "mkv", "mp4", "mov", "webm", "avi" }; _fileExtensionDropdown = CreateDropdownField( ctx, CreateFieldSlot(advLeft), 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; } ); _processAudioToggle = CreateToggleField( ctx, CreateFieldSlot(advLeft), 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 ); _randomSeedInput = CreateInputField( ctx, CreateFieldSlot(advRight), 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; } ); _nonTileToggle = CreateToggleField( ctx, CreateFieldSlot(advRight), 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", () => { }) ); } // 카드(또는 상단 섹션)당 하나의 좌/우 컨테이너를 만들어 // 가로 한 줄 두 필드 사이 중앙이 행마다 어긋나지 않도록 한다. // - 패널 1개 = 중앙선 1개 (행마다 HLG를 만들던 기존 방식의 P1-P2 어긋남 방지) // - 좌/우 컬럼은 preferredWidth=0 + flexibleWidth=1 로 항상 50:50 // - 각 필드는 Field 슬롯(label 18 + 간격 3 + 컨트롤 50)으로 높이를 고정해 // 좌/우 스택을 써도 행 단위 Y 정렬이 유지되도록 한다. private static (RectTransform left, RectTransform right) CreateTwoColumnPanel( Transform parent, float spacing = 8f, float columnSpacing = 8f) { var panelObj = new GameObject("TwoColPanel"); panelObj.transform.SetParent(parent, false); panelObj.AddComponent(); var hLayout = panelObj.AddComponent(); hLayout.spacing = spacing; hLayout.padding = new RectOffset(0, 0, 0, 0); hLayout.childAlignment = TextAnchor.UpperCenter; hLayout.childControlWidth = true; hLayout.childControlHeight = true; hLayout.childForceExpandWidth = true; hLayout.childForceExpandHeight = false; var panelLE = panelObj.AddComponent(); panelLE.flexibleWidth = 1f; var left = CreateColumn(panelObj.transform, "LeftCol", columnSpacing); var right = CreateColumn(panelObj.transform, "RightCol", columnSpacing); return (left, right); } private static RectTransform CreateColumn(Transform parent, string name, float spacing) { var colObj = new GameObject(name); colObj.transform.SetParent(parent, false); var colRect = colObj.AddComponent(); var colLayout = colObj.AddComponent(); colLayout.spacing = spacing; colLayout.padding = new RectOffset(0, 0, 0, 0); colLayout.childControlWidth = true; colLayout.childControlHeight = true; colLayout.childForceExpandWidth = true; colLayout.childForceExpandHeight = false; // 핵심: 좌/우 선호 너비를 0으로 강제해 항상 정확히 절반씩 나누도록 한다. // (기존 행마다 HLG 방식은 라벨/값 텍스트 길이에 따라 preferred가 달라져 // 중앙이 (W + P1 - P2) / 2 로 행마다 틀어졌음) var colLE = colObj.AddComponent(); colLE.minWidth = 0f; colLE.preferredWidth = 0f; colLE.flexibleWidth = 1f; return colRect; } private static RectTransform CreateFieldSlot(Transform column) { var slotObj = new GameObject("Field"); slotObj.transform.SetParent(column, false); var slotRect = slotObj.AddComponent(); var slotLayout = slotObj.AddComponent(); slotLayout.spacing = 3f; slotLayout.padding = new RectOffset(0, 0, 0, 0); slotLayout.childControlWidth = true; slotLayout.childControlHeight = true; slotLayout.childForceExpandWidth = true; slotLayout.childForceExpandHeight = false; var slotLE = slotObj.AddComponent(); slotLE.flexibleWidth = 1f; return slotRect; } 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; // 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) { _configWindow?.Rect.gameObject.SetActive(enabled); } }