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; 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(); // Like Godot: title, subtitle and stat rows never wrap (a long row widens the tooltip); // only text, note and warning wrap at the tooltip width. 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.Label(tip.Title, "HudValue", head); } if (tip.Subtitle.Length > 0) LuneUi.Label(tip.Subtitle, "HudMuted", _column); if (tip.Stats.Count > 0) { // Godot GridContainer with three columns (glyph, name, value; 8 apart, rows 2 apart). // The rows are single lines, so three columns of equally high cells line up like // the grid and the widest name sets the second column. VisualElement grid = LuneUi.Box(false, 8f, _column); grid.style.alignItems = Align.FlexStart; VisualElement glyphs = LuneUi.Box(true, 2f, grid); VisualElement names = LuneUi.Box(true, 2f, grid); VisualElement values = LuneUi.Box(true, 2f, grid); foreach (string[] row in tip.Stats) { LuneUi.Glyph(row[0], LuneUiTheme.Muted, 14f, Cell(glyphs)); LuneUi.Label(row[1], "HudMuted", Cell(names)); LuneUi.Label(row[2], "HudText", Cell(values)); } } 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(); } /// One grid cell: a line high, content centred vertically. static VisualElement Cell(VisualElement column) { var cell = new VisualElement { pickingMode = PickingMode.Ignore }; cell.style.height = RowHeight; cell.style.justifyContent = Justify.Center; cell.style.alignItems = Align.FlexStart; column.Add(cell); return cell; } /// Height of a stat row (a 15 px Cantarell line). const float RowHeight = 19f; 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); } } }