Source code for simvx.core.debug.ui_inspector

"""UI event inspector — logs input routing and control state."""


from __future__ import annotations

import logging
import time
from dataclasses import dataclass

log = logging.getLogger(__name__)

__all__ = ["UIInspector", "UIInputLog"]

_MAX_LOG = 100


[docs] @dataclass class UIInputLog: """Single UI input event record.""" timestamp: float event_type: str position: tuple[float, float] = (0.0, 0.0) target_path: str = "" outcome: str = "" details: str = ""
[docs] class UIInspector: """Event logger and control state tracker. No-ops when disabled.""" def __init__(self): self.enabled: bool = True self._log: list[UIInputLog] = [] self._focused_path: str = "" self._hovered_path: str = "" self._popup_count: int = 0
[docs] def log_event(self, event_type: str, event, target, outcome: str): if not self.enabled: return pos = (0.0, 0.0) if event and hasattr(event, "position"): p = event.position pos = (float(p.x) if hasattr(p, "x") else float(p[0]), float(p.y) if hasattr(p, "y") else float(p[1])) target_path = target.path if target and hasattr(target, "path") else str(target) self._log.append( UIInputLog( timestamp=time.monotonic(), event_type=event_type, position=pos, target_path=target_path, outcome=outcome, ) ) if len(self._log) > _MAX_LOG: self._log = self._log[-_MAX_LOG:]
[docs] def log_focus_change(self, old, new): if not self.enabled: return old_path = old.path if old and hasattr(old, "path") else "None" new_path = new.path if new and hasattr(new, "path") else "None" self._focused_path = new_path self._log.append( UIInputLog( timestamp=time.monotonic(), event_type="focus_change", target_path=new_path, outcome=f"{old_path} -> {new_path}", ) ) if len(self._log) > _MAX_LOG: self._log = self._log[-_MAX_LOG:]
[docs] def log_hit_test(self, point, result): if not self.enabled: return pos = ( float(point[0]) if not hasattr(point, "x") else float(point.x), float(point[1]) if not hasattr(point, "y") else float(point.y), ) target_path = result.path if result and hasattr(result, "path") else "None" self._log.append( UIInputLog( timestamp=time.monotonic(), event_type="hit_test", position=pos, target_path=target_path, outcome="hit" if result else "miss", ) ) if len(self._log) > _MAX_LOG: self._log = self._log[-_MAX_LOG:]
[docs] def snapshot_state(self, tree): """Capture focused/hovered/popup state for overlay display.""" if not self.enabled: return self._focused_path = tree._focused_control.path if tree._focused_control else "None" self._popup_count = len(tree._popup_stack)
[docs] def get_recent_log(self, count: int = 20) -> list[UIInputLog]: return self._log[-count:]
[docs] def inspect_control(self, control) -> dict: """Full diagnostic dump of a single control.""" return { "path": control.path, "type": type(control).__name__, "position": (float(control.position.x), float(control.position.y)), "size": (float(control.size.x), float(control.size.y)), "visible": control.visible, "disabled": getattr(control, "disabled", False), "focused": getattr(control, "focused", False), "mouse_over": getattr(control, "mouse_over", False), "mouse_filter": getattr(control, "mouse_filter", True), }