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,