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, )