From b6f1c0e4d2cbd8f59c8971476ad1bc2a7108fb9d1bd288239d3234381ebefa54 Mon Sep 17 00:00:00 2001 From: kyulio Date: Mon, 5 Oct 2026 16:21:26 +0900 Subject: [PATCH] Fix Render Config UI Prototype Layout --- UI/Window/RenderConfigWindow.cs | 250 ++++++++++++++++++-------------- 1 file changed, 138 insertions(+), 112 deletions(-) diff --git a/UI/Window/RenderConfigWindow.cs b/UI/Window/RenderConfigWindow.cs index 19a73b9..cda2d9e 100644 --- a/UI/Window/RenderConfigWindow.cs +++ b/UI/Window/RenderConfigWindow.cs @@ -86,56 +86,50 @@ internal static class RenderConfigWindow var config = RenderManager.Instance.Configuration ??= new RenderConfig(); EnsureDefaultConfig(config); - // 1. RenderFPS & VideoFPS - var (row1Left, row1Right) = CreateTwoColumnRow(scrollContent); + // 1-3. Top section: 좌/우 컨테이너 1개씩으로 3행을 쌓는다 (중앙선 1개) + var (topLeft, topRight) = CreateTwoColumnPanel(scrollContent, spacing: 8f, columnSpacing: 10f); _renderFpsInput = CreateInputField( - ctx, row1Left, + 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; } ); - _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, + 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; } ); - _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, + 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, row3Right, + ctx, CreateFieldSlot(topRight), LocalizationManager.Get("ui.render_config.show_judgement"), false, config.ShowJudgementText, LocalizationManager.Get("ui.render_config.show_judgement"), @@ -154,56 +148,52 @@ internal static class RenderConfigWindow showActiveToggle: false ); - var (vRow1Left, vRow1Right) = CreateTwoColumnRow(videoContent); + var (videoLeft, videoRight) = CreateTwoColumnPanel(videoContent); var codecs = (Codec[])Enum.GetValues(typeof(Codec)); _codecDropdown = CreateDropdownField( - ctx, vRow1Left, + 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 ); - - 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, + 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; } ); - _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, + 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, vRow3Right, + ctx, CreateFieldSlot(videoRight), LocalizationManager.Get("ui.render_config.keyframe_interval"), "2.0", config.VideoConfig.KeyframeInterval.ToString("0.##"), "input_keyframe_interval", @@ -230,38 +220,36 @@ internal static class RenderConfigWindow showActiveToggle: false ); - var (aRow1Left, aRow1Right) = CreateTwoColumnRow(audioContent); + var (audioLeft, audioRight) = CreateTwoColumnPanel(audioContent); _renderAudioToggle = CreateToggleField( - ctx, aRow1Left, + 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 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, + 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, aRow2Right, + ctx, CreateFieldSlot(audioRight), LocalizationManager.Get("ui.render_config.audio_bitrate"), "256", (config.AudioConfig.AudioBitrate / 1000).ToString(), "input_audio_bitrate", @@ -301,9 +289,9 @@ internal static class RenderConfigWindow } ); - var (gBtnLeft, gBtnRight) = CreateTwoColumnRow(globalContent); + var (globalLeft, globalRight) = CreateTwoColumnPanel(globalContent); O5Factory.Button( - ctx, gBtnLeft, + ctx, globalLeft, () => { var initial = string.IsNullOrWhiteSpace(_outputDirInput.Value) @@ -324,7 +312,7 @@ internal static class RenderConfigWindow ); O5Factory.Button( - ctx, gBtnRight, + ctx, globalRight, () => { _outputDirInput.Set(GlobalSettings.DefaultOutputDirectory); @@ -346,19 +334,27 @@ internal static class RenderConfigWindow showActiveToggle: false ); - var (adRow1Left, adRow1Right) = CreateTwoColumnRow(advContent); + var (advLeft, advRight) = CreateTwoColumnPanel(advContent); var extensions = new[] { "mkv", "mp4", "mov", "webm", "avi" }; _fileExtensionDropdown = CreateDropdownField( - ctx, adRow1Left, + 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, adRow1Right, + ctx, CreateFieldSlot(advRight), LocalizationManager.Get("ui.render_config.random_seed"), "", config.AdvancedConfig.RandomSeed?.ToString() ?? "", "input_random_seed", @@ -369,18 +365,8 @@ internal static class RenderConfigWindow 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, + ctx, CreateFieldSlot(advRight), LocalizationManager.Get("ui.render_config.non_tile"), false, config.AdvancedConfig.NonTile, LocalizationManager.Get("ui.render_config.non_tile"), @@ -564,41 +550,81 @@ internal static class RenderConfigWindow ); } - private static (RectTransform left, RectTransform right) CreateTwoColumnRow(Transform parent, float spacing = 8f) + // 카드(또는 상단 섹션)당 하나의 좌/우 컨테이너를 만들어 + // 가로 한 줄 두 필드 사이 중앙이 행마다 어긋나지 않도록 한다. + // - 패널 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 rowObj = new GameObject("TwoColRow"); - rowObj.transform.SetParent(parent, false); + var panelObj = new GameObject("TwoColPanel"); + panelObj.transform.SetParent(parent, false); - var rowRect = rowObj.AddComponent(); + panelObj.AddComponent(); - var hLayout = rowObj.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 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 panelLE = panelObj.AddComponent(); + panelLE.flexibleWidth = 1f; - 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; + var left = CreateColumn(panelObj.transform, "LeftCol", columnSpacing); + var right = CreateColumn(panelObj.transform, "RightCol", columnSpacing); - return (col1Rect, col2Rect); + 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( -- 2.47.3