From 7eaf4f48e6ba5652a46cff9652ffad8e36fa04853c799861e9b84a1ac72f11dc Mon Sep 17 00:00:00 2001 From: copecone Date: Thu, 22 Oct 2026 11:46:42 +0900 Subject: [PATCH] Add `UIComponentWrapper` --- ADOFAIRenderer.csproj | 2 +- UI/Window/RenderConfigWindow.cs | 217 +++++++++++--------------------- Util/UI/UIComponentWrapper.cs | 42 +++++++ 3 files changed, 117 insertions(+), 144 deletions(-) create mode 100644 Util/UI/UIComponentWrapper.cs diff --git a/ADOFAIRenderer.csproj b/ADOFAIRenderer.csproj index 8bbe202..ea0e20b 100644 --- a/ADOFAIRenderer.csproj +++ b/ADOFAIRenderer.csproj @@ -8,7 +8,7 @@ false true true - 11 + 12 GPL-3.0-or-later diff --git a/UI/Window/RenderConfigWindow.cs b/UI/Window/RenderConfigWindow.cs index c48a3ac..4971b11 100644 --- a/UI/Window/RenderConfigWindow.cs +++ b/UI/Window/RenderConfigWindow.cs @@ -8,6 +8,7 @@ using ADOFAIRenderer.Core.Data; using ADOFAIRenderer.Data; using ADOFAIRenderer.Localization; using ADOFAIRenderer.Util.OS; +using ADOFAIRenderer.Util.UI; using MelonLoader; using Newtonsoft.Json; using O5Kit.Control; @@ -62,7 +63,7 @@ internal static class RenderConfigWindow // Presets controls /// Preset selector: preset names, plus the Custom label at index 0 while the values differ from the preset. - private static O5Dropdown? _presetDropdown; + private static O5DropdownWrapper? _presetDropdown; /// Name used by the Save button; set to the selected preset's name when a preset is loaded. private static O5InputField? _presetNameInput; /// Saves the current values under the name in . @@ -105,39 +106,22 @@ internal static class RenderConfigWindow private static O5Slider? _renderWidthSlider; private static O5Slider? _renderHeightSlider; private static O5Slider? _delayAfterEndSlider; - private static O5Toggle? _showJudgementToggle; + private static O5ToggleWrapper? _showJudgementToggle; private static SegmentedControl? _outputModeControl; - private static O5Dropdown? _codecDropdown; - private static TextMeshProUGUI? _codecLabel; - private static ITweenHandle? _codecLabelTween; + private static O5DropdownWrapper? _codecDropdown; + private static O5DropdownWrapper? _rateControlDropdown; - private static O5Dropdown? _rateControlDropdown; - private static TextMeshProUGUI? _rateControlLabel; - private static ITweenHandle? _rateControlLabelTween; - // Bitrate / max bitrate / QP inputs keep their label and the running label color tween, so UpdateRateControlUI - // can dim the label together with the blocked input (see SetFieldBlocked). - private static O5InputField? _bitrateInput; - private static TextMeshProUGUI? _bitrateLabel; - private static ITweenHandle? _bitrateLabelTween; - - private static O5InputField? _maxBitrateInput; - private static TextMeshProUGUI? _maxBitrateLabel; - private static ITweenHandle? _maxBitrateLabelTween; - - private static O5InputField? _qpInput; - private static TextMeshProUGUI? _qpLabel; - private static ITweenHandle? _qpLabelTween; - - private static O5Toggle? _forceSoftwareToggle; - private static TextMeshProUGUI? _forceSoftwareLabel; - private static ITweenHandle? _forceSoftwareLabelTween; + private static O5InputFieldWrapper? _bitrateInput; + private static O5InputFieldWrapper? _maxBitrateInput; + private static O5InputFieldWrapper? _qpInput; + private static O5ToggleWrapper? _forceSoftwareToggle; private static O5Slider? _keyframeIntervalSlider; - private static O5Toggle? _renderAudioToggle; - private static O5Dropdown? _sampleRateDropdown; - private static O5Dropdown? _channelsDropdown; + private static O5ToggleWrapper? _renderAudioToggle; + private static O5DropdownWrapper? _sampleRateDropdown; + private static O5DropdownWrapper? _channelsDropdown; private static O5Slider? _audioBitrateSlider; /// @@ -150,22 +134,22 @@ internal static class RenderConfigWindow /// private static O5InputField? _fileNameFormatInput; - private static O5Dropdown? _fileExtensionDropdown; - private static O5InputField? _randomSeedInput; - private static O5Toggle? _processAudioToggle; - private static O5Toggle? _nonTileToggle; - private static O5Toggle? _highResBloomCorrectionToggle; - private static O5Toggle? _fixCustomFrameRateToggle; - private static O5Toggle? _disableTileCullingToggle; - private static O5Toggle? _hideFailedDecorationsToggle; + private static O5DropdownWrapper? _fileExtensionDropdown; + private static O5InputFieldWrapper? _randomSeedInput; + private static O5ToggleWrapper? _processAudioToggle; + private static O5ToggleWrapper? _nonTileToggle; + private static O5ToggleWrapper? _highResBloomCorrectionToggle; + private static O5ToggleWrapper? _fixCustomFrameRateToggle; + private static O5ToggleWrapper? _disableTileCullingToggle; + private static O5ToggleWrapper? _hideFailedDecorationsToggle; // Planet color overrides: the toggle enables the override, the picker holds the color. A disabled toggle stores // null in AdvancedConfig (no override is applied by PersistencePatch) and blocks the picker. - private static O5Toggle? _firePlanetToggle; + private static O5ToggleWrapper? _firePlanetToggle; private static O5ColorPicker? _firePlanetPicker; - private static O5Toggle? _icePlanetToggle; + private static O5ToggleWrapper? _icePlanetToggle; private static O5ColorPicker? _icePlanetPicker; - private static O5Toggle? _windPlanetToggle; + private static O5ToggleWrapper? _windPlanetToggle; private static O5ColorPicker? _windPlanetPicker; // Colors shown in a planet picker when its override is off (and its middle-click default value) @@ -257,30 +241,31 @@ internal static class RenderConfigWindow // Render FPS: standard video rates, then multiples of 120 and of 288 up to the 3000 maximum. // Video FPS: standard video rates, then multiples of 60 and of 72 up to the 600 maximum. // Audio bitrate: kbps values. Width/height: common resolutions grouped by aspect ratio. - double[] renderFpsSnap = { 23.976, 24, 29.97, 30, 59.94, 60, 144, + double[] renderFpsSnap = [ 23.976, 24, 29.97, 30, 59.94, 60, 144, 120, 240, 360, 480, 600, 720, // +120 288, 576, 864, 1152, 1440, 1728, // +288 3000 // MAX - }; - double[] videoFpsSnap = { 23.976, 24, 29.97, 30, 59.94, 60, 72, + ]; + + double[] videoFpsSnap = [ 23.976, 24, 29.97, 30, 59.94, 60, 72, 120, 180, 240, 300, 360, 420, 480, 540, // +60 144, 216, 288, 432, 504, 576, // +72 600 // MAX - }; + ]; - double[] audioBitrateSnap = { 64, 96, 128, 192, 256, 320, 384 }; + double[] audioBitrateSnap = [64, 96, 128, 192, 256, 320, 384]; - double[] renderWidthSnap = { + double[] renderWidthSnap = [ 256, 426, 640, 854, 960, 1280, 1366, 1600, 1920, 2048, 2560, 3200, 3840, 4096, 5120, 5760, 6016, 7680, 10240, 15360, // 16:9 230, 384, 576, 768, 1152, 1440, 1680, 2880, 3072, // 16:10 192, 320, 480, 800, 1024, 1400, 2732, 6400, 11520 // 4:3 - }; + ]; - double[] renderHeightSnap = { + double[] renderHeightSnap = [ 144, 240, 360, 480, 540, 720, 768, 900, 1080, 1152, 1440, 1800, 2160, 2304, 2880, 3240, 3384, 4320, 5760, 8640, // 16:9 400, 600, 800, 1050, 1200, 1600, 1920, 2400, 3200, 4800, 9600, // 16:10 864, 960, 1536, 2048, 3072 // 4:3 - }; + ]; // --------------------------------------------------------------------------------------------- // Section 1: Presets card - preset dropdown, then a row with name input, Save and Delete buttons @@ -474,7 +459,7 @@ internal static class RenderConfigWindow ctx: ctx, parent: CreateFieldSlot(topLeft), labelKey: "ui.render_config.delay_after_end", id: "slider_delay_after_end", defaultValue: 15, min: 0, max: 9999.999, value: 15, - format: "F3", clampMode: ClampMode.All, + format: "F3", clampMode: ClampMode.Slider, onChanged: val => { config.DelayAfterEnd = val; MarkConfigModified(); } ); @@ -497,11 +482,11 @@ internal static class RenderConfigWindow labelKey: "ui.render_config.output_mode", defaultValue: RenderConfig.RenderOutputMode.Video, value: config.OutputMode, - options: new SegmentOption[] - { - new(RenderConfig.RenderOutputMode.Video, "ui.render_config.output_mode_video"), - new(RenderConfig.RenderOutputMode.PNGSequence, "ui.render_config.output_mode_png_sequence") - }, + options: + [ + new SegmentOption(RenderConfig.RenderOutputMode.Video, "ui.render_config.output_mode_video"), + new SegmentOption(RenderConfig.RenderOutputMode.PNGSequence, "ui.render_config.output_mode_png_sequence") + ], id: "seg_output_mode", onChanged: val => { @@ -535,7 +520,7 @@ internal static class RenderConfigWindow "ui.render_config.codec", Codec.H264, config.VideoConfig.Codec, codecs, c => c.ToString(), - "dd_video_codec", out _codecLabel, + "dd_video_codec", c => { config.VideoConfig.Codec = c; @@ -549,7 +534,7 @@ internal static class RenderConfigWindow ctx, CreateFieldSlot(videoLeft), "ui.render_config.bitrate", "30000", config.VideoConfig.Bitrate.ToString(), - "input_bitrate", out _bitrateLabel, + "input_bitrate", TMP_InputField.CharacterValidation.Integer, val => { if (int.TryParse(val, out var b) && b > 0) config.VideoConfig.Bitrate = b; MarkConfigModified(); } ); @@ -558,7 +543,7 @@ internal static class RenderConfigWindow ctx, CreateFieldSlot(videoLeft), "ui.render_config.qp", "18", config.VideoConfig.QP.ToString(), - "input_qp", out _qpLabel, + "input_qp", TMP_InputField.CharacterValidation.Integer, val => { if (int.TryParse(val, out var qp) && qp >= 0) config.VideoConfig.QP = qp; MarkConfigModified(); } ); @@ -569,7 +554,7 @@ internal static class RenderConfigWindow "ui.render_config.rate_control", RateControlMode.CBR, config.VideoConfig.RateControlMode, rateModes, m => m.ToString(), - "dd_rate_control", out _rateControlLabel, + "dd_rate_control", m => { config.VideoConfig.RateControlMode = m; @@ -582,7 +567,7 @@ internal static class RenderConfigWindow ctx, CreateFieldSlot(videoRight), "ui.render_config.max_bitrate", "50000", config.VideoConfig.MaxBitrate.ToString(), - "input_max_bitrate", out _maxBitrateLabel, + "input_max_bitrate", TMP_InputField.CharacterValidation.Integer, val => { if (int.TryParse(val, out var mb) && mb > 0) config.VideoConfig.MaxBitrate = mb; MarkConfigModified(); } ); @@ -593,7 +578,7 @@ internal static class RenderConfigWindow defaultValue: false, value: config.VideoConfig.ForceSoftwareEncoder, toggleTextKey: "ui.render_config.force_software", - id: "toggle_force_software", out _forceSoftwareLabel, + id: "toggle_force_software", onChanged: val => { config.VideoConfig.ForceSoftwareEncoder = val; MarkConfigModified(); }, generateLabel: true ); @@ -1108,7 +1093,7 @@ internal static class RenderConfigWindow _codecDropdown?.Set(config.VideoConfig.Codec, false); _rateControlDropdown?.Set(config.VideoConfig.RateControlMode, false); - _bitrateInput?.Set(config.VideoConfig.Bitrate.ToString(), false); + _bitrateInput?.component.Set(config.VideoConfig.Bitrate.ToString(), false); _maxBitrateInput?.Set(config.VideoConfig.MaxBitrate.ToString(), false); _qpInput?.Set(config.VideoConfig.QP.ToString(), false); _keyframeIntervalSlider?.Set(config.VideoConfig.KeyframeInterval, false, noFilter: true); @@ -1985,20 +1970,6 @@ internal static class RenderConfigWindow return button; } - /// Same as the CreateInputField overload with an out label parameter, without returning the label. - private static O5InputField CreateInputField( - O5Context ctx, - Transform colParent, - string labelKey, - string? defaultValue, - string value, - string id, - TMP_InputField.CharacterValidation characterValidation = TMP_InputField.CharacterValidation.None, - Action? onChanged = null) - { - return CreateInputField(ctx, colParent, labelKey, defaultValue, value, id, out _, characterValidation, onChanged); - } - /// /// Creates a labeled text input in (normally a field slot): an 18-unit label row and an /// O5Kit input below it. The label and the placeholder are both bound to . @@ -2009,24 +1980,22 @@ internal static class RenderConfigWindow /// Value restored by middle click; null for none. /// Initial text. /// O5Kit control id. - /// Receives the label text component (used to dim the label of a blocked field). /// TextMeshPro character filter for typed text, e.g. Integer. /// Called with the raw text on every change; parsing and validation are up to the caller. /// The input field. - private static O5InputField CreateInputField( + private static O5InputFieldWrapper CreateInputField( O5Context ctx, Transform colParent, string labelKey, string? defaultValue, string value, string id, - out TextMeshProUGUI label, TMP_InputField.CharacterValidation characterValidation = TMP_InputField.CharacterValidation.None, Action? onChanged = null) { // Labeled text input: an 18-unit label row (13 pt, dim) above an O5Kit input. The label key also serves as the // placeholder. characterValidation restricts typed characters (e.g. Integer). onChanged receives the raw text. - label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true); + var label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true); label.text = LocalizationManager.Get(labelKey); label.color = ctx.Theme.TextDim; LocalizedText.Bind(label, labelKey); @@ -2053,7 +2022,7 @@ internal static class RenderConfigWindow } inputField.InputField.characterValidation = characterValidation; - return inputField; + return new O5InputFieldWrapper(inputField, label); } // =================================================================================================== @@ -2061,42 +2030,36 @@ internal static class RenderConfigWindow // =================================================================================================== private static void SetFieldBlocked( - O5InputField? input, - TextMeshProUGUI? label, - ref ITweenHandle? tweenHandle, + O5InputFieldWrapper? input, bool blocked, bool noAnimate = false) { - input?.SetBlocked(blocked, noAnimate); + input?.component.SetBlocked(blocked, noAnimate); - if (label == null) return; - UpdateLabelDim(label, ref tweenHandle, blocked, noAnimate); + if (input?.label == null) return; + UpdateLabelDim(input.label, ref input.tween, blocked, noAnimate); } private static void SetToggleBlocked( - O5Toggle? toggle, - TextMeshProUGUI? label, - ref ITweenHandle? tweenHandle, + O5ToggleWrapper? toggle, bool blocked, bool noAnimate = false) { - toggle?.SetBlocked(blocked, noAnimate); + toggle?.component.SetBlocked(blocked, noAnimate); - if (label == null) return; - UpdateLabelDim(label, ref tweenHandle, blocked, noAnimate); + if (toggle?.label == null) return; + UpdateLabelDim(toggle.label, ref toggle.tween, blocked, noAnimate); } private static void SetDropdownBlocked( - O5Dropdown? dropdown, - TextMeshProUGUI? label, - ref ITweenHandle? tweenHandle, + O5DropdownWrapper? dropdown, bool blocked, bool noAnimate = false) { - dropdown?.SetBlocked(blocked, noAnimate); + dropdown?.component.SetBlocked(blocked, noAnimate); - if (label == null) return; - UpdateLabelDim(label, ref tweenHandle, blocked, noAnimate); + if (dropdown?.label == null) return; + UpdateLabelDim(dropdown.label, ref dropdown.tween, blocked, noAnimate); } private static void UpdateLabelDim(TextMeshProUGUI label, ref ITweenHandle? tweenHandle, bool blocked, bool noAnimate = false) @@ -2149,12 +2112,12 @@ internal static class RenderConfigWindow blockForceSoftwareToggle |= blockVideoEncodingSettings; blockKeyframeIntervalSlider |= blockVideoEncodingSettings; - SetDropdownBlocked(_codecDropdown, _codecLabel, ref _codecLabelTween, blockCodecDropdown, noAnimate); - SetDropdownBlocked(_rateControlDropdown, _rateControlLabel, ref _rateControlLabelTween, blockRateControlModeDropdown, noAnimate); - SetFieldBlocked(_bitrateInput, _bitrateLabel, ref _bitrateLabelTween, blockBitrateInput, noAnimate); - SetFieldBlocked(_maxBitrateInput, _maxBitrateLabel, ref _maxBitrateLabelTween, blockMaxBitrateInput, noAnimate); - SetFieldBlocked(_qpInput, _qpLabel, ref _qpLabelTween, blockQPInput, noAnimate); - SetToggleBlocked(_forceSoftwareToggle, _forceSoftwareLabel, ref _forceSoftwareLabelTween, blockForceSoftwareToggle, noAnimate); + SetDropdownBlocked(_codecDropdown, blockCodecDropdown, noAnimate); + SetDropdownBlocked(_rateControlDropdown, blockRateControlModeDropdown, noAnimate); + SetFieldBlocked(_bitrateInput, blockBitrateInput, noAnimate); + SetFieldBlocked(_maxBitrateInput, blockMaxBitrateInput, noAnimate); + SetFieldBlocked(_qpInput, blockQPInput, noAnimate); + SetToggleBlocked(_forceSoftwareToggle, blockForceSoftwareToggle, noAnimate); _keyframeIntervalSlider?.SetBlocked(blockKeyframeIntervalSlider, noAnimate); } @@ -2214,28 +2177,13 @@ internal static class RenderConfigWindow return input; } - - /// Same as the CreateToggleField overload with an out label parameter, without returning the label. - private static O5Toggle CreateToggleField( - O5Context ctx, - Transform colParent, - string labelKey, - bool? defaultValue, - bool value, - string toggleTextKey, - string id, - Action? onChanged = null, - bool generateLabel = true) - { - return CreateToggleField(ctx, colParent, labelKey, defaultValue, value, toggleTextKey, id, out _, onChanged, generateLabel); - } /// /// Creates an O5Kit toggle whose text is localized with . With /// an 18-unit label row is added above it (text from , /// empty when the key is empty), which keeps the toggle aligned with labeled fields in the neighboring column. /// - private static O5Toggle CreateToggleField( + private static O5ToggleWrapper CreateToggleField( O5Context ctx, Transform colParent, string labelKey, @@ -2243,11 +2191,10 @@ internal static class RenderConfigWindow bool value, string toggleTextKey, string id, - out TextMeshProUGUI? label, Action? onChanged = null, bool generateLabel = true) { - label = null; + TextMeshProUGUI? label = null; if (generateLabel) { label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true); @@ -2276,22 +2223,7 @@ internal static class RenderConfigWindow LocalizedText.Bind(toggle.Label, toggleTextKey); } - return toggle; - } - - /// Same as the CreateDropdownField overload with an out label parameter, without returning the label. - private static O5Dropdown CreateDropdownField( - O5Context ctx, - Transform colParent, - string labelKey, - T defaultValue, - T value, - IReadOnlyList values, - Func display, - string id, - Action? onChanged = null) - { - return CreateDropdownField(ctx, colParent, labelKey, defaultValue, value, values, display, id, out _, onChanged); + return new O5ToggleWrapper(toggle, label); } /// @@ -2299,7 +2231,7 @@ internal static class RenderConfigWindow /// dropdown. converts values to their shown text. The change callback is not invoked for /// a null value. /// - private static O5Dropdown CreateDropdownField( + private static O5DropdownWrapper CreateDropdownField( O5Context ctx, Transform colParent, string labelKey, @@ -2308,10 +2240,9 @@ internal static class RenderConfigWindow IReadOnlyList values, Func display, string id, - out TextMeshProUGUI label, Action? onChanged = null) { - label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true); + var label = O5Factory.ControlText(ctx, colParent, 13f, noPad: true); label.text = LocalizationManager.Get(labelKey); label.color = ctx.Theme.TextDim; LocalizedText.Bind(label, labelKey); @@ -2329,7 +2260,7 @@ internal static class RenderConfigWindow ddLE.preferredHeight = 50f; ddLE.minHeight = 0f; - return O5Factory.DropDown( + return new O5DropdownWrapper(O5Factory.DropDown( ctx: ctx, parent: ddContainer.transform, defaultValue: defaultValue, @@ -2338,7 +2269,7 @@ internal static class RenderConfigWindow display: display, onChanged: val => { if (val != null) onChanged?.Invoke(val); }, id: id - ); + ), label); } /// diff --git a/Util/UI/UIComponentWrapper.cs b/Util/UI/UIComponentWrapper.cs new file mode 100644 index 0000000..4ca94c4 --- /dev/null +++ b/Util/UI/UIComponentWrapper.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using O5Kit.Control; +using O5Kit.Core; +using TMPro; +using UnityEngine; + +namespace ADOFAIRenderer.Util.UI; + +internal abstract record UIWrapperBase(TextMeshProUGUI? label) +{ + public readonly TextMeshProUGUI? label = label; + + #pragma warning disable CS0649 + public ITweenHandle? tween; + #pragma warning restore CS0649 +} + +internal record O5InputFieldWrapper(O5InputField component, TextMeshProUGUI? label) : UIWrapperBase(label) +{ + public readonly O5InputField component = component; + + public string Value => component.Value; + public void Set(string value, bool invoke = true) => component.Set(value, invoke); +} + +internal record O5ToggleWrapper(O5Toggle component, TextMeshProUGUI? label) : UIWrapperBase(label) +{ + public readonly O5Toggle component = component; + + public bool Value => component.Value; + public void Set(bool value, bool invoke = true) => component.Set(value, invoke); +} + +internal record O5DropdownWrapper(O5Dropdown component, TextMeshProUGUI? label) : UIWrapperBase(label) +{ + public readonly O5Dropdown component = component; + + public T? Value => component.Value; + public IReadOnlyList Values => component.Values; + public void Set(T? value, bool invoke = true) => component.Set(value, invoke); + public void SetValues(IReadOnlyList values) => component.SetValues(values); +} \ No newline at end of file