2026-05-02 20:57:02 -07:00
|
|
|
using Godot;
|
|
|
|
|
|
|
|
|
|
namespace Theriapolis.GodotHost.Scenes.Widgets;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Shared overlay layer that owns one reusable trait popover panel.
|
|
|
|
|
/// Per GODOT_PORTING_GUIDE.md §6.1 — TraitChip triggers ask
|
|
|
|
|
/// <see cref="Instance"/> to show the popover at their global rect; the
|
2026-05-03 22:04:24 -07:00
|
|
|
/// popover hides as soon as the trigger fires MouseExited.
|
|
|
|
|
///
|
|
|
|
|
/// The popover itself is MouseFilter=Ignore so it never intercepts
|
|
|
|
|
/// input — clicks pass through to the chip's parent (card selection),
|
|
|
|
|
/// scroll wheel events go to the underlying ScrollContainer, and the
|
|
|
|
|
/// chip's hover state stays accurate when the cursor moves onto the
|
|
|
|
|
/// popover area (the cursor is registered as "outside the chip", so
|
|
|
|
|
/// MouseExited fires and we hide). This lets adjacent chips fire
|
|
|
|
|
/// reliably even when the previous popover overlaps them spatially.
|
2026-05-02 20:57:02 -07:00
|
|
|
///
|
|
|
|
|
/// One PopoverLayer per scene; lives as a CanvasLayer child of
|
|
|
|
|
/// Wizard.tscn so popovers float above every step's content. Mirrors
|
|
|
|
|
/// <c>src/trait-hint.jsx</c>'s viewport-clamp / flip-above behaviour.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public partial class PopoverLayer : CanvasLayer
|
|
|
|
|
{
|
|
|
|
|
public static PopoverLayer? Instance { get; private set; }
|
|
|
|
|
|
|
|
|
|
private const float ArrowOffsetPx = 6f;
|
|
|
|
|
private const int ViewportPadPx = 8;
|
|
|
|
|
|
|
|
|
|
private PanelContainer _popup = null!;
|
|
|
|
|
private Label _titleLabel = null!;
|
|
|
|
|
private Label _tagLabel = null!;
|
|
|
|
|
private Label _descLabel = null!;
|
|
|
|
|
|
|
|
|
|
public override void _EnterTree()
|
|
|
|
|
{
|
|
|
|
|
Instance = this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void _ExitTree()
|
|
|
|
|
{
|
|
|
|
|
if (Instance == this) Instance = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void _Ready()
|
|
|
|
|
{
|
|
|
|
|
Layer = 100;
|
|
|
|
|
BuildPopover();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void BuildPopover()
|
|
|
|
|
{
|
2026-05-03 22:04:24 -07:00
|
|
|
// Ignore so clicks/scroll/hover all pass through to whatever's
|
|
|
|
|
// beneath. The popover is purely a visual readout; the chip
|
|
|
|
|
// owns the lifecycle entirely.
|
2026-05-02 20:57:02 -07:00
|
|
|
_popup = new PanelContainer
|
|
|
|
|
{
|
|
|
|
|
Visible = false,
|
2026-05-03 22:04:24 -07:00
|
|
|
MouseFilter = Control.MouseFilterEnum.Ignore,
|
2026-05-02 20:57:02 -07:00
|
|
|
ZIndex = 100,
|
|
|
|
|
};
|
|
|
|
|
AddChild(_popup);
|
|
|
|
|
|
|
|
|
|
var v = new VBoxContainer { CustomMinimumSize = new Vector2(240, 0) };
|
|
|
|
|
v.AddThemeConstantOverride("separation", 6);
|
|
|
|
|
_popup.AddChild(v);
|
|
|
|
|
|
|
|
|
|
var nameRow = new HBoxContainer();
|
|
|
|
|
nameRow.AddThemeConstantOverride("separation", 8);
|
|
|
|
|
v.AddChild(nameRow);
|
|
|
|
|
|
|
|
|
|
_titleLabel = new Label();
|
|
|
|
|
nameRow.AddChild(_titleLabel);
|
|
|
|
|
|
|
|
|
|
_tagLabel = new Label { Visible = false };
|
|
|
|
|
nameRow.AddChild(_tagLabel);
|
|
|
|
|
|
|
|
|
|
_descLabel = new Label
|
|
|
|
|
{
|
|
|
|
|
AutowrapMode = TextServer.AutowrapMode.WordSmart,
|
|
|
|
|
CustomMinimumSize = new Vector2(220, 0),
|
|
|
|
|
};
|
|
|
|
|
v.AddChild(_descLabel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void ShowFor(Control trigger, string title, string description, string tag, bool detriment)
|
|
|
|
|
{
|
|
|
|
|
_titleLabel.Text = title;
|
|
|
|
|
_descLabel.Text = description;
|
|
|
|
|
_tagLabel.Visible = !string.IsNullOrEmpty(tag) || detriment;
|
|
|
|
|
_tagLabel.Text = !string.IsNullOrEmpty(tag) ? tag.ToUpperInvariant()
|
|
|
|
|
: (detriment ? "DETRIMENT" : "");
|
|
|
|
|
|
|
|
|
|
// M6.3 default-theme tint: detriment popover gets a red modulate so
|
|
|
|
|
// it reads visually distinct from a regular trait. The proper
|
|
|
|
|
// codex StyleBox swap lands in the theming pass.
|
|
|
|
|
_popup.Modulate = detriment ? new Color(1f, 0.78f, 0.78f) : Colors.White;
|
|
|
|
|
|
|
|
|
|
_popup.Visible = true;
|
|
|
|
|
_popup.ResetSize();
|
|
|
|
|
Reposition(trigger);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 22:04:24 -07:00
|
|
|
/// <summary>Hide the popover. Was previously a 80ms-grace timer when
|
|
|
|
|
/// the popover stayed alive across chip→popover hover transitions, but
|
|
|
|
|
/// the popover is now non-interactive so there's no transition to
|
|
|
|
|
/// cover for — close immediately.</summary>
|
|
|
|
|
public void ScheduleClose() => _popup.Visible = false;
|
2026-05-02 20:57:02 -07:00
|
|
|
|
|
|
|
|
private void Reposition(Control trigger)
|
|
|
|
|
{
|
|
|
|
|
var trigRect = trigger.GetGlobalRect();
|
|
|
|
|
var popSize = _popup.GetCombinedMinimumSize();
|
|
|
|
|
var vp = trigger.GetViewportRect().Size;
|
|
|
|
|
|
|
|
|
|
// Default: under trigger, left-aligned with trigger.
|
|
|
|
|
float left = trigRect.Position.X;
|
|
|
|
|
float top = trigRect.End.Y + ArrowOffsetPx;
|
|
|
|
|
|
|
|
|
|
// Flip above if no room below and there's room above.
|
|
|
|
|
if (top + popSize.Y + ViewportPadPx > vp.Y &&
|
|
|
|
|
trigRect.Position.Y - ArrowOffsetPx - popSize.Y >= ViewportPadPx)
|
|
|
|
|
{
|
|
|
|
|
top = trigRect.Position.Y - ArrowOffsetPx - popSize.Y;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clamp horizontally + vertically to keep popover in-viewport.
|
|
|
|
|
left = Mathf.Clamp(left, ViewportPadPx, vp.X - popSize.X - ViewportPadPx);
|
|
|
|
|
top = Mathf.Clamp(top, ViewportPadPx, vp.Y - popSize.Y - ViewportPadPx);
|
|
|
|
|
_popup.Position = new Vector2(left, top);
|
|
|
|
|
}
|
|
|
|
|
}
|