Source code for simvx.core.ui.tooltip
"""TooltipManager — hover-triggered floating tooltip utility."""
from __future__ import annotations
import logging
from ..math.types import Vec2
from .core import Control
from .theme import get_theme
log = logging.getLogger(__name__)
__all__ = ["TooltipManager"]
# ============================================================================
# TooltipManager — utility class, NOT a Control subclass
# ============================================================================
[docs]
class TooltipManager:
"""Manages tooltip display for the UI system.
Not a Control subclass. Intended to be owned by SceneTree or the
application layer and updated each frame.
Example:
tip = TooltipManager()
# In the frame loop:
control = tree._find_control_at_point(mouse_pos)
tip.update(dt, mouse_pos, control)
tip.draw(renderer)
"""
_FONT_SIZE = 12.0
_PADDING_X = 8.0
_PADDING_Y = 4.0
_OFFSET_X = 15.0
_OFFSET_Y = 15.0
def __init__(self, show_delay: float = 0.5, screen_width: float = 800, screen_height: float = 600):
self._hover_control: Control | None = None
self._hover_time: float = 0.0
self._show_delay: float = show_delay
self._visible: bool = False
self._text: str = ""
self._position: Vec2 = Vec2()
self._screen_width: float = screen_width
self._screen_height: float = screen_height
[docs]
def update(self, dt: float, mouse_pos, control_at_pos: Control | None):
"""Update tooltip state. Call once per frame.
Args:
dt: Frame delta time in seconds.
mouse_pos: Current mouse position (Vec2 or tuple).
control_at_pos: The topmost Control under the cursor, or None.
"""
mx = mouse_pos.x if hasattr(mouse_pos, "x") else mouse_pos[0]
my = mouse_pos.y if hasattr(mouse_pos, "y") else mouse_pos[1]
# Check if control has a non-empty tooltip attribute
tooltip_text = ""
if control_at_pos is not None:
tooltip_text = getattr(control_at_pos, "tooltip", "")
if tooltip_text:
if control_at_pos is self._hover_control:
# Same control — accumulate hover time
self._hover_time += dt
else:
# New control — reset timer
self._hover_control = control_at_pos
self._hover_time = 0.0
self._visible = False
if self._hover_time >= self._show_delay:
self._visible = True
self._text = tooltip_text
self._position = Vec2(mx + self._OFFSET_X, my + self._OFFSET_Y)
else:
# No control or no tooltip — hide
self._hover_control = None
self._hover_time = 0.0
self._visible = False
self._text = ""
[docs]
def draw(self, renderer):
"""Draw the tooltip if visible. Call after all other UI drawing."""
if not self._visible or not self._text:
return
theme = get_theme()
scale = self._FONT_SIZE / 14.0
text_w = renderer.text_width(self._text, scale)
box_w = text_w + self._PADDING_X * 2
box_h = self._FONT_SIZE + self._PADDING_Y * 2
# Clamp to screen bounds so tooltip doesn't overflow
tx = self._position.x
ty = self._position.y
if tx + box_w > self._screen_width:
tx = self._screen_width - box_w
if ty + box_h > self._screen_height:
ty = self._screen_height - box_h
if tx < 0:
tx = 0
if ty < 0:
ty = 0
# Background
renderer.draw_filled_rect(tx, ty, box_w, box_h, theme.bg_darker)
# Border
renderer.draw_rect_coloured(tx, ty, box_w, box_h, theme.border_light)
# Text
renderer.draw_text_coloured(
self._text,
tx + self._PADDING_X,
ty + self._PADDING_Y,
scale,
theme.text_bright,
)