ui: child widget support (#37594)

* child widgets!

* cmt

* missing

* group

* add debug flag

* use in scroller

* not clean yet

* restore
This commit is contained in:
Shane Smiskol
2026-03-07 05:07:03 -08:00
committed by GitHub
parent 797b769478
commit 4bf2bfb122
4 changed files with 41 additions and 16 deletions
+1
View File
@@ -111,6 +111,7 @@ class MiciHomeLayout(Widget):
self._version_commit_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)
def show_event(self):
super().show_event()
self._version_text = self._get_version_text()
self._update_params()
+38 -5
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import abc
import pyray as rl
from enum import IntEnum
from typing import TypeVar
from collections.abc import Callable
from openpilot.system.ui.lib.application import gui_app, MousePos, MAX_TOUCH_SLOTS, MouseEvent
@@ -13,6 +14,10 @@ except ImportError:
awake = True
device = Device()
W = TypeVar('W', bound='Widget')
DEBUG = True
class DialogResult(IntEnum):
CANCEL = 0
@@ -24,11 +29,14 @@ class Widget(abc.ABC):
def __init__(self):
self._rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0)
self._parent_rect: rl.Rectangle | None = None
self._children: list[Widget] = []
self._enabled: bool | Callable[[], bool] = True
self._is_visible: bool | Callable[[], bool] = True
self.__is_pressed = [False] * MAX_TOUCH_SLOTS
# if current mouse/touch down started within the widget's rectangle
self.__tracking_is_pressed = [False] * MAX_TOUCH_SLOTS
self._enabled: bool | Callable[[], bool] = True
self._is_visible: bool | Callable[[], bool] = True
self._touch_valid_callback: Callable[[], bool] | None = None
self._click_delay: float | None = None # seconds to hold is_pressed after release
self._click_release_time: float | None = None
@@ -197,12 +205,37 @@ class Widget(abc.ABC):
"""Optionally handle mouse events. This is called before rendering."""
# Default implementation does nothing, can be overridden by subclasses
def _child(self, widget: W) -> W:
"""
Register a widget as a child. Lifecycle events (show/hide) propagate to registered children.
- If the widget is pushed onto the nav stack, do NOT register it (gui_app manages its lifecycle).
- If the widget is rendered inline in _render(), register it.
"""
assert widget not in self._children, f"{type(widget).__name__} already a child of {type(self).__name__}"
self._children.append(widget)
return widget
_show_hide_depth = 0
def show_event(self):
"""Optionally handle show event. Parent must manually call this"""
# TODO: iterate through all child objects, check for subclassing from Widget/Layout (Scroller)
"""Called when widget becomes visible. Propagates to registered children."""
if DEBUG:
print(f"{' ' * Widget._show_hide_depth}show_event: {type(self).__name__}")
Widget._show_hide_depth += 1
for child in self._children:
child.show_event()
if DEBUG:
Widget._show_hide_depth -= 1
def hide_event(self):
"""Optionally handle hide event. Parent must manually call this"""
"""Called when widget is hidden. Propagates to registered children."""
if DEBUG:
print(f"{' ' * Widget._show_hide_depth}hide_event: {type(self).__name__}")
Widget._show_hide_depth += 1
for child in self._children:
child.hide_event()
if DEBUG:
Widget._show_hide_depth -= 1
def dismiss(self, callback: Callable[[], None] | None = None):
"""Immediately dismiss the widget, firing the callback after."""
+1 -2
View File
@@ -69,7 +69,7 @@ class NavWidget(Widget, abc.ABC):
self._shown_callback: Callable[[], None] | None = None # transient callback fired after show animation completes
# TODO: move this state into NavBar
self._nav_bar = NavBar()
self._nav_bar = self._child(NavBar())
self._nav_bar_show_time = 0.0
self._nav_bar_y_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
@@ -214,7 +214,6 @@ class NavWidget(Widget, abc.ABC):
def show_event(self):
super().show_event()
self._nav_bar.show_event()
# Reset state
self._drag_start_pos = None
+1 -9
View File
@@ -422,18 +422,10 @@ class Scroller(Widget):
"""Wrapper for _Scroller so that children do not need to call events or pass down enabled for nav stack."""
def __init__(self, **kwargs):
super().__init__()
self._scroller = _Scroller([], **kwargs)
self._scroller = self._child(_Scroller([], **kwargs))
# pass down enabled to child widget for nav stack
self._scroller.set_enabled(lambda: self.enabled)
def show_event(self):
super().show_event()
self._scroller.show_event()
def hide_event(self):
super().hide_event()
self._scroller.hide_event()
def _render(self, _):
self._scroller.render(self._rect)