Files
Christopher Wiebe 83c6343783 M6.21: Dark theme parity + card shadow polish + hover/selection fixes
--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 <noreply@anthropic.com>
2026-05-09 22:55:40 -07:00

103 lines
3.9 KiB
C#

using Godot;
namespace Theriapolis.GodotHost.Scenes.Widgets;
/// <summary>
/// Card-style PanelContainer helpers. The codex Theme defines three
/// styleboxes for type-variation "Card":
/// - "panel" → unselected look (Bg2 fill, Rule border)
/// - "panel_hover" → gild border, slightly heavier weight
/// - "panel_selected" → seal-red border + soft red shadow
///
/// State is held in Godot meta on the card so hover and selected can be
/// driven independently by different call sites (Make wires the hover
/// signals; SetSelected is called by step Refresh handlers). The active
/// stylebox is picked by Apply: selected beats hover beats default.
/// </summary>
public static class CodexCard
{
private const string SelectedMeta = "codex_card_selected";
private const string HoverMeta = "codex_card_hover";
/// <summary>
/// Creates a PanelContainer with ThemeTypeVariation = "Card" plus
/// 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.
/// </summary>
public static PanelContainer Make()
{
var card = new PanelContainer
{
ThemeTypeVariation = "Card",
MouseFilter = Control.MouseFilterEnum.Stop,
};
card.MouseEntered += () => SetHover(card, true);
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);
ApplyOrDefer(card);
}
private static void SetHover(PanelContainer card, bool hover)
{
card.SetMeta(HoverMeta, hover);
ApplyOrDefer(card);
}
/// <summary>
/// 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.
/// </summary>
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)
{
bool selected = card.HasMeta(SelectedMeta) && (bool)card.GetMeta(SelectedMeta);
bool hover = card.HasMeta(HoverMeta) && (bool)card.GetMeta(HoverMeta);
// Priority: selected > hover > default. The default branch removes
// the override so the type variation's "panel" stylebox applies.
StringName picked = selected ? "panel_selected"
: hover ? "panel_hover"
: "panel";
if (picked == "panel" || !card.HasThemeStylebox(picked, "Card"))
card.RemoveThemeStyleboxOverride("panel");
else
card.AddThemeStyleboxOverride("panel", card.GetThemeStylebox(picked, "Card"));
}
}