using Godot; namespace Theriapolis.GodotHost.Scenes.Widgets; /// /// Shared overlay layer that owns one reusable trait popover panel. /// Per GODOT_PORTING_GUIDE.md §6.1 — TraitChip triggers ask /// to show the popover at their global rect; the /// 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. /// /// One PopoverLayer per scene; lives as a CanvasLayer child of /// Wizard.tscn so popovers float above every step's content. Mirrors /// src/trait-hint.jsx's viewport-clamp / flip-above behaviour. /// 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(); // Theme inheritance walks Control descendants only — CanvasLayer is // a plain Node, so it breaks the propagation chain from the Wizard // Control above. _Ready is bottom-up, so the parent Wizard hasn't // assigned its codex theme yet — defer the lookup until parent's // _Ready has run, then pull its theme onto the popup directly. CallDeferred(MethodName.InheritParentTheme); } private void InheritParentTheme() { if (GetParent() is Control parentControl && parentControl.Theme is not null) _popup.Theme = parentControl.Theme; } private void BuildPopover() { // 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. ThemeTypeVariation pulls the // CodexPopover stylebox (parchment bg2 + gild border + rounded // corners + soft shadow) defined in CodexTheme. _popup = new PanelContainer { Visible = false, MouseFilter = Control.MouseFilterEnum.Ignore, ZIndex = 100, ThemeTypeVariation = "CodexPopover", }; 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", 10); v.AddChild(nameRow); // Display-serif title at H3 size (20px) — pulls the trait name // out of the body copy below. _titleLabel = new Label { ThemeTypeVariation = "H3" }; nameRow.AddChild(_titleLabel); // Mono uppercase tag label, vertically centred against the title. _tagLabel = new Label { Visible = false, ThemeTypeVariation = "Eyebrow", VerticalAlignment = VerticalAlignment.Center, SizeFlagsVertical = Control.SizeFlags.ShrinkCenter, }; nameRow.AddChild(_tagLabel); _descLabel = new Label { AutowrapMode = TextServer.AutowrapMode.WordSmart, CustomMinimumSize = new Vector2(220, 0), ThemeTypeVariation = "CardBody", }; 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" : ""); // Detriment popover swaps to the seal-bordered stylebox via // override; non-detriment clears the override so the default // CodexPopover panel takes effect again. if (detriment && _popup.HasThemeStylebox("panel_detriment", "CodexPopover")) { var box = _popup.GetThemeStylebox("panel_detriment", "CodexPopover"); _popup.AddThemeStyleboxOverride("panel", box); } else { _popup.RemoveThemeStyleboxOverride("panel"); } _popup.Visible = true; _popup.ResetSize(); Reposition(trigger); } /// 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. public void ScheduleClose() => _popup.Visible = false; 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); } }