2026-05-03 22:04:24 -07:00
|
|
|
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
|
2026-05-09 22:55:40 -07:00
|
|
|
/// 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.
|
2026-05-03 22:04:24 -07:00
|
|
|
/// </summary>
|
|
|
|
|
public static PanelContainer Make()
|
|
|
|
|
{
|
|
|
|
|
var card = new PanelContainer
|
|
|
|
|
{
|
|
|
|
|
ThemeTypeVariation = "Card",
|
|
|
|
|
MouseFilter = Control.MouseFilterEnum.Stop,
|
|
|
|
|
};
|
|
|
|
|
card.MouseEntered += () => SetHover(card, true);
|
2026-05-09 22:55:40 -07:00
|
|
|
card.MouseExited += () =>
|
|
|
|
|
Callable.From(() => RecheckHover(card)).CallDeferred();
|
2026-05-03 22:04:24 -07:00
|
|
|
return card;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 22:55:40 -07:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 22:04:24 -07:00
|
|
|
public static void SetSelected(PanelContainer card, bool selected)
|
|
|
|
|
{
|
|
|
|
|
card.SetMeta(SelectedMeta, selected);
|
2026-05-09 22:55:40 -07:00
|
|
|
ApplyOrDefer(card);
|
2026-05-03 22:04:24 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void SetHover(PanelContainer card, bool hover)
|
|
|
|
|
{
|
|
|
|
|
card.SetMeta(HoverMeta, hover);
|
2026-05-09 22:55:40 -07:00
|
|
|
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();
|
2026-05-03 22:04:24 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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"));
|
|
|
|
|
}
|
|
|
|
|
}
|