using Godot; namespace Theriapolis.GodotHost.Scenes.Widgets; /// /// 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. /// public static class CodexCard { private const string SelectedMeta = "codex_card_selected"; private const string HoverMeta = "codex_card_hover"; /// /// 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. /// 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); } /// /// 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) { 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")); } }