From 83c6343783bb3f78bfb5ac833f2bb37d6458da0a Mon Sep 17 00:00:00 2001 From: Christopher Wiebe Date: Sat, 9 May 2026 22:55:40 -0700 Subject: [PATCH] M6.21: Dark theme parity + card shadow polish + hover/selection fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --dark command-line flag swaps CodexTheme.DefaultPalette to Dark before any UI mounts; both TitleScreen and Wizard pick it up via the no-arg Build() overload. Stepper colours track the active palette. ApplyStateColors and the Active step's gild underline previously read from a stub that hardcoded parchment values, so the Active label rendered as brown-black ink against the dark bg (invisible). Both sites now read CodexTheme.DefaultPalette directly. Card hover stays applied while the cursor is over an inner Button. PanelContainer.MouseExited fires when the cursor crosses onto a child that captures input (Sire/Dam toggles, Sheep/Goat toggles, trait pickers); the recheck defers and uses GetGlobalRect.HasPoint on the cursor position so hover only drops when the cursor truly leaves the card area. Selection stylebox lands on first refresh. SetSelected was previously called inside BuildCard before AddChild, so HasThemeStylebox returned false (theme cascade unreachable) and the override silently dropped — it only re-attached when MouseEntered later re-ran Apply. Refactored SetSelected/SetHover through a new ApplyOrDefer helper that uses CallDeferred when the card isn't in tree yet, so the seal border + drop shadow appear immediately on selection rather than only after the first hover. Selection drop shadow refined. Was a 14px shadow at offset (0,14) which overlapped the next card by 16px in the v_separation:12 grid. Now offset (4,4) + size 6 — diagonal "light from upper-left" direction, total reach 10px, leaves a 2px clearance before the next card so the shadow reads as a shadow on the surface below. Co-Authored-By: Claude Opus 4.7 --- Theriapolis.Godot/Main.cs | 13 ++++++ Theriapolis.Godot/Scenes/Widgets/CodexCard.cs | 45 ++++++++++++++++--- Theriapolis.Godot/UI/CodexTheme.cs | 22 +++++++-- Theriapolis.Godot/UI/Widgets/CodexStepper.cs | 22 ++++----- 4 files changed, 80 insertions(+), 22 deletions(-) diff --git a/Theriapolis.Godot/Main.cs b/Theriapolis.Godot/Main.cs index ff9f30f..f83c8b8 100644 --- a/Theriapolis.Godot/Main.cs +++ b/Theriapolis.Godot/Main.cs @@ -29,6 +29,19 @@ public partial class Main : Control bool runCodexTest = false; bool runWizard = false; (ulong seed, int tx, int ty)? tacticalArgs = null; + + // --dark is independent of the entry-point flags: it sets the codex + // palette default before any UI mounts so both TitleScreen and the + // wizard pick it up via CodexTheme.Build()'s no-arg overload. + for (int i = 0; i < args.Length; i++) + { + if (args[i] == "--dark") + { + CodexTheme.DefaultPalette = CodexPalette.Dark; + break; + } + } + for (int i = 0; i < args.Length; i++) { if (args[i] == "--codex-test") diff --git a/Theriapolis.Godot/Scenes/Widgets/CodexCard.cs b/Theriapolis.Godot/Scenes/Widgets/CodexCard.cs index af615a7..47c2979 100644 --- a/Theriapolis.Godot/Scenes/Widgets/CodexCard.cs +++ b/Theriapolis.Godot/Scenes/Widgets/CodexCard.cs @@ -21,8 +21,11 @@ public static class CodexCard /// /// Creates a PanelContainer with ThemeTypeVariation = "Card" plus - /// hover signal wiring. The MouseEntered/MouseExited handlers update - /// the hover meta and re-apply the right stylebox. + /// hover signal wiring. MouseEntered marks hover; MouseExited defers + /// a recheck against the card's global rect so moving the cursor + /// from the card body onto an inner Button (which captures the + /// parent's MouseExited via mouse-filter Stop) does not clear the + /// hover state — the cursor is still visually within the card. /// public static PanelContainer Make() { @@ -32,20 +35,52 @@ public static class CodexCard MouseFilter = Control.MouseFilterEnum.Stop, }; card.MouseEntered += () => SetHover(card, true); - card.MouseExited += () => SetHover(card, false); + card.MouseExited += () => + Callable.From(() => RecheckHover(card)).CallDeferred(); return card; } + private static void RecheckHover(PanelContainer card) + { + if (!GodotObject.IsInstanceValid(card)) return; + // Hover stays true as long as the cursor is anywhere within the + // card's rect (including over any child control). Drop only when + // the cursor has truly left the card area. + bool stillOver = card.GetGlobalRect().HasPoint(card.GetGlobalMousePosition()); + SetHover(card, stillOver); + } + public static void SetSelected(PanelContainer card, bool selected) { card.SetMeta(SelectedMeta, selected); - Apply(card); + ApplyOrDefer(card); } private static void SetHover(PanelContainer card, bool hover) { card.SetMeta(HoverMeta, hover); - Apply(card); + ApplyOrDefer(card); + } + + /// + /// Apply now if the card is already in the scene tree, otherwise defer + /// until end-of-frame so the parent theme cascade is reachable. Step + /// builders call SetSelected on a freshly-created card before + /// AddChild — the theme isn't visible at that point and HasThemeStylebox + /// returns false, which previously meant the override silently dropped + /// and only re-attached when MouseEntered later re-ran Apply. + /// + private static void ApplyOrDefer(PanelContainer card) + { + if (card.IsInsideTree()) + { + Apply(card); + return; + } + Callable.From(() => + { + if (GodotObject.IsInstanceValid(card)) Apply(card); + }).CallDeferred(); } private static void Apply(PanelContainer card) diff --git a/Theriapolis.Godot/UI/CodexTheme.cs b/Theriapolis.Godot/UI/CodexTheme.cs index d7b4b61..a66b08e 100644 --- a/Theriapolis.Godot/UI/CodexTheme.cs +++ b/Theriapolis.Godot/UI/CodexTheme.cs @@ -29,7 +29,15 @@ public static class CodexTheme private static FontFile? _mono; private static bool _fontsLoaded; - public static Theme Build() => Build(CodexPalette.Parchment); + /// + /// Palette used by the no-arg . Set this before any + /// UI mounts to swap the active codex palette globally — e.g. Main reads + /// the --dark command-line flag and assigns + /// here. Defaults to . + /// + public static CodexPalette DefaultPalette { get; set; } = CodexPalette.Parchment; + + public static Theme Build() => Build(DefaultPalette); public static Theme Build(CodexPalette palette) { @@ -101,9 +109,15 @@ public static class CodexTheme var cardSelected = (StyleBoxFlat)card.Duplicate(); cardSelected.BorderColor = p.Seal; cardSelected.SetBorderWidthAll(3); - cardSelected.ShadowColor = WithAlpha(p.Seal, 0.6f); - cardSelected.ShadowSize = 14; - cardSelected.ShadowOffset = new Vector2(0, 14); + // Drop shadow: directional (light from upper-left) and sized so the + // shadow's bottom edge stays clear of the next card. Card grids + // separate cards by 12px (v_separation in StepClade / StepSpecies / + // StepClass / etc.) — offset.y + size ≤ 11 keeps a 1px-minimum gap + // before the next card so the shadow reads as a shadow on the + // surface below, not as a smudge between cards. + cardSelected.ShadowColor = WithAlpha(p.Seal, 0.55f); + cardSelected.ShadowSize = 6; + cardSelected.ShadowOffset = new Vector2(4, 4); theme.SetStylebox("panel_selected", "Card", cardSelected); // Popover frame — gild border + soft shadow + rounded corners. diff --git a/Theriapolis.Godot/UI/Widgets/CodexStepper.cs b/Theriapolis.Godot/UI/Widgets/CodexStepper.cs index 35e67a6..3c00dc3 100644 --- a/Theriapolis.Godot/UI/Widgets/CodexStepper.cs +++ b/Theriapolis.Godot/UI/Widgets/CodexStepper.cs @@ -104,7 +104,7 @@ public partial class CodexStepper : HBoxContainer { var underline = new ColorRect { - Color = TryGetThemeColor("font_color", "Gild") ?? new Color("#b48a3c"), + Color = CodexTheme.DefaultPalette.Gild, MouseFilter = MouseFilterEnum.Ignore, }; underline.AnchorTop = 1.0f; @@ -124,16 +124,20 @@ public partial class CodexStepper : HBoxContainer // Default theme colours come from the StepperNum/StepperName variations // (ink-mute). State overrides bring active steps to ink and complete // to seal-red. Locked uses the dim default plus reduced opacity. + // Pull the colours from CodexTheme.DefaultPalette so the stepper + // tracks the active palette (parchment vs dark) instead of forcing + // the parchment values regardless. + var palette = CodexTheme.DefaultPalette; Color? numColor = state switch { - StepState.Active => TryGetGlobalThemeColor("Ink", new Color("#2b1d10")), - StepState.Complete => TryGetGlobalThemeColor("Seal", new Color("#7a1f12")), + StepState.Active => palette.Ink, + StepState.Complete => palette.Seal, _ => null, }; Color? nameColor = state switch { - StepState.Active => TryGetGlobalThemeColor("Ink", new Color("#2b1d10")), - _ => null, + StepState.Active => palette.Ink, + _ => null, }; if (numColor.HasValue) num.AddThemeColorOverride("font_color", numColor.Value); @@ -149,14 +153,6 @@ public partial class CodexStepper : HBoxContainer } } - private static Color? TryGetGlobalThemeColor(string name, Color fallback) => fallback; - - private Color? TryGetThemeColor(string property, string variation) - { - if (HasThemeColor(property, variation)) return GetThemeColor(property, variation); - return null; - } - private static string Roman(int n) => n switch { 1 => "I",