class_name LuneTooltip extends PanelContainer ## The one tooltip of the HUD. A single panel is reused for every hover: it appears after a ## short delay next to the hovered control, stays inside the window, never takes mouse input ## and hides as soon as the control is left, hidden, disabled for input, freed or clicked. ## Content is a small dictionary: title, subtitle, stats ([glyph, name, value] rows), text, ## note and warn. Components register with `LuneTooltip.attach(control, builder)`. const DELAY: float = .28 const QUICK: float = .05 const WIDTH: float = 300.0 static var current: LuneTooltip var _owner: Control var _builder: Callable var _wait: float = 0.0 var _last_hide: float = -10.0 var _clock: float = 0.0 var _column: VBoxContainer ## Last pointer position from input events (canvas coordinates); real and synthetic motion. var _pointer: Vector2 = Vector2(-1, -1) func _init() -> void: theme_type_variation = "HudTooltip" mouse_filter = Control.MOUSE_FILTER_IGNORE visible = false z_index = 100 _column = VBoxContainer.new() _column.add_theme_constant_override("separation", 4) _column.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(_column) current = self static func attach(control: Control, builder: Callable) -> void: control.mouse_entered.connect(func() -> void: if current != null and is_instance_valid(current): current.hover(control, builder)) control.mouse_exited.connect(func() -> void: if current != null and is_instance_valid(current): current.leave(control)) control.gui_input.connect(func(event: InputEvent) -> void: if event is InputEventMouseButton and event.pressed and current != null and is_instance_valid(current): current.leave(control)) func hover(control: Control, builder: Callable) -> void: if _pointer.x < 0.0: _pointer = control.get_global_mouse_position() _owner = control _builder = builder _wait = QUICK if _clock - _last_hide < .6 else DELAY visible = false func leave(control: Control) -> void: if control == _owner: _hide() func _hide() -> void: if visible: _last_hide = _clock _owner = null visible = false ## Refreshes the text of an open tooltip (values changed while hovering). func refresh(control: Control) -> void: if control == _owner and visible: _show() func _input(event: InputEvent) -> void: if event is InputEventMouse: _pointer = (event as InputEventMouse).position func _process(delta: float) -> void: _clock += delta if _owner == null: return if not is_instance_valid(_owner) or not _owner.is_visible_in_tree() or not _owner.get_global_rect().has_point(_pointer): _hide() return if not visible: _wait -= delta if _wait <= 0.0: _show() func _show() -> void: var content: Dictionary = _builder.call() if _builder.is_valid() else {} if content.is_empty(): visible = false return for child: Node in _column.get_children(): child.free() if content.has("title"): var head := HBoxContainer.new() head.add_theme_constant_override("separation", 6) head.mouse_filter = Control.MOUSE_FILTER_IGNORE _column.add_child(head) if content.has("glyph"): LuneGlyph.make(str(content.glyph), content.get("glyph_color", LuneUiTheme.BRONZE_LIGHT), 18.0, head) LuneUiTheme.label(str(content.title), "HudValue", head) if content.has("subtitle"): LuneUiTheme.label(str(content.subtitle), "HudMuted", _column) var stats: Array = content.get("stats", []) if not stats.is_empty(): var grid := GridContainer.new() grid.columns = 3 grid.add_theme_constant_override("h_separation", 8) grid.add_theme_constant_override("v_separation", 2) grid.mouse_filter = Control.MOUSE_FILTER_IGNORE _column.add_child(grid) for row: Array in stats: LuneGlyph.make(str(row[0]), LuneUiTheme.MUTED, 14.0, grid) LuneUiTheme.label(str(row[1]), "HudMuted", grid) var value := LuneUiTheme.label(str(row[2]), "HudText", grid) value.size_flags_horizontal = Control.SIZE_EXPAND_FILL for key: String in ["text", "note", "warn"]: if content.has(key) and not str(content[key]).is_empty(): var line := LuneUiTheme.label(str(content[key]), "HudText" if key == "text" else "HudMuted", _column) line.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART line.custom_minimum_size.x = WIDTH - 24.0 if key == "warn": line.add_theme_color_override("font_color", LuneUiTheme.DANGER) visible = true reset_size() custom_minimum_size.x = 0.0 await get_tree().process_frame if not visible or _owner == null or not is_instance_valid(_owner): return _place() func _place() -> void: reset_size() var parent_rect: Rect2 = (get_parent() as Control).get_global_rect() if get_parent() is Control else get_viewport_rect() var anchor: Rect2 = _owner.get_global_rect() var scale_factor: float = get_global_transform().get_scale().x var extent: Vector2 = size * scale_factor var at := Vector2(anchor.get_center().x - extent.x * .5, anchor.position.y - extent.y - 8.0) if at.y < parent_rect.position.y + 6.0: at.y = anchor.end.y + 8.0 at.x = clampf(at.x, parent_rect.position.x + 6.0, parent_rect.end.x - extent.x - 6.0) at.y = clampf(at.y, parent_rect.position.y + 6.0, parent_rect.end.y - extent.y - 6.0) global_position = at