Source code for simvx.core.ui.marks
"""One canonical answer to: which visible controls are interactive, and where are they on screen.
A single pure-data pass over a scene tree that returns a :class:`ControlMark` per visible
:class:`~simvx.core.ui.core.Control`, carrying its screen-space rect (from ``get_global_rect``)
and the interaction flags derived from the very predicates the engine uses at runtime:
- ``clickable`` -> ``mouse_filter`` truthy and the rect is non-degenerate (matches
``UIInputManager._find_control_at_point``: a control receives clicks iff it is visible,
``mouse_filter`` is truthy, and the point lands inside its rect).
- ``focusable`` -> ``focus_mode != FocusMode.NONE`` (matches the focus/tab predicates).
No new member is added to ``Control``: every flag is derived from existing state so there is
exactly one source of truth. This module is backend-agnostic (no renderer, no Vulkan, no
``simvx.ai``): the two render adapters that consume it are the in-game ``DebugOverlay`` (Draw2D)
and the AI set-of-marks pixel overlay.
The tree must be laid out for the current frame before calling this: ``get_global_rect`` is
per-frame cached keyed on ``Control._current_frame`` (bumped by ``SceneTree.on_update``), so a
pre-layout call returns stale or zero rects.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ..node import Node
from .core import Control
__all__ = ["ControlMark", "enumerate_interactive_controls"]
[docs]
@dataclass(frozen=True, slots=True)
class ControlMark:
"""A visible control plus its screen rect and runtime-derived interaction flags."""
control: Control
rect: tuple[float, float, float, float] # screen-space (x, y, w, h) from get_global_rect()
path: str
type_name: str
text: str
clickable: bool # mouse_filter truthy AND rect non-degenerate (matches the hit-test)
focusable: bool # focus_mode != FocusMode.NONE (matches the focus/tab predicate)
disabled: bool
focused: bool
mouse_over: bool
[docs]
@property
def interactive(self) -> bool:
"""True when this control can receive mouse clicks or keyboard focus."""
return self.clickable or self.focusable
[docs]
def enumerate_interactive_controls(root: Node, *, interactive_only: bool = False) -> list[ControlMark]:
"""Enumerate visible Controls under ``root`` as screen-space :class:`ControlMark` records.
Single DFS pass over ``root.walk(include_self=True)``. A node is included when it is a
``Control`` that is visible in the hierarchy and has a non-degenerate rect; degenerate rects
(``w <= 0`` or ``h <= 0``) are skipped silently because that is normal for a 0-sized or
anchor/margin-collapsed decorative node (``get_rect`` already warns only on *negative* size).
With ``interactive_only=True``, records that are neither ``clickable`` nor ``focusable`` are
dropped, leaving only what the mouse and an agent can act on. Note a control is ``clickable``
whenever ``mouse_filter`` is truthy (the engine default), so decorative containers drop out
only when the author opts them out of hit-testing (``mouse_filter = False``), exactly matching
what the runtime hit-test would route a click to.
"""
from .core import Control
from .enums import FocusMode
marks: list[ControlMark] = []
for node in root.walk(include_self=True):
if not isinstance(node, Control):
continue
if not node._visible_in_hierarchy:
continue
x, y, w, h = node.get_global_rect()
if w <= 0 or h <= 0:
continue
clickable = bool(node.mouse_filter)
focusable = node.focus_mode != FocusMode.NONE
if interactive_only and not (clickable or focusable):
continue
marks.append(
ControlMark(
control=node,
rect=(float(x), float(y), float(w), float(h)),
path=node.path,
type_name=type(node).__name__,
text=(getattr(node, "text", "") or "")[:40],
clickable=clickable,
focusable=focusable,
disabled=node.disabled,
focused=node.focused,
mouse_over=node.mouse_over,
)
)
return marks