diff --git a/Settings/Localization/ModSettingsUi/eng.json b/Settings/Localization/ModSettingsUi/eng.json index d95a372..2bcc97d 100644 --- a/Settings/Localization/ModSettingsUi/eng.json +++ b/Settings/Localization/ModSettingsUi/eng.json @@ -194,6 +194,9 @@ "sidebar.title": "Mods", "sidebar.subtitle": "Browse registered mods, pages, and sections.", "sidebar.modMeta": "{0} pages", + "sidebar.modHeader.none": "No mod selected", + "sidebar.modPreview.empty": "No preview", + "sidebar.modPreview.noImage": "No resources", "button.open": "Open", "button.back": "Back", "button.remove": "Remove", diff --git a/Settings/Localization/ModSettingsUi/zhs.json b/Settings/Localization/ModSettingsUi/zhs.json index 19b1bed..1fe8c6c 100644 --- a/Settings/Localization/ModSettingsUi/zhs.json +++ b/Settings/Localization/ModSettingsUi/zhs.json @@ -194,6 +194,9 @@ "sidebar.title": "模组", "sidebar.subtitle": "浏览已注册的模组、页面与分区。", "sidebar.modMeta": "共 {0} 个页面", + "sidebar.modHeader.none": "未选择模组", + "sidebar.modPreview.empty": "无预览", + "sidebar.modPreview.noImage": "无资源", "button.open": "打开", "button.back": "返回", "button.remove": "删除", diff --git a/Settings/ModSettings/ModSettingsLocalization.cs b/Settings/ModSettings/ModSettingsLocalization.cs index d93714a..af1c895 100644 --- a/Settings/ModSettings/ModSettingsLocalization.cs +++ b/Settings/ModSettings/ModSettingsLocalization.cs @@ -1,4 +1,5 @@ using System.Reflection; +using MegaCrit.Sts2.Core.Modding; using STS2RitsuLib.Compat; using STS2RitsuLib.Utils; @@ -27,9 +28,13 @@ public static string ResolveModName(string modId, string fallback) if (!string.IsNullOrWhiteSpace(configuredName)) return configuredName; - return Sts2ModManagerCompat.EnumerateModsForManifestLookup().FirstOrDefault(mod => - string.Equals(mod.manifest?.id, modId, StringComparison.OrdinalIgnoreCase))?.manifest?.name - ?? fallback; + var match = Sts2ModManagerCompat.EnumerateModsForManifestLookup() + .FirstOrDefault(mod => + string.Equals(mod.manifest?.id, modId, StringComparison.OrdinalIgnoreCase)); + if (match?.manifest is ModManifest mm && !string.IsNullOrWhiteSpace(mm.name)) + return mm.name; + + return fallback; } diff --git a/Settings/ModSettingsUi/ModSettingsModInfoResolver.cs b/Settings/ModSettingsUi/ModSettingsModInfoResolver.cs new file mode 100644 index 0000000..25ff49c --- /dev/null +++ b/Settings/ModSettingsUi/ModSettingsModInfoResolver.cs @@ -0,0 +1,182 @@ +using System.IO; +using System.Reflection; +using Godot; +using MegaCrit.Sts2.Core.Assets; +using MegaCrit.Sts2.Core.Modding; +using STS2RitsuLib.Compat; + +namespace STS2RitsuLib.Settings +{ + /// + /// Resolves installed mod manifest fields (name, version, icon) for the settings sidebar header. + /// Matches vanilla modding screen: fields and res://<id>/mod_image.png. + /// + internal static class ModSettingsModInfoResolver + { + internal static Mod? TryFindMod(string modId) + { + if (string.IsNullOrWhiteSpace(modId)) + return null; + + foreach (var m in Sts2ModManagerCompat.EnumerateModsForManifestLookup()) + { + if (string.Equals(m.manifest?.id, modId, StringComparison.OrdinalIgnoreCase)) + return m; + } + + foreach (var m in Sts2ModManagerCompat.EnumerateModsForManifestLookup()) + { + if (string.IsNullOrWhiteSpace(m.path)) + continue; + var trimmed = m.path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + var folder = Path.GetFileName(trimmed); + if (string.Equals(folder, modId, StringComparison.OrdinalIgnoreCase)) + return m; + } + + return null; + } + + internal static string ResolveTitle(Mod? mod, string modId) + { + if (mod?.manifest is ModManifest mm && !string.IsNullOrWhiteSpace(mm.name)) + return mm.name; + + if (mod != null) + { + var n = GetManifestMemberString(mod.manifest, "name", "Name"); + if (!string.IsNullOrWhiteSpace(n)) + return n; + } + + return ModSettingsLocalization.ResolveModName(modId, modId); + } + + internal static string? ResolveVersion(Mod? mod) + { + if (mod?.manifest is ModManifest mm && !string.IsNullOrWhiteSpace(mm.version)) + return mm.version; + return mod == null ? null : GetManifestMemberString(mod.manifest, "version", "Version"); + } + + internal static string? ResolveAuthor(Mod? mod) + { + if (mod?.manifest is ModManifest mm && !string.IsNullOrWhiteSpace(mm.author)) + return mm.author; + return mod == null ? null : GetManifestMemberString(mod.manifest, "author", "Author"); + } + + internal static string? ResolveDescription(Mod? mod, int maxLen = 220) + { + string? d; + if (mod?.manifest is ModManifest mm && !string.IsNullOrWhiteSpace(mm.description)) + d = mm.description; + else + d = mod == null ? null : GetManifestMemberString(mod.manifest, "description", "Description"); + if (string.IsNullOrWhiteSpace(d)) + return null; + d = d.Trim().Replace("\r\n", "\n"); + return d.Length <= maxLen ? d : d[..maxLen].TrimEnd() + "…"; + } + + /// + /// Optional manifest icon paths, then vanilla res://<manifest id>/mod_image.png. + /// + internal static Texture2D? TryLoadModIcon(Mod? mod, string modId) + { + var fromManifest = TryLoadManifestCustomIcon(mod); + if (fromManifest != null) + return fromManifest; + + var id = mod?.manifest is ModManifest mm ? mm.id : null; + foreach (var key in new[] { id, modId }) + { + if (string.IsNullOrWhiteSpace(key)) + continue; + var tex = TryLoadVanillaModImageRes(key); + if (tex != null) + return tex; + } + + return null; + } + + private static Texture2D? TryLoadManifestCustomIcon(Mod? mod) + { + if (mod?.manifest == null) + return null; + + var path = GetManifestMemberString(mod.manifest, "icon", "Icon", "thumbnail", "Thumbnail", "icon_path", + "iconPath"); + if (string.IsNullOrWhiteSpace(path)) + return null; + + path = path.Trim(); + try + { + if (path.StartsWith("res://", StringComparison.Ordinal)) + { + if (ResourceLoader.Exists(path)) + return PreloadManager.Cache.GetAsset(path); + return GD.Load(path); + } + + if (File.Exists(path)) + { + var img = Image.LoadFromFile(path); + if (img != null) + return ImageTexture.CreateFromImage(img); + } + } + catch + { + // ignored + } + + return null; + } + + private static Texture2D? TryLoadVanillaModImageRes(string manifestId) + { + var path = $"res://{manifestId}/mod_image.png"; + try + { + if (!ResourceLoader.Exists(path)) + return null; + return PreloadManager.Cache.GetAsset(path); + } + catch + { + try + { + return GD.Load(path); + } + catch + { + return null; + } + } + } + + private static string? GetManifestMemberString(object? manifest, params string[] names) + { + if (manifest == null) + return null; + + var t = manifest.GetType(); + foreach (var name in names) + { + var p = t.GetProperty(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + if (p?.GetValue(manifest) is string s && !string.IsNullOrWhiteSpace(s)) + return s; + + var f = t.GetField(name, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase); + if (f?.GetValue(manifest) is string s2 && !string.IsNullOrWhiteSpace(s2)) + return s2; + } + + return null; + } + } +} diff --git a/Settings/ModSettingsUi/ModSettingsUiControls.cs b/Settings/ModSettingsUi/ModSettingsUiControls.cs index 863bd83..108c2c8 100644 --- a/Settings/ModSettingsUi/ModSettingsUiControls.cs +++ b/Settings/ModSettingsUi/ModSettingsUiControls.cs @@ -3736,13 +3736,28 @@ internal enum ModSettingsSidebarItemKind Utility, } - internal sealed partial class ModSettingsSidebarButton : ModSettingsGamepadCompatibleButton + /// + /// Sidebar row: matches BaseLib NModListButton — child + one + /// , updated from press/focus hooks. Avoids Godot + /// multi-state theme styleboxes stacking on hold / repeat click. + /// + internal sealed partial class ModSettingsSidebarButton : NButton { + private readonly Panel _backgroundPanel; + private readonly StyleBoxFlat _styleBox; + private readonly Label _label; + private readonly LabelSettings _labelSettings; private readonly int _indentLevel; private readonly ModSettingsSidebarItemKind _kind; private readonly string? _prefix; private readonly string? _rawText; + private readonly Action? _action; + private readonly Callable _releasedCallable; + private bool _selected; + private bool _isButtonDown; + private bool _hovering; + private bool _releasedHooked; public ModSettingsSidebarButton(string text, Action? action, ModSettingsSidebarItemKind kind = ModSettingsSidebarItemKind.Page, @@ -3753,7 +3768,44 @@ public ModSettingsSidebarButton(string text, Action? action, _indentLevel = Math.Max(0, indentLevel); _kind = kind; _prefix = prefix; - Text = text; + _action = action; + _releasedCallable = Callable.From(OnReleased); + + _styleBox = new StyleBoxFlat(); + _backgroundPanel = new Panel + { + MouseFilter = MouseFilterEnum.Ignore, + }; + _backgroundPanel.SetAnchorsPreset(LayoutPreset.FullRect); + _backgroundPanel.AddThemeStyleboxOverride("panel", _styleBox); + AddChild(_backgroundPanel); + + _labelSettings = new LabelSettings + { + Font = kind == ModSettingsSidebarItemKind.ModGroup + ? ModSettingsUiResources.KreonBold + : ModSettingsUiResources.KreonRegular, + FontSize = kind switch + { + ModSettingsSidebarItemKind.ModGroup => 22, + ModSettingsSidebarItemKind.Page => 19, + ModSettingsSidebarItemKind.Section => 16, + _ => 17, + }, + }; + + _label = new Label + { + MouseFilter = MouseFilterEnum.Ignore, + TextOverrunBehavior = TextServer.OverrunBehavior.TrimEllipsis, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + LabelSettings = _labelSettings, + }; + _label.SetAnchorsPreset(LayoutPreset.FullRect); + ApplyLabelOffsets(); + AddChild(_label); + TooltipText = text; CustomMinimumSize = new(0f, kind switch { @@ -3765,67 +3817,198 @@ public ModSettingsSidebarButton(string text, Action? action, SizeFlagsHorizontal = SizeFlags.ExpandFill; FocusMode = FocusModeEnum.All; MouseFilter = MouseFilterEnum.Stop; - Flat = false; - TextOverrunBehavior = TextServer.OverrunBehavior.TrimEllipsis; - Alignment = HorizontalAlignment.Left; - IconAlignment = HorizontalAlignment.Left; - - AddThemeFontOverride("font", kind == ModSettingsSidebarItemKind.ModGroup - ? ModSettingsUiResources.KreonBold - : ModSettingsUiResources.KreonRegular); - AddThemeFontSizeOverride("font_size", kind switch - { - ModSettingsSidebarItemKind.ModGroup => 22, - ModSettingsSidebarItemKind.Page => 19, - ModSettingsSidebarItemKind.Section => 16, - _ => 17, - }); - AddThemeColorOverride("font_color", kind == ModSettingsSidebarItemKind.Section - ? ModSettingsUiPalette.SidebarSection - : ModSettingsUiPalette.LabelPrimary); - AddThemeColorOverride("font_hover_color", new(0.98f, 1f, 1f)); - AddThemeColorOverride("font_pressed_color", new(1f, 1f, 1f)); - AddThemeColorOverride("font_focus_color", new(1f, 1f, 1f)); + ClipContents = false; - AddThemeStyleboxOverride("normal", CreateStyle(false, false, _kind, _indentLevel)); - AddThemeStyleboxOverride("hover", CreateStyle(false, true, _kind, _indentLevel)); - AddThemeStyleboxOverride("pressed", CreateStyle(true, true, _kind, _indentLevel)); - AddThemeStyleboxOverride("focus", CreateStyle(false, true, _kind, _indentLevel)); - AddThemeStyleboxOverride("disabled", CreateDisabledStyle()); + MouseEntered += OnMouseEntered; + MouseExited += OnMouseExited; - Pressed += () => action?.Invoke(); + UpdateVisualState(); } public ModSettingsSidebarButton() { + _backgroundPanel = null!; + _styleBox = null!; + _label = null!; + _labelSettings = null!; + _indentLevel = 0; + _kind = ModSettingsSidebarItemKind.Page; + _prefix = null; + _rawText = null; + _action = null; + _releasedCallable = default; } public override void _Ready() { - Text = string.IsNullOrWhiteSpace(_prefix) ? _rawText ?? string.Empty : $"{_prefix} {_rawText}"; + if (_styleBox == null || _label == null) + return; + + ConnectSignals(); + ModSettingsFocusChrome.AttachControllerSelectionReticle(this); + Connect(NClickableControl.SignalName.Released, _releasedCallable); + _releasedHooked = true; + + _label.Text = string.IsNullOrWhiteSpace(_prefix) ? _rawText ?? string.Empty : $"{_prefix} {_rawText}"; + TooltipText = _label.Text; SetSelected(_selected); + base._Ready(); + } + + public override void _ExitTree() + { + if (_releasedHooked && IsConnected(NClickableControl.SignalName.Released, _releasedCallable)) + Disconnect(NClickableControl.SignalName.Released, _releasedCallable); + base._ExitTree(); + } + + private void OnReleased(NButton _) => _action?.Invoke(); + + private void OnMouseEntered() + { + _hovering = true; + UpdateVisualState(); + } + + private void OnMouseExited() + { + _hovering = false; + UpdateVisualState(); } public void SetSelected(bool selected) { _selected = selected; - AddThemeStyleboxOverride("normal", CreateStyle(_selected, false, _kind, _indentLevel)); - AddThemeStyleboxOverride("hover", CreateStyle(_selected, true, _kind, _indentLevel)); - AddThemeStyleboxOverride("pressed", CreateStyle(true, true, _kind, _indentLevel)); - AddThemeStyleboxOverride("focus", CreateStyle(_selected, true, _kind, _indentLevel)); + UpdateVisualState(); } - internal static StyleBoxFlat CreateStyle(bool selected, bool hovered, - ModSettingsSidebarItemKind kind = ModSettingsSidebarItemKind.Page, - int indentLevel = 0) + protected override void OnFocus() + { + base.OnFocus(); + UpdateVisualState(); + } + + protected override void OnUnfocus() { + base.OnUnfocus(); + UpdateVisualState(); + } + + protected override void OnPress() + { + base.OnPress(); + _isButtonDown = true; + UpdateVisualState(); + } + + protected override void OnRelease() + { + base.OnRelease(); + _isButtonDown = false; + UpdateVisualState(); + } + + private void ApplyLabelOffsets() + { + if (_label == null) + return; + var left = (_kind == ModSettingsSidebarItemKind.Section ? 14 : 18) + _indentLevel * 14; + if (_kind == ModSettingsSidebarItemKind.ModGroup) + left += (int)ModSettingsUiMetrics.SidebarModAccentBarWidth + ModSettingsUiMetrics.SidebarModAccentTextGutter; + var right = _kind == ModSettingsSidebarItemKind.Section ? 14 : 18; + var top = _kind == ModSettingsSidebarItemKind.Section ? 8 : 10; + var bottom = _kind == ModSettingsSidebarItemKind.Section ? 8 : 10; + _label.OffsetLeft = left; + _label.OffsetRight = -right; + _label.OffsetTop = top; + _label.OffsetBottom = -bottom; + } + + private void UpdateVisualState() + { + if (_styleBox == null) + return; + + var hoveredEffective = _hovering || IsFocused; + if (_isButtonDown) + hoveredEffective = true; + + ApplySidebarPanelStyle(_styleBox, _selected, hoveredEffective, _kind, _indentLevel, _isButtonDown); + + if (_kind == ModSettingsSidebarItemKind.ModGroup) + { + _labelSettings.FontColor = ModSettingsUiPalette.LabelPrimary; + ApplyLabelOffsets(); + } + else if (_kind == ModSettingsSidebarItemKind.Section) + { + _labelSettings.FontColor = hoveredEffective ? Colors.White : ModSettingsUiPalette.SidebarSection; + } + else + { + _labelSettings.FontColor = hoveredEffective ? new Color(0.98f, 1f, 1f) : ModSettingsUiPalette.LabelPrimary; + } + } + + private static void ApplySidebarPanelStyle(StyleBoxFlat box, bool selected, bool hovered, + ModSettingsSidebarItemKind kind, int indentLevel, bool pressed) + { + if (kind == ModSettingsSidebarItemKind.ModGroup) + { + var a = ModSettingsUiMetrics.SidebarModListSubtleAlpha; + var stripWhite = new Color(1f, 1f, 1f, a); + if (pressed && selected) + stripWhite = new Color(1f, 1f, 1f, a * 0.75f); + var dimLine = new Color(1f, 1f, 1f, a); + + Color modGroupBg; + Color bottomColor; + int bottomBorderW; + if (selected) + { + modGroupBg = stripWhite; + bottomColor = Colors.Transparent; + bottomBorderW = 0; + } + else + { + modGroupBg = Colors.Transparent; + bottomColor = dimLine; + bottomBorderW = ModSettingsUiMetrics.SidebarModListBottomBorderWidth; + } + + box.BgColor = modGroupBg; + if (selected) + { + box.BorderColor = ModSettingsUiPalette.SidebarModActiveAccent; + box.BorderWidthLeft = (int)ModSettingsUiMetrics.SidebarModAccentBarWidth; + box.BorderWidthTop = 0; + box.BorderWidthRight = 0; + box.BorderWidthBottom = 0; + } + else + { + box.BorderColor = bottomColor; + box.BorderWidthLeft = 0; + box.BorderWidthTop = 0; + box.BorderWidthRight = 0; + box.BorderWidthBottom = bottomBorderW; + } + box.CornerRadiusTopLeft = 0; + box.CornerRadiusTopRight = 0; + box.CornerRadiusBottomRight = 0; + box.CornerRadiusBottomLeft = 0; + box.ShadowSize = 0; + box.ContentMarginLeft = 18 + indentLevel * 14 + (int)ModSettingsUiMetrics.SidebarModAccentBarWidth + + ModSettingsUiMetrics.SidebarModAccentTextGutter; + box.ContentMarginTop = 10; + box.ContentMarginRight = 18; + box.ContentMarginBottom = 10; + return; + } + var bg = kind switch { - ModSettingsSidebarItemKind.ModGroup => selected - ? new(0.17f, 0.28f, 0.36f, 0.99f) - : hovered - ? new(0.14f, 0.23f, 0.30f, 0.98f) - : new Color(0.11f, 0.18f, 0.24f, 0.97f), ModSettingsSidebarItemKind.Section => selected ? new(0.12f, 0.22f, 0.29f, 0.98f) : hovered @@ -3845,9 +4028,6 @@ internal static StyleBoxFlat CreateStyle(bool selected, bool hovered, var border = kind switch { - ModSettingsSidebarItemKind.ModGroup => selected - ? new(0.72f, 0.88f, 0.95f, 0.90f) - : new Color(0.47f, 0.63f, 0.73f, 0.62f), ModSettingsSidebarItemKind.Section => selected ? new(0.56f, 0.80f, 0.90f, 0.84f) : new Color(0.27f, 0.42f, 0.52f, 0.45f), @@ -3858,29 +4038,24 @@ internal static StyleBoxFlat CreateStyle(bool selected, bool hovered, var leftBorder = selected ? kind == ModSettingsSidebarItemKind.Section ? 3 : 4 - : kind == ModSettingsSidebarItemKind.ModGroup - ? 2 - : 1; - - return new() - { - BgColor = bg, - BorderColor = border, - BorderWidthLeft = leftBorder, - BorderWidthTop = 1, - BorderWidthRight = 1, - BorderWidthBottom = 1, - CornerRadiusTopLeft = ModSettingsUiMetrics.CornerRadius, - CornerRadiusTopRight = ModSettingsUiMetrics.CornerRadius, - CornerRadiusBottomRight = ModSettingsUiMetrics.CornerRadius, - CornerRadiusBottomLeft = ModSettingsUiMetrics.CornerRadius, - ShadowColor = new(0f, 0f, 0f, 0.18f), - ShadowSize = kind == ModSettingsSidebarItemKind.ModGroup ? 4 : 2, - ContentMarginLeft = (kind == ModSettingsSidebarItemKind.Section ? 14 : 18) + indentLevel * 14, - ContentMarginTop = kind == ModSettingsSidebarItemKind.Section ? 8 : 10, - ContentMarginRight = kind == ModSettingsSidebarItemKind.Section ? 14 : 18, - ContentMarginBottom = kind == ModSettingsSidebarItemKind.Section ? 8 : 10, - }; + : 1; + + box.BgColor = bg; + box.BorderColor = border; + box.BorderWidthLeft = leftBorder; + box.BorderWidthTop = 1; + box.BorderWidthRight = 1; + box.BorderWidthBottom = 1; + box.CornerRadiusTopLeft = ModSettingsUiMetrics.CornerRadius; + box.CornerRadiusTopRight = ModSettingsUiMetrics.CornerRadius; + box.CornerRadiusBottomRight = ModSettingsUiMetrics.CornerRadius; + box.CornerRadiusBottomLeft = ModSettingsUiMetrics.CornerRadius; + box.ShadowColor = new(0f, 0f, 0f, 0.18f); + box.ShadowSize = 2; + box.ContentMarginLeft = (kind == ModSettingsSidebarItemKind.Section ? 14 : 18) + indentLevel * 14; + box.ContentMarginTop = kind == ModSettingsSidebarItemKind.Section ? 8 : 10; + box.ContentMarginRight = kind == ModSettingsSidebarItemKind.Section ? 14 : 18; + box.ContentMarginBottom = kind == ModSettingsSidebarItemKind.Section ? 8 : 10; } internal static StyleBoxFlat CreateDisabledStyle() diff --git a/Settings/ModSettingsUi/ModSettingsUiFactory.Chrome.cs b/Settings/ModSettingsUi/ModSettingsUiFactory.Chrome.cs index 227268b..c7ee3ee 100644 --- a/Settings/ModSettingsUi/ModSettingsUiFactory.Chrome.cs +++ b/Settings/ModSettingsUi/ModSettingsUiFactory.Chrome.cs @@ -24,6 +24,46 @@ public static ColorRect CreateDivider() }; } + /// + /// Rule above the sidebar mod scroll: same white/subtle language as ModGroup row bottoms, but thicker. + /// + public static ColorRect CreateSidebarScrollTopDivider() + { + var a = ModSettingsUiMetrics.SidebarModListSubtleAlpha; + // 2px line: slightly higher alpha than 1px row border so it still reads, without looking heavy. + var alpha = Mathf.Clamp(a * 2.15f, 0.052f, 0.10f); + return new ColorRect + { + CustomMinimumSize = new(0f, ModSettingsUiMetrics.SidebarScrollTopDividerHeight), + MouseFilter = Control.MouseFilterEnum.Ignore, + Color = new Color(1f, 1f, 1f, alpha), + }; + } + + /// + /// Flat rectangular tag behind the mod version (reference: stencil / inventory label). + /// + internal static StyleBoxFlat CreateSidebarModVersionBadgeStyle() + { + return new() + { + BgColor = new Color(0.14f, 0.15f, 0.17f, 0.78f), + BorderColor = Colors.Transparent, + BorderWidthLeft = 0, + BorderWidthTop = 0, + BorderWidthRight = 0, + BorderWidthBottom = 0, + CornerRadiusTopLeft = 0, + CornerRadiusTopRight = 0, + CornerRadiusBottomRight = 0, + CornerRadiusBottomLeft = 0, + ContentMarginLeft = 6, + ContentMarginTop = 3, + ContentMarginRight = 6, + ContentMarginBottom = 3, + }; + } + private static MarginContainer CreateSettingLine(ModSettingsUiContext context, Func labelProvider, Func descriptionBodyProvider, Control valueControl, IModSettingsValueBinding binding) @@ -561,17 +601,84 @@ private static MegaRichTextLabel CreateHeaderLabel(string text, int fontSize, Ho return label; } - private static MegaRichTextLabel CreatePageToolbarTitleLabel(string primaryTitle, string fallbackId) + /// + /// Large left-aligned page title (uppercase), vanilla-style header line. + /// + private static MegaRichTextLabel CreatePageMainTitleLabel(string primaryTitle, string fallbackId) { var text = !string.IsNullOrWhiteSpace(primaryTitle) ? primaryTitle : !string.IsNullOrWhiteSpace(fallbackId) ? fallbackId : ModSettingsLocalization.Get("page.untitled", "Untitled"); - var label = CreateHeaderLabel(text, 24, HorizontalAlignment.Center, null, - ModSettingsUiPalette.RichTextTitle); + var upper = text.ToUpperInvariant(); + var label = CreateHeaderLabel(upper, 28, HorizontalAlignment.Left, null, ModSettingsUiPalette.RichTextTitle); + label.AddThemeFontOverride("normal_font", ModSettingsUiResources.KreonBold); + label.AddThemeFontOverride("bold_font", ModSettingsUiResources.KreonBold); + label.AddThemeFontSizeOverride("normal_font_size", 28); + label.AddThemeFontSizeOverride("bold_font_size", 28); + label.AddThemeFontSizeOverride("italics_font_size", 28); + label.AddThemeFontSizeOverride("bold_italics_font_size", 28); + label.AddThemeFontSizeOverride("mono_font_size", 28); + label.MinFontSize = 18; + label.MaxFontSize = 28; + // FitContent + narrow column can collapse width; fill row and disable wrap for one-line title. + label.FitContent = false; + label.AutowrapMode = TextServer.AutowrapMode.Off; label.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; - label.CustomMinimumSize = new(0f, 30f); + label.SizeFlagsStretchRatio = 1f; + label.CustomMinimumSize = new(0f, 36f); + return label; + } + + private static string EscapeBbcodeUserText(string? s) + { + if (string.IsNullOrEmpty(s)) + return string.Empty; + return s.Replace("[", "[lb]"); + } + + /// + /// Content page header: mod name + current page (breadcrumb row). + /// + private static string BuildPageBreadcrumbBbcode(ModSettingsPage page) + { + var mod = EscapeBbcodeUserText(ModSettingsLocalization.ResolveModName(page.ModId, page.ModId)); + var pageName = EscapeBbcodeUserText(ModSettingsLocalization.ResolvePageDisplayName(page)); + const string sep = "[color=#4a5058] // [/color]"; + const string muted = "#7a8088"; + const string accentHex = "#ea9104"; + return $"[font_size=12][color={muted}]{mod}[/color]{sep}[color={accentHex}]{pageName}[/color][/font_size]"; + } + + private static MegaRichTextLabel CreateRefreshableBreadcrumbLabel(ModSettingsUiContext context, + ModSettingsPage page) + { + var label = new MegaRichTextLabel + { + BbcodeEnabled = true, + AutoSizeEnabled = false, + FitContent = true, + ScrollActive = false, + ClipContents = false, + FocusMode = Control.FocusModeEnum.None, + MouseFilter = Control.MouseFilterEnum.Ignore, + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Left, + Theme = ModSettingsUiResources.SettingsLineTheme, + IsHorizontallyBound = true, + }; + label.AddThemeFontOverride("normal_font", ModSettingsUiResources.KreonBold); + label.AddThemeFontOverride("bold_font", ModSettingsUiResources.KreonBold); + label.AddThemeFontSizeOverride("normal_font_size", 12); + label.AddThemeFontSizeOverride("bold_font_size", 12); + label.AddThemeFontSizeOverride("italics_font_size", 12); + label.AddThemeFontSizeOverride("bold_italics_font_size", 12); + label.AddThemeFontSizeOverride("mono_font_size", 12); + label.MinFontSize = 10; + label.MaxFontSize = 12; + label.SetTextAutoSize(BuildPageBreadcrumbBbcode(page)); + RegisterRefreshWhenAlive(context, label, () => label.SetTextAutoSize(BuildPageBreadcrumbBbcode(page))); return label; } @@ -814,6 +921,30 @@ internal static StyleBoxFlat CreateInsetSurfaceStyle() }; } + /// + /// Sidebar mod cover: square clip, no border (image or placeholder draws edge-to-edge). + /// + internal static StyleBoxFlat CreateModSidebarPreviewFrameStyle() + { + return new() + { + BgColor = Colors.Transparent, + BorderColor = Colors.Transparent, + BorderWidthLeft = 0, + BorderWidthTop = 0, + BorderWidthRight = 0, + BorderWidthBottom = 0, + CornerRadiusTopLeft = ModSettingsUiMetrics.CornerRadius, + CornerRadiusTopRight = ModSettingsUiMetrics.CornerRadius, + CornerRadiusBottomRight = ModSettingsUiMetrics.CornerRadius, + CornerRadiusBottomLeft = ModSettingsUiMetrics.CornerRadius, + ContentMarginLeft = 0, + ContentMarginTop = 0, + ContentMarginRight = 0, + ContentMarginBottom = 0, + }; + } + internal static StyleBoxFlat CreateChromeActionsMenuStyle(bool highlighted) { return new() @@ -839,24 +970,28 @@ internal static StyleBoxFlat CreateChromeActionsMenuStyle(bool highlighted) }; } - internal static StyleBoxFlat CreatePageToolbarTrayStyle() + /// + /// Vanilla-style page header: no panel fill; single bottom hairline separator. + /// + internal static StyleBoxFlat CreatePageHeaderTrayStyle() { return new() { - BgColor = new(0.055f, 0.068f, 0.09f, 0.88f), - BorderColor = new(0.28f, 0.42f, 0.54f, 0.32f), - BorderWidthLeft = 1, - BorderWidthTop = 1, - BorderWidthRight = 1, + BgColor = Colors.Transparent, + BorderColor = new Color(0.42f, 0.46f, 0.52f, 0.55f), + BorderWidthLeft = 0, + BorderWidthTop = 0, + BorderWidthRight = 0, BorderWidthBottom = 1, - CornerRadiusTopLeft = ModSettingsUiMetrics.CornerRadius, - CornerRadiusTopRight = ModSettingsUiMetrics.CornerRadius, - CornerRadiusBottomRight = ModSettingsUiMetrics.CornerRadius, - CornerRadiusBottomLeft = ModSettingsUiMetrics.CornerRadius, - ContentMarginLeft = 10, + CornerRadiusTopLeft = 0, + CornerRadiusTopRight = 0, + CornerRadiusBottomRight = 0, + CornerRadiusBottomLeft = 0, + ShadowSize = 0, + ContentMarginLeft = 4, ContentMarginTop = 8, - ContentMarginRight = 10, - ContentMarginBottom = 8, + ContentMarginRight = 4, + ContentMarginBottom = 12, }; } @@ -890,23 +1025,23 @@ private static Control CreatePageHeaderBar(ModSettingsUiContext context, ModSett MouseFilter = Control.MouseFilterEnum.Ignore, ClipContents = false, }; - tray.AddThemeStyleboxOverride("panel", CreatePageToolbarTrayStyle()); + tray.AddThemeStyleboxOverride("panel", CreatePageHeaderTrayStyle()); - var row = new HBoxContainer + var column = new VBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, MouseFilter = Control.MouseFilterEnum.Ignore, - Alignment = BoxContainer.AlignmentMode.Center, }; - row.AddThemeConstantOverride("separation", 10); + column.AddThemeConstantOverride("separation", 6); - var left = new HBoxContainer + var crumbRow = new HBoxContainer { - CustomMinimumSize = new(sideSlotMin, 44f), - SizeFlagsHorizontal = Control.SizeFlags.ShrinkBegin, + SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, MouseFilter = Control.MouseFilterEnum.Ignore, - Alignment = BoxContainer.AlignmentMode.Begin, + Alignment = BoxContainer.AlignmentMode.Center, }; + crumbRow.AddThemeConstantOverride("separation", 10); + if (showBack) { var back = new ModSettingsMiniButton(ModSettingsLocalization.Get("button.back", "Back"), onBack) @@ -914,32 +1049,23 @@ private static Control CreatePageHeaderBar(ModSettingsUiContext context, ModSett SizeFlagsVertical = Control.SizeFlags.ShrinkCenter, CustomMinimumSize = new(88f, 38f), }; - left.AddChild(back); + crumbRow.AddChild(back); } - var center = new VBoxContainer + var breadcrumb = CreateRefreshableBreadcrumbLabel(context, page); + breadcrumb.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; + crumbRow.AddChild(breadcrumb); + + var titleRow = new HBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, MouseFilter = Control.MouseFilterEnum.Ignore, Alignment = BoxContainer.AlignmentMode.Center, }; - center.AddThemeConstantOverride("separation", 5); - - var titleLabel = CreatePageToolbarTitleLabel(pageTitle, page.Id); - center.AddChild(titleLabel); + titleRow.AddThemeConstantOverride("separation", 10); - var pageDescription = CreateRefreshableDescriptionLabel(context, - () => ModSettingsUiContext.ResolvePageDescription(page) ?? string.Empty); - pageDescription.HorizontalAlignment = HorizontalAlignment.Center; - pageDescription.AddThemeFontSizeOverride("normal_font_size", 18); - pageDescription.AddThemeFontSizeOverride("bold_font_size", 18); - pageDescription.AddThemeFontSizeOverride("italics_font_size", 18); - pageDescription.AddThemeFontSizeOverride("bold_italics_font_size", 18); - pageDescription.AddThemeFontSizeOverride("mono_font_size", 18); - pageDescription.MinFontSize = 16; - pageDescription.MaxFontSize = 18; - pageDescription.Modulate = ModSettingsUiPalette.RichTextSecondary; - center.AddChild(pageDescription); + var titleLabel = CreatePageMainTitleLabel(pageTitle, page.Id); + titleLabel.SizeFlagsVertical = Control.SizeFlags.ShrinkCenter; var right = new HBoxContainer { @@ -951,10 +1077,25 @@ private static Control CreatePageHeaderBar(ModSettingsUiContext context, ModSett if (trailingMenu != null) right.AddChild(trailingMenu); - row.AddChild(left); - row.AddChild(center); - row.AddChild(right); - tray.AddChild(row); + titleRow.AddChild(titleLabel); + titleRow.AddChild(right); + + var pageDescription = CreateRefreshableDescriptionLabel(context, + () => ModSettingsUiContext.ResolvePageDescription(page) ?? string.Empty); + pageDescription.HorizontalAlignment = HorizontalAlignment.Left; + pageDescription.AddThemeFontSizeOverride("normal_font_size", 16); + pageDescription.AddThemeFontSizeOverride("bold_font_size", 16); + pageDescription.AddThemeFontSizeOverride("italics_font_size", 16); + pageDescription.AddThemeFontSizeOverride("bold_italics_font_size", 16); + pageDescription.AddThemeFontSizeOverride("mono_font_size", 16); + pageDescription.MinFontSize = 14; + pageDescription.MaxFontSize = 16; + pageDescription.Modulate = ModSettingsUiPalette.RichTextMuted; + + column.AddChild(crumbRow); + column.AddChild(titleRow); + column.AddChild(pageDescription); + tray.AddChild(column); return tray; } diff --git a/Settings/ModSettingsUi/ModSettingsUiMetrics.cs b/Settings/ModSettingsUi/ModSettingsUiMetrics.cs index 6f298fc..8724267 100644 --- a/Settings/ModSettingsUi/ModSettingsUiMetrics.cs +++ b/Settings/ModSettingsUi/ModSettingsUiMetrics.cs @@ -89,5 +89,57 @@ public static class ModSettingsUiMetrics /// Square size used by compact stepper buttons. /// public const int MiniStepperButtonSize = 40; + + /// + /// Minimum width of the left sidebar column (base 324px + one third). + /// + public const float SidebarPanelMinWidth = 432f; + + /// + /// Horizontal inset (px) for sidebar scroll content and the top mod info card so edges line up. + /// + public const int SidebarContentMarginH = 16; + + /// + /// Left accent bar width (px) on the selected mod row in the sidebar mod list. + /// + public const float SidebarModAccentBarWidth = 6f; + + /// + /// Extra gap (px) between the accent bar and the mod title text when selected. + /// + public const int SidebarModAccentTextGutter = 6; + + /// + /// Shared white alpha for mod list rows: selected strip fill and unselected bottom divider. + /// + public const float SidebarModListSubtleAlpha = 0.04f; + + /// + /// Bottom border thickness (px) for unselected mod list rows (ModGroup). + /// + public const int SidebarModListBottomBorderWidth = 1; + + /// + /// Horizontal rule above the sidebar mod list (aligned with ModGroup row weight; not heavier than 2px). + /// + public const float SidebarScrollTopDividerHeight = 2f; + + /// + /// Margin (px) above and below the card-to-list divider row (rule is its own mainVBox sibling + /// between the info card and the scroll; total gutter = 2×this + ). + /// + public const int SidebarListDividerPadSymmetric = 8; + + /// + /// Font size for the mod title row version pill (caps tag). + /// + public const int SidebarModVersionBadgeFontSize = 11; + + /// + /// Sidebar mod cover / placeholder preview: square (1:1) edge length in px. + /// + public const float ModSidebarPreviewOuterSize = 96f; + } } diff --git a/Settings/ModSettingsUi/ModSettingsUiPalette.cs b/Settings/ModSettingsUi/ModSettingsUiPalette.cs index d78bef1..5078c76 100644 --- a/Settings/ModSettingsUi/ModSettingsUiPalette.cs +++ b/Settings/ModSettingsUi/ModSettingsUiPalette.cs @@ -46,5 +46,10 @@ public static class ModSettingsUiPalette /// Accent color used by section labels in the settings sidebar. /// public static readonly Color SidebarSection = new(0.88f, 0.855f, 0.795f); + + /// + /// Left accent bar on the selected mod row in the sidebar mod list (#EA9104). + /// + public static readonly Color SidebarModActiveAccent = Color.FromHtml("#EA9104"); } } diff --git a/Settings/ModSettingsUi/RitsuModSettingsSubmenu.cs b/Settings/ModSettingsUi/RitsuModSettingsSubmenu.cs index 5c3ca3c..560b3c4 100644 --- a/Settings/ModSettingsUi/RitsuModSettingsSubmenu.cs +++ b/Settings/ModSettingsUi/RitsuModSettingsSubmenu.cs @@ -14,11 +14,10 @@ namespace STS2RitsuLib.Settings { /// - /// Full-screen mod settings browser: sidebar (mods, pages, sections) and content pane. + /// Full-screen mod settings browser: sidebar (mod list + header) and content pane. /// public partial class RitsuModSettingsSubmenu : NSubmenu { - private const float SidebarWidth = 324f; private const double AutosaveDelaySeconds = 0.35; private const int ScrollContentRightGutter = 12; @@ -51,7 +50,6 @@ private readonly Dictionary new(StringComparer.OrdinalIgnoreCase); private readonly List<(Control Control, Func Predicate)> _sidebarDynamicVisibilityTargets = []; - private readonly List _sidebarFocusChain = []; private Control _contentBuildOverlay = null!; @@ -63,6 +61,7 @@ private readonly Dictionary private Control _contentPanelRoot = null!; private bool _contentStructureDirty = true; private bool _focusNavigationRefreshScheduled; + private bool _focusSelectedModButtonOnNextRefresh; private bool _focusSelectedPageButtonOnNextRefresh; private bool _guiFocusSignalConnected; private Action? _hotkeyPaneContent; @@ -88,10 +87,19 @@ private readonly Dictionary private bool _selectionDirty = true; private Control _sidebarPanelRoot = null!; private ScrollContainer _sidebarScrollContainer = null!; + private Control _sidebarModHeaderRoot = null!; + private Panel _sidebarModPreviewFrame = null!; + private Control _sidebarModPreviewPlaceholder = null!; + private MegaRichTextLabel _sidebarModPreviewCaption = null!; + private TextureRect _sidebarModIcon = null!; + private MegaRichTextLabel _sidebarModTitleLabel = null!; + private PanelContainer _sidebarModVersionBadgePanel = null!; + private MegaRichTextLabel _sidebarModVersionLabel = null!; + private MegaRichTextLabel _sidebarModMetaLabel = null!; + private MegaRichTextLabel _sidebarModDescLabel = null!; private bool _sidebarStructureDirty = true; - private MegaRichTextLabel _subtitleLabel; + private MegaRichTextLabel? _subtitleLabel; private bool _suppressScrollSync; - private MegaRichTextLabel _titleLabel; private Callable _updatePaneHotkeyIconsCallable; /// @@ -132,25 +140,6 @@ public RitsuModSettingsSubmenu() root.AddThemeConstantOverride("separation", 18); frame.AddChild(root); - var header = new VBoxContainer - { - SizeFlagsHorizontal = SizeFlags.ExpandFill, - MouseFilter = MouseFilterEnum.Ignore, - }; - header.AddThemeConstantOverride("separation", 6); - root.AddChild(header); - - _titleLabel = CreateTitleLabel(32, HorizontalAlignment.Left); - _titleLabel.CustomMinimumSize = new(0f, 42f); - _titleLabel.SizeFlagsHorizontal = SizeFlags.ExpandFill; - header.AddChild(_titleLabel); - - _subtitleLabel = CreateTitleLabel(16, HorizontalAlignment.Left); - _subtitleLabel.CustomMinimumSize = new(0f, 24f); - _subtitleLabel.Modulate = new(0.82f, 0.79f, 0.72f, 0.92f); - _subtitleLabel.SizeFlagsHorizontal = SizeFlags.ExpandFill; - header.AddChild(_subtitleLabel); - root.AddChild(CreatePaneHotkeyHintRow()); var body = new HBoxContainer @@ -460,6 +449,7 @@ public void SelectMod(string modId, string? pageId = null) _selectedSectionId = null; ExpandOnlyMod(modId); _selectionDirty = true; + _focusSelectedModButtonOnNextRefresh = true; _focusSelectedPageButtonOnNextRefresh = true; EnsureUiUpToDate(); } @@ -491,11 +481,6 @@ public void NavigateToSection(string pageId, string sectionId) { Callable.From(ScrollToSelectedAnchor).CallDeferred(); RefreshFocusNavigation(); - Callable.From(() => - { - if (_sectionButtons.TryGetValue(sectionId, out var btn) && btn.IsVisibleInTree()) - btn.GrabFocus(); - }).CallDeferred(); return; } @@ -743,8 +728,9 @@ private void FocusSidebarPaneFromInput() { var selectedPageKey = GetSelectedPageKey(); var selectedSectionKey = GetSelectedSectionKey(); - if (_focusSelectedPageButtonOnNextRefresh) + if (_focusSelectedModButtonOnNextRefresh || _focusSelectedPageButtonOnNextRefresh) { + _focusSelectedModButtonOnNextRefresh = false; _focusSelectedPageButtonOnNextRefresh = false; if (!string.IsNullOrWhiteSpace(selectedPageKey) && _pageButtons.TryGetValue(selectedPageKey, out var pageButton) @@ -780,56 +766,152 @@ private Control CreateSidebarPanel() var panel = new Panel { Name = "RitsuSidebarPanel", - CustomMinimumSize = new(SidebarWidth, 0f), + CustomMinimumSize = new(ModSettingsUiMetrics.SidebarPanelMinWidth, 0f), SizeFlagsVertical = SizeFlags.ExpandFill, MouseFilter = MouseFilterEnum.Ignore, }; _sidebarPanelRoot = panel; - panel.AddThemeStyleboxOverride("panel", CreatePanelStyle(new(0.10f, 0.115f, 0.145f, 0.96f))); + panel.AddThemeStyleboxOverride("panel", CreateTransparentPanelStyle()); - var frame = new MarginContainer + var mainVBox = new VBoxContainer { AnchorRight = 1f, AnchorBottom = 1f, MouseFilter = MouseFilterEnum.Ignore, + SizeFlagsHorizontal = SizeFlags.ExpandFill, + SizeFlagsVertical = SizeFlags.ExpandFill, }; - frame.AddThemeConstantOverride("margin_left", 16); - frame.AddThemeConstantOverride("margin_top", 16); - frame.AddThemeConstantOverride("margin_right", 16); - frame.AddThemeConstantOverride("margin_bottom", 16); - panel.AddChild(frame); + // Divider sits on its own row between the info card and the scroll list (not inside the expanding list). + mainVBox.AddThemeConstantOverride("separation", 0); + panel.AddChild(mainVBox); - var root = new VBoxContainer + var modHeaderOuter = new MarginContainer { - AnchorRight = 1f, - AnchorBottom = 1f, MouseFilter = MouseFilterEnum.Ignore, + SizeFlagsHorizontal = SizeFlags.ExpandFill, }; - root.AddThemeConstantOverride("separation", 14); - frame.AddChild(root); + modHeaderOuter.AddThemeConstantOverride("margin_left", ModSettingsUiMetrics.SidebarContentMarginH); + modHeaderOuter.AddThemeConstantOverride("margin_right", ModSettingsUiMetrics.SidebarContentMarginH); + modHeaderOuter.AddThemeConstantOverride("margin_top", 0); + modHeaderOuter.AddThemeConstantOverride("margin_bottom", 0); + + _sidebarModHeaderRoot = new PanelContainer + { + MouseFilter = MouseFilterEnum.Ignore, + SizeFlagsHorizontal = SizeFlags.ExpandFill, + }; + _sidebarModHeaderRoot.AddThemeStyleboxOverride("panel", ModSettingsUiFactory.CreateModSidebarPreviewFrameStyle()); - var headerCard = new PanelContainer + var headerRow = new HBoxContainer + { + MouseFilter = MouseFilterEnum.Ignore, + SizeFlagsVertical = SizeFlags.ShrinkBegin, + Alignment = BoxContainer.AlignmentMode.Begin, + }; + headerRow.AddThemeConstantOverride("separation", 12); + _sidebarModHeaderRoot.AddChild(headerRow); + + headerRow.AddChild(CreateSidebarModPreviewFrame()); + + var textCol = new VBoxContainer { MouseFilter = MouseFilterEnum.Ignore, SizeFlagsHorizontal = SizeFlags.ExpandFill, + SizeFlagsVertical = SizeFlags.ShrinkBegin, }; - headerCard.AddThemeStyleboxOverride("panel", ModSettingsUiFactory.CreateInsetSurfaceStyle()); - root.AddChild(headerCard); + textCol.AddThemeConstantOverride("separation", 6); + headerRow.AddChild(textCol); - var headerBox = new VBoxContainer + var titleRow = new HBoxContainer { MouseFilter = MouseFilterEnum.Ignore, + SizeFlagsHorizontal = SizeFlags.ExpandFill, }; - headerBox.AddThemeConstantOverride("separation", 4); - headerCard.AddChild(headerBox); + titleRow.AddThemeConstantOverride("separation", 10); + + _sidebarModTitleLabel = CreateSidebarWrapLabel(22, HorizontalAlignment.Left); + _sidebarModTitleLabel.SizeFlagsHorizontal = SizeFlags.ExpandFill; + _sidebarModTitleLabel.SizeFlagsVertical = SizeFlags.ShrinkCenter; + titleRow.AddChild(_sidebarModTitleLabel); - var headerTitle = - ModSettingsUiFactory.CreateSectionTitle(ModSettingsLocalization.Get("sidebar.title", "Mods")); - headerTitle.CustomMinimumSize = new(0f, 30f); - headerBox.AddChild(headerTitle); + _sidebarModVersionBadgePanel = new PanelContainer + { + MouseFilter = MouseFilterEnum.Ignore, + Visible = false, + SizeFlagsHorizontal = SizeFlags.ShrinkBegin, + SizeFlagsVertical = SizeFlags.ShrinkCenter, + }; + _sidebarModVersionBadgePanel.AddThemeStyleboxOverride("panel", + ModSettingsUiFactory.CreateSidebarModVersionBadgeStyle()); + + _sidebarModVersionLabel = new MegaRichTextLabel + { + Theme = ModSettingsUiResources.SettingsLineTheme, + BbcodeEnabled = false, + AutoSizeEnabled = false, + ScrollActive = false, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + MouseFilter = MouseFilterEnum.Ignore, + FocusMode = FocusModeEnum.None, + FitContent = true, + Modulate = new Color(0.88f, 0.86f, 0.82f), + }; + _sidebarModVersionLabel.AddThemeFontOverride("normal_font", ModSettingsUiResources.KreonBold); + _sidebarModVersionLabel.AddThemeFontOverride("bold_font", ModSettingsUiResources.KreonBold); + var vs = ModSettingsUiMetrics.SidebarModVersionBadgeFontSize; + _sidebarModVersionLabel.AddThemeFontSizeOverride("normal_font_size", vs); + _sidebarModVersionLabel.AddThemeFontSizeOverride("bold_font_size", vs); + _sidebarModVersionLabel.AddThemeFontSizeOverride("italics_font_size", vs); + _sidebarModVersionLabel.AddThemeFontSizeOverride("bold_italics_font_size", vs); + _sidebarModVersionLabel.AddThemeFontSizeOverride("mono_font_size", vs); + _sidebarModVersionLabel.MinFontSize = vs; + _sidebarModVersionLabel.MaxFontSize = vs; + _sidebarModVersionBadgePanel.AddChild(_sidebarModVersionLabel); + titleRow.AddChild(_sidebarModVersionBadgePanel); + + textCol.AddChild(titleRow); + + _sidebarModMetaLabel = CreateSidebarWrapLabel(14, HorizontalAlignment.Left); + _sidebarModMetaLabel.Modulate = new(0.75f, 0.72f, 0.65f, 0.95f); + _sidebarModMetaLabel.SizeFlagsHorizontal = SizeFlags.ExpandFill; + textCol.AddChild(_sidebarModMetaLabel); + + _sidebarModDescLabel = CreateSidebarWrapLabel(13, HorizontalAlignment.Left); + _sidebarModDescLabel.Modulate = new(0.65f, 0.62f, 0.58f, 0.9f); + _sidebarModDescLabel.SizeFlagsHorizontal = SizeFlags.ExpandFill; + _sidebarModDescLabel.Visible = false; + textCol.AddChild(_sidebarModDescLabel); + + modHeaderOuter.AddChild(_sidebarModHeaderRoot); + mainVBox.AddChild(modHeaderOuter); + + var dividerPad = ModSettingsUiMetrics.SidebarListDividerPadSymmetric; + var cardToListDivider = new MarginContainer + { + MouseFilter = MouseFilterEnum.Ignore, + SizeFlagsHorizontal = SizeFlags.ExpandFill, + SizeFlagsVertical = SizeFlags.ShrinkBegin, + }; + cardToListDivider.AddThemeConstantOverride("margin_left", ModSettingsUiMetrics.SidebarContentMarginH); + cardToListDivider.AddThemeConstantOverride("margin_right", ModSettingsUiMetrics.SidebarContentMarginH); + cardToListDivider.AddThemeConstantOverride("margin_top", dividerPad); + cardToListDivider.AddThemeConstantOverride("margin_bottom", dividerPad); + var dividerLine = ModSettingsUiFactory.CreateSidebarScrollTopDivider(); + dividerLine.SizeFlagsHorizontal = SizeFlags.ExpandFill; + cardToListDivider.AddChild(dividerLine); + mainVBox.AddChild(cardToListDivider); - headerBox.AddChild(ModSettingsUiFactory.CreateInlineDescription( - ModSettingsLocalization.Get("sidebar.subtitle", "Browse mods, pages, and sections."))); + var listFrame = new MarginContainer + { + MouseFilter = MouseFilterEnum.Ignore, + SizeFlagsHorizontal = SizeFlags.ExpandFill, + SizeFlagsVertical = SizeFlags.ExpandFill, + }; + listFrame.AddThemeConstantOverride("margin_left", ModSettingsUiMetrics.SidebarContentMarginH); + listFrame.AddThemeConstantOverride("margin_top", 0); + listFrame.AddThemeConstantOverride("margin_right", ModSettingsUiMetrics.SidebarContentMarginH); + listFrame.AddThemeConstantOverride("margin_bottom", 16); var scroll = new ScrollContainer { @@ -840,27 +922,125 @@ private Control CreateSidebarPanel() FocusMode = FocusModeEnum.None, }; _sidebarScrollContainer = scroll; - root.AddChild(scroll); - var sidebarScrollFrame = new MarginContainer + var scrollInner = new VBoxContainer { - SizeFlagsHorizontal = SizeFlags.ExpandFill, - SizeFlagsVertical = SizeFlags.ExpandFill, + Name = "SidebarScrollInner", MouseFilter = MouseFilterEnum.Ignore, + SizeFlagsHorizontal = SizeFlags.ExpandFill, }; - sidebarScrollFrame.AddThemeConstantOverride("margin_right", ScrollContentRightGutter); - scroll.AddChild(sidebarScrollFrame); + scrollInner.AddThemeConstantOverride("separation", 10); - _modButtonList = new() + _modButtonList = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill, MouseFilter = MouseFilterEnum.Ignore, }; - _modButtonList.AddThemeConstantOverride("separation", 12); - sidebarScrollFrame.AddChild(_modButtonList); + _modButtonList.AddThemeConstantOverride("separation", 8); + scrollInner.AddChild(_modButtonList); + + scroll.AddChild(scrollInner); + listFrame.AddChild(scroll); + mainVBox.AddChild(listFrame); return panel; } + private Panel CreateSidebarModPreviewFrame() + { + var outer = ModSettingsUiMetrics.ModSidebarPreviewOuterSize; + + _sidebarModPreviewFrame = new Panel + { + Name = "ModPreviewFrame", + MouseFilter = MouseFilterEnum.Ignore, + CustomMinimumSize = new(outer, outer), + ClipContents = true, + SizeFlagsHorizontal = SizeFlags.ShrinkBegin, + SizeFlagsVertical = SizeFlags.ShrinkBegin, + }; + _sidebarModPreviewFrame.AddThemeStyleboxOverride("panel", + ModSettingsUiFactory.CreateModSidebarPreviewFrameStyle()); + + var inner = new Control + { + Name = "ModPreviewInner", + MouseFilter = MouseFilterEnum.Ignore, + ClipContents = true, + }; + inner.SetAnchorsAndOffsetsPreset(Control.LayoutPreset.FullRect); + _sidebarModPreviewFrame.AddChild(inner); + + _sidebarModIcon = new TextureRect + { + Name = "ModIcon", + MouseFilter = MouseFilterEnum.Ignore, + ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize, + StretchMode = TextureRect.StretchModeEnum.KeepAspectCentered, + }; + _sidebarModIcon.SetAnchorsAndOffsetsPreset(Control.LayoutPreset.FullRect); + inner.AddChild(_sidebarModIcon); + + _sidebarModPreviewPlaceholder = new Control + { + Name = "ModPreviewPlaceholder", + MouseFilter = MouseFilterEnum.Ignore, + Visible = true, + }; + _sidebarModPreviewPlaceholder.SetAnchorsAndOffsetsPreset(Control.LayoutPreset.FullRect); + inner.AddChild(_sidebarModPreviewPlaceholder); + + var bg = new ColorRect + { + MouseFilter = MouseFilterEnum.Ignore, + Color = Colors.Black, + }; + bg.SetAnchorsAndOffsetsPreset(Control.LayoutPreset.FullRect); + _sidebarModPreviewPlaceholder.AddChild(bg); + + var captionMargin = new MarginContainer + { + MouseFilter = MouseFilterEnum.Ignore, + }; + captionMargin.SetAnchorsAndOffsetsPreset(Control.LayoutPreset.FullRect); + captionMargin.AddThemeConstantOverride("margin_left", 4); + captionMargin.AddThemeConstantOverride("margin_right", 4); + captionMargin.AddThemeConstantOverride("margin_top", 4); + captionMargin.AddThemeConstantOverride("margin_bottom", 4); + _sidebarModPreviewPlaceholder.AddChild(captionMargin); + + _sidebarModPreviewCaption = CreateSidebarWrapLabel(13, HorizontalAlignment.Center, VerticalAlignment.Center); + _sidebarModPreviewCaption.Modulate = Colors.White; + _sidebarModPreviewCaption.SizeFlagsHorizontal = SizeFlags.ExpandFill; + _sidebarModPreviewCaption.SizeFlagsVertical = SizeFlags.ExpandFill; + captionMargin.AddChild(_sidebarModPreviewCaption); + + return _sidebarModPreviewFrame; + } + + private void ApplySidebarModPreviewState(Texture2D? tex, bool noModSelected) + { + if (!IsInstanceValid(_sidebarModPreviewPlaceholder) || !IsInstanceValid(_sidebarModIcon)) + return; + + var hasArt = tex != null && !noModSelected; + if (hasArt) + { + _sidebarModIcon.Texture = tex; + _sidebarModIcon.Visible = true; + _sidebarModPreviewPlaceholder.Visible = false; + _sidebarModIcon.Modulate = Colors.White; + return; + } + + _sidebarModIcon.Texture = null; + _sidebarModIcon.Visible = false; + _sidebarModPreviewPlaceholder.Visible = true; + var caption = noModSelected + ? ModSettingsLocalization.Get("sidebar.modPreview.empty", "No preview") + : ModSettingsLocalization.Get("sidebar.modPreview.noImage", "No resources"); + _sidebarModPreviewCaption.SetTextAutoSize(caption); + } + private Control CreateContentPanel() { var panel = new Panel @@ -871,7 +1051,7 @@ private Control CreateContentPanel() MouseFilter = MouseFilterEnum.Ignore, }; _contentPanelRoot = panel; - panel.AddThemeStyleboxOverride("panel", CreatePanelStyle(new(0.08f, 0.095f, 0.125f, 0.98f))); + panel.AddThemeStyleboxOverride("panel", CreateTransparentPanelStyle()); var frame = new MarginContainer { @@ -962,6 +1142,7 @@ private void EnsureUiUpToDate(bool forceStructure = false, bool includeAllPagesR EnsureSelectedPageContentStructure(); RefreshSelectionState(); + RefreshSelectedModHeader(); RefreshVisibleContent(includeAllPagesRefresh); } @@ -999,6 +1180,7 @@ private void EnsureSelectionIsValid() _selectedModId = null; _selectedPageId = null; _selectedSectionId = null; + RefreshSelectedModHeader(); return; } @@ -1085,6 +1267,64 @@ private void RebuildSidebar() _selectionDirty = true; } + private void RefreshSelectedModHeader() + { + if (!IsInstanceValid(_sidebarModTitleLabel)) + return; + + if (string.IsNullOrWhiteSpace(_selectedModId)) + { + _sidebarModTitleLabel.SetTextAutoSize( + ModSettingsLocalization.Get("sidebar.modHeader.none", "No mod")); + if (IsInstanceValid(_sidebarModVersionBadgePanel)) + _sidebarModVersionBadgePanel.Visible = false; + _sidebarModMetaLabel.SetTextAutoSize(""); + _sidebarModDescLabel.SetTextAutoSize(""); + _sidebarModDescLabel.Visible = false; + ApplySidebarModPreviewState(null, true); + return; + } + + var mod = ModSettingsModInfoResolver.TryFindMod(_selectedModId); + _sidebarModTitleLabel.SetTextAutoSize(ModSettingsModInfoResolver.ResolveTitle(mod, _selectedModId)); + + var ver = ModSettingsModInfoResolver.ResolveVersion(mod); + if (IsInstanceValid(_sidebarModVersionBadgePanel) && IsInstanceValid(_sidebarModVersionLabel)) + { + if (string.IsNullOrWhiteSpace(ver)) + { + _sidebarModVersionBadgePanel.Visible = false; + } + else + { + _sidebarModVersionBadgePanel.Visible = true; + _sidebarModVersionLabel.SetTextAutoSize(FormatSidebarVersionBadgeText(ver)); + } + } + + var author = ModSettingsModInfoResolver.ResolveAuthor(mod); + var metaParts = new List(); + if (!string.IsNullOrWhiteSpace(author)) + metaParts.Add(author); + metaParts.Add(_selectedModId); + _sidebarModMetaLabel.SetTextAutoSize(string.Join(" · ", metaParts)); + + var desc = ModSettingsModInfoResolver.ResolveDescription(mod); + if (string.IsNullOrWhiteSpace(desc)) + { + _sidebarModDescLabel.Visible = false; + _sidebarModDescLabel.SetTextAutoSize(""); + } + else + { + _sidebarModDescLabel.Visible = true; + _sidebarModDescLabel.SetTextAutoSize(desc); + } + + var tex = ModSettingsModInfoResolver.TryLoadModIcon(mod, _selectedModId); + ApplySidebarModPreviewState(tex, false); + } + private void EnsureSelectedPageContentStructure() { if (_contentStructureDirty) @@ -1205,7 +1445,6 @@ private void RefreshVisibleContent(bool includeAllPagesRefresh) _pageTabRow.Visible = false; HideContentBuildOverlay(); HideTransientContentState(); - if (string.IsNullOrWhiteSpace(_selectedModId)) { ShowTransientContentState(ModSettingsLocalization.Get("empty.none", @@ -1841,8 +2080,6 @@ private void OnContentScrollChanged(double value) return; _selectedSectionId = bestSectionId; - foreach (var pair in _sectionButtons) - pair.Value.SetSelected(string.Equals(pair.Key, _selectedSectionId, StringComparison.OrdinalIgnoreCase)); } private void RefreshFocusNavigation() @@ -1971,13 +2208,6 @@ or ModSettingsActionsButton or NButton }; } - private void ApplyStaticTexts() - { - _titleLabel.SetTextAutoSize(ModSettingsLocalization.Get("entry.title", "Mod Settings (RitsuLib)")); - _subtitleLabel.SetTextAutoSize(ModSettingsLocalization.Get("entry.subtitle", - "Edit player-facing mod options here.")); - } - private void ExpandOnlyMod(string? modId) { _expandedModIds.Clear(); @@ -2050,6 +2280,49 @@ private void OnLocaleChanged() Callable.From(() => EnsureUiUpToDate(true, true)).CallDeferred(); } + private static string FormatSidebarVersionBadgeText(string raw) + { + var t = raw.Trim(); + if (t.Length == 0) + return string.Empty; + if (t.StartsWith('v') || t.StartsWith('V')) + t = t[1..].TrimStart(); + return $"V{t}".ToUpperInvariant(); + } + + /// + /// Sidebar mod title / meta / description: bounded width, word wrap, height grows with content. + /// + private static MegaRichTextLabel CreateSidebarWrapLabel(int fontSize, HorizontalAlignment alignment, + VerticalAlignment verticalAlignment = VerticalAlignment.Top) + { + var label = new MegaRichTextLabel + { + Theme = ModSettingsUiResources.SettingsLineTheme, + BbcodeEnabled = true, + AutoSizeEnabled = false, + ScrollActive = false, + HorizontalAlignment = alignment, + VerticalAlignment = verticalAlignment, + MouseFilter = MouseFilterEnum.Ignore, + FocusMode = FocusModeEnum.None, + IsHorizontallyBound = true, + FitContent = true, + AutowrapMode = TextServer.AutowrapMode.WordSmart, + }; + + label.AddThemeFontOverride("normal_font", ModSettingsUiResources.KreonRegular); + label.AddThemeFontOverride("bold_font", ModSettingsUiResources.KreonBold); + label.AddThemeFontSizeOverride("normal_font_size", fontSize); + label.AddThemeFontSizeOverride("bold_font_size", fontSize); + label.AddThemeFontSizeOverride("italics_font_size", fontSize); + label.AddThemeFontSizeOverride("bold_italics_font_size", fontSize); + label.AddThemeFontSizeOverride("mono_font_size", fontSize); + label.MinFontSize = Math.Min(fontSize, 16); + label.MaxFontSize = fontSize; + return label; + } + private static MegaRichTextLabel CreateTitleLabel(int fontSize, HorizontalAlignment alignment) { var label = new MegaRichTextLabel @@ -2085,22 +2358,17 @@ private static MegaRichTextLabel CreateEmptyStateLabel(string text) return label; } - private static StyleBoxFlat CreatePanelStyle(Color bg) + private static StyleBoxFlat CreateTransparentPanelStyle() { return new() { - BgColor = bg, - BorderColor = new(0.44f, 0.68f, 0.80f, 0.36f), - BorderWidthLeft = 1, - BorderWidthTop = 1, - BorderWidthRight = 1, - BorderWidthBottom = 1, - CornerRadiusTopLeft = ModSettingsUiMetrics.CornerRadius, - CornerRadiusTopRight = ModSettingsUiMetrics.CornerRadius, - CornerRadiusBottomRight = ModSettingsUiMetrics.CornerRadius, - CornerRadiusBottomLeft = ModSettingsUiMetrics.CornerRadius, - ShadowColor = new(0f, 0f, 0f, 0.32f), - ShadowSize = 12, + BgColor = Colors.Transparent, + BorderColor = Colors.Transparent, + BorderWidthLeft = 0, + BorderWidthTop = 0, + BorderWidthRight = 0, + BorderWidthBottom = 0, + ShadowSize = 0, ContentMarginLeft = 0, ContentMarginTop = 0, ContentMarginRight = 0,