"""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]
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),
}