Files
TheriapolisV3/Theriapolis.Godot/Scenes/Widgets/PopoverLayer.cs
T

131 lines
4.3 KiB
C#
Raw Normal View History

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
/// popover stays open while either the trigger or the popover itself is
/// hovered (80 ms grace via close timer).
///
/// 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 GracePeriodSec = 0.08f;
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!;
private Timer _closeTimer = 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()
{
_popup = new PanelContainer
{
Visible = false,
MouseFilter = Control.MouseFilterEnum.Pass,
ZIndex = 100,
};
_popup.MouseEntered += CancelClose;
_popup.MouseExited += ScheduleClose;
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);
_closeTimer = new Timer { OneShot = true, WaitTime = GracePeriodSec };
_closeTimer.Timeout += HidePopover;
AddChild(_closeTimer);
}
public void ShowFor(Control trigger, string title, string description, string tag, bool detriment)
{
CancelClose();
_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);
}
public void ScheduleClose() => _closeTimer.Start();
public void CancelClose() => _closeTimer.Stop();
private void HidePopover() => _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);
}
}