using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace Luneraria.UI
{
///
/// The one tooltip of the HUD (port of components/lune_tooltip.gd). A single panel is reused
/// for every hover: it appears after a short delay next to the hovered element, stays inside
/// the window, never takes mouse input and hides as soon as the element is left, hidden,
/// detached or clicked. Content is a LuneTip (title, subtitle, stat rows, text, note, warn).
/// Elements register with LuneTooltip.Attach(element, builder); the HUD calls Tick each frame.
///
public sealed class LuneTooltip : LunePanel
{
public const float Delay = .28f;
public const float Quick = .05f;
public const float Width = 300f;
public static LuneTooltip Current;
VisualElement _owner;
Func _builder;
float _wait;
float _lastHide = -10f;
float _clock;
bool _placed;
Vector2 _placedSize;
readonly VisualElement _column;
/// Last pointer position in panel coordinates (real and synthetic events).
public Vector2 Pointer = new Vector2(-1f, -1f);
/// Text of the visible tooltip (tests).
public string Shown { get; private set; } = "";
public LuneTooltip() : base(LunePanels.Tooltip, false)
{
style.position = Position.Absolute;
style.maxWidth = Width;
pickingMode = PickingMode.Ignore;
_column = LuneUi.Box(true, 4f, this);
LuneUi.Show(this, false);
RegisterCallback(_ => Place());
Current = this;
}
public bool Open { get { return style.display != DisplayStyle.None; } }
public VisualElement Owner { get { return _owner; } }
public static void Attach(VisualElement element, Func builder)
{
if (element.pickingMode == PickingMode.Ignore)
element.pickingMode = PickingMode.Position;
element.RegisterCallback(evt =>
{
if (Current != null)
Current.Hover(element, builder, evt.position);
});
element.RegisterCallback(_ => Current?.Leave(element));
element.RegisterCallback(_ => Current?.Leave(element));
}
public void Hover(VisualElement element, Func builder, Vector2 at)
{
if (Pointer.x < 0f)
Pointer = at;
_owner = element;
_builder = builder;
_wait = _clock - _lastHide < .6f ? Quick : Delay;
Hide(false);
}
public void Leave(VisualElement element)
{
if (element == _owner)
{
Hide(true);
_owner = null;
}
}
void Hide(bool remember)
{
if (Open && remember)
_lastHide = _clock;
LuneUi.Show(this, false);
Shown = "";
}
/// Refreshes an open tooltip (values changed while hovering).
public void Refresh(VisualElement element)
{
if (element == _owner && Open)
ShowNow();
}
public void Tick(float delta)
{
_clock += delta;
if (_owner == null)
return;
if (_owner.panel == null || !Showing(_owner) || !_owner.worldBound.Contains(Pointer))
{
Hide(true);
_owner = null;
return;
}
if (!Open)
{
_wait -= delta;
if (_wait <= 0f)
ShowNow();
}
}
/// Visible in the hierarchy (Godot is_visible_in_tree).
static bool Showing(VisualElement element)
{
for (VisualElement node = element; node != null; node = node.parent)
{
if (node.style.display == DisplayStyle.None || node.resolvedStyle.display == DisplayStyle.None)
return false;
}
return true;
}
void ShowNow()
{
LuneTip tip = _builder != null ? _builder() : null;
if (tip == null || tip.IsEmpty)
{
Hide(false);
return;
}
_column.Clear();
if (tip.Title.Length > 0)
{
VisualElement head = LuneUi.Box(false, 6f, _column);
if (tip.Glyph.Length > 0)
LuneUi.Glyph(tip.Glyph, tip.GlyphColor ?? LuneUiTheme.BronzeLight, 18f, head);
LuneUi.Wrap(LuneUi.Label(tip.Title, "HudValue", head));
}
if (tip.Subtitle.Length > 0)
LuneUi.Wrap(LuneUi.Label(tip.Subtitle, "HudMuted", _column));
if (tip.Stats.Count > 0)
{
VisualElement grid = LuneUi.Box(true, 2f, _column);
foreach (string[] row in tip.Stats)
{
VisualElement line = LuneUi.Box(false, 8f, grid);
LuneUi.Glyph(row[0], LuneUiTheme.Muted, 14f, line);
Label name = LuneUi.Label(row[1], "HudMuted", line);
name.style.minWidth = 104f;
LuneUi.Wrap(LuneUi.Label(row[2], "HudText", line)).style.flexGrow = 1f;
}
}
AddLine(tip.Text, "HudText", null);
AddLine(tip.Note, "HudMuted", null);
AddLine(tip.Warn, "HudMuted", LuneUiTheme.Danger);
Shown = tip.ToString();
// Measure at the top left with the full width available, then place at that size.
_placed = false;
style.left = 0f;
style.top = 0f;
style.width = StyleKeyword.Auto;
LuneUi.Show(this, true);
BringToFront();
}
void AddLine(string text, string variation, Color? color)
{
if (string.IsNullOrEmpty(text))
return;
Label line = LuneUi.Wrap(LuneUi.Label(text, variation, _column));
line.style.width = Width - 24f;
if (color.HasValue)
LuneUi.SetColor(line, color.Value);
}
///
/// Above the owner, centred; below when there is no room; always inside the HUD. Placed once
/// per content: measured at the top left with the full width, then the measured width is
/// fixed and the tooltip moves to whole pixels. Later geometry changes are ignored; a
/// re-placement could change the pixel snapping, the wrapping and the size again (a layout
/// loop found in the U3 tests).
///
void Place()
{
if (_placed || !Open || _owner == null || parent == null)
return;
float width = layout.width;
float height = layout.height;
if (float.IsNaN(width) || width <= 0f || float.IsNaN(height))
return;
Rect area = parent.contentRect;
Rect anchor = parent.WorldToLocal(_owner.worldBound);
float x = anchor.center.x - width * .5f;
float y = anchor.y - height - 8f;
if (y < area.y + 6f)
y = anchor.yMax + 8f;
x = Mathf.Clamp(x, area.x + 6f, Mathf.Max(area.x + 6f, area.xMax - width - 6f));
y = Mathf.Clamp(y, area.y + 6f, Mathf.Max(area.y + 6f, area.yMax - height - 6f));
_placed = true;
_placedSize = new Vector2(width, height);
style.width = width;
style.left = Mathf.Round(x);
style.top = Mathf.Round(y);
}
}
}