From d36d52db8821914c0af24f3ae647d6215375d661 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 5 Feb 2026 16:04:55 -0800 Subject: [PATCH 01/73] initial --- selfdrive/ui/mici/layouts/main.py | 5 +- .../ui/mici/layouts/settings/settings.py | 9 ++- selfdrive/ui/ui.py | 3 +- system/ui/lib/application.py | 55 ++++++++++++++++--- system/ui/widgets/__init__.py | 11 +++- 5 files changed, 66 insertions(+), 17 deletions(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index b52f9ed39a06f9..ab9d402c2703b1 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -138,7 +138,10 @@ def _set_mode_for_started(self, onroad_transition: bool = False): self._scroll_to(self._home_layout) def _on_settings_clicked(self): - self._set_mode(MainState.SETTINGS) + # print('on settings clicked') + gui_app.push_widget(self._settings_layout) + # gui_app.set_modal_overlay(self._settings_layout) + # self._set_mode(MainState.SETTINGS) def _on_settings_closed(self): self._set_mode(MainState.MAIN) diff --git a/selfdrive/ui/mici/layouts/settings/settings.py b/selfdrive/ui/mici/layouts/settings/settings.py index 3917899032e743..06e7961c1872bb 100644 --- a/selfdrive/ui/mici/layouts/settings/settings.py +++ b/selfdrive/ui/mici/layouts/settings/settings.py @@ -59,7 +59,8 @@ def __init__(self): ], snap_items=False) # Set up back navigation - self.set_back_callback(self.close_settings) + # self.set_back_callback(self.close_settings) + self.set_back_callback(gui_app.pop_widget) self.set_back_enabled(lambda: self._current_panel is None) self._panels = { @@ -95,6 +96,7 @@ def _render(self, rect: rl.Rectangle): self._draw_current_panel() else: self._scroller.render(rect) + return -1 def _draw_current_panel(self): panel = self._panels[self._current_panel] @@ -109,5 +111,6 @@ def _set_current_panel(self, panel_type: PanelType | None): self._panels[self._current_panel].instance.show_event() def close_settings(self): - if self._close_callback: - self._close_callback() + gui_app.pop_widget() + # if self._close_callback: + # self._close_callback() diff --git a/selfdrive/ui/ui.py b/selfdrive/ui/ui.py index 7fe0dfbbc9a382..4a402f9ef48ddb 100755 --- a/selfdrive/ui/ui.py +++ b/selfdrive/ui/ui.py @@ -20,10 +20,11 @@ def main(): else: main_layout = MiciMainLayout() main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + gui_app.push_widget(main_layout) for should_render in gui_app.render(): ui_state.update() if should_render: - main_layout.render() + # main_layout.render() # reaffine after power save offlines our core if TICI and os.sched_getaffinity(0) != cores: diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index da314a394f57e1..4a863d80ca27b5 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -12,7 +12,7 @@ from contextlib import contextmanager from collections.abc import Callable from collections import deque -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import StrEnum from pathlib import Path from typing import NamedTuple @@ -120,6 +120,11 @@ class ModalOverlay: callback: Callable | None = None +@dataclass +class NavStack: + widgets: list[object] = field(default_factory=list) + + class MousePos(NamedTuple): x: float y: float @@ -226,6 +231,7 @@ def __init__(self, width: int | None = None, height: int | None = None): self._frame = 0 self._window_close_requested = False self._modal_overlay = ModalOverlay() + self._nav_stack = NavStack() self._modal_overlay_shown = False self._modal_overlay_tick: Callable[[], None] | None = None @@ -370,6 +376,30 @@ def _ffmpeg_writer_thread(self): except Exception: break + def push_widget(self, widget): + # disable previous widget to prevent input processing, but keep rendering for smooth transitions + if len(self._nav_stack.widgets) > 0: + prev_widget = self._nav_stack.widgets[-1] + print('Disabling and hide_event for', prev_widget.__class__.__name__) + prev_widget.hide_event() + prev_widget.set_enabled(False) + + print('Pushing and show_event for', widget.__class__.__name__) + self._nav_stack.widgets.append(widget) + widget.show_event() + + def pop_widget(self): + # reenable previous widget if exists and show event to allow it to update state if needed (e.g. refresh after settings change) + if len(self._nav_stack.widgets) > 1: + prev_widget = self._nav_stack.widgets[-2] + print('Re-enabling and show_event for', prev_widget.__class__.__name__) + prev_widget.show_event() + prev_widget.set_enabled(True) + if len(self._nav_stack.widgets) > 1: + print('Popping and hide_event for', self._nav_stack.widgets[-1].__class__.__name__) + widget = self._nav_stack.widgets.pop() + widget.hide_event() + def set_modal_overlay(self, overlay, callback: Callable | None = None): if self._modal_overlay.overlay is not None: if hasattr(self._modal_overlay.overlay, 'hide_event'): @@ -525,14 +555,21 @@ def render(self): rl.begin_drawing() rl.clear_background(rl.BLACK) - # Handle modal overlay rendering and input processing - if self._handle_modal_overlay(): - # Allow a Widget to still run a function while overlay is shown - if self._modal_overlay_tick is not None: - self._modal_overlay_tick() - yield False - else: - yield True + if len(self._nav_stack.widgets) > 1: + self._nav_stack.widgets[-2].render(rl.Rectangle(0, 0, self.width, self.height)) + rl.draw_rectangle(0, 0, self.width, self.height, rl.Color(0, 0, 0, 150)) + + if len(self._nav_stack.widgets) > 0: + self._nav_stack.widgets[-1].render(rl.Rectangle(0, 0, self.width, self.height)) + + # # Handle modal overlay rendering and input processing + # if self._handle_modal_overlay(): + # # Allow a Widget to still run a function while overlay is shown + # if self._modal_overlay_tick is not None: + # self._modal_overlay_tick() + # yield False + # else: + # yield True if self._render_texture: rl.end_texture_mode() diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 5d474e8aedf334..d32c1a6005ebff 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -60,6 +60,10 @@ def enabled(self) -> bool: def set_enabled(self, enabled: bool | Callable[[], bool]) -> None: self._enabled = enabled + if hasattr(self, '_scroller'): + self._scroller.set_enabled(enabled) + elif hasattr(self, '_scroll_panel'): + self._scroll_panel.set_enabled(enabled) @property def is_visible(self) -> bool: @@ -362,6 +366,7 @@ def _update_state(self): self.set_position(self._rect.x, new_y) def render(self, rect: rl.Rectangle | None = None) -> bool | int | None: + rl.draw_rectangle(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height + 20), rl.BLACK) ret = super().render(rect) if self.back_enabled: @@ -371,9 +376,9 @@ def render(self, rect: rl.Rectangle | None = None) -> bool | int | None: else: self._nav_bar_y_filter.update(NAV_BAR_MARGIN) - # draw black above widget when dismissing - if self._rect.y > 0: - rl.draw_rectangle(int(self._rect.x), 0, int(self._rect.width), int(self._rect.y), rl.BLACK) + # # draw black above widget when dismissing + # if self._rect.y > 0: + # rl.draw_rectangle(int(self._rect.x), 0, int(self._rect.width), int(self._rect.y), rl.BLACK) self._nav_bar.set_position(bar_x, round(self._nav_bar_y_filter.x)) self._nav_bar.render() From 6d9fb09602d163c48f3f0855ebb83bb4e9a52fd9 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 5 Feb 2026 16:39:22 -0800 Subject: [PATCH 02/73] start to support nav stack in settings panels + fix some navwidget bugs --- selfdrive/ui/mici/layouts/main.py | 4 -- .../ui/mici/layouts/settings/settings.py | 58 +++++++++---------- system/ui/lib/application.py | 6 +- system/ui/widgets/__init__.py | 5 ++ 4 files changed, 35 insertions(+), 38 deletions(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index ab9d402c2703b1..501c4e2e6aa7b8 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -68,7 +68,6 @@ def __init__(self): def _setup_callbacks(self): self._home_layout.set_callbacks(on_settings=self._on_settings_clicked) - self._settings_layout.set_callbacks(on_close=self._on_settings_closed) self._onroad_layout.set_click_callback(lambda: self._scroll_to(self._home_layout)) device.add_interactive_timeout_callback(self._set_mode_for_started) @@ -143,9 +142,6 @@ def _on_settings_clicked(self): # gui_app.set_modal_overlay(self._settings_layout) # self._set_mode(MainState.SETTINGS) - def _on_settings_closed(self): - self._set_mode(MainState.MAIN) - def _on_bookmark_clicked(self): user_bookmark = messaging.new_message('bookmarkButton') user_bookmark.valid = True diff --git a/selfdrive/ui/mici/layouts/settings/settings.py b/selfdrive/ui/mici/layouts/settings/settings.py index 06e7961c1872bb..36b739f1ece6bb 100644 --- a/selfdrive/ui/mici/layouts/settings/settings.py +++ b/selfdrive/ui/mici/layouts/settings/settings.py @@ -34,7 +34,7 @@ class SettingsLayout(NavWidget): def __init__(self): super().__init__() self._params = Params() - self._current_panel = None # PanelType.DEVICE + # self._current_panel = None # PanelType.DEVICE toggles_btn = BigButton("toggles", "", "icons_mici/settings.png") toggles_btn.set_click_callback(lambda: self._set_current_panel(PanelType.TOGGLES)) @@ -59,9 +59,8 @@ def __init__(self): ], snap_items=False) # Set up back navigation - # self.set_back_callback(self.close_settings) self.set_back_callback(gui_app.pop_widget) - self.set_back_enabled(lambda: self._current_panel is None) + # self.set_back_enabled(lambda: self._current_panel is None) self._panels = { PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayoutMici(back_callback=lambda: self._set_current_panel(None))), @@ -73,44 +72,39 @@ def __init__(self): self._font_medium = gui_app.font(FontWeight.MEDIUM) - # Callbacks - self._close_callback: Callable | None = None - def show_event(self): super().show_event() - self._set_current_panel(None) + # self._set_current_panel(None) self._scroller.show_event() - if self._current_panel is not None: - self._panels[self._current_panel].instance.show_event() + # if self._current_panel is not None: + # self._panels[self._current_panel].instance.show_event() def hide_event(self): super().hide_event() - if self._current_panel is not None: - self._panels[self._current_panel].instance.hide_event() - - def set_callbacks(self, on_close: Callable): - self._close_callback = on_close + self._scroller.hide_event() + # if self._current_panel is not None: + # self._panels[self._current_panel].instance.hide_event() def _render(self, rect: rl.Rectangle): - if self._current_panel is not None: - self._draw_current_panel() - else: - self._scroller.render(rect) + # if self._current_panel is not None: + # self._draw_current_panel() + # else: + self._scroller.render(rect) return -1 - def _draw_current_panel(self): - panel = self._panels[self._current_panel] - panel.instance.render(self._rect) + # def _draw_current_panel(self): + # panel = self._panels[self._current_panel] + # panel.instance.render(self._rect) def _set_current_panel(self, panel_type: PanelType | None): - if panel_type != self._current_panel: - if self._current_panel is not None: - self._panels[self._current_panel].instance.hide_event() - self._current_panel = panel_type - if self._current_panel is not None: - self._panels[self._current_panel].instance.show_event() - - def close_settings(self): - gui_app.pop_widget() - # if self._close_callback: - # self._close_callback() + if panel_type is None: + # TODO: move this into each layout's class above + gui_app.pop_widget() + else: + gui_app.push_widget(self._panels[panel_type].instance) + # if panel_type != self._current_panel: + # if self._current_panel is not None: + # self._panels[self._current_panel].instance.hide_event() + # self._current_panel = panel_type + # if self._current_panel is not None: + # self._panels[self._current_panel].instance.show_event() diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 4a863d80ca27b5..eef449fa752bdd 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -381,24 +381,26 @@ def push_widget(self, widget): if len(self._nav_stack.widgets) > 0: prev_widget = self._nav_stack.widgets[-1] print('Disabling and hide_event for', prev_widget.__class__.__name__) - prev_widget.hide_event() + # prev_widget.hide_event() prev_widget.set_enabled(False) print('Pushing and show_event for', widget.__class__.__name__) self._nav_stack.widgets.append(widget) widget.show_event() + print() def pop_widget(self): # reenable previous widget if exists and show event to allow it to update state if needed (e.g. refresh after settings change) if len(self._nav_stack.widgets) > 1: prev_widget = self._nav_stack.widgets[-2] print('Re-enabling and show_event for', prev_widget.__class__.__name__) - prev_widget.show_event() + # prev_widget.show_event() prev_widget.set_enabled(True) if len(self._nav_stack.widgets) > 1: print('Popping and hide_event for', self._nav_stack.widgets[-1].__class__.__name__) widget = self._nav_stack.widgets.pop() widget.hide_event() + print() def set_modal_overlay(self, overlay, callback: Callable | None = None): if self._modal_overlay.overlay is not None: diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index d32c1a6005ebff..d714c9e384cc24 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -264,6 +264,7 @@ def set_back_callback(self, callback: Callable[[], None]) -> None: self._back_callback = callback def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: + # FIXME: disabling this widget on new push widget still causes this widget to track mouse events without mouse down super()._handle_mouse_event(mouse_event) if not self.back_enabled: @@ -338,6 +339,10 @@ def _update_state(self): new_y = 0.0 + if not self.enabled: + self._back_button_start_pos = None + + # TODO: why not in handle_mouse_event? have to hack above if self._back_button_start_pos is not None: last_mouse_event = gui_app.last_mouse_event # push entire widget as user drags it away From 8ecc21f4227acfde36bc72c3dff14aa98d5d6ab7 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 5 Feb 2026 17:03:46 -0800 Subject: [PATCH 03/73] add deprecation warning and move more to new nav stack --- .../ui/mici/layouts/settings/developer.py | 4 +- selfdrive/ui/mici/layouts/settings/device.py | 5 +- .../ui/mici/layouts/settings/firehose.py | 4 +- .../mici/layouts/settings/network/__init__.py | 49 +++++++------- .../mici/layouts/settings/network/wifi_ui.py | 4 +- .../ui/mici/layouts/settings/settings.py | 67 +++++++------------ selfdrive/ui/mici/layouts/settings/toggles.py | 4 +- selfdrive/ui/mici/widgets/dialog.py | 27 ++++---- system/ui/lib/application.py | 1 + 9 files changed, 79 insertions(+), 86 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/developer.py b/selfdrive/ui/mici/layouts/settings/developer.py index b6145e042eb3b1..c2abbe7426bf89 100644 --- a/selfdrive/ui/mici/layouts/settings/developer.py +++ b/selfdrive/ui/mici/layouts/settings/developer.py @@ -13,9 +13,9 @@ class DeveloperLayoutMici(NavWidget): - def __init__(self, back_callback: Callable): + def __init__(self): super().__init__() - self.set_back_callback(back_callback) + self.set_back_callback(gui_app.pop_widget) def github_username_callback(username: str): if username: diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index c12a92482c67c7..a85e96d0a34df4 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -266,7 +266,7 @@ def _update_state(self): class DeviceLayoutMici(NavWidget): - def __init__(self, back_callback: Callable): + def __init__(self): super().__init__() self._fcc_dialog: HtmlModal | None = None @@ -345,7 +345,8 @@ def selected_language_callback(): ], snap_items=False) # Set up back navigation - self.set_back_callback(back_callback) + # TODO: can this somehow be generic in widgets/__init__.py or application.py? + self.set_back_callback(gui_app.pop_widget) # Hide power off button when onroad ui_state.add_offroad_transition_callback(self._offroad_transition) diff --git a/selfdrive/ui/mici/layouts/settings/firehose.py b/selfdrive/ui/mici/layouts/settings/firehose.py index d305906e13de57..0a9c6111a2bdd4 100644 --- a/selfdrive/ui/mici/layouts/settings/firehose.py +++ b/selfdrive/ui/mici/layouts/settings/firehose.py @@ -223,6 +223,6 @@ def _update_loop(self): class FirehoseLayout(FirehoseLayoutBase, NavWidget): BACK_TOUCH_AREA_PERCENTAGE = 0.1 - def __init__(self, back_callback): + def __init__(self): super().__init__() - self.set_back_callback(back_callback) + self.set_back_callback(gui_app.pop_widget) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index fb1d56a1f64125..80f4d964c874d4 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -13,21 +13,21 @@ from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType -class NetworkPanelType(IntEnum): - NONE = 0 - WIFI = 1 +# class NetworkPanelType(IntEnum): +# NONE = 0 +# WIFI = 1 class NetworkLayoutMici(NavWidget): - def __init__(self, back_callback: Callable): + def __init__(self): super().__init__() - self._current_panel = NetworkPanelType.WIFI - self.set_back_enabled(lambda: self._current_panel == NetworkPanelType.NONE) + # self._current_panel = NetworkPanelType.WIFI + # self.set_back_enabled(lambda: self._current_panel == NetworkPanelType.NONE) self._wifi_manager = WifiManager() self._wifi_manager.set_active(False) - self._wifi_ui = WifiUIMici(self._wifi_manager, back_callback=lambda: self._switch_to_panel(NetworkPanelType.NONE)) + self._wifi_ui = WifiUIMici(self._wifi_manager) self._wifi_manager.add_callbacks( networks_updated=self._on_network_updated, @@ -49,7 +49,8 @@ def tethering_password_clicked(): tethering_password = self._wifi_manager.tethering_password dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8, confirm_callback=tethering_password_callback) - gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) + # gui_app.set_modal_overlay(dlg) txt_tethering = gui_app.texture("icons_mici/settings/network/tethering.png", 64, 54) self._tethering_password_btn = BigButton("tethering password", "", txt_tethering) @@ -76,7 +77,8 @@ def network_metered_callback(value: str): self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 64, 47) self._wifi_button = BigButton("wi-fi", "not connected", self._wifi_slash_txt, scroll=True) - self._wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI)) + # self._wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI)) + self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui)) # ******** Advanced settings ******** # ******** Roaming toggle ******** @@ -108,7 +110,7 @@ def network_metered_callback(value: str): self._wifi_manager.update_gsm_settings(roaming_enabled, ui_state.params.get("GsmApn") or "", metered) # Set up back navigation - self.set_back_callback(back_callback) + self.set_back_callback(gui_app.pop_widget) def _update_state(self): super()._update_state() @@ -122,13 +124,13 @@ def _update_state(self): def show_event(self): super().show_event() - self._current_panel = NetworkPanelType.NONE - self._wifi_ui.show_event() + # self._current_panel = NetworkPanelType.NONE + # self._wifi_ui.show_event() self._scroller.show_event() - def hide_event(self): - super().hide_event() - self._wifi_ui.hide_event() + # def hide_event(self): + # super().hide_event() + # self._wifi_ui.hide_event() def _toggle_roaming(self, checked: bool): self._wifi_manager.update_gsm_settings(checked, ui_state.params.get("GsmApn") or "", ui_state.params.get_bool("GsmMetered")) @@ -183,15 +185,16 @@ def _on_network_updated(self, networks: list[Network]): MeteredType.NO: 'unmetered' }.get(self._wifi_manager.current_network_metered, 'default')) - def _switch_to_panel(self, panel_type: NetworkPanelType): - if panel_type == NetworkPanelType.WIFI: - self._wifi_ui.show_event() - self._current_panel = panel_type + # def _switch_to_panel(self, panel_type: NetworkPanelType): + # if panel_type == NetworkPanelType.WIFI: + # self._wifi_ui.show_event() + # self._current_panel = panel_type def _render(self, rect: rl.Rectangle): + # TODO: make sure process_callbacks still gets called with new nav stack self._wifi_manager.process_callbacks() - if self._current_panel == NetworkPanelType.WIFI: - self._wifi_ui.render(rect) - else: - self._scroller.render(rect) + # if self._current_panel == NetworkPanelType.WIFI: + # self._wifi_ui.render(rect) + # else: + self._scroller.render(rect) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 7791f18cf736fb..1a83764afdd920 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -317,11 +317,11 @@ class WifiUIMici(BigMultiOptionDialog): # Wait this long after user interacts with widget to update network list INACTIVITY_TIMEOUT = 1 - def __init__(self, wifi_manager: WifiManager, back_callback: Callable): + def __init__(self, wifi_manager: WifiManager): super().__init__([], None, None, right_btn_callback=None) # Set up back navigation - self.set_back_callback(back_callback) + self.set_back_callback(gui_app.pop_widget) self._network_info_page = NetworkInfoPage(wifi_manager, self._connect_to_network, self._forget_network, self._open_network_manage_page) self._network_info_page.set_connecting(lambda: self._connecting) diff --git a/selfdrive/ui/mici/layouts/settings/settings.py b/selfdrive/ui/mici/layouts/settings/settings.py index 36b739f1ece6bb..bbedba2846c980 100644 --- a/selfdrive/ui/mici/layouts/settings/settings.py +++ b/selfdrive/ui/mici/layouts/settings/settings.py @@ -1,7 +1,6 @@ import pyray as rl from dataclasses import dataclass from enum import IntEnum -from collections.abc import Callable from openpilot.common.params import Params from openpilot.system.ui.widgets.scroller import Scroller @@ -15,38 +14,32 @@ from openpilot.system.ui.widgets import Widget, NavWidget -class PanelType(IntEnum): - TOGGLES = 0 - NETWORK = 1 - DEVICE = 2 - DEVELOPER = 3 - USER_MANUAL = 4 - FIREHOSE = 5 - - -@dataclass -class PanelInfo: - name: str - instance: Widget - - class SettingsLayout(NavWidget): def __init__(self): super().__init__() self._params = Params() # self._current_panel = None # PanelType.DEVICE + toggles_panel = TogglesLayoutMici() + network_panel = NetworkLayoutMici() + device_panel = DeviceLayoutMici() + developer_panel = DeveloperLayoutMici() + firehose_panel = FirehoseLayout() + toggles_btn = BigButton("toggles", "", "icons_mici/settings.png") - toggles_btn.set_click_callback(lambda: self._set_current_panel(PanelType.TOGGLES)) + toggles_btn.set_click_callback(lambda: gui_app.push_widget(toggles_panel)) + network_btn = BigButton("network", "", "icons_mici/settings/network/wifi_strength_full.png", icon_size=(76, 56)) - network_btn.set_click_callback(lambda: self._set_current_panel(PanelType.NETWORK)) + network_btn.set_click_callback(lambda: gui_app.push_widget(network_panel)) + device_btn = BigButton("device", "", "icons_mici/settings/device_icon.png", icon_size=(74, 60)) - device_btn.set_click_callback(lambda: self._set_current_panel(PanelType.DEVICE)) + device_btn.set_click_callback(lambda: gui_app.push_widget(device_panel)) + developer_btn = BigButton("developer", "", "icons_mici/settings/developer_icon.png", icon_size=(64, 60)) - developer_btn.set_click_callback(lambda: self._set_current_panel(PanelType.DEVELOPER)) + developer_btn.set_click_callback(lambda: gui_app.push_widget(developer_panel)) firehose_btn = BigButton("firehose", "", "icons_mici/settings/firehose.png", icon_size=(52, 62)) - firehose_btn.set_click_callback(lambda: self._set_current_panel(PanelType.FIREHOSE)) + firehose_btn.set_click_callback(lambda: gui_app.push_widget(firehose_panel)) self._scroller = Scroller([ toggles_btn, @@ -62,14 +55,6 @@ def __init__(self): self.set_back_callback(gui_app.pop_widget) # self.set_back_enabled(lambda: self._current_panel is None) - self._panels = { - PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayoutMici(back_callback=lambda: self._set_current_panel(None))), - PanelType.NETWORK: PanelInfo("Network", NetworkLayoutMici(back_callback=lambda: self._set_current_panel(None))), - PanelType.DEVICE: PanelInfo("Device", DeviceLayoutMici(back_callback=lambda: self._set_current_panel(None))), - PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayoutMici(back_callback=lambda: self._set_current_panel(None))), - PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout(back_callback=lambda: self._set_current_panel(None))), - } - self._font_medium = gui_app.font(FontWeight.MEDIUM) def show_event(self): @@ -96,15 +81,15 @@ def _render(self, rect: rl.Rectangle): # panel = self._panels[self._current_panel] # panel.instance.render(self._rect) - def _set_current_panel(self, panel_type: PanelType | None): - if panel_type is None: - # TODO: move this into each layout's class above - gui_app.pop_widget() - else: - gui_app.push_widget(self._panels[panel_type].instance) - # if panel_type != self._current_panel: - # if self._current_panel is not None: - # self._panels[self._current_panel].instance.hide_event() - # self._current_panel = panel_type - # if self._current_panel is not None: - # self._panels[self._current_panel].instance.show_event() + # def _set_current_panel(self, panel_type: PanelType | None): + # if panel_type is None: + # # TODO: move this into each layout's class above + # gui_app.pop_widget() + # else: + # gui_app.push_widget(self._panels[panel_type].instance) + # # if panel_type != self._current_panel: + # # if self._current_panel is not None: + # # self._panels[self._current_panel].instance.hide_event() + # # self._current_panel = panel_type + # # if self._current_panel is not None: + # # self._panels[self._current_panel].instance.show_event() diff --git a/selfdrive/ui/mici/layouts/settings/toggles.py b/selfdrive/ui/mici/layouts/settings/toggles.py index c16504fac8ba3a..47b60e7325c2e7 100644 --- a/selfdrive/ui/mici/layouts/settings/toggles.py +++ b/selfdrive/ui/mici/layouts/settings/toggles.py @@ -13,9 +13,9 @@ class TogglesLayoutMici(NavWidget): - def __init__(self, back_callback: Callable): + def __init__(self): super().__init__() - self.set_back_callback(back_callback) + self.set_back_callback(gui_app.pop_widget) self._personality_toggle = BigMultiParamToggle("driving personality", "LongitudinalPersonality", ["aggressive", "standard", "relaxed"]) self._experimental_btn = BigParamControl("experimental mode", "ExperimentalMode") diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 49d73f7b0d44b7..6d2898056a9d99 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -24,9 +24,10 @@ class BigDialogBase(NavWidget, abc.ABC): def __init__(self, right_btn: str | None = None, right_btn_callback: Callable | None = None): super().__init__() - self._ret = DialogResult.NO_ACTION + # self._ret = DialogResult.NO_ACTION self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - self.set_back_callback(lambda: setattr(self, '_ret', DialogResult.CANCEL)) + # self.set_back_callback(lambda: setattr(self, '_ret', DialogResult.CANCEL)) + self.set_back_callback(gui_app.pop_widget) self._right_btn = None if right_btn: @@ -40,7 +41,7 @@ def right_btn_callback_wrapper(): # move to right side self._right_btn._rect.x = self._rect.x + self._rect.width - self._right_btn._rect.width - def _render(self, _) -> DialogResult: + def _render(self, _): """ Allows `gui_app.set_modal_overlay(BigDialog(...))`. The overlay runner keeps calling until result != NO_ACTION. @@ -49,7 +50,7 @@ def _render(self, _) -> DialogResult: self._right_btn.set_position(self._right_btn._rect.x, self._rect.y) self._right_btn.render() - return self._ret + # return self._ret class BigDialog(BigDialogBase): @@ -62,7 +63,7 @@ def __init__(self, self._title = title self._description = description - def _render(self, _) -> DialogResult: + def _render(self, _): super()._render(_) # draw title @@ -94,7 +95,7 @@ def _render(self, _) -> DialogResult: gui_label(desc_rect, desc_wrapped, 30, font_weight=FontWeight.MEDIUM, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) - return self._ret + # return self._ret class BigConfirmationDialogV2(BigDialogBase): @@ -117,16 +118,17 @@ def _on_confirm(self): if self._confirm_callback: self._confirm_callback() if self._exit_on_confirm: - self._ret = DialogResult.CONFIRM + gui_app.pop_widget() + # self._ret = DialogResult.CONFIRM def _update_state(self): super()._update_state() if self._swiping_away and not self._slider.confirmed: self._slider.reset() - def _render(self, _) -> DialogResult: + def _render(self, _): self._slider.render(self._rect) - return self._ret + # return self._ret class BigInputDialog(BigDialogBase): @@ -160,9 +162,10 @@ def __init__(self, self._top_right_button_rect = rl.Rectangle(0, 0, 0, 0) def confirm_callback_wrapper(): - self._ret = DialogResult.CONFIRM + # self._ret = DialogResult.CONFIRM if confirm_callback: confirm_callback(self._keyboard.text()) + gui_app.pop_widget() self._confirm_callback = confirm_callback_wrapper def _update_state(self): @@ -253,7 +256,7 @@ def _render(self, _): rl.draw_rectangle_lines_ex(self._top_right_button_rect, 1, rl.Color(0, 255, 0, 255)) rl.draw_rectangle_lines_ex(self._top_left_button_rect, 1, rl.Color(0, 255, 0, 255)) - return self._ret + # return self._ret def _handle_mouse_press(self, mouse_pos: MousePos): super()._handle_mouse_press(mouse_pos) @@ -410,7 +413,7 @@ def _render(self, _): super()._render(_) self._scroller.render(self._rect) - return self._ret + # return self._ret class BigDialogButton(BigButton): diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index eef449fa752bdd..32dfb41bfc1210 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -403,6 +403,7 @@ def pop_widget(self): print() def set_modal_overlay(self, overlay, callback: Callable | None = None): + print('WARNING! set_modal_overlay is DEPRECATED, but was attempted to use to show', overlay.__class__.__name__) if self._modal_overlay.overlay is not None: if hasattr(self._modal_overlay.overlay, 'hide_event'): self._modal_overlay.overlay.hide_event() From 6ec007e431512d06895b7ad1c1ece74ff7230468 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 5 Feb 2026 17:10:53 -0800 Subject: [PATCH 04/73] fix overriding NavWidget enabled and do developer panel --- selfdrive/ui/mici/layouts/main.py | 1 + selfdrive/ui/mici/layouts/settings/developer.py | 9 ++++++--- system/ui/widgets/__init__.py | 12 ++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index 501c4e2e6aa7b8..3e786cb8e79e5d 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -49,6 +49,7 @@ def __init__(self): self._onroad_layout, ], spacing=0, pad_start=0, pad_end=0) self._scroller.set_reset_scroll_at_show(False) + self._scroller.set_enabled(lambda: self.enabled) # Disable scrolling when onroad is interacting with bookmark self._scroller.set_scrolling_enabled(lambda: not self._onroad_layout.is_swiping_left()) diff --git a/selfdrive/ui/mici/layouts/settings/developer.py b/selfdrive/ui/mici/layouts/settings/developer.py index c2abbe7426bf89..db3583ef30fc09 100644 --- a/selfdrive/ui/mici/layouts/settings/developer.py +++ b/selfdrive/ui/mici/layouts/settings/developer.py @@ -25,16 +25,19 @@ def github_username_callback(username: str): self._ssh_keys_btn.set_value(username) else: dlg = BigDialog("", ssh_keys._error_message) - gui_app.set_modal_overlay(dlg) + # gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) def ssh_keys_callback(): github_username = ui_state.params.get("GithubUsername") or "" dlg = BigInputDialog("enter GitHub username", github_username, confirm_callback=github_username_callback) if not system_time_valid(): dlg = BigDialog("Please connect to Wi-Fi to fetch your key", "") - gui_app.set_modal_overlay(dlg) + # gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) return - gui_app.set_modal_overlay(dlg) + # gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) txt_ssh = gui_app.texture("icons_mici/settings/developer/ssh.png", 56, 64) github_username = ui_state.params.get("GithubUsername") or "" diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index d714c9e384cc24..4d504b7d9147a5 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -60,10 +60,6 @@ def enabled(self) -> bool: def set_enabled(self, enabled: bool | Callable[[], bool]) -> None: self._enabled = enabled - if hasattr(self, '_scroller'): - self._scroller.set_enabled(enabled) - elif hasattr(self, '_scroll_panel'): - self._scroll_panel.set_enabled(enabled) @property def is_visible(self) -> bool: @@ -325,12 +321,12 @@ def _update_state(self): self._set_up = True if hasattr(self, '_scroller'): original_enabled = self._scroller._enabled - self._scroller.set_enabled(lambda: not self._swiping_away and (original_enabled() if callable(original_enabled) else - original_enabled)) + self._scroller.set_enabled(lambda: self.enabled and not self._swiping_away and (original_enabled() if callable(original_enabled) else + original_enabled)) elif hasattr(self, '_scroll_panel'): original_enabled = self._scroll_panel.enabled - self._scroll_panel.set_enabled(lambda: not self._swiping_away and (original_enabled() if callable(original_enabled) else - original_enabled)) + self._scroll_panel.set_enabled(lambda: self.enabled and not self._swiping_away and (original_enabled() if callable(original_enabled) else + original_enabled)) if self._trigger_animate_in: self._pos_filter.x = self._rect.height From 7dfb8497f7405adab801e8059418fc1b8dd3e1dc Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 5 Feb 2026 18:43:28 -0800 Subject: [PATCH 05/73] fix interactive timeout and do main --- selfdrive/ui/mici/layouts/main.py | 36 ++++++++++--------- .../mici/layouts/settings/network/wifi_ui.py | 12 ++++--- system/ui/lib/application.py | 6 ++++ 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index 3e786cb8e79e5d..2fc71beb4dae68 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -26,7 +26,7 @@ def __init__(self): self._pm = messaging.PubMaster(['bookmarkButton']) - self._current_mode: MainState | None = None + # self._current_mode: MainState | None = None self._prev_onroad = False self._prev_standstill = False self._onroad_time_delay: float | None = None @@ -78,8 +78,8 @@ def _scroll_to(self, layout: Widget): def _render(self, _): # Initial show event - if self._current_mode is None: - self._set_mode(MainState.MAIN) + # if self._current_mode is None: + # self._set_mode(MainState.MAIN) if not self._setup: if self._alerts_layout.active_alerts() > 0: @@ -89,20 +89,20 @@ def _render(self, _): self._setup = True # Render - if self._current_mode == MainState.MAIN: - self._scroller.render(self._rect) + # if self._current_mode == MainState.MAIN: + self._scroller.render(self._rect) - elif self._current_mode == MainState.SETTINGS: - self._settings_layout.render(self._rect) + # elif self._current_mode == MainState.SETTINGS: + # self._settings_layout.render(self._rect) self._handle_transitions() - def _set_mode(self, mode: MainState): - if mode != self._current_mode: - if self._current_mode is not None: - self._layouts[self._current_mode].hide_event() - self._layouts[mode].show_event() - self._current_mode = mode + # def _set_mode(self, mode: MainState): + # if mode != self._current_mode: + # if self._current_mode is not None: + # self._layouts[self._current_mode].hide_event() + # self._layouts[mode].show_event() + # self._current_mode = mode def _handle_transitions(self): if ui_state.started != self._prev_onroad: @@ -120,21 +120,25 @@ def _handle_transitions(self): CS = ui_state.sm["carState"] if not CS.standstill and self._prev_standstill: - self._set_mode(MainState.MAIN) + # self._set_mode(MainState.MAIN) + gui_app.pop_widget_to(self) self._scroll_to(self._onroad_layout) self._prev_standstill = CS.standstill def _set_mode_for_started(self, onroad_transition: bool = False): + print('_set_mode_for_started', onroad_transition) if ui_state.started: CS = ui_state.sm["carState"] # Only go onroad if car starts or is not at a standstill if not CS.standstill or onroad_transition: - self._set_mode(MainState.MAIN) + # self._set_mode(MainState.MAIN) + gui_app.pop_widget_to(self) self._scroll_to(self._onroad_layout) else: # Stay in settings if car turns off while in settings if not onroad_transition or self._current_mode != MainState.SETTINGS: - self._set_mode(MainState.MAIN) + # self._set_mode(MainState.MAIN) + gui_app.pop_widget_to(self) self._scroll_to(self._home_layout) def _on_settings_clicked(self): diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 1a83764afdd920..00191cae328b78 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -209,7 +209,8 @@ def __init__(self, wifi_manager, connect_callback: Callable, forget_callback: Ca self._subtitle = UnifiedLabel("", 36, FontWeight.ROMAN, rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)), alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) - self.set_back_callback(lambda: gui_app.set_modal_overlay(None)) + # self.set_back_callback(lambda: gui_app.set_modal_overlay(None)) + self.set_back_callback(gui_app.pop_widget) # State self._network: Network | None = None @@ -227,7 +228,8 @@ def update_networks(self, networks: dict[str, Network]): break else: # network disappeared, close page - gui_app.set_modal_overlay(None) + # gui_app.set_modal_overlay(None) + gui_app.pop_widget() def _update_state(self): super()._update_state() @@ -356,7 +358,8 @@ def hide_event(self): def _open_network_manage_page(self, result=None): self._network_info_page.update_networks(self._networks) - gui_app.set_modal_overlay(self._network_info_page) + # gui_app.set_modal_overlay(self._network_info_page) + gui_app.push_widget(self._network_info_page) def _forget_network(self, ssid: str): network = self._networks.get(ssid) @@ -429,7 +432,8 @@ def _on_need_auth(self, ssid, incorrect_password=True): dlg = BigInputDialog(hint, "", minimum_length=8, confirm_callback=lambda _password: self._connect_with_password(ssid, _password)) # go back to the manage network page - gui_app.set_modal_overlay(dlg, self._open_network_manage_page) + # gui_app.set_modal_overlay(dlg, self._open_network_manage_page) + gui_app.push_widget(dlg) def _on_activated(self): self._connecting = None diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 32dfb41bfc1210..ed5fa55f6e0f2b 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -402,6 +402,11 @@ def pop_widget(self): widget.hide_event() print() + def pop_widget_to(self, widget): + # pops all widgets after specified widget + while len(self._nav_stack.widgets) > 0 and self._nav_stack.widgets[-1] != widget: + self.pop_widget() + def set_modal_overlay(self, overlay, callback: Callable | None = None): print('WARNING! set_modal_overlay is DEPRECATED, but was attempted to use to show', overlay.__class__.__name__) if self._modal_overlay.overlay is not None: @@ -573,6 +578,7 @@ def render(self): # yield False # else: # yield True + yield True if self._render_texture: rl.end_texture_mode() From 0cdf32cd0af762286d666ce2b84e64c1d52b183e Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 5 Feb 2026 18:49:18 -0800 Subject: [PATCH 06/73] more device, not done yet --- selfdrive/ui/mici/layouts/settings/device.py | 25 ++++++++++++-------- selfdrive/ui/mici/widgets/dialog.py | 12 ++++------ selfdrive/ui/mici/widgets/pairing_dialog.py | 2 +- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index a85e96d0a34df4..843f2552a1bf4b 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -29,7 +29,7 @@ class MiciFccModal(NavWidget): def __init__(self, file_path: str | None = None, text: str | None = None): super().__init__() - self.set_back_callback(lambda: gui_app.set_modal_overlay(None)) + self.set_back_callback(gui_app.pop_widget) self._content = HtmlRenderer(file_path=file_path, text=text) self._scroll_panel = GuiScrollPanel2(horizontal=False) self._fcc_logo = gui_app.texture("icons_mici/settings/device/fcc_logo.png", 76, 64) @@ -75,10 +75,10 @@ def confirm_callback(): dlg: BigConfirmationDialogV2 | BigDialog = BigConfirmationDialogV2(f"slide to\n{action_text.lower()}", icon, red=red, exit_on_confirm=action_text == "reset", confirm_callback=confirm_callback) - gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) else: dlg = BigDialog(f"Disengage to {action_text}", "") - gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) class DeviceInfoLayoutMici(Widget): @@ -145,7 +145,7 @@ def _handle_mouse_release(self, mouse_pos: MousePos): dlg = BigDialog(tr("Device must be registered with the comma.ai backend to pair"), "") else: dlg = PairingDialog() - gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) UPDATER_TIMEOUT = 10.0 # seconds to wait for updater to respond @@ -171,7 +171,7 @@ def offroad_transition(self): def _handle_mouse_release(self, mouse_pos: MousePos): if not system_time_valid(): dlg = BigDialog(tr("Please connect to Wi-Fi to update"), "") - gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) return self.set_enabled(False) @@ -314,7 +314,7 @@ def selected_language_callback(): current_language = next(name for name, lang in self._languages.items() if lang == current_language_name) dlg = BigMultiOptionDialog(list(self._languages), default=current_language, right_btn_callback=selected_language_callback) - gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) # lang_button = BigButton("change language", "", "icons_mici/settings/device/language.png") # lang_button.set_click_callback(language_callback) @@ -354,7 +354,8 @@ def selected_language_callback(): def _on_regulatory(self): if not self._fcc_dialog: self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/mici_fcc.html")) - gui_app.set_modal_overlay(self._fcc_dialog, callback=setattr(self, '_fcc_dialog', None)) + # gui_app.set_modal_overlay(self._fcc_dialog, callback=setattr(self, '_fcc_dialog', None)) + gui_app.push_widget(self._fcc_dialog) def _offroad_transition(self): self._power_off_btn.set_visible(ui_state.is_offroad()) @@ -362,15 +363,19 @@ def _offroad_transition(self): def _show_driver_camera(self): if not self._driver_camera: self._driver_camera = DriverCameraDialog() - gui_app.set_modal_overlay(self._driver_camera, callback=lambda result: setattr(self, '_driver_camera', None)) + # gui_app.set_modal_overlay(self._driver_camera, callback=lambda result: setattr(self, '_driver_camera', None)) + gui_app.push_widget(self._driver_camera) def _on_review_training_guide(self): if not self._training_guide: def completed_callback(): - gui_app.set_modal_overlay(None) + # gui_app.set_modal_overlay(None) + gui_app.pop_widget() self._training_guide = TrainingGuide(completed_callback=completed_callback) - gui_app.set_modal_overlay(self._training_guide, callback=lambda result: setattr(self, '_training_guide', None)) + # TODO: callbacks. do we need them? + # gui_app.set_modal_overlay(self._training_guide, callback=lambda result: setattr(self, '_training_guide', None)) + gui_app.push_widget(self._training_guide) def _load_languages(self): with open(os.path.join(BASEDIR, "selfdrive/ui/translations/languages.json")) as f: diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 6d2898056a9d99..61f6876fdf4ab1 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -32,7 +32,8 @@ def __init__(self, right_btn: str | None = None, right_btn_callback: Callable | self._right_btn = None if right_btn: def right_btn_callback_wrapper(): - gui_app.set_modal_overlay(None) + # gui_app.set_modal_overlay(None) + gui_app.pop_widget() if right_btn_callback: right_btn_callback() @@ -42,16 +43,10 @@ def right_btn_callback_wrapper(): self._right_btn._rect.x = self._rect.x + self._rect.width - self._right_btn._rect.width def _render(self, _): - """ - Allows `gui_app.set_modal_overlay(BigDialog(...))`. - The overlay runner keeps calling until result != NO_ACTION. - """ if self._right_btn: self._right_btn.set_position(self._right_btn._rect.x, self._rect.y) self._right_btn.render() - # return self._ret - class BigDialog(BigDialogBase): def __init__(self, @@ -425,4 +420,5 @@ def _handle_mouse_release(self, mouse_pos: MousePos): super()._handle_mouse_release(mouse_pos) dlg = BigDialog(self.text, self._description) - gui_app.set_modal_overlay(dlg) + # gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) diff --git a/selfdrive/ui/mici/widgets/pairing_dialog.py b/selfdrive/ui/mici/widgets/pairing_dialog.py index 88bab2d00112bd..9121822f89568b 100644 --- a/selfdrive/ui/mici/widgets/pairing_dialog.py +++ b/selfdrive/ui/mici/widgets/pairing_dialog.py @@ -19,7 +19,7 @@ class PairingDialog(NavWidget): def __init__(self): super().__init__() - self.set_back_callback(lambda: gui_app.set_modal_overlay(None)) + self.set_back_callback(gui_app.pop_widget) self._params = Params() self._qr_texture: rl.Texture | None = None self._last_qr_generation = float("-inf") From fb263953f1336513c79deb3d79e118e16d3a94ab Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 5 Feb 2026 19:22:55 -0800 Subject: [PATCH 07/73] minor network fixes --- selfdrive/ui/mici/layouts/main.py | 9 +++--- .../mici/layouts/settings/network/__init__.py | 3 +- .../mici/layouts/settings/network/wifi_ui.py | 28 ++++++++----------- selfdrive/ui/mici/widgets/dialog.py | 1 - system/ui/lib/application.py | 7 ++++- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index 2fc71beb4dae68..1731564527be40 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -65,7 +65,8 @@ def __init__(self): # Start onboarding if terms or training not completed self._onboarding_window = OnboardingWindow() if not self._onboarding_window.completed: - gui_app.set_modal_overlay(self._onboarding_window) + # gui_app.set_modal_overlay(self._onboarding_window) + gui_app.push_widget(self._onboarding_window) def _setup_callbacks(self): self._home_layout.set_callbacks(on_settings=self._on_settings_clicked) @@ -121,7 +122,7 @@ def _handle_transitions(self): CS = ui_state.sm["carState"] if not CS.standstill and self._prev_standstill: # self._set_mode(MainState.MAIN) - gui_app.pop_widget_to(self) + gui_app.pop_widgets_to(self) self._scroll_to(self._onroad_layout) self._prev_standstill = CS.standstill @@ -132,13 +133,13 @@ def _set_mode_for_started(self, onroad_transition: bool = False): # Only go onroad if car starts or is not at a standstill if not CS.standstill or onroad_transition: # self._set_mode(MainState.MAIN) - gui_app.pop_widget_to(self) + gui_app.pop_widgets_to(self) self._scroll_to(self._onroad_layout) else: # Stay in settings if car turns off while in settings if not onroad_transition or self._current_mode != MainState.SETTINGS: # self._set_mode(MainState.MAIN) - gui_app.pop_widget_to(self) + gui_app.pop_widgets_to(self) self._scroll_to(self._home_layout) def _on_settings_clicked(self): diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index 80f4d964c874d4..7bf5b561b64b80 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -50,7 +50,6 @@ def tethering_password_clicked(): dlg = BigInputDialog("enter password...", tethering_password, minimum_length=8, confirm_callback=tethering_password_callback) gui_app.push_widget(dlg) - # gui_app.set_modal_overlay(dlg) txt_tethering = gui_app.texture("icons_mici/settings/network/tethering.png", 64, 54) self._tethering_password_btn = BigButton("tethering password", "", txt_tethering) @@ -147,7 +146,7 @@ def update_apn(apn: str): current_apn = ui_state.params.get("GsmApn") or "" dlg = BigInputDialog("enter APN", current_apn, minimum_length=0, confirm_callback=update_apn) - gui_app.set_modal_overlay(dlg) + gui_app.push_widget(dlg) def _toggle_cellular_metered(self, checked: bool): self._wifi_manager.update_gsm_settings(ui_state.params.get_bool("GsmRoaming"), ui_state.params.get("GsmApn") or "", checked) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 00191cae328b78..d6505d7f258dad 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -166,10 +166,9 @@ def _render(self, _): class ForgetButton(Widget): HORIZONTAL_MARGIN = 8 - def __init__(self, forget_network: Callable, open_network_manage_page): + def __init__(self, forget_network: Callable): super().__init__() self._forget_network = forget_network - self._open_network_manage_page = open_network_manage_page self._bg_txt = gui_app.texture("icons_mici/settings/network/new/forget_button.png", 100, 100) self._bg_pressed_txt = gui_app.texture("icons_mici/settings/network/new/forget_button_pressed.png", 100, 100) @@ -180,7 +179,7 @@ def _handle_mouse_release(self, mouse_pos: MousePos): super()._handle_mouse_release(mouse_pos) dlg = BigConfirmationDialogV2("slide to forget", "icons_mici/settings/network/new/trash.png", red=True, confirm_callback=self._forget_network) - gui_app.set_modal_overlay(dlg, callback=self._open_network_manage_page) + gui_app.push_widget(dlg) def _render(self, _): bg_txt = self._bg_pressed_txt if self.is_pressed else self._bg_txt @@ -192,15 +191,14 @@ def _render(self, _): class NetworkInfoPage(NavWidget): - def __init__(self, wifi_manager, connect_callback: Callable, forget_callback: Callable, open_network_manage_page: Callable): + def __init__(self, wifi_manager, connect_callback: Callable, forget_callback: Callable): super().__init__() self._wifi_manager = wifi_manager self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) self._wifi_icon = WifiIcon() - self._forget_btn = ForgetButton(lambda: forget_callback(self._network.ssid) if self._network is not None else None, - open_network_manage_page) + self._forget_btn = ForgetButton(lambda: forget_callback(self._network.ssid) if self._network is not None else None) self._connect_btn = ConnectButton() self._connect_btn.set_click_callback(lambda: connect_callback(self._network.ssid) if self._network is not None else None) @@ -228,8 +226,10 @@ def update_networks(self, networks: dict[str, Network]): break else: # network disappeared, close page + # FIXME: this closes wifi page # gui_app.set_modal_overlay(None) - gui_app.pop_widget() + if gui_app.get_active_widget() == self: + gui_app.pop_widget() def _update_state(self): super()._update_state() @@ -325,7 +325,7 @@ def __init__(self, wifi_manager: WifiManager): # Set up back navigation self.set_back_callback(gui_app.pop_widget) - self._network_info_page = NetworkInfoPage(wifi_manager, self._connect_to_network, self._forget_network, self._open_network_manage_page) + self._network_info_page = NetworkInfoPage(wifi_manager, self._connect_to_network, self._forget_network) self._network_info_page.set_connecting(lambda: self._connecting) self._loading_animation = LoadingAnimation() @@ -356,11 +356,6 @@ def hide_event(self): super().hide_event() self._wifi_manager.set_active(False) - def _open_network_manage_page(self, result=None): - self._network_info_page.update_networks(self._networks) - # gui_app.set_modal_overlay(self._network_info_page) - gui_app.push_widget(self._network_info_page) - def _forget_network(self, ssid: str): network = self._networks.get(ssid) if network is None: @@ -370,6 +365,7 @@ def _forget_network(self, ssid: str): self._wifi_manager.forget_connection(network.ssid) def _on_network_updated(self, networks: list[Network]): + print('WifiUIMici->_on_network_updated') self._networks = {network.ssid: network for network in networks} self._update_buttons() self._network_info_page.update_networks(self._networks) @@ -408,7 +404,9 @@ def _on_option_selected(self, option: str): if option in self._networks: self._network_info_page.set_current_network(self._networks[option]) - self._open_network_manage_page() + # self._network_info_page.update_networks(self._networks) + # gui_app.set_modal_overlay(self._network_info_page) + gui_app.push_widget(self._network_info_page) def _connect_to_network(self, ssid: str): network = self._networks.get(ssid) @@ -431,8 +429,6 @@ def _on_need_auth(self, ssid, incorrect_password=True): hint = "incorrect password..." if incorrect_password else "enter password..." dlg = BigInputDialog(hint, "", minimum_length=8, confirm_callback=lambda _password: self._connect_with_password(ssid, _password)) - # go back to the manage network page - # gui_app.set_modal_overlay(dlg, self._open_network_manage_page) gui_app.push_widget(dlg) def _on_activated(self): diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 61f6876fdf4ab1..053ede24ae47cc 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -420,5 +420,4 @@ def _handle_mouse_release(self, mouse_pos: MousePos): super()._handle_mouse_release(mouse_pos) dlg = BigDialog(self.text, self._description) - # gui_app.set_modal_overlay(dlg) gui_app.push_widget(dlg) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index ed5fa55f6e0f2b..84b7faf159fd23 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -402,11 +402,16 @@ def pop_widget(self): widget.hide_event() print() - def pop_widget_to(self, widget): + def pop_widgets_to(self, widget): # pops all widgets after specified widget while len(self._nav_stack.widgets) > 0 and self._nav_stack.widgets[-1] != widget: self.pop_widget() + def get_active_widget(self): + if len(self._nav_stack.widgets) > 0: + return self._nav_stack.widgets[-1] + return None + def set_modal_overlay(self, overlay, callback: Callable | None = None): print('WARNING! set_modal_overlay is DEPRECATED, but was attempted to use to show', overlay.__class__.__name__) if self._modal_overlay.overlay is not None: From 952d4060423286daf69e2a49ca08eadc34d1b759 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 5 Feb 2026 19:24:06 -0800 Subject: [PATCH 08/73] dcam dialog --- selfdrive/ui/mici/onroad/driver_camera_dialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/mici/onroad/driver_camera_dialog.py b/selfdrive/ui/mici/onroad/driver_camera_dialog.py index bab3d6e6f1df21..c55d2b7177c336 100644 --- a/selfdrive/ui/mici/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/mici/onroad/driver_camera_dialog.py @@ -34,8 +34,8 @@ def __init__(self, no_escape=False): self._pm: messaging.PubMaster | None = None if not no_escape: # TODO: this can grow unbounded, should be given some thought - device.add_interactive_timeout_callback(lambda: gui_app.set_modal_overlay(None)) - self.set_back_callback(lambda: gui_app.set_modal_overlay(None)) + device.add_interactive_timeout_callback(gui_app.pop_widget) + self.set_back_callback(gui_app.pop_widget) self.set_back_enabled(not no_escape) # Load eye icons From ebe52daaa677520aeb8e2b41202d13d2a19f7da6 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 5 Feb 2026 19:27:50 -0800 Subject: [PATCH 09/73] start onboarding --- selfdrive/ui/mici/layouts/main.py | 2 ++ selfdrive/ui/mici/layouts/onboarding.py | 6 ++++-- selfdrive/ui/ui.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index 1731564527be40..b3ffc92fbeb71c 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -62,6 +62,8 @@ def __init__(self): # Set callbacks self._setup_callbacks() + gui_app.push_widget(self) + # Start onboarding if terms or training not completed self._onboarding_window = OnboardingWindow() if not self._onboarding_window.completed: diff --git a/selfdrive/ui/mici/layouts/onboarding.py b/selfdrive/ui/mici/layouts/onboarding.py index 4248fef2ecc957..d480209eeb5c73 100644 --- a/selfdrive/ui/mici/layouts/onboarding.py +++ b/selfdrive/ui/mici/layouts/onboarding.py @@ -7,7 +7,7 @@ from openpilot.common.filter_simple import FirstOrderFilter from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.application import FontWeight, gui_app -from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets import Widget, NavWidget from openpilot.system.ui.widgets.button import SmallButton, SmallCircleIconButton from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.slider import SmallSlider @@ -435,9 +435,11 @@ def _render_content(self, scroll_offset): )) -class OnboardingWindow(Widget): +class OnboardingWindow(NavWidget): def __init__(self): super().__init__() + self.set_back_enabled(False) + self._accepted_terms: bool = ui_state.params.get("HasAcceptedTerms") == terms_version self._training_done: bool = ui_state.params.get("CompletedTrainingVersion") == training_version diff --git a/selfdrive/ui/ui.py b/selfdrive/ui/ui.py index 4a402f9ef48ddb..de02381995e10c 100755 --- a/selfdrive/ui/ui.py +++ b/selfdrive/ui/ui.py @@ -20,7 +20,7 @@ def main(): else: main_layout = MiciMainLayout() main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - gui_app.push_widget(main_layout) + # gui_app.push_widget(main_layout) for should_render in gui_app.render(): ui_state.update() if should_render: From 3925b49a8d993c744ffd4665a6f9ada06438d7ca Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 5 Feb 2026 20:09:33 -0800 Subject: [PATCH 10/73] fix onboarding --- selfdrive/ui/mici/layouts/main.py | 7 ++++--- selfdrive/ui/mici/layouts/onboarding.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index b3ffc92fbeb71c..327a5c99668923 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -98,7 +98,8 @@ def _render(self, _): # elif self._current_mode == MainState.SETTINGS: # self._settings_layout.render(self._rect) - self._handle_transitions() + if self.enabled: + self._handle_transitions() # def _set_mode(self, mode: MainState): # if mode != self._current_mode: @@ -129,7 +130,7 @@ def _handle_transitions(self): self._prev_standstill = CS.standstill def _set_mode_for_started(self, onroad_transition: bool = False): - print('_set_mode_for_started', onroad_transition) + print(f'_set_mode_for_started, {ui_state.started=}, {onroad_transition=}') if ui_state.started: CS = ui_state.sm["carState"] # Only go onroad if car starts or is not at a standstill @@ -139,7 +140,7 @@ def _set_mode_for_started(self, onroad_transition: bool = False): self._scroll_to(self._onroad_layout) else: # Stay in settings if car turns off while in settings - if not onroad_transition or self._current_mode != MainState.SETTINGS: + if not onroad_transition or gui_app.get_active_widget() != self: # self._set_mode(MainState.MAIN) gui_app.pop_widgets_to(self) self._scroll_to(self._home_layout) diff --git a/selfdrive/ui/mici/layouts/onboarding.py b/selfdrive/ui/mici/layouts/onboarding.py index d480209eeb5c73..7ae6ff2924966b 100644 --- a/selfdrive/ui/mici/layouts/onboarding.py +++ b/selfdrive/ui/mici/layouts/onboarding.py @@ -472,7 +472,7 @@ def _on_decline_back(self): def close(self): ui_state.params.put_bool("IsDriverViewEnabled", False) - gui_app.set_modal_overlay(None) + gui_app.pop_widget() def _on_terms_accepted(self): ui_state.params.put("HasAcceptedTerms", terms_version) From afed1e84f83cd9f7204b849aaaefbe3962510f47 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 5 Feb 2026 23:33:48 -0800 Subject: [PATCH 11/73] do mici setup --- selfdrive/ui/ui_state.py | 1 + system/ui/lib/application.py | 2 + system/ui/mici_setup.py | 124 +++++++++++++++++++++-------------- 3 files changed, 77 insertions(+), 50 deletions(-) diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index 30a656509551e0..1874643a53b54e 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -208,6 +208,7 @@ def set_override_interactive_timeout(self, timeout: int | None) -> None: @property def interactive_timeout(self) -> int: + return 5 if self._override_interactive_timeout is not None: return self._override_interactive_timeout diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 84b7faf159fd23..0f688d89459af9 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -575,6 +575,8 @@ def render(self): if len(self._nav_stack.widgets) > 0: self._nav_stack.widgets[-1].render(rl.Rectangle(0, 0, self.width, self.height)) + print('widget stack', len(self._nav_stack.widgets), [w.__class__.__name__ for w in self._nav_stack.widgets]) + # # Handle modal overlay rendering and input processing # if self._handle_modal_overlay(): # # Allow a Widget to still run a function while overlay is shown diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index fac26f06eac718..9898c46b8bf54a 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -427,15 +427,16 @@ def _render(self, rect: rl.Rectangle): )) -class NetworkSetupState(IntEnum): - MAIN = 0 - WIFI_PANEL = 1 +# class NetworkSetupState(IntEnum): +# MAIN = 0 +# WIFI_PANEL = 1 class NetworkSetupPage(Widget): def __init__(self, wifi_manager, continue_callback: Callable, back_callback: Callable): super().__init__() - self._wifi_ui = WifiUIMici(wifi_manager, back_callback=lambda: self.set_state(NetworkSetupState.MAIN)) + # self._wifi_ui = WifiUIMici(wifi_manager, back_callback=lambda: self.set_state(NetworkSetupState.MAIN)) + self._wifi_ui = WifiUIMici(wifi_manager) self._no_wifi_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 58, 50) self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 58, 50) @@ -447,37 +448,41 @@ def __init__(self, wifi_manager, continue_callback: Callable, back_callback: Cal self._back_button.set_click_callback(back_callback) self._wifi_button = SmallerRoundedButton("wifi") - self._wifi_button.set_click_callback(lambda: self.set_state(NetworkSetupState.WIFI_PANEL)) + # self._wifi_button.set_click_callback(lambda: self.set_state(NetworkSetupState.WIFI_PANEL)) + self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui)) + self._wifi_button.set_enabled(lambda: self.enabled) self._continue_button = WidishRoundedButton("continue") self._continue_button.set_enabled(False) self._continue_button.set_click_callback(continue_callback) - self._state = NetworkSetupState.MAIN + # self._state = NetworkSetupState.MAIN self._prev_has_internet = False - def set_state(self, state: NetworkSetupState): - self._state = state - if state == NetworkSetupState.WIFI_PANEL: - self._wifi_ui.show_event() + # def set_state(self, state: NetworkSetupState): + # self._state = state + # if state == NetworkSetupState.WIFI_PANEL: + # self._wifi_ui.show_event() def set_has_internet(self, has_internet: bool): if has_internet: self._network_header.set_title("connected to internet") self._network_header.set_icon(self._wifi_full_txt) - self._continue_button.set_enabled(True) + self._continue_button.set_enabled(self.enabled) else: self._network_header.set_title(self._waiting_text) self._network_header.set_icon(self._no_wifi_txt) self._continue_button.set_enabled(False) if has_internet and not self._prev_has_internet: - self.set_state(NetworkSetupState.MAIN) + pass + # self.set_state(NetworkSetupState.MAIN) + # gui_app.pop_widgets_to(self) self._prev_has_internet = has_internet def show_event(self): super().show_event() - self._state = NetworkSetupState.MAIN + # self._state = NetworkSetupState.MAIN self._wifi_ui.show_event() def hide_event(self): @@ -485,36 +490,36 @@ def hide_event(self): self._wifi_ui.hide_event() def _render(self, _): - if self._state == NetworkSetupState.MAIN: - self._network_header.render(rl.Rectangle( - self._rect.x + 16, - self._rect.y + 16, - self._rect.width - 32, - self._network_header.rect.height, - )) - - self._back_button.render(rl.Rectangle( - self._rect.x + 8, - self._rect.y + self._rect.height - self._back_button.rect.height, - self._back_button.rect.width, - self._back_button.rect.height, - )) - - self._wifi_button.render(rl.Rectangle( - self._rect.x + 8 + self._back_button.rect.width + 10, - self._rect.y + self._rect.height - self._wifi_button.rect.height, - self._wifi_button.rect.width, - self._wifi_button.rect.height, - )) - - self._continue_button.render(rl.Rectangle( - self._rect.x + self._rect.width - self._continue_button.rect.width - 8, - self._rect.y + self._rect.height - self._continue_button.rect.height, - self._continue_button.rect.width, - self._continue_button.rect.height, - )) - else: - self._wifi_ui.render(self._rect) + # if self._state == NetworkSetupState.MAIN: + self._network_header.render(rl.Rectangle( + self._rect.x + 16, + self._rect.y + 16, + self._rect.width - 32, + self._network_header.rect.height, + )) + + self._back_button.render(rl.Rectangle( + self._rect.x + 8, + self._rect.y + self._rect.height - self._back_button.rect.height, + self._back_button.rect.width, + self._back_button.rect.height, + )) + + self._wifi_button.render(rl.Rectangle( + self._rect.x + 8 + self._back_button.rect.width + 10, + self._rect.y + self._rect.height - self._wifi_button.rect.height, + self._wifi_button.rect.width, + self._wifi_button.rect.height, + )) + + self._continue_button.render(rl.Rectangle( + self._rect.x + self._rect.width - self._continue_button.rect.width - 8, + self._rect.y + self._rect.height - self._continue_button.rect.height, + self._continue_button.rect.width, + self._continue_button.rect.height, + )) + # else: + # self._wifi_ui.render(self._rect) class Setup(Widget): @@ -572,6 +577,7 @@ def _set_state(self, state: SetupState): self._network_setup_page.hide_event() def _render(self, rect: rl.Rectangle): + print(f"Rendering state: {self.state.name}") if self.state == SetupState.GETTING_STARTED: self._start_page.render(rect) elif self.state in (SetupState.NETWORK_SETUP, SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE): @@ -618,7 +624,21 @@ def _network_setup_continue_button_callback(self): if self.state == SetupState.NETWORK_SETUP: self.download(OPENPILOT_URL) elif self.state == SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE: - self._set_state(SetupState.CUSTOM_SOFTWARE) + # Don't leave state since we're pushing keyboard widget onto the stack + # self._set_state(SetupState.CUSTOM_SOFTWARE) + + def handle_keyboard_result(text): + url = text.strip() + if url: + self.download(url) + + # def handle_keyboard_exit(result): + # if result == DialogResult.CANCEL: + # self._set_state(SetupState.SOFTWARE_SELECTION) + + keyboard = BigInputDialog("custom software URL", confirm_callback=handle_keyboard_result) + # gui_app.set_modal_overlay(keyboard, callback=handle_keyboard_exit) + gui_app.push_widget(keyboard) def close(self): self._network_monitor.stop() @@ -639,12 +659,14 @@ def handle_keyboard_result(text): if url: self.download(url) - def handle_keyboard_exit(result): - if result == DialogResult.CANCEL: - self._set_state(SetupState.SOFTWARE_SELECTION) + # def handle_keyboard_exit(result): + # if result == DialogResult.CANCEL: + # self._set_state(SetupState.SOFTWARE_SELECTION) keyboard = BigInputDialog("custom software URL", confirm_callback=handle_keyboard_result) - gui_app.set_modal_overlay(keyboard, callback=handle_keyboard_exit) + # gui_app.set_modal_overlay(keyboard, callback=handle_keyboard_exit) + gui_app.push_widget(keyboard) + self._set_state(SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE) def use_openpilot(self): if os.path.isdir(INSTALL_PATH) and os.path.isfile(VALID_CACHE_PATH): @@ -742,9 +764,11 @@ def main(): try: gui_app.init_window("Setup") setup = Setup() + gui_app.push_widget(setup) for should_render in gui_app.render(): - if should_render: - setup.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + pass + # if should_render: + # setup.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) setup.close() except Exception as e: print(f"Setup error: {e}") From c13f4f0309b9b20b7713c8577b09997f63015cbc Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 5 Feb 2026 23:44:46 -0800 Subject: [PATCH 12/73] remove now useless CUSTOM_SOFTWARE --- system/ui/mici_setup.py | 33 ++++----------------------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 8ea58acc099ecc..ffa0a59f6cb35b 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -96,10 +96,9 @@ class SetupState(IntEnum): NETWORK_SETUP = 1 NETWORK_SETUP_CUSTOM_SOFTWARE = 2 SOFTWARE_SELECTION = 3 - CUSTOM_SOFTWARE = 4 - DOWNLOADING = 5 - DOWNLOAD_FAILED = 6 - CUSTOM_SOFTWARE_WARNING = 7 + DOWNLOADING = 4 + DOWNLOAD_FAILED = 5 + CUSTOM_SOFTWARE_WARNING = 6 class StartPage(Widget): @@ -536,6 +535,7 @@ def __init__(self): self._network_monitor = NetworkConnectivityMonitor() self._network_monitor.start() self._prev_has_internet = False + # TODO: fix this if broken gui_app.set_modal_overlay_tick(self._modal_overlay_tick) self._start_page = StartPage() @@ -586,8 +586,6 @@ def _render(self, rect: rl.Rectangle): self._software_selection_page.render(rect) elif self.state == SetupState.CUSTOM_SOFTWARE_WARNING: self._custom_software_warning_page.render(rect) - elif self.state == SetupState.CUSTOM_SOFTWARE: - self.render_custom_software() elif self.state == SetupState.DOWNLOADING: self.render_downloading(rect) elif self.state == SetupState.DOWNLOAD_FAILED: @@ -618,20 +616,12 @@ def _network_setup_continue_button_callback(self): if self.state == SetupState.NETWORK_SETUP: self.download(OPENPILOT_URL) elif self.state == SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE: - # Don't leave state since we're pushing keyboard widget onto the stack - # self._set_state(SetupState.CUSTOM_SOFTWARE) - def handle_keyboard_result(text): url = text.strip() if url: self.download(url) - # def handle_keyboard_exit(result): - # if result == DialogResult.CANCEL: - # self._set_state(SetupState.SOFTWARE_SELECTION) - keyboard = BigInputDialog("custom software URL", confirm_callback=handle_keyboard_result) - # gui_app.set_modal_overlay(keyboard, callback=handle_keyboard_exit) gui_app.push_widget(keyboard) def close(self): @@ -647,21 +637,6 @@ def render_downloading(self, rect: rl.Rectangle): self._downloading_page.set_progress(self.download_progress) self._downloading_page.render(rect) - def render_custom_software(self): - def handle_keyboard_result(text): - url = text.strip() - if url: - self.download(url) - - # def handle_keyboard_exit(result): - # if result == DialogResult.CANCEL: - # self._set_state(SetupState.SOFTWARE_SELECTION) - - keyboard = BigInputDialog("custom software URL", confirm_callback=handle_keyboard_result) - # gui_app.set_modal_overlay(keyboard, callback=handle_keyboard_exit) - gui_app.push_widget(keyboard) - self._set_state(SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE) - def use_openpilot(self): if os.path.isdir(INSTALL_PATH) and os.path.isfile(VALID_CACHE_PATH): os.remove(VALID_CACHE_PATH) From 96a254439bf58e2fbac8c8a9982aa71e33e58b2e Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 5 Feb 2026 23:56:49 -0800 Subject: [PATCH 13/73] support big ui with old modal overlay --- selfdrive/ui/mici/layouts/main.py | 2 +- selfdrive/ui/ui.py | 11 ++++--- system/ui/lib/application.py | 48 ++++++++++++++++++++----------- 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index 327a5c99668923..fc7712307df124 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -64,7 +64,7 @@ def __init__(self): gui_app.push_widget(self) - # Start onboarding if terms or training not completed + # Start onboarding if terms or training not completed, make sure to push after self self._onboarding_window = OnboardingWindow() if not self._onboarding_window.completed: # gui_app.set_modal_overlay(self._onboarding_window) diff --git a/selfdrive/ui/ui.py b/selfdrive/ui/ui.py index de02381995e10c..a4cc7c77404f58 100755 --- a/selfdrive/ui/ui.py +++ b/selfdrive/ui/ui.py @@ -9,22 +9,25 @@ from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout from openpilot.selfdrive.ui.ui_state import ui_state +BIG_UI = gui_app.big_ui() + def main(): cores = {5, } config_realtime_process(0, 51) - gui_app.init_window("UI") - if gui_app.big_ui(): + if BIG_UI: + gui_app.init_window("UI", old_modal=True) main_layout = MainLayout() else: + gui_app.init_window("UI") main_layout = MiciMainLayout() main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - # gui_app.push_widget(main_layout) for should_render in gui_app.render(): ui_state.update() if should_render: - # main_layout.render() + if BIG_UI: + main_layout.render() # reaffine after power save offlines our core if TICI and os.sched_getaffinity(0) != cores: diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 0f688d89459af9..3eca5acd7173c5 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -219,6 +219,8 @@ def __init__(self, width: int | None = None, height: int | None = None): self._scaled_width += self._scaled_width % 2 self._scaled_height += self._scaled_height % 2 + # TODO: move BIG ui over and deprecate + self._old_modal = False self._render_texture: rl.RenderTexture | None = None self._burn_in_shader: rl.Shader | None = None self._ffmpeg_proc: subprocess.Popen | None = None @@ -267,7 +269,7 @@ def target_fps(self): def request_close(self): self._window_close_requested = True - def init_window(self, title: str, fps: int = _DEFAULT_FPS): + def init_window(self, title: str, fps: int = _DEFAULT_FPS, old_modal: bool = False): with self._startup_profile_context(): def _close(sig, frame): self.close() @@ -275,6 +277,8 @@ def _close(sig, frame): signal.signal(signal.SIGINT, _close) atexit.register(self.close) + self._old_modal = old_modal + flags = rl.ConfigFlags.FLAG_MSAA_4X_HINT if ENABLE_VSYNC: flags |= rl.ConfigFlags.FLAG_VSYNC_HINT @@ -377,6 +381,8 @@ def _ffmpeg_writer_thread(self): break def push_widget(self, widget): + assert not self._old_modal + # disable previous widget to prevent input processing, but keep rendering for smooth transitions if len(self._nav_stack.widgets) > 0: prev_widget = self._nav_stack.widgets[-1] @@ -390,6 +396,8 @@ def push_widget(self, widget): print() def pop_widget(self): + assert not self._old_modal + # reenable previous widget if exists and show event to allow it to update state if needed (e.g. refresh after settings change) if len(self._nav_stack.widgets) > 1: prev_widget = self._nav_stack.widgets[-2] @@ -403,6 +411,8 @@ def pop_widget(self): print() def pop_widgets_to(self, widget): + assert not self._old_modal + # pops all widgets after specified widget while len(self._nav_stack.widgets) > 0 and self._nav_stack.widgets[-1] != widget: self.pop_widget() @@ -413,7 +423,8 @@ def get_active_widget(self): return None def set_modal_overlay(self, overlay, callback: Callable | None = None): - print('WARNING! set_modal_overlay is DEPRECATED, but was attempted to use to show', overlay.__class__.__name__) + assert self._old_modal, "set_modal_overlay is deprecated, use push_widget instead" + if self._modal_overlay.overlay is not None: if hasattr(self._modal_overlay.overlay, 'hide_event'): self._modal_overlay.overlay.hide_event() @@ -568,24 +579,27 @@ def render(self): rl.begin_drawing() rl.clear_background(rl.BLACK) - if len(self._nav_stack.widgets) > 1: - self._nav_stack.widgets[-2].render(rl.Rectangle(0, 0, self.width, self.height)) - rl.draw_rectangle(0, 0, self.width, self.height, rl.Color(0, 0, 0, 150)) + if not self._old_modal: + if len(self._nav_stack.widgets) > 1: + self._nav_stack.widgets[-2].render(rl.Rectangle(0, 0, self.width, self.height)) + rl.draw_rectangle(0, 0, self.width, self.height, rl.Color(0, 0, 0, 150)) + + if len(self._nav_stack.widgets) > 0: + self._nav_stack.widgets[-1].render(rl.Rectangle(0, 0, self.width, self.height)) - if len(self._nav_stack.widgets) > 0: - self._nav_stack.widgets[-1].render(rl.Rectangle(0, 0, self.width, self.height)) + print('widget stack', len(self._nav_stack.widgets), [w.__class__.__name__ for w in self._nav_stack.widgets]) - print('widget stack', len(self._nav_stack.widgets), [w.__class__.__name__ for w in self._nav_stack.widgets]) + yield True - # # Handle modal overlay rendering and input processing - # if self._handle_modal_overlay(): - # # Allow a Widget to still run a function while overlay is shown - # if self._modal_overlay_tick is not None: - # self._modal_overlay_tick() - # yield False - # else: - # yield True - yield True + else: + # Handle modal overlay rendering and input processing + if self._handle_modal_overlay(): + # Allow a Widget to still run a function while overlay is shown + if self._modal_overlay_tick is not None: + self._modal_overlay_tick() + yield False + else: + yield True if self._render_texture: rl.end_texture_mode() From 391c91132215094076ee30d6a63f8ff307f7b9f1 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 6 Feb 2026 00:02:23 -0800 Subject: [PATCH 14/73] reset can be old modal overlay, but updater needs new since it uses wifiui --- selfdrive/ui/ui.py | 4 ++-- system/ui/lib/application.py | 4 ++-- system/ui/mici_setup.py | 2 +- system/ui/mici_updater.py | 10 ++++++---- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/selfdrive/ui/ui.py b/selfdrive/ui/ui.py index a4cc7c77404f58..007be85ad81b04 100755 --- a/selfdrive/ui/ui.py +++ b/selfdrive/ui/ui.py @@ -17,10 +17,10 @@ def main(): config_realtime_process(0, 51) if BIG_UI: - gui_app.init_window("UI", old_modal=True) + gui_app.init_window("UI") main_layout = MainLayout() else: - gui_app.init_window("UI") + gui_app.init_window("UI", old_modal=False) main_layout = MiciMainLayout() main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) for should_render in gui_app.render(): diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 3eca5acd7173c5..7af6245957a8af 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -220,7 +220,7 @@ def __init__(self, width: int | None = None, height: int | None = None): self._scaled_height += self._scaled_height % 2 # TODO: move BIG ui over and deprecate - self._old_modal = False + self._old_modal = True self._render_texture: rl.RenderTexture | None = None self._burn_in_shader: rl.Shader | None = None self._ffmpeg_proc: subprocess.Popen | None = None @@ -269,7 +269,7 @@ def target_fps(self): def request_close(self): self._window_close_requested = True - def init_window(self, title: str, fps: int = _DEFAULT_FPS, old_modal: bool = False): + def init_window(self, title: str, fps: int = _DEFAULT_FPS, old_modal: bool = True): with self._startup_profile_context(): def _close(sig, frame): self.close() diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index ffa0a59f6cb35b..2705f81761d794 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -731,7 +731,7 @@ def download_failed(self, url: str, reason: str): def main(): try: - gui_app.init_window("Setup") + gui_app.init_window("Setup", old_modal=False) setup = Setup() gui_app.push_widget(setup) for should_render in gui_app.render(): diff --git a/system/ui/mici_updater.py b/system/ui/mici_updater.py index 7ebb4262ff9501..e0a6fd3256a0df 100755 --- a/system/ui/mici_updater.py +++ b/system/ui/mici_updater.py @@ -185,11 +185,13 @@ def main(): manifest_path = sys.argv[2] try: - gui_app.init_window("System Update") + gui_app.init_window("System Update", old_modal=False) updater = Updater(updater_path, manifest_path) - for should_render in gui_app.render(): - if should_render: - updater.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + gui_app.push_widget(updater) + for _ in gui_app.render(): + pass + # if should_render: + # updater.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) updater.close() except Exception as e: print(f"Updater error: {e}") From 14962b5886c1e6517654ae970465f8f136464620 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 6 Feb 2026 00:04:12 -0800 Subject: [PATCH 15/73] flip name truthiness to inspire excitement --- selfdrive/ui/ui.py | 2 +- system/ui/lib/application.py | 18 ++++++++++-------- system/ui/mici_setup.py | 4 ++-- system/ui/mici_updater.py | 2 +- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/selfdrive/ui/ui.py b/selfdrive/ui/ui.py index 007be85ad81b04..7e242cf41dec87 100755 --- a/selfdrive/ui/ui.py +++ b/selfdrive/ui/ui.py @@ -20,7 +20,7 @@ def main(): gui_app.init_window("UI") main_layout = MainLayout() else: - gui_app.init_window("UI", old_modal=False) + gui_app.init_window("UI", new_modal=True) main_layout = MiciMainLayout() main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) for should_render in gui_app.render(): diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 7af6245957a8af..63c51f43acea7e 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -220,7 +220,7 @@ def __init__(self, width: int | None = None, height: int | None = None): self._scaled_height += self._scaled_height % 2 # TODO: move BIG ui over and deprecate - self._old_modal = True + self._new_modal = False self._render_texture: rl.RenderTexture | None = None self._burn_in_shader: rl.Shader | None = None self._ffmpeg_proc: subprocess.Popen | None = None @@ -269,7 +269,7 @@ def target_fps(self): def request_close(self): self._window_close_requested = True - def init_window(self, title: str, fps: int = _DEFAULT_FPS, old_modal: bool = True): + def init_window(self, title: str, fps: int = _DEFAULT_FPS, new_modal: bool = False): with self._startup_profile_context(): def _close(sig, frame): self.close() @@ -277,7 +277,7 @@ def _close(sig, frame): signal.signal(signal.SIGINT, _close) atexit.register(self.close) - self._old_modal = old_modal + self._new_modal = new_modal flags = rl.ConfigFlags.FLAG_MSAA_4X_HINT if ENABLE_VSYNC: @@ -381,7 +381,7 @@ def _ffmpeg_writer_thread(self): break def push_widget(self, widget): - assert not self._old_modal + assert self._new_modal # disable previous widget to prevent input processing, but keep rendering for smooth transitions if len(self._nav_stack.widgets) > 0: @@ -396,7 +396,7 @@ def push_widget(self, widget): print() def pop_widget(self): - assert not self._old_modal + assert self._new_modal # reenable previous widget if exists and show event to allow it to update state if needed (e.g. refresh after settings change) if len(self._nav_stack.widgets) > 1: @@ -411,19 +411,21 @@ def pop_widget(self): print() def pop_widgets_to(self, widget): - assert not self._old_modal + assert self._new_modal # pops all widgets after specified widget while len(self._nav_stack.widgets) > 0 and self._nav_stack.widgets[-1] != widget: self.pop_widget() def get_active_widget(self): + assert self._new_modal + if len(self._nav_stack.widgets) > 0: return self._nav_stack.widgets[-1] return None def set_modal_overlay(self, overlay, callback: Callable | None = None): - assert self._old_modal, "set_modal_overlay is deprecated, use push_widget instead" + assert not self._new_modal, "set_modal_overlay is deprecated, use push_widget instead" if self._modal_overlay.overlay is not None: if hasattr(self._modal_overlay.overlay, 'hide_event'): @@ -579,7 +581,7 @@ def render(self): rl.begin_drawing() rl.clear_background(rl.BLACK) - if not self._old_modal: + if self._new_modal: if len(self._nav_stack.widgets) > 1: self._nav_stack.widgets[-2].render(rl.Rectangle(0, 0, self.width, self.height)) rl.draw_rectangle(0, 0, self.width, self.height, rl.Color(0, 0, 0, 150)) diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 2705f81761d794..156f16f1f1f470 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -731,10 +731,10 @@ def download_failed(self, url: str, reason: str): def main(): try: - gui_app.init_window("Setup", old_modal=False) + gui_app.init_window("Setup", new_modal=True) setup = Setup() gui_app.push_widget(setup) - for should_render in gui_app.render(): + for _ in gui_app.render(): pass # if should_render: # setup.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) diff --git a/system/ui/mici_updater.py b/system/ui/mici_updater.py index e0a6fd3256a0df..bf49e109a891af 100755 --- a/system/ui/mici_updater.py +++ b/system/ui/mici_updater.py @@ -185,7 +185,7 @@ def main(): manifest_path = sys.argv[2] try: - gui_app.init_window("System Update", old_modal=False) + gui_app.init_window("System Update", new_modal=True) updater = Updater(updater_path, manifest_path) gui_app.push_widget(updater) for _ in gui_app.render(): From a0a905743a828354efa430eb6bdb5876b5d6aba7 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 6 Feb 2026 00:19:37 -0800 Subject: [PATCH 16/73] all *should* work, but will do pass later --- selfdrive/ui/mici/onroad/augmented_road_view.py | 2 +- selfdrive/ui/mici/onroad/driver_camera_dialog.py | 5 +++-- selfdrive/ui/mici/widgets/pairing_dialog.py | 11 +++++++---- selfdrive/ui/tests/diff/replay.py | 7 ++----- selfdrive/ui/tests/profile_onroad.py | 6 ++---- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/selfdrive/ui/mici/onroad/augmented_road_view.py b/selfdrive/ui/mici/onroad/augmented_road_view.py index 69bcca401d91a6..99e33e8644d580 100644 --- a/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -363,7 +363,7 @@ def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray: if __name__ == "__main__": gui_app.init_window("OnRoad Camera View") - road_camera_view = AugmentedRoadView(ROAD_CAM) + road_camera_view = AugmentedRoadView(lambda: None, stream_type=ROAD_CAM) print("***press space to switch camera view***") try: for _ in gui_app.render(): diff --git a/selfdrive/ui/mici/onroad/driver_camera_dialog.py b/selfdrive/ui/mici/onroad/driver_camera_dialog.py index c55d2b7177c336..9bfb171d6b6d1d 100644 --- a/selfdrive/ui/mici/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/mici/onroad/driver_camera_dialog.py @@ -232,12 +232,13 @@ def _draw_eyes(self, rect: rl.Rectangle, driver_data): if __name__ == "__main__": - gui_app.init_window("Driver Camera View (mici)") + gui_app.init_window("Driver Camera View (mici)", new_modal=True) driver_camera_view = DriverCameraDialog() + gui_app.push_widget(driver_camera_view) try: for _ in gui_app.render(): ui_state.update() - driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + # driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) finally: driver_camera_view.close() diff --git a/selfdrive/ui/mici/widgets/pairing_dialog.py b/selfdrive/ui/mici/widgets/pairing_dialog.py index 9121822f89568b..56b87de8a19dc0 100644 --- a/selfdrive/ui/mici/widgets/pairing_dialog.py +++ b/selfdrive/ui/mici/widgets/pairing_dialog.py @@ -105,12 +105,15 @@ def __del__(self): if __name__ == "__main__": - gui_app.init_window("pairing device") + gui_app.init_window("pairing device", new_modal=True) pairing = PairingDialog() + gui_app.push_widget(pairing) + # TODO: black screen? try: for _ in gui_app.render(): - result = pairing.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - if result != -1: - break + pass + # result = pairing.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + # if result != -1: + # break finally: del pairing diff --git a/selfdrive/ui/tests/diff/replay.py b/selfdrive/ui/tests/diff/replay.py index 9da157660e6cf0..2ac2da4d8856d1 100755 --- a/selfdrive/ui/tests/diff/replay.py +++ b/selfdrive/ui/tests/diff/replay.py @@ -84,14 +84,14 @@ def run_replay(): if not HEADLESS: rl.set_config_flags(rl.FLAG_WINDOW_HIDDEN) - gui_app.init_window("ui diff test", fps=FPS) + gui_app.init_window("ui diff test", fps=FPS, new_modal=True) main_layout = MiciMainLayout() main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) frame = 0 script_index = 0 - for should_render in gui_app.render(): + for _ in gui_app.render(): while script_index < len(SCRIPT) and SCRIPT[script_index][0] == frame: _, event = SCRIPT[script_index] handle_event(event) @@ -99,9 +99,6 @@ def run_replay(): ui_state.update() - if should_render: - main_layout.render() - frame += 1 if script_index >= len(SCRIPT): diff --git a/selfdrive/ui/tests/profile_onroad.py b/selfdrive/ui/tests/profile_onroad.py index fde4f25ffed40d..e14bc958353901 100755 --- a/selfdrive/ui/tests/profile_onroad.py +++ b/selfdrive/ui/tests/profile_onroad.py @@ -81,7 +81,7 @@ def mock_update(timeout=None): if args.headless: os.environ['SDL_VIDEODRIVER'] = 'dummy' - gui_app.init_window("UI Profiling", fps=600) + gui_app.init_window("UI Profiling", fps=600, new_modal=True) main_layout = MiciMainLayout() main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) @@ -95,15 +95,13 @@ def mock_update(timeout=None): yuv_buffer_size = W * H + (W // 2) * (H // 2) * 2 yuv_data = np.random.randint(0, 256, yuv_buffer_size, dtype=np.uint8).tobytes() with cProfile.Profile() as pr: - for should_render in gui_app.render(): + for _ in gui_app.render(): if ui_state.sm.frame >= len(message_chunks): break if ui_state.sm.frame % 3 == 0: eof = int((ui_state.sm.frame % 3) * 0.05 * 1e9) vipc.send(VisionStreamType.VISION_STREAM_ROAD, yuv_data, ui_state.sm.frame % 3, eof, eof) ui_state.update() - if should_render: - main_layout.render() pr.dump_stats(f'{args.output}_deterministic.stats') rl.close_window() From 6cac211a5e85af86f580be9a31473f9da417c968 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 6 Feb 2026 00:23:19 -0800 Subject: [PATCH 17/73] clean up main --- selfdrive/ui/mici/layouts/main.py | 39 ++----------------------------- 1 file changed, 2 insertions(+), 37 deletions(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index fc7712307df124..7421e64cd18189 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -1,5 +1,4 @@ import pyray as rl -from enum import IntEnum import cereal.messaging as messaging from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsLayout @@ -15,11 +14,6 @@ ONROAD_DELAY = 2.5 # seconds -class MainState(IntEnum): - MAIN = 0 - SETTINGS = 1 - - class MiciMainLayout(Widget): def __init__(self): super().__init__() @@ -54,11 +48,6 @@ def __init__(self): # Disable scrolling when onroad is interacting with bookmark self._scroller.set_scrolling_enabled(lambda: not self._onroad_layout.is_swiping_left()) - self._layouts = { - MainState.MAIN: self._scroller, - MainState.SETTINGS: self._settings_layout, - } - # Set callbacks self._setup_callbacks() @@ -67,11 +56,10 @@ def __init__(self): # Start onboarding if terms or training not completed, make sure to push after self self._onboarding_window = OnboardingWindow() if not self._onboarding_window.completed: - # gui_app.set_modal_overlay(self._onboarding_window) gui_app.push_widget(self._onboarding_window) def _setup_callbacks(self): - self._home_layout.set_callbacks(on_settings=self._on_settings_clicked) + self._home_layout.set_callbacks(on_settings=lambda: gui_app.push_widget(self._settings_layout)) self._onroad_layout.set_click_callback(lambda: self._scroll_to(self._home_layout)) device.add_interactive_timeout_callback(self._set_mode_for_started) @@ -80,10 +68,6 @@ def _scroll_to(self, layout: Widget): self._scroller.scroll_to(layout_x, smooth=True) def _render(self, _): - # Initial show event - # if self._current_mode is None: - # self._set_mode(MainState.MAIN) - if not self._setup: if self._alerts_layout.active_alerts() > 0: self._scroller.scroll_to(self._alerts_layout.rect.x) @@ -92,22 +76,12 @@ def _render(self, _): self._setup = True # Render - # if self._current_mode == MainState.MAIN: self._scroller.render(self._rect) - # elif self._current_mode == MainState.SETTINGS: - # self._settings_layout.render(self._rect) - + # If not in background stack if self.enabled: self._handle_transitions() - # def _set_mode(self, mode: MainState): - # if mode != self._current_mode: - # if self._current_mode is not None: - # self._layouts[self._current_mode].hide_event() - # self._layouts[mode].show_event() - # self._current_mode = mode - def _handle_transitions(self): if ui_state.started != self._prev_onroad: self._prev_onroad = ui_state.started @@ -124,7 +98,6 @@ def _handle_transitions(self): CS = ui_state.sm["carState"] if not CS.standstill and self._prev_standstill: - # self._set_mode(MainState.MAIN) gui_app.pop_widgets_to(self) self._scroll_to(self._onroad_layout) self._prev_standstill = CS.standstill @@ -135,22 +108,14 @@ def _set_mode_for_started(self, onroad_transition: bool = False): CS = ui_state.sm["carState"] # Only go onroad if car starts or is not at a standstill if not CS.standstill or onroad_transition: - # self._set_mode(MainState.MAIN) gui_app.pop_widgets_to(self) self._scroll_to(self._onroad_layout) else: # Stay in settings if car turns off while in settings if not onroad_transition or gui_app.get_active_widget() != self: - # self._set_mode(MainState.MAIN) gui_app.pop_widgets_to(self) self._scroll_to(self._home_layout) - def _on_settings_clicked(self): - # print('on settings clicked') - gui_app.push_widget(self._settings_layout) - # gui_app.set_modal_overlay(self._settings_layout) - # self._set_mode(MainState.SETTINGS) - def _on_bookmark_clicked(self): user_bookmark = messaging.new_message('bookmarkButton') user_bookmark.valid = True From 0d383edce6d1bed454bfc8e9161630b3d02b5679 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 6 Feb 2026 00:26:57 -0800 Subject: [PATCH 18/73] clean up settiings --- selfdrive/ui/mici/layouts/main.py | 1 - .../ui/mici/layouts/settings/settings.py | 41 +++---------------- system/ui/widgets/__init__.py | 1 + 3 files changed, 6 insertions(+), 37 deletions(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index 7421e64cd18189..f708f5d0c72cc3 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -20,7 +20,6 @@ def __init__(self): self._pm = messaging.PubMaster(['bookmarkButton']) - # self._current_mode: MainState | None = None self._prev_onroad = False self._prev_standstill = False self._onroad_time_delay: float | None = None diff --git a/selfdrive/ui/mici/layouts/settings/settings.py b/selfdrive/ui/mici/layouts/settings/settings.py index bbedba2846c980..e1ca8f57afeee8 100644 --- a/selfdrive/ui/mici/layouts/settings/settings.py +++ b/selfdrive/ui/mici/layouts/settings/settings.py @@ -1,6 +1,4 @@ import pyray as rl -from dataclasses import dataclass -from enum import IntEnum from openpilot.common.params import Params from openpilot.system.ui.widgets.scroller import Scroller @@ -11,33 +9,31 @@ from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.widgets import Widget, NavWidget +from openpilot.system.ui.widgets import NavWidget class SettingsLayout(NavWidget): def __init__(self): super().__init__() self._params = Params() - # self._current_panel = None # PanelType.DEVICE toggles_panel = TogglesLayoutMici() - network_panel = NetworkLayoutMici() - device_panel = DeviceLayoutMici() - developer_panel = DeveloperLayoutMici() - firehose_panel = FirehoseLayout() - toggles_btn = BigButton("toggles", "", "icons_mici/settings.png") toggles_btn.set_click_callback(lambda: gui_app.push_widget(toggles_panel)) + network_panel = NetworkLayoutMici() network_btn = BigButton("network", "", "icons_mici/settings/network/wifi_strength_full.png", icon_size=(76, 56)) network_btn.set_click_callback(lambda: gui_app.push_widget(network_panel)) + device_panel = DeviceLayoutMici() device_btn = BigButton("device", "", "icons_mici/settings/device_icon.png", icon_size=(74, 60)) device_btn.set_click_callback(lambda: gui_app.push_widget(device_panel)) + developer_panel = DeveloperLayoutMici() developer_btn = BigButton("developer", "", "icons_mici/settings/developer_icon.png", icon_size=(64, 60)) developer_btn.set_click_callback(lambda: gui_app.push_widget(developer_panel)) + firehose_panel = FirehoseLayout() firehose_btn = BigButton("firehose", "", "icons_mici/settings/firehose.png", icon_size=(52, 62)) firehose_btn.set_click_callback(lambda: gui_app.push_widget(firehose_panel)) @@ -53,43 +49,16 @@ def __init__(self): # Set up back navigation self.set_back_callback(gui_app.pop_widget) - # self.set_back_enabled(lambda: self._current_panel is None) self._font_medium = gui_app.font(FontWeight.MEDIUM) def show_event(self): super().show_event() - # self._set_current_panel(None) self._scroller.show_event() - # if self._current_panel is not None: - # self._panels[self._current_panel].instance.show_event() def hide_event(self): super().hide_event() self._scroller.hide_event() - # if self._current_panel is not None: - # self._panels[self._current_panel].instance.hide_event() def _render(self, rect: rl.Rectangle): - # if self._current_panel is not None: - # self._draw_current_panel() - # else: self._scroller.render(rect) - return -1 - - # def _draw_current_panel(self): - # panel = self._panels[self._current_panel] - # panel.instance.render(self._rect) - - # def _set_current_panel(self, panel_type: PanelType | None): - # if panel_type is None: - # # TODO: move this into each layout's class above - # gui_app.pop_widget() - # else: - # gui_app.push_widget(self._panels[panel_type].instance) - # # if panel_type != self._current_panel: - # # if self._current_panel is not None: - # # self._panels[self._current_panel].instance.hide_event() - # # self._current_panel = panel_type - # # if self._current_panel is not None: - # # self._panels[self._current_panel].instance.show_event() diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 4d504b7d9147a5..38c10d99221b79 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -181,6 +181,7 @@ def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: 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) def hide_event(self): """Optionally handle hide event. Parent must manually call this""" From 62d89a69a8331509f38f14fb4cec1b4bd1745545 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 6 Feb 2026 00:31:06 -0800 Subject: [PATCH 19/73] clean up dialog and developer --- selfdrive/ui/mici/layouts/settings/developer.py | 4 ---- selfdrive/ui/mici/layouts/settings/device.py | 3 +++ selfdrive/ui/mici/widgets/dialog.py | 13 +------------ 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/developer.py b/selfdrive/ui/mici/layouts/settings/developer.py index db3583ef30fc09..559ee294525ffc 100644 --- a/selfdrive/ui/mici/layouts/settings/developer.py +++ b/selfdrive/ui/mici/layouts/settings/developer.py @@ -1,5 +1,4 @@ import pyray as rl -from collections.abc import Callable from openpilot.common.time_helpers import system_time_valid from openpilot.system.ui.widgets.scroller import Scroller @@ -25,7 +24,6 @@ def github_username_callback(username: str): self._ssh_keys_btn.set_value(username) else: dlg = BigDialog("", ssh_keys._error_message) - # gui_app.set_modal_overlay(dlg) gui_app.push_widget(dlg) def ssh_keys_callback(): @@ -33,10 +31,8 @@ def ssh_keys_callback(): dlg = BigInputDialog("enter GitHub username", github_username, confirm_callback=github_username_callback) if not system_time_valid(): dlg = BigDialog("Please connect to Wi-Fi to fetch your key", "") - # gui_app.set_modal_overlay(dlg) gui_app.push_widget(dlg) return - # gui_app.set_modal_overlay(dlg) gui_app.push_widget(dlg) txt_ssh = gui_app.texture("icons_mici/settings/developer/ssh.png", 56, 64) diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index 843f2552a1bf4b..9be0c8502d5c5c 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -355,6 +355,7 @@ def _on_regulatory(self): if not self._fcc_dialog: self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/mici_fcc.html")) # gui_app.set_modal_overlay(self._fcc_dialog, callback=setattr(self, '_fcc_dialog', None)) + # TODO: can we keep it around? gui_app.push_widget(self._fcc_dialog) def _offroad_transition(self): @@ -363,6 +364,7 @@ def _offroad_transition(self): def _show_driver_camera(self): if not self._driver_camera: self._driver_camera = DriverCameraDialog() + # TODO: can we keep it around? # gui_app.set_modal_overlay(self._driver_camera, callback=lambda result: setattr(self, '_driver_camera', None)) gui_app.push_widget(self._driver_camera) @@ -374,6 +376,7 @@ def completed_callback(): self._training_guide = TrainingGuide(completed_callback=completed_callback) # TODO: callbacks. do we need them? + # TODO: can we keep it around? # gui_app.set_modal_overlay(self._training_guide, callback=lambda result: setattr(self, '_training_guide', None)) gui_app.push_widget(self._training_guide) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 053ede24ae47cc..377d5c6d3bf8a1 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -4,7 +4,7 @@ from typing import Union from collections.abc import Callable from typing import cast -from openpilot.system.ui.widgets import Widget, NavWidget, DialogResult +from openpilot.system.ui.widgets import Widget, NavWidget from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label from openpilot.system.ui.widgets.mici_keyboard import MiciKeyboard from openpilot.system.ui.lib.text_measure import measure_text_cached @@ -24,9 +24,7 @@ class BigDialogBase(NavWidget, abc.ABC): def __init__(self, right_btn: str | None = None, right_btn_callback: Callable | None = None): super().__init__() - # self._ret = DialogResult.NO_ACTION self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - # self.set_back_callback(lambda: setattr(self, '_ret', DialogResult.CANCEL)) self.set_back_callback(gui_app.pop_widget) self._right_btn = None @@ -90,8 +88,6 @@ def _render(self, _): gui_label(desc_rect, desc_wrapped, 30, font_weight=FontWeight.MEDIUM, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) - # return self._ret - class BigConfirmationDialogV2(BigDialogBase): def __init__(self, title: str, icon: str, red: bool = False, @@ -114,7 +110,6 @@ def _on_confirm(self): self._confirm_callback() if self._exit_on_confirm: gui_app.pop_widget() - # self._ret = DialogResult.CONFIRM def _update_state(self): super()._update_state() @@ -123,7 +118,6 @@ def _update_state(self): def _render(self, _): self._slider.render(self._rect) - # return self._ret class BigInputDialog(BigDialogBase): @@ -157,7 +151,6 @@ def __init__(self, self._top_right_button_rect = rl.Rectangle(0, 0, 0, 0) def confirm_callback_wrapper(): - # self._ret = DialogResult.CONFIRM if confirm_callback: confirm_callback(self._keyboard.text()) gui_app.pop_widget() @@ -251,8 +244,6 @@ def _render(self, _): rl.draw_rectangle_lines_ex(self._top_right_button_rect, 1, rl.Color(0, 255, 0, 255)) rl.draw_rectangle_lines_ex(self._top_left_button_rect, 1, rl.Color(0, 255, 0, 255)) - # return self._ret - def _handle_mouse_press(self, mouse_pos: MousePos): super()._handle_mouse_press(mouse_pos) # TODO: need to track where press was so enter and back can activate on release rather than press @@ -408,8 +399,6 @@ def _render(self, _): super()._render(_) self._scroller.render(self._rect) - # return self._ret - class BigDialogButton(BigButton): def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", description: str = ""): From ca97c354d118b010d2b2546ec4f6fc56ea107704 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 6 Feb 2026 00:32:32 -0800 Subject: [PATCH 20/73] cleanup mici setup some --- system/ui/mici_setup.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 156f16f1f1f470..bb57dc82af1ee5 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -426,15 +426,9 @@ def _render(self, rect: rl.Rectangle): )) -# class NetworkSetupState(IntEnum): -# MAIN = 0 -# WIFI_PANEL = 1 - - class NetworkSetupPage(Widget): def __init__(self, wifi_manager, continue_callback: Callable, back_callback: Callable): super().__init__() - # self._wifi_ui = WifiUIMici(wifi_manager, back_callback=lambda: self.set_state(NetworkSetupState.MAIN)) self._wifi_ui = WifiUIMici(wifi_manager) self._no_wifi_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 58, 50) @@ -447,7 +441,6 @@ def __init__(self, wifi_manager, continue_callback: Callable, back_callback: Cal self._back_button.set_click_callback(back_callback) self._wifi_button = SmallerRoundedButton("wifi") - # self._wifi_button.set_click_callback(lambda: self.set_state(NetworkSetupState.WIFI_PANEL)) self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui)) self._wifi_button.set_enabled(lambda: self.enabled) @@ -455,14 +448,8 @@ def __init__(self, wifi_manager, continue_callback: Callable, back_callback: Cal self._continue_button.set_enabled(False) self._continue_button.set_click_callback(continue_callback) - # self._state = NetworkSetupState.MAIN self._prev_has_internet = False - # def set_state(self, state: NetworkSetupState): - # self._state = state - # if state == NetworkSetupState.WIFI_PANEL: - # self._wifi_ui.show_event() - def set_has_internet(self, has_internet: bool): if has_internet: self._network_header.set_title("connected to internet") @@ -475,13 +462,11 @@ def set_has_internet(self, has_internet: bool): if has_internet and not self._prev_has_internet: pass - # self.set_state(NetworkSetupState.MAIN) # gui_app.pop_widgets_to(self) self._prev_has_internet = has_internet def show_event(self): super().show_event() - # self._state = NetworkSetupState.MAIN self._wifi_ui.show_event() def hide_event(self): @@ -489,7 +474,6 @@ def hide_event(self): self._wifi_ui.hide_event() def _render(self, _): - # if self._state == NetworkSetupState.MAIN: self._network_header.render(rl.Rectangle( self._rect.x + 16, self._rect.y + 16, @@ -517,8 +501,6 @@ def _render(self, _): self._continue_button.rect.width, self._continue_button.rect.height, )) - # else: - # self._wifi_ui.render(self._rect) class Setup(Widget): From 78a81c8a457c8274f82b3fb4b24c6a9b779d9f68 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 6 Feb 2026 00:36:19 -0800 Subject: [PATCH 21/73] rm one more --- selfdrive/ui/mici/widgets/dialog.py | 1 - 1 file changed, 1 deletion(-) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 377d5c6d3bf8a1..fd96904c0be9ec 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -30,7 +30,6 @@ def __init__(self, right_btn: str | None = None, right_btn_callback: Callable | self._right_btn = None if right_btn: def right_btn_callback_wrapper(): - # gui_app.set_modal_overlay(None) gui_app.pop_widget() if right_btn_callback: right_btn_callback() From 9ef225557255171d07e3f42cf52ef70866a721ea Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 6 Feb 2026 00:48:40 -0800 Subject: [PATCH 22/73] fix keyboard --- system/ui/widgets/mici_keyboard.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/system/ui/widgets/mici_keyboard.py b/system/ui/widgets/mici_keyboard.py index 6d2e08e0539f77..1b146eb2bb540e 100644 --- a/system/ui/widgets/mici_keyboard.py +++ b/system/ui/widgets/mici_keyboard.py @@ -61,7 +61,7 @@ def set_position(self, x: float, y: float, smooth: bool = True): self._x_filter.x = x self._y_filter.x = local_y # keep track of original position so dragging around feels consistent. also move touch area down a bit - self.original_position = rl.Vector2(x, y + KEY_TOUCH_AREA_OFFSET) + self.original_position = rl.Vector2(x, local_y + KEY_TOUCH_AREA_OFFSET) self._position_initialized = True if not smooth: @@ -264,7 +264,8 @@ def _get_closest_key(self) -> tuple[Key | None, float]: for key in row: mouse_pos = gui_app.last_mouse_event.pos # approximate distance for comparison is accurate enough - dist = abs(key.original_position.x - mouse_pos.x) + abs(key.original_position.y - mouse_pos.y) + # use local y coords so parent widget offset (e.g. NavWidget animate-in) doesn't affect hit testing + dist = abs(key.original_position.x - mouse_pos.x) + abs(key.original_position.y - (mouse_pos.y - self._rect.y)) if dist < closest_key[1]: if self._closest_key[0] is None or key is self._closest_key[0] or dist < self._closest_key[1] - KEY_DRAG_HYSTERESIS: closest_key = (key, dist) From 02b3ddccc23bbba70e0cbb24eee1cf2762adad7b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 6 Feb 2026 00:49:22 -0800 Subject: [PATCH 23/73] revert --- system/ui/widgets/mici_keyboard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/ui/widgets/mici_keyboard.py b/system/ui/widgets/mici_keyboard.py index 1b146eb2bb540e..e04df3cc1b50e0 100644 --- a/system/ui/widgets/mici_keyboard.py +++ b/system/ui/widgets/mici_keyboard.py @@ -264,8 +264,8 @@ def _get_closest_key(self) -> tuple[Key | None, float]: for key in row: mouse_pos = gui_app.last_mouse_event.pos # approximate distance for comparison is accurate enough - # use local y coords so parent widget offset (e.g. NavWidget animate-in) doesn't affect hit testing - dist = abs(key.original_position.x - mouse_pos.x) + abs(key.original_position.y - (mouse_pos.y - self._rect.y)) + # use (mouse_pos.y - self._rect.y) to correctly handle pressing while animating in, not important + dist = abs(key.original_position.x - mouse_pos.x) + abs(key.original_position.y - mouse_pos.y) if dist < closest_key[1]: if self._closest_key[0] is None or key is self._closest_key[0] or dist < self._closest_key[1] - KEY_DRAG_HYSTERESIS: closest_key = (key, dist) From 96c3c251cc02b732847e807789ab1ea55ace3a50 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 6 Feb 2026 00:50:24 -0800 Subject: [PATCH 24/73] might as well but clarify --- system/ui/widgets/mici_keyboard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/ui/widgets/mici_keyboard.py b/system/ui/widgets/mici_keyboard.py index e04df3cc1b50e0..f8afd506760a48 100644 --- a/system/ui/widgets/mici_keyboard.py +++ b/system/ui/widgets/mici_keyboard.py @@ -264,8 +264,8 @@ def _get_closest_key(self) -> tuple[Key | None, float]: for key in row: mouse_pos = gui_app.last_mouse_event.pos # approximate distance for comparison is accurate enough - # use (mouse_pos.y - self._rect.y) to correctly handle pressing while animating in, not important - dist = abs(key.original_position.x - mouse_pos.x) + abs(key.original_position.y - mouse_pos.y) + # use local y coords so parent widget offset (e.g. during NavWidget animate-in) doesn't affect hit testing + dist = abs(key.original_position.x - mouse_pos.x) + abs(key.original_position.y - (mouse_pos.y - self._rect.y)) if dist < closest_key[1]: if self._closest_key[0] is None or key is self._closest_key[0] or dist < self._closest_key[1] - KEY_DRAG_HYSTERESIS: closest_key = (key, dist) From da637a5ca9156bdaa4b644daa193d4d8604f9aee Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 6 Feb 2026 00:59:34 -0800 Subject: [PATCH 25/73] fix networkinfopage buttons --- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index d6505d7f258dad..f4a2b53678e25b 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -199,6 +199,7 @@ def __init__(self, wifi_manager, connect_callback: Callable, forget_callback: Ca self._wifi_icon = WifiIcon() self._forget_btn = ForgetButton(lambda: forget_callback(self._network.ssid) if self._network is not None else None) + self._forget_btn.set_enabled(lambda: self.enabled) # for stack self._connect_btn = ConnectButton() self._connect_btn.set_click_callback(lambda: connect_callback(self._network.ssid) if self._network is not None else None) @@ -251,7 +252,7 @@ def _update_state(self): self._connect_btn.set_enabled(False) else: # saved or unknown self._connect_btn.set_label("connect") - self._connect_btn.set_enabled(True) + self._connect_btn.set_enabled(self.enabled) self._title.set_text(normalize_ssid(self._network.ssid)) if self._network.security_type == SecurityType.OPEN: From 67424e287753ab1318bff9be3e4a05382e050c20 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 6 Feb 2026 01:11:53 -0800 Subject: [PATCH 26/73] lint --- selfdrive/ui/mici/layouts/settings/network/__init__.py | 2 -- selfdrive/ui/mici/layouts/settings/toggles.py | 1 - system/ui/mici_setup.py | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index 7bf5b561b64b80..96117c7e34ba58 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -1,6 +1,4 @@ import pyray as rl -from enum import IntEnum -from collections.abc import Callable from openpilot.system.ui.widgets.scroller import Scroller from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici, WifiIcon, normalize_ssid diff --git a/selfdrive/ui/mici/layouts/settings/toggles.py b/selfdrive/ui/mici/layouts/settings/toggles.py index 47b60e7325c2e7..d6fb75a4d8dc86 100644 --- a/selfdrive/ui/mici/layouts/settings/toggles.py +++ b/selfdrive/ui/mici/layouts/settings/toggles.py @@ -1,5 +1,4 @@ import pyray as rl -from collections.abc import Callable from cereal import log from openpilot.system.ui.widgets.scroller import Scroller diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index bb57dc82af1ee5..349fecebf5f3c4 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -19,7 +19,7 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 -from openpilot.system.ui.widgets import Widget, DialogResult +from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import (IconButton, SmallButton, WideRoundedButton, SmallerRoundedButton, SmallCircleIconButton, WidishRoundedButton, SmallRedPillButton, FullRoundedButton) From e77f5ce3121f85d6dd512cc4208f1fb9b6745212 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 6 Feb 2026 01:34:57 -0800 Subject: [PATCH 27/73] nice clean up from cursor --- system/ui/lib/application.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 63c51f43acea7e..0cbad2d6b8c525 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -582,12 +582,10 @@ def render(self): rl.clear_background(rl.BLACK) if self._new_modal: - if len(self._nav_stack.widgets) > 1: - self._nav_stack.widgets[-2].render(rl.Rectangle(0, 0, self.width, self.height)) - rl.draw_rectangle(0, 0, self.width, self.height, rl.Color(0, 0, 0, 150)) - - if len(self._nav_stack.widgets) > 0: - self._nav_stack.widgets[-1].render(rl.Rectangle(0, 0, self.width, self.height)) + # Only render top two + for widget in self._nav_stack.widgets[-2:]: + # TODO: need scaled sizes? + widget.render(rl.Rectangle(0, 0, self.width, self.height)) print('widget stack', len(self._nav_stack.widgets), [w.__class__.__name__ for w in self._nav_stack.widgets]) From 589f816f66ecb839ccf1fecacb10df47afc9b607 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 6 Feb 2026 01:37:49 -0800 Subject: [PATCH 28/73] animate background fade with position --- system/ui/widgets/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 38c10d99221b79..b928b70baeb79d 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -367,8 +367,13 @@ def _update_state(self): self.set_position(self._rect.x, new_y) - def render(self, rect: rl.Rectangle | None = None) -> bool | int | None: + def _layout(self): + # Dim whatever is behind this widget, fading with position (runs after _update_state so position is correct) + overlay_alpha = int(200 * max(0.0, min(1.0, 1.0 - self._rect.y / self._rect.height))) if self._rect.height > 0 else 0 + rl.draw_rectangle(0, 0, int(self._rect.width), int(self._rect.height), rl.Color(0, 0, 0, overlay_alpha)) rl.draw_rectangle(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height + 20), rl.BLACK) + + def render(self, rect: rl.Rectangle | None = None) -> bool | int | None: ret = super().render(rect) if self.back_enabled: From f59d35a810a80ba1ddd7bd37d64cba89a0a0addc Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 6 Feb 2026 23:08:20 -0800 Subject: [PATCH 29/73] fix device overlays --- selfdrive/ui/mici/layouts/onboarding.py | 1 + selfdrive/ui/mici/layouts/settings/device.py | 27 ++------------------ 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/selfdrive/ui/mici/layouts/onboarding.py b/selfdrive/ui/mici/layouts/onboarding.py index 7ae6ff2924966b..cdb17aa2134590 100644 --- a/selfdrive/ui/mici/layouts/onboarding.py +++ b/selfdrive/ui/mici/layouts/onboarding.py @@ -364,6 +364,7 @@ def _advance_step(self): self._completed_callback() def _render(self, _): + rl.draw_rectangle_rec(self._rect, rl.BLACK) if self._step < len(self._steps): self._steps[self._step].render(self._rect) return -1 diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index 89c976f4a878cd..9f68bb7ecff4cf 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -270,8 +270,6 @@ def __init__(self): super().__init__() self._fcc_dialog: HtmlModal | None = None - self._driver_camera: DriverCameraDialog | None = None - self._training_guide: TrainingGuide | None = None def power_off_callback(): ui_state.params.put_bool("DoShutdown", True) @@ -323,11 +321,11 @@ def selected_language_callback(): regulatory_btn.set_click_callback(self._on_regulatory) driver_cam_btn = BigButton("driver\ncamera preview", "", "icons_mici/settings/device/cameras.png") - driver_cam_btn.set_click_callback(self._show_driver_camera) + driver_cam_btn.set_click_callback(lambda: gui_app.push_widget(DriverCameraDialog())) driver_cam_btn.set_enabled(lambda: ui_state.is_offroad()) review_training_guide_btn = BigButton("review\ntraining guide", "", "icons_mici/settings/device/info.png") - review_training_guide_btn.set_click_callback(self._on_review_training_guide) + review_training_guide_btn.set_click_callback(lambda: gui_app.push_widget(TrainingGuide(completed_callback=gui_app.pop_widget))) review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad()) self._scroller = Scroller([ @@ -354,32 +352,11 @@ def selected_language_callback(): def _on_regulatory(self): if not self._fcc_dialog: self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/mici_fcc.html")) - # gui_app.set_modal_overlay(self._fcc_dialog, callback=setattr(self, '_fcc_dialog', None)) - # TODO: can we keep it around? gui_app.push_widget(self._fcc_dialog) def _offroad_transition(self): self._power_off_btn.set_visible(ui_state.is_offroad()) - def _show_driver_camera(self): - if not self._driver_camera: - self._driver_camera = DriverCameraDialog() - # TODO: can we keep it around? - # gui_app.set_modal_overlay(self._driver_camera, callback=lambda result: setattr(self, '_driver_camera', None)) - gui_app.push_widget(self._driver_camera) - - def _on_review_training_guide(self): - if not self._training_guide: - def completed_callback(): - # gui_app.set_modal_overlay(None) - gui_app.pop_widget() - - self._training_guide = TrainingGuide(completed_callback=completed_callback) - # TODO: callbacks. do we need them? - # TODO: can we keep it around? - # gui_app.set_modal_overlay(self._training_guide, callback=lambda result: setattr(self, '_training_guide', None)) - gui_app.push_widget(self._training_guide) - def _load_languages(self): with open(os.path.join(BASEDIR, "selfdrive/ui/translations/languages.json")) as f: self._languages = json.load(f) From 3e9ee7a7b38822d550f78a3491070432c2c20601 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 7 Feb 2026 00:11:10 -0800 Subject: [PATCH 30/73] cursor fix pt1 cursor fix pt2 --- selfdrive/ui/mici/layouts/main.py | 42 ++++++++++++++++++------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index f708f5d0c72cc3..db10771597259b 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -42,7 +42,7 @@ def __init__(self): self._onroad_layout, ], spacing=0, pad_start=0, pad_end=0) self._scroller.set_reset_scroll_at_show(False) - self._scroller.set_enabled(lambda: self.enabled) + self._scroller.set_enabled(lambda: self.enabled) # for nav stack # Disable scrolling when onroad is interacting with bookmark self._scroller.set_scrolling_enabled(lambda: not self._onroad_layout.is_swiping_left()) @@ -60,7 +60,7 @@ def __init__(self): def _setup_callbacks(self): self._home_layout.set_callbacks(on_settings=lambda: gui_app.push_widget(self._settings_layout)) self._onroad_layout.set_click_callback(lambda: self._scroll_to(self._home_layout)) - device.add_interactive_timeout_callback(self._set_mode_for_started) + device.add_interactive_timeout_callback(self._on_interactive_timeout) def _scroll_to(self, layout: Widget): layout_x = int(layout.rect.x) @@ -77,43 +77,51 @@ def _render(self, _): # Render self._scroller.render(self._rect) - # If not in background stack - if self.enabled: - self._handle_transitions() + self._handle_transitions() def _handle_transitions(self): + # Don't pop if onboarding + if gui_app.get_active_widget() == self._onboarding_window: + return + if ui_state.started != self._prev_onroad: self._prev_onroad = ui_state.started + # onroad: after delay, pop nav stack and scroll to onroad + # offroad: immediately scroll to home, but don't pop nav stack (can stay in settings) if ui_state.started: self._onroad_time_delay = rl.get_time() else: - self._set_mode_for_started(True) + self._scroll_to(self._home_layout) - # delay so we show home for a bit after starting if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY: - self._set_mode_for_started(True) + gui_app.pop_widgets_to(self) + self._scroll_to(self._onroad_layout) self._onroad_time_delay = None + # when car leaves standstill, pop nav stack and scroll to onroad CS = ui_state.sm["carState"] if not CS.standstill and self._prev_standstill: gui_app.pop_widgets_to(self) self._scroll_to(self._onroad_layout) self._prev_standstill = CS.standstill - def _set_mode_for_started(self, onroad_transition: bool = False): - print(f'_set_mode_for_started, {ui_state.started=}, {onroad_transition=}') + def _on_interactive_timeout(self): + print(f'_on_interactive_timeout, {ui_state.started=}') + + # Don't pop if onboarding + print('get active', gui_app.get_active_widget(), self._onboarding_window.completed) + if gui_app.get_active_widget() == self._onboarding_window: + return + if ui_state.started: - CS = ui_state.sm["carState"] - # Only go onroad if car starts or is not at a standstill - if not CS.standstill or onroad_transition: + # Don't pop if at standstill + if not ui_state.sm["carState"].standstill: gui_app.pop_widgets_to(self) self._scroll_to(self._onroad_layout) else: - # Stay in settings if car turns off while in settings - if not onroad_transition or gui_app.get_active_widget() != self: - gui_app.pop_widgets_to(self) - self._scroll_to(self._home_layout) + gui_app.pop_widgets_to(self) + self._scroll_to(self._home_layout) def _on_bookmark_clicked(self): user_bookmark = messaging.new_message('bookmarkButton') From 46accb959ec9748f2997571da58659361cea8274 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 7 Feb 2026 00:11:37 -0800 Subject: [PATCH 31/73] rm print --- selfdrive/ui/mici/layouts/main.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index db10771597259b..195675600d7cf0 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -107,10 +107,7 @@ def _handle_transitions(self): self._prev_standstill = CS.standstill def _on_interactive_timeout(self): - print(f'_on_interactive_timeout, {ui_state.started=}') - # Don't pop if onboarding - print('get active', gui_app.get_active_widget(), self._onboarding_window.completed) if gui_app.get_active_widget() == self._onboarding_window: return From 09a2271a6fb85e08793423692ae385492c53df06 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 7 Feb 2026 00:14:12 -0800 Subject: [PATCH 32/73] capital --- selfdrive/ui/mici/layouts/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py index 195675600d7cf0..113187e835aa56 100644 --- a/selfdrive/ui/mici/layouts/main.py +++ b/selfdrive/ui/mici/layouts/main.py @@ -99,7 +99,7 @@ def _handle_transitions(self): self._scroll_to(self._onroad_layout) self._onroad_time_delay = None - # when car leaves standstill, pop nav stack and scroll to onroad + # When car leaves standstill, pop nav stack and scroll to onroad CS = ui_state.sm["carState"] if not CS.standstill and self._prev_standstill: gui_app.pop_widgets_to(self) From 516a944bab7e7396ff5d081facb2f8ec8f0a84f5 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 7 Feb 2026 01:08:46 -0800 Subject: [PATCH 33/73] temp fix from cursor for onboarding not freeing space after reviewing training guide --- selfdrive/ui/mici/layouts/onboarding.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/mici/layouts/onboarding.py b/selfdrive/ui/mici/layouts/onboarding.py index cdb17aa2134590..657633e20fe458 100644 --- a/selfdrive/ui/mici/layouts/onboarding.py +++ b/selfdrive/ui/mici/layouts/onboarding.py @@ -124,8 +124,10 @@ class TrainingGuideDMTutorial(Widget): def __init__(self, continue_callback): super().__init__() + self_ref = weakref.ref(self) + self._back_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_question.png", 28, 48)) - self._back_button.set_click_callback(self._show_bad_face_page) + self._back_button.set_click_callback(lambda: self_ref() and self_ref()._show_bad_face_page()) self._good_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 42, 42)) # Wrap the continue callback to restore settings @@ -138,7 +140,7 @@ def wrapped_continue_callback(): self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps) self._dialog = DriverCameraSetupDialog() - self._bad_face_page = DMBadFaceDetected(HARDWARE.shutdown, self._hide_bad_face_page) + self._bad_face_page = DMBadFaceDetected(HARDWARE.shutdown, lambda: self_ref() and self_ref()._hide_bad_face_page()) self._should_show_bad_face_page = False # Disable driver monitoring model when device times out for inactivity From ad144eb19064227a129dcfb3b4bc10efd1a02759 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 19 Feb 2026 16:00:31 -0800 Subject: [PATCH 34/73] fix home screen scroller snap not resetting --- system/ui/widgets/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 59b49027010801..05a9e3241f9bdd 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -108,6 +108,10 @@ def render(self, rect: rl.Rectangle | None = None) -> bool | int | None: # Keep track of whether mouse down started within the widget's rectangle if self.enabled and self.__was_awake: self._process_mouse_events() + else: + # TODO: ideally we emit release events when going disabled + self.__is_pressed = [False] * MAX_TOUCH_SLOTS + self.__tracking_is_pressed = [False] * MAX_TOUCH_SLOTS self.__was_awake = device.awake From 488d65188b07ddc1af3f8694b04134af4ece3e1e Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 19 Feb 2026 16:26:14 -0800 Subject: [PATCH 35/73] stash --- system/ui/widgets/__init__.py | 6 ++++-- system/ui/widgets/scroller.py | 12 ++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 05a9e3241f9bdd..5eecba4e2b8b0b 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -376,8 +376,10 @@ def _update_state(self): def _layout(self): # Dim whatever is behind this widget, fading with position (runs after _update_state so position is correct) overlay_alpha = int(200 * max(0.0, min(1.0, 1.0 - self._rect.y / self._rect.height))) if self._rect.height > 0 else 0 - rl.draw_rectangle(0, 0, int(self._rect.width), int(self._rect.height), rl.Color(0, 0, 0, overlay_alpha)) - rl.draw_rectangle(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height + 20), rl.BLACK) + # rl.draw_rectangle(0, 0, int(self._rect.width), int(self._rect.height), rl.Color(0, 0, 0, overlay_alpha)) + # clear to black from 0 to 50px for nice fade + # rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y - 30), int(self._rect.width), 30, rl.BLANK, rl.Color(150, 0, 0, 100)) + # rl.draw_rectangle(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height + 20), rl.Color(0, 0, 150, 100)) def render(self, rect: rl.Rectangle | None = None) -> bool | int | None: ret = super().render(rect) diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 5930a2a6ebf6a6..863ceb54ca0068 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -291,14 +291,14 @@ def _render(self, _): # Draw edge shadows on top of scroller content if self._edge_shadows: - rl.draw_rectangle_gradient_h(int(self._rect.x), int(self._rect.y), - EDGE_SHADOW_WIDTH, int(self._rect.y), - rl.Color(0, 0, 0, 166), rl.BLANK) + rl.draw_rectangle_gradient_h(int(self._rect.x), 0, + EDGE_SHADOW_WIDTH, int(self._rect.height), + rl.Color(200, 0, 0, 204), rl.BLANK) right_x = int(self._rect.x + self._rect.width - EDGE_SHADOW_WIDTH) - rl.draw_rectangle_gradient_h(right_x, int(self._rect.y), - EDGE_SHADOW_WIDTH, int(self._rect.y), - rl.BLANK, rl.Color(0, 0, 0, 166)) + rl.draw_rectangle_gradient_h(right_x, 0, + EDGE_SHADOW_WIDTH, int(self._rect.height), + rl.BLANK, rl.Color(0, 200, 0, 204)) # Draw scroll indicator on top of edge shadows if self._show_scroll_indicator and len(self._visible_items) > 0: From 61551a6335df098988644e31a94d98a20d990ca9 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 19 Feb 2026 16:53:36 -0800 Subject: [PATCH 36/73] nice gradient on top --- system/ui/widgets/__init__.py | 10 ++++++---- system/ui/widgets/scroller.py | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 5eecba4e2b8b0b..f253477e3b69c4 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -376,10 +376,12 @@ def _update_state(self): def _layout(self): # Dim whatever is behind this widget, fading with position (runs after _update_state so position is correct) overlay_alpha = int(200 * max(0.0, min(1.0, 1.0 - self._rect.y / self._rect.height))) if self._rect.height > 0 else 0 - # rl.draw_rectangle(0, 0, int(self._rect.width), int(self._rect.height), rl.Color(0, 0, 0, overlay_alpha)) - # clear to black from 0 to 50px for nice fade - # rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y - 30), int(self._rect.width), 30, rl.BLANK, rl.Color(150, 0, 0, 100)) - # rl.draw_rectangle(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height + 20), rl.Color(0, 0, 150, 100)) + rl.draw_rectangle(0, 0, int(self._rect.width), int(self._rect.height), rl.Color(0, 0, 0, overlay_alpha)) + + gradient_height = 60 + bounce_height = 20 + rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y - gradient_height), int(self._rect.width), gradient_height, rl.BLANK, rl.BLACK) + rl.draw_rectangle(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height + bounce_height), rl.BLACK) def render(self, rect: rl.Rectangle | None = None) -> bool | int | None: ret = super().render(rect) diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 863ceb54ca0068..69a50ed84b38d4 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -293,12 +293,12 @@ def _render(self, _): if self._edge_shadows: rl.draw_rectangle_gradient_h(int(self._rect.x), 0, EDGE_SHADOW_WIDTH, int(self._rect.height), - rl.Color(200, 0, 0, 204), rl.BLANK) + rl.Color(0, 0, 0, 204), rl.BLANK) right_x = int(self._rect.x + self._rect.width - EDGE_SHADOW_WIDTH) rl.draw_rectangle_gradient_h(right_x, 0, EDGE_SHADOW_WIDTH, int(self._rect.height), - rl.BLANK, rl.Color(0, 200, 0, 204)) + rl.BLANK, rl.Color(0, 0, 0, 204)) # Draw scroll indicator on top of edge shadows if self._show_scroll_indicator and len(self._visible_items) > 0: From 8a14fe0964c4718777cff64e951235c4383972b6 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 19 Feb 2026 17:26:04 -0800 Subject: [PATCH 37/73] 40 --- system/ui/widgets/__init__.py | 2 +- system/ui/widgets/scroller.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index f253477e3b69c4..c19c60f62e6106 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -378,7 +378,7 @@ def _layout(self): overlay_alpha = int(200 * max(0.0, min(1.0, 1.0 - self._rect.y / self._rect.height))) if self._rect.height > 0 else 0 rl.draw_rectangle(0, 0, int(self._rect.width), int(self._rect.height), rl.Color(0, 0, 0, overlay_alpha)) - gradient_height = 60 + gradient_height = 40 bounce_height = 20 rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y - gradient_height), int(self._rect.width), gradient_height, rl.BLANK, rl.BLACK) rl.draw_rectangle(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height + bounce_height), rl.BLACK) diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 69a50ed84b38d4..9bb7883215d2b5 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -291,12 +291,12 @@ def _render(self, _): # Draw edge shadows on top of scroller content if self._edge_shadows: - rl.draw_rectangle_gradient_h(int(self._rect.x), 0, + rl.draw_rectangle_gradient_h(int(self._rect.x), int(self._rect.y), EDGE_SHADOW_WIDTH, int(self._rect.height), rl.Color(0, 0, 0, 204), rl.BLANK) right_x = int(self._rect.x + self._rect.width - EDGE_SHADOW_WIDTH) - rl.draw_rectangle_gradient_h(right_x, 0, + rl.draw_rectangle_gradient_h(right_x, int(self._rect.y), EDGE_SHADOW_WIDTH, int(self._rect.height), rl.BLANK, rl.Color(0, 0, 0, 204)) From 12896b5ca61f8832e83e148ad7b5d71a670666f4 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 19 Feb 2026 17:27:00 -0800 Subject: [PATCH 38/73] 20 --- system/ui/widgets/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index c19c60f62e6106..2c47c3817be385 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -378,7 +378,7 @@ def _layout(self): overlay_alpha = int(200 * max(0.0, min(1.0, 1.0 - self._rect.y / self._rect.height))) if self._rect.height > 0 else 0 rl.draw_rectangle(0, 0, int(self._rect.width), int(self._rect.height), rl.Color(0, 0, 0, overlay_alpha)) - gradient_height = 40 + gradient_height = 20 bounce_height = 20 rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y - gradient_height), int(self._rect.width), gradient_height, rl.BLANK, rl.BLACK) rl.draw_rectangle(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height + bounce_height), rl.BLACK) From e1d0ad8897f11c58395b0a7943ab16a2f2eed87a Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 19 Feb 2026 17:31:24 -0800 Subject: [PATCH 39/73] no gradient --- system/ui/widgets/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 2c47c3817be385..49a8513e0900d6 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -378,9 +378,7 @@ def _layout(self): overlay_alpha = int(200 * max(0.0, min(1.0, 1.0 - self._rect.y / self._rect.height))) if self._rect.height > 0 else 0 rl.draw_rectangle(0, 0, int(self._rect.width), int(self._rect.height), rl.Color(0, 0, 0, overlay_alpha)) - gradient_height = 20 bounce_height = 20 - rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y - gradient_height), int(self._rect.width), gradient_height, rl.BLANK, rl.BLACK) rl.draw_rectangle(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height + bounce_height), rl.BLACK) def render(self, rect: rl.Rectangle | None = None) -> bool | int | None: From 6a691f39e0ff2e65eea747b74ec2b637ac90ca5a Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 19 Feb 2026 18:48:49 -0800 Subject: [PATCH 40/73] return unused returns and always show regulatory btn --- selfdrive/ui/layouts/settings/device.py | 3 +-- selfdrive/ui/mici/layouts/onboarding.py | 5 +---- selfdrive/ui/mici/layouts/settings/device.py | 2 -- selfdrive/ui/mici/layouts/settings/firehose.py | 3 --- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 2 -- selfdrive/ui/mici/onroad/driver_camera_dialog.py | 4 ++-- selfdrive/ui/mici/widgets/pairing_dialog.py | 2 -- 7 files changed, 4 insertions(+), 17 deletions(-) diff --git a/selfdrive/ui/layouts/settings/device.py b/selfdrive/ui/layouts/settings/device.py index 00ae6a188ea3fc..d1f66a07fdda31 100644 --- a/selfdrive/ui/layouts/settings/device.py +++ b/selfdrive/ui/layouts/settings/device.py @@ -64,11 +64,10 @@ def _initialize_items(self): self._reset_calib_btn, button_item(lambda: tr("Review Training Guide"), lambda: tr("REVIEW"), lambda: tr(DESCRIPTIONS['review_guide']), self._on_review_training_guide, enabled=ui_state.is_offroad), - regulatory_btn := button_item(lambda: tr("Regulatory"), lambda: tr("VIEW"), callback=self._on_regulatory, enabled=ui_state.is_offroad), + button_item(lambda: tr("Regulatory"), lambda: tr("VIEW"), callback=self._on_regulatory, enabled=ui_state.is_offroad), button_item(lambda: tr("Change Language"), lambda: tr("CHANGE"), callback=self._show_language_dialog), self._power_off_btn, ] - regulatory_btn.set_visible(TICI) return items def _offroad_transition(self): diff --git a/selfdrive/ui/mici/layouts/onboarding.py b/selfdrive/ui/mici/layouts/onboarding.py index 657633e20fe458..0498087d0a6526 100644 --- a/selfdrive/ui/mici/layouts/onboarding.py +++ b/selfdrive/ui/mici/layouts/onboarding.py @@ -42,7 +42,7 @@ def _render(self, rect): gui_label(rect, tr("camera starting"), font_size=64, font_weight=FontWeight.BOLD, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) rl.end_scissor_mode() - return -1 + return # Position dmoji on opposite side from driver is_rhd = self.driver_state_renderer.is_rhd @@ -55,7 +55,6 @@ def _render(self, rect): self._draw_face_detection(rect) rl.end_scissor_mode() - return -1 class TrainingGuidePreDMTutorial(SetupTermsPage): @@ -369,7 +368,6 @@ def _render(self, _): rl.draw_rectangle_rec(self._rect, rl.BLACK) if self._step < len(self._steps): self._steps[self._step].render(self._rect) - return -1 class DeclinePage(Widget): @@ -492,4 +490,3 @@ def _render(self, _): self._training_guide.render(self._rect) elif self._state == OnboardingState.DECLINE: self._decline_page.render(self._rect) - return -1 diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index 6947a5a98bf021..e1beae4fe3076c 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -47,8 +47,6 @@ def _render(self, rect: rl.Rectangle): rl.draw_texture_ex(self._fcc_logo, fcc_pos, 0.0, 1.0, rl.WHITE) - return -1 - def _engaged_confirmation_callback(callback: Callable, action_text: str): if not ui_state.engaged: diff --git a/selfdrive/ui/mici/layouts/settings/firehose.py b/selfdrive/ui/mici/layouts/settings/firehose.py index 0a9c6111a2bdd4..a2288752eafc6f 100644 --- a/selfdrive/ui/mici/layouts/settings/firehose.py +++ b/selfdrive/ui/mici/layouts/settings/firehose.py @@ -132,9 +132,6 @@ def _render(self, rect: rl.Rectangle): y = self._draw_wrapped_text(x, y, w, tr(answer), gui_app.font(FontWeight.ROMAN), 32, self.LIGHT_GRAY) y += 20 - # return value not used by NavWidget - return -1 - def _draw_wrapped_text(self, x, y, width, text, font, font_size, color): wrapped = wrap_text(font, text, font_size, width) for line in wrapped: diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 39fc815172738a..6e9e0db9e0b501 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -343,8 +343,6 @@ def _render(self, _): self._forget_btn.rect.height, )) - return -1 - class WifiUIMici(BigMultiOptionDialog): def __init__(self, wifi_manager: WifiManager): diff --git a/selfdrive/ui/mici/onroad/driver_camera_dialog.py b/selfdrive/ui/mici/onroad/driver_camera_dialog.py index 9bfb171d6b6d1d..1d4d7aa8bbd856 100644 --- a/selfdrive/ui/mici/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/mici/onroad/driver_camera_dialog.py @@ -87,7 +87,7 @@ def _render(self, rect): alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) rl.end_scissor_mode() self._publish_alert_sound(None) - return -1 + return driver_data = self._draw_face_detection(rect) if driver_data is not None: @@ -105,7 +105,7 @@ def _render(self, rect): self._render_dm_alerts(rect) rl.end_scissor_mode() - return -1 + return def _publish_alert_sound(self, dm_state): """Publish selfdriveState with only alertSound field set""" diff --git a/selfdrive/ui/mici/widgets/pairing_dialog.py b/selfdrive/ui/mici/widgets/pairing_dialog.py index 56b87de8a19dc0..02fc370a31f61f 100644 --- a/selfdrive/ui/mici/widgets/pairing_dialog.py +++ b/selfdrive/ui/mici/widgets/pairing_dialog.py @@ -85,8 +85,6 @@ def _render(self, rect: rl.Rectangle) -> int: rl.draw_texture_ex(self._txt_pair, rl.Vector2(label_x, self._rect.y + self._rect.height - self._txt_pair.height - 16), 0.0, 1.0, rl.Color(255, 255, 255, int(255 * 0.35))) - return -1 - def _render_qr_code(self) -> None: if not self._qr_texture: error_font = gui_app.font(FontWeight.BOLD) From 3b856a694193d56cd6c761c113f05bb9ce66c309 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 19 Feb 2026 22:39:13 -0800 Subject: [PATCH 41/73] nice! --- selfdrive/ui/mici/tests/test_widget_leaks.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/selfdrive/ui/mici/tests/test_widget_leaks.py b/selfdrive/ui/mici/tests/test_widget_leaks.py index ffa256b716a340..a05615ebe5226a 100755 --- a/selfdrive/ui/mici/tests/test_widget_leaks.py +++ b/selfdrive/ui/mici/tests/test_widget_leaks.py @@ -36,14 +36,12 @@ "openpilot.system.ui.widgets.confirm_dialog.ConfirmDialog", "openpilot.system.ui.widgets.label.Label", "openpilot.system.ui.widgets.button.Button", - "openpilot.selfdrive.ui.mici.widgets.dialog.BigDialog", "openpilot.system.ui.widgets.html_render.HtmlRenderer", "openpilot.system.ui.widgets.NavBar", "openpilot.system.ui.widgets.inputbox.InputBox", "openpilot.system.ui.widgets.scroller_tici.Scroller", "openpilot.system.ui.widgets.scroller.Scroller", "openpilot.system.ui.widgets.label.UnifiedLabel", - "openpilot.selfdrive.ui.mici.widgets.dialog.BigMultiOptionDialog", "openpilot.system.ui.widgets.mici_keyboard.MiciKeyboard", "openpilot.selfdrive.ui.mici.widgets.dialog.BigConfirmationDialogV2", "openpilot.system.ui.widgets.keyboard.Keyboard", From 6d2463082cee3b33edb6a005dcc37c250edc5314 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 02:48:33 -0800 Subject: [PATCH 42/73] clean up --- system/ui/lib/application.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 28445946caa23e..4a4dbd65dcc40a 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -12,7 +12,7 @@ from contextlib import contextmanager from collections.abc import Callable from collections import deque -from dataclasses import dataclass, field +from dataclasses import dataclass from enum import StrEnum from pathlib import Path from typing import NamedTuple @@ -121,11 +121,6 @@ class ModalOverlay: callback: Callable | None = None -@dataclass -class NavStack: - widgets: list[object] = field(default_factory=list) - - class MousePos(NamedTuple): x: float y: float @@ -234,7 +229,6 @@ def __init__(self, width: int | None = None, height: int | None = None): self._frame = 0 self._window_close_requested = False self._modal_overlay = ModalOverlay() - self._nav_stack = NavStack() self._modal_overlay_shown = False self._modal_overlay_tick: Callable[[], None] | None = None @@ -387,7 +381,7 @@ def _ffmpeg_writer_thread(self): except Exception: break - def push_widget(self, widget): + def push_widget(self, widget: object): assert self._new_modal # disable previous widget to prevent input processing, but keep rendering for smooth transitions @@ -405,7 +399,11 @@ def push_widget(self, widget): def pop_widget(self): assert self._new_modal - # reenable previous widget if exists and show event to allow it to update state if needed (e.g. refresh after settings change) + if len(self._nav_stack) < 2: + cloudlog.warning("At least one widget should remain on the stack, ignoring pop") + return + + # re-enable previous widget if exists if len(self._nav_stack) > 1: prev_widget = self._nav_stack[-2] print('Re-enabling and show_event for', prev_widget.__class__.__name__) @@ -589,9 +587,9 @@ def render(self): rl.clear_background(rl.BLACK) if self._new_modal: + # TODO: only render top 1 if BIG via flag # Only render top two for widget in self._nav_stack[-2:]: - # TODO: need scaled sizes? widget.render(rl.Rectangle(0, 0, self.width, self.height)) print('widget stack', len(self._nav_stack), [w.__class__.__name__ for w in self._nav_stack]) From b179c0d671044672c7018f0ff8b3800ddc12de6a Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 15:01:08 -0800 Subject: [PATCH 43/73] new_modal is always true! --- selfdrive/ui/ui.py | 11 ++------ system/ui/lib/application.py | 54 +++++------------------------------- 2 files changed, 10 insertions(+), 55 deletions(-) diff --git a/selfdrive/ui/ui.py b/selfdrive/ui/ui.py index 3068c40bcb144e..d62aebec8da695 100755 --- a/selfdrive/ui/ui.py +++ b/selfdrive/ui/ui.py @@ -16,20 +16,15 @@ def main(): cores = {5, } config_realtime_process(0, 51) + gui_app.init_window("UI") if BIG_UI: - gui_app.init_window("UI") - main_layout = MainLayout() + MainLayout() else: - gui_app.init_window("UI", new_modal=True) - main_layout = MiciMainLayout() - main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + MiciMainLayout() for should_render in gui_app.render(): ui_state.update() if should_render: - if BIG_UI: - main_layout.render() - # reaffine after power save offlines our core if TICI and os.sched_getaffinity(0) != cores: try: diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 4a4dbd65dcc40a..95483d847ca691 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -215,8 +215,6 @@ def __init__(self, width: int | None = None, height: int | None = None): self._scaled_width += self._scaled_width % 2 self._scaled_height += self._scaled_height % 2 - # TODO: move BIG ui over and deprecate - self._new_modal = False self._render_texture: rl.RenderTexture | None = None self._burn_in_shader: rl.Shader | None = None self._ffmpeg_proc: subprocess.Popen | None = None @@ -232,8 +230,6 @@ def __init__(self, width: int | None = None, height: int | None = None): self._modal_overlay_shown = False self._modal_overlay_tick: Callable[[], None] | None = None - # TODO: move over the entire ui and deprecate - self._new_modal = False self._nav_stack: list[object] = [] self._mouse = MouseState(self._scale) @@ -268,7 +264,7 @@ def target_fps(self): def request_close(self): self._window_close_requested = True - def init_window(self, title: str, fps: int = _DEFAULT_FPS, new_modal: bool = False): + def init_window(self, title: str, fps: int = _DEFAULT_FPS): with self._startup_profile_context(): def _close(sig, frame): self.close() @@ -276,8 +272,6 @@ def _close(sig, frame): signal.signal(signal.SIGINT, _close) atexit.register(self.close) - self._new_modal = new_modal - flags = rl.ConfigFlags.FLAG_MSAA_4X_HINT if ENABLE_VSYNC: flags |= rl.ConfigFlags.FLAG_VSYNC_HINT @@ -382,8 +376,6 @@ def _ffmpeg_writer_thread(self): break def push_widget(self, widget: object): - assert self._new_modal - # disable previous widget to prevent input processing, but keep rendering for smooth transitions if len(self._nav_stack) > 0: prev_widget = self._nav_stack[-1] @@ -397,8 +389,6 @@ def push_widget(self, widget: object): print() def pop_widget(self): - assert self._new_modal - if len(self._nav_stack) < 2: cloudlog.warning("At least one widget should remain on the stack, ignoring pop") return @@ -416,34 +406,15 @@ def pop_widget(self): print() def pop_widgets_to(self, widget): - assert self._new_modal - # pops all widgets after specified widget while len(self._nav_stack) > 0 and self._nav_stack[-1] != widget: self.pop_widget() def get_active_widget(self): - assert self._new_modal - if len(self._nav_stack) > 0: return self._nav_stack[-1] return None - def set_modal_overlay(self, overlay, callback: Callable | None = None): - assert not self._new_modal, "set_modal_overlay is deprecated, use push_widget instead" - - if self._modal_overlay.overlay is not None: - if hasattr(self._modal_overlay.overlay, 'hide_event'): - self._modal_overlay.overlay.hide_event() - - if self._modal_overlay.callback is not None: - self._modal_overlay.callback(-1) - - self._modal_overlay = ModalOverlay(overlay=overlay, callback=callback) - - def set_modal_overlay_tick(self, tick_function: Callable | None): - self._modal_overlay_tick = tick_function - def set_should_render(self, should_render: bool): self._should_render = should_render @@ -586,25 +557,14 @@ def render(self): rl.begin_drawing() rl.clear_background(rl.BLACK) - if self._new_modal: - # TODO: only render top 1 if BIG via flag - # Only render top two - for widget in self._nav_stack[-2:]: - widget.render(rl.Rectangle(0, 0, self.width, self.height)) - - print('widget stack', len(self._nav_stack), [w.__class__.__name__ for w in self._nav_stack]) + # TODO: only render top 1 if BIG via flag + # Only render top two + for widget in self._nav_stack[-2:]: + widget.render(rl.Rectangle(0, 0, self.width, self.height)) - yield True + print('widget stack', len(self._nav_stack), [w.__class__.__name__ for w in self._nav_stack]) - else: - # Handle modal overlay rendering and input processing - if self._handle_modal_overlay(): - # Allow a Widget to still run a function while overlay is shown - if self._modal_overlay_tick is not None: - self._modal_overlay_tick() - yield False - else: - yield True + yield True if self._render_texture: rl.end_texture_mode() From 7b50d7cee2a8675bb2a33bfb3c4a86a0df913439 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 15:01:59 -0800 Subject: [PATCH 44/73] more clean up --- system/ui/lib/application.py | 39 ------------------------------------ 1 file changed, 39 deletions(-) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 95483d847ca691..179955edfab1c0 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -10,9 +10,7 @@ import platform import subprocess from contextlib import contextmanager -from collections.abc import Callable from collections import deque -from dataclasses import dataclass from enum import StrEnum from pathlib import Path from typing import NamedTuple @@ -115,12 +113,6 @@ def font_fallback(font: rl.Font) -> rl.Font: return font -@dataclass -class ModalOverlay: - overlay: object = None - callback: Callable | None = None - - class MousePos(NamedTuple): x: float y: float @@ -226,10 +218,6 @@ def __init__(self, width: int | None = None, height: int | None = None): self._last_fps_log_time: float = time.monotonic() self._frame = 0 self._window_close_requested = False - self._modal_overlay = ModalOverlay() - self._modal_overlay_shown = False - self._modal_overlay_tick: Callable[[], None] | None = None - self._nav_stack: list[object] = [] self._mouse = MouseState(self._scale) @@ -618,33 +606,6 @@ def width(self): def height(self): return self._height - def _handle_modal_overlay(self) -> bool: - if self._modal_overlay.overlay: - if hasattr(self._modal_overlay.overlay, 'render'): - result = self._modal_overlay.overlay.render(rl.Rectangle(0, 0, self.width, self.height)) - elif callable(self._modal_overlay.overlay): - result = self._modal_overlay.overlay() - else: - raise Exception - - # Send show event to Widget - if not self._modal_overlay_shown and hasattr(self._modal_overlay.overlay, 'show_event'): - self._modal_overlay.overlay.show_event() - self._modal_overlay_shown = True - - if result >= 0: - # Clear the overlay and execute the callback - original_modal = self._modal_overlay - self._modal_overlay = ModalOverlay() - if hasattr(original_modal.overlay, 'hide_event'): - original_modal.overlay.hide_event() - if original_modal.callback is not None: - original_modal.callback(result) - return True - else: - self._modal_overlay_shown = False - return False - def _load_fonts(self): for font_weight_file in FontWeight: with as_file(FONT_DIR) as fspath: From 482b9c3be74ddea5f6a25ab569a6086355f2d0f2 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 15:09:34 -0800 Subject: [PATCH 45/73] clean up --- selfdrive/ui/mici/onroad/driver_camera_dialog.py | 3 +-- selfdrive/ui/mici/widgets/pairing_dialog.py | 5 +---- selfdrive/ui/onroad/augmented_road_view.py | 2 +- selfdrive/ui/onroad/driver_camera_dialog.py | 2 +- selfdrive/ui/tests/diff/replay.py | 2 +- selfdrive/ui/tests/profile_onroad.py | 3 +-- selfdrive/ui/widgets/pairing_dialog.py | 2 +- system/ui/mici_setup.py | 4 +--- system/ui/mici_updater.py | 4 +--- system/ui/tici_reset.py | 2 +- system/ui/tici_setup.py | 2 +- system/ui/tici_updater.py | 3 ++- system/ui/widgets/keyboard.py | 2 +- system/ui/widgets/network.py | 2 +- 14 files changed, 15 insertions(+), 23 deletions(-) diff --git a/selfdrive/ui/mici/onroad/driver_camera_dialog.py b/selfdrive/ui/mici/onroad/driver_camera_dialog.py index 1d4d7aa8bbd856..26a5d132c69f2d 100644 --- a/selfdrive/ui/mici/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/mici/onroad/driver_camera_dialog.py @@ -232,13 +232,12 @@ def _draw_eyes(self, rect: rl.Rectangle, driver_data): if __name__ == "__main__": - gui_app.init_window("Driver Camera View (mici)", new_modal=True) + gui_app.init_window("Driver Camera View (mici)") driver_camera_view = DriverCameraDialog() gui_app.push_widget(driver_camera_view) try: for _ in gui_app.render(): ui_state.update() - # driver_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) finally: driver_camera_view.close() diff --git a/selfdrive/ui/mici/widgets/pairing_dialog.py b/selfdrive/ui/mici/widgets/pairing_dialog.py index 02fc370a31f61f..0e3a34890351df 100644 --- a/selfdrive/ui/mici/widgets/pairing_dialog.py +++ b/selfdrive/ui/mici/widgets/pairing_dialog.py @@ -103,15 +103,12 @@ def __del__(self): if __name__ == "__main__": - gui_app.init_window("pairing device", new_modal=True) + gui_app.init_window("pairing device") pairing = PairingDialog() gui_app.push_widget(pairing) # TODO: black screen? try: for _ in gui_app.render(): pass - # result = pairing.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - # if result != -1: - # break finally: del pairing diff --git a/selfdrive/ui/onroad/augmented_road_view.py b/selfdrive/ui/onroad/augmented_road_view.py index f8fb589b617687..17d89fbd509d87 100644 --- a/selfdrive/ui/onroad/augmented_road_view.py +++ b/selfdrive/ui/onroad/augmented_road_view.py @@ -219,7 +219,7 @@ def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray: if __name__ == "__main__": - gui_app.init_window("OnRoad Camera View", new_modal=True) + gui_app.init_window("OnRoad Camera View") road_camera_view = AugmentedRoadView(ROAD_CAM) gui_app.push_widget(road_camera_view) print("***press space to switch camera view***") diff --git a/selfdrive/ui/onroad/driver_camera_dialog.py b/selfdrive/ui/onroad/driver_camera_dialog.py index a4518a25200d19..e66e04b8241275 100644 --- a/selfdrive/ui/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/onroad/driver_camera_dialog.py @@ -100,7 +100,7 @@ def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray: if __name__ == "__main__": - gui_app.init_window("Driver Camera View", new_modal=True) + gui_app.init_window("Driver Camera View") driver_camera_view = DriverCameraDialog() gui_app.push_widget(driver_camera_view) diff --git a/selfdrive/ui/tests/diff/replay.py b/selfdrive/ui/tests/diff/replay.py index 9b79cdde5b1212..bfeebad4b7599b 100755 --- a/selfdrive/ui/tests/diff/replay.py +++ b/selfdrive/ui/tests/diff/replay.py @@ -38,7 +38,7 @@ def run_replay(variant: LayoutVariant) -> None: from openpilot.system.ui.lib.application import gui_app # Import here for accurate coverage from openpilot.selfdrive.ui.tests.diff.replay_script import build_script - gui_app.init_window("ui diff test", fps=FPS, new_modal=True) + gui_app.init_window("ui diff test", fps=FPS) # Dynamically import main layout based on variant if variant == "mici": diff --git a/selfdrive/ui/tests/profile_onroad.py b/selfdrive/ui/tests/profile_onroad.py index e14bc958353901..18194d73630199 100755 --- a/selfdrive/ui/tests/profile_onroad.py +++ b/selfdrive/ui/tests/profile_onroad.py @@ -81,9 +81,8 @@ def mock_update(timeout=None): if args.headless: os.environ['SDL_VIDEODRIVER'] = 'dummy' - gui_app.init_window("UI Profiling", fps=600, new_modal=True) + gui_app.init_window("UI Profiling", fps=600) main_layout = MiciMainLayout() - main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) print("Running...") patch_submaster(message_chunks) diff --git a/selfdrive/ui/widgets/pairing_dialog.py b/selfdrive/ui/widgets/pairing_dialog.py index c07b2463f34af0..1ff550e4b6d524 100644 --- a/selfdrive/ui/widgets/pairing_dialog.py +++ b/selfdrive/ui/widgets/pairing_dialog.py @@ -160,7 +160,7 @@ def __del__(self): if __name__ == "__main__": - gui_app.init_window("pairing device", new_modal=True) + gui_app.init_window("pairing device") pairing = PairingDialog() gui_app.push_widget(pairing) try: diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 8f4ce30ea17612..cf675f4047d08d 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -714,13 +714,11 @@ def download_failed(self, url: str, reason: str): def main(): try: - gui_app.init_window("Setup", new_modal=True) + gui_app.init_window("Setup") setup = Setup() gui_app.push_widget(setup) for _ in gui_app.render(): pass - # if should_render: - # setup.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) setup.close() except Exception as e: print(f"Setup error: {e}") diff --git a/system/ui/mici_updater.py b/system/ui/mici_updater.py index ed302840646baa..9c2069f82abef0 100755 --- a/system/ui/mici_updater.py +++ b/system/ui/mici_updater.py @@ -180,13 +180,11 @@ def main(): manifest_path = sys.argv[2] try: - gui_app.init_window("System Update", new_modal=True) + gui_app.init_window("System Update") updater = Updater(updater_path, manifest_path) gui_app.push_widget(updater) for _ in gui_app.render(): pass - # if should_render: - # updater.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) updater.close() except Exception as e: print(f"Updater error: {e}") diff --git a/system/ui/tici_reset.py b/system/ui/tici_reset.py index b22b240850817c..23f6b344ec6bd3 100755 --- a/system/ui/tici_reset.py +++ b/system/ui/tici_reset.py @@ -116,7 +116,7 @@ def main(): elif sys.argv[1] == "--format": mode = ResetMode.FORMAT - gui_app.init_window("System Reset", 20, new_modal=True) + gui_app.init_window("System Reset", 20) reset = Reset(mode) if mode == ResetMode.FORMAT: diff --git a/system/ui/tici_setup.py b/system/ui/tici_setup.py index bb70b75b20de91..39f95cc8a00111 100755 --- a/system/ui/tici_setup.py +++ b/system/ui/tici_setup.py @@ -436,7 +436,7 @@ def download_failed(self, url: str, reason: str): def main(): try: - gui_app.init_window("Setup", 20, new_modal=True) + gui_app.init_window("Setup", 20) setup = Setup() gui_app.push_widget(setup) for _ in gui_app.render(): diff --git a/system/ui/tici_updater.py b/system/ui/tici_updater.py index c040e1a4041693..c8f389fd1b28ff 100755 --- a/system/ui/tici_updater.py +++ b/system/ui/tici_updater.py @@ -160,7 +160,8 @@ def main(): manifest_path = sys.argv[2] try: - gui_app.init_window("System Update", new_modal=True) + # todo: popping can keep keyboard up! + gui_app.init_window("System Update") gui_app.push_widget(Updater(updater_path, manifest_path)) for _ in gui_app.render(): pass diff --git a/system/ui/widgets/keyboard.py b/system/ui/widgets/keyboard.py index 531725688a2d98..49c59a431f12e5 100644 --- a/system/ui/widgets/keyboard.py +++ b/system/ui/widgets/keyboard.py @@ -272,7 +272,7 @@ def callback(result: DialogResult): print("Canceled") gui_app.request_close() - gui_app.init_window("Keyboard", new_modal=True) + gui_app.init_window("Keyboard") keyboard = Keyboard(min_text_size=8, show_password_toggle=True, callback=callback) keyboard.set_title("Keyboard Input", "Type your text below") diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index fcd56607c0be77..668565a033ade2 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -481,7 +481,7 @@ def _on_disconnected(self): def main(): - gui_app.init_window("Wi-Fi Manager", new_modal=True) + gui_app.init_window("Wi-Fi Manager") gui_app.push_widget(WifiManagerUI(WifiManager())) for _ in gui_app.render(): From 67b82d74abaa8dd04f3e1439c70936484b818ee2 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 15:16:04 -0800 Subject: [PATCH 46/73] big only renders top 1 --- system/ui/lib/application.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 179955edfab1c0..618b0cd0d1e709 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -219,6 +219,7 @@ def __init__(self, width: int | None = None, height: int | None = None): self._frame = 0 self._window_close_requested = False self._nav_stack: list[object] = [] + self._nav_stack_widgets_to_render = 1 if self.big_ui() else 2 self._mouse = MouseState(self._scale) self._mouse_events: list[MouseEvent] = [] @@ -545,9 +546,8 @@ def render(self): rl.begin_drawing() rl.clear_background(rl.BLACK) - # TODO: only render top 1 if BIG via flag - # Only render top two - for widget in self._nav_stack[-2:]: + # Only render top widgets + for widget in self._nav_stack[-self._nav_stack_widgets_to_render:]: widget.render(rl.Rectangle(0, 0, self.width, self.height)) print('widget stack', len(self._nav_stack), [w.__class__.__name__ for w in self._nav_stack]) From 5667d4cf41220634355e78aabe393395444b7f57 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 16:47:57 -0800 Subject: [PATCH 47/73] fixup setup and updater --- .../mici/layouts/settings/network/wifi_ui.py | 2 ++ system/ui/lib/application.py | 23 +++++++++++++++- system/ui/lib/wifi_manager.py | 1 + system/ui/mici_reset.py | 8 +++--- system/ui/mici_setup.py | 27 +++++-------------- system/ui/mici_updater.py | 1 + system/ui/tici_updater.py | 1 - 7 files changed, 36 insertions(+), 27 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 7a44f14492f70d..6c6cc6cb6c999f 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -369,6 +369,7 @@ def show_event(self): def hide_event(self): super().hide_event() + print('WifiUIMici->hide_event') self._scroller.hide_event() def _on_network_updated(self, networks: list[Network]): @@ -407,6 +408,7 @@ def _connect_with_password(self, ssid: str, password: str): def _on_option_selected(self, option: str): super()._on_option_selected(option) + print(f"WifiUIMici->_on_option_selected: {option}") if option in self._networks: self._network_info_page.set_current_network(self._networks[option]) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 618b0cd0d1e709..15292b391e7ed9 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -10,6 +10,7 @@ import platform import subprocess from contextlib import contextmanager +from collections.abc import Callable from collections import deque from enum import StrEnum from pathlib import Path @@ -220,6 +221,7 @@ def __init__(self, width: int | None = None, height: int | None = None): self._window_close_requested = False self._nav_stack: list[object] = [] self._nav_stack_widgets_to_render = 1 if self.big_ui() else 2 + self._nav_stack_tick: Callable[[], None] | None = None self._mouse = MouseState(self._scale) self._mouse_events: list[MouseEvent] = [] @@ -365,11 +367,19 @@ def _ffmpeg_writer_thread(self): break def push_widget(self, widget: object): + import traceback + traceback.print_stack(limit=5) + + if widget in self._nav_stack: + cloudlog.warning("Widget already in stack, cannot push again!") + return + # disable previous widget to prevent input processing, but keep rendering for smooth transitions if len(self._nav_stack) > 0: prev_widget = self._nav_stack[-1] print('Disabling and hide_event for', prev_widget.__class__.__name__) # prev_widget.hide_event() + # TODO: change these to touch_valid prev_widget.set_enabled(False) print('Pushing and show_event for', widget.__class__.__name__) @@ -379,7 +389,7 @@ def push_widget(self, widget: object): def pop_widget(self): if len(self._nav_stack) < 2: - cloudlog.warning("At least one widget should remain on the stack, ignoring pop") + cloudlog.warning("At least one widget should remain on the stack, ignoring pop!") return # re-enable previous widget if exists @@ -395,6 +405,10 @@ def pop_widget(self): print() def pop_widgets_to(self, widget): + if widget not in self._nav_stack: + cloudlog.warning("Widget not in stack, cannot pop to it!") + return + # pops all widgets after specified widget while len(self._nav_stack) > 0 and self._nav_stack[-1] != widget: self.pop_widget() @@ -404,6 +418,9 @@ def get_active_widget(self): return self._nav_stack[-1] return None + def set_nav_stack_tick(self, tick_function: Callable | None): + self._nav_stack_tick = tick_function + def set_should_render(self, should_render: bool): self._should_render = should_render @@ -546,6 +563,10 @@ def render(self): rl.begin_drawing() rl.clear_background(rl.BLACK) + # Allow a Widget to still run a function regardless of the stack depth + if self._nav_stack_tick is not None: + self._nav_stack_tick() + # Only render top widgets for widget in self._nav_stack[-self._nav_stack_widgets_to_render:]: widget.render(rl.Rectangle(0, 0, self.width, self.height)) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 25c4548e945550..7f3893fc9d55b1 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -439,6 +439,7 @@ def _network_scanner(self): if time.monotonic() - self._last_network_scan > SCAN_PERIOD_SECONDS: self._request_scan() self._last_network_scan = time.monotonic() + # print('WifiManager active', self._active) time.sleep(1 / 2.) def _wait_for_wifi_device(self): diff --git a/system/ui/mici_reset.py b/system/ui/mici_reset.py index 925afd7d10a9a6..357e67293154a4 100755 --- a/system/ui/mici_reset.py +++ b/system/ui/mici_reset.py @@ -150,10 +150,10 @@ def main(): if mode == ResetMode.FORMAT: reset.start_reset() - for should_render in gui_app.render(): - if should_render: - if not reset.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)): - break + gui_app.push_widget(reset) + + for _ in gui_app.render(): + pass if __name__ == "__main__": diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index cf675f4047d08d..7e93fc353049f3 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -439,6 +439,7 @@ def __init__(self, wifi_manager, continue_callback: Callable, back_callback: Cal back_txt = gui_app.texture("icons_mici/setup/back_new.png", 37, 32) self._back_button = SmallCircleIconButton(back_txt) self._back_button.set_click_callback(back_callback) + self._back_button.set_enabled(lambda: self.enabled) # for nav stack self._wifi_button = SmallerRoundedButton("wifi") self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui)) @@ -448,8 +449,6 @@ def __init__(self, wifi_manager, continue_callback: Callable, back_callback: Cal self._continue_button.set_enabled(False) self._continue_button.set_click_callback(continue_callback) - self._prev_has_internet = False - def set_has_internet(self, has_internet: bool): if has_internet: self._network_header.set_title("connected to internet") @@ -460,20 +459,6 @@ def set_has_internet(self, has_internet: bool): self._network_header.set_icon(self._no_wifi_txt) self._continue_button.set_enabled(False) - if has_internet and not self._prev_has_internet: - pass - # gui_app.pop_widgets_to(self) - self._prev_has_internet = has_internet - - def show_event(self): - super().show_event() - self._wifi_ui.show_event() - - def hide_event(self): - super().hide_event() - if self._state == NetworkSetupState.WIFI_PANEL: - self._wifi_ui.hide_event() - def _render(self, _): self._network_header.render(rl.Rectangle( self._rect.x + 16, @@ -518,14 +503,15 @@ def __init__(self): self._network_monitor = NetworkConnectivityMonitor() self._network_monitor.start() self._prev_has_internet = False - # TODO: fix this if broken - gui_app.set_modal_overlay_tick(self._modal_overlay_tick) + gui_app.set_nav_stack_tick(self._nav_stack_tick) self._start_page = StartPage() self._start_page.set_click_callback(self._getting_started_button_callback) self._network_setup_page = NetworkSetupPage(self._wifi_manager, self._network_setup_continue_button_callback, self._network_setup_back_button_callback) + # TODO: change these to touch_valid + self._network_setup_page.set_enabled(lambda: self.enabled) # for nav stack self._software_selection_page = SoftwareSelectionPage(self._software_selection_continue_button_callback, self._software_selection_custom_software_button_callback) @@ -537,10 +523,10 @@ def __init__(self): self._downloading_page = DownloadingPage() - def _modal_overlay_tick(self): + def _nav_stack_tick(self): has_internet = self._network_monitor.network_connected.is_set() if has_internet and not self._prev_has_internet: - gui_app.set_modal_overlay(None) + gui_app.pop_widgets_to(self) self._prev_has_internet = has_internet def _update_state(self): @@ -612,7 +598,6 @@ def close(self): def render_network_setup(self, rect: rl.Rectangle): has_internet = self._network_monitor.network_connected.is_set() - self._prev_has_internet = has_internet self._network_setup_page.set_has_internet(has_internet) self._network_setup_page.render(rect) diff --git a/system/ui/mici_updater.py b/system/ui/mici_updater.py index 9c2069f82abef0..5de72ac8c4923f 100755 --- a/system/ui/mici_updater.py +++ b/system/ui/mici_updater.py @@ -38,6 +38,7 @@ def __init__(self, updater_path, manifest_path): self._network_setup_page = NetworkSetupPage(self._wifi_manager, self._network_setup_continue_callback, self._network_setup_back_callback) + self._network_setup_page.set_enabled(lambda: self.enabled) # for nav stack self._network_monitor = NetworkConnectivityMonitor() self._network_monitor.start() diff --git a/system/ui/tici_updater.py b/system/ui/tici_updater.py index c8f389fd1b28ff..9824638cd06626 100755 --- a/system/ui/tici_updater.py +++ b/system/ui/tici_updater.py @@ -160,7 +160,6 @@ def main(): manifest_path = sys.argv[2] try: - # todo: popping can keep keyboard up! gui_app.init_window("System Update") gui_app.push_widget(Updater(updater_path, manifest_path)) for _ in gui_app.render(): From 3cfb226ccb51869ed1f7d630b5fdd6725ad094d5 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 16:49:47 -0800 Subject: [PATCH 48/73] stash --- system/ui/mici_setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 7e93fc353049f3..fa4e915d94cabf 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -79,7 +79,9 @@ def _run(self): try: request = urllib.request.Request(OPENPILOT_URL, method="HEAD") urllib.request.urlopen(request, timeout=1.0) - self.network_connected.set() + time.sleep(5) + # print('HAS INTERNET!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') + # self.network_connected.set() if HARDWARE.get_network_type() == NetworkType.wifi: self.wifi_connected.set() except Exception: From 2ffd48f36082466ded6ac2b5db72520d975a4ff0 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 16:49:50 -0800 Subject: [PATCH 49/73] Revert "stash" This reverts commit 3cfb226ccb51869ed1f7d630b5fdd6725ad094d5. --- system/ui/mici_setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index fa4e915d94cabf..7e93fc353049f3 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -79,9 +79,7 @@ def _run(self): try: request = urllib.request.Request(OPENPILOT_URL, method="HEAD") urllib.request.urlopen(request, timeout=1.0) - time.sleep(5) - # print('HAS INTERNET!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') - # self.network_connected.set() + self.network_connected.set() if HARDWARE.get_network_type() == NetworkType.wifi: self.wifi_connected.set() except Exception: From fbae22a71af818b11d7c32013511c3a14e4dd1b7 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 17:03:00 -0800 Subject: [PATCH 50/73] fix mici keys coming in from top --- system/ui/widgets/mici_keyboard.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/system/ui/widgets/mici_keyboard.py b/system/ui/widgets/mici_keyboard.py index f8afd506760a48..59a2451387d000 100644 --- a/system/ui/widgets/mici_keyboard.py +++ b/system/ui/widgets/mici_keyboard.py @@ -227,6 +227,8 @@ def _set_keys(self, keys: list[list[Key]]): for current_row, row in zip(self._current_keys, keys, strict=False): # not all layouts have the same number of keys for current_key, key in zip_repeat(current_row, row): + # reset parent rect for new keys + key.set_parent_rect(self._rect) current_pos = current_key.get_position() key.set_position(current_pos[0], current_pos[1], smooth=False) From 3650fed929ae54fd59c4f457238b46d18362ce25 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 17:03:17 -0800 Subject: [PATCH 51/73] clean up --- system/ui/lib/application.py | 25 +++++++++++-------------- system/ui/mici_setup.py | 1 - 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 15292b391e7ed9..63d6a1e96f5017 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -367,9 +367,6 @@ def _ffmpeg_writer_thread(self): break def push_widget(self, widget: object): - import traceback - traceback.print_stack(limit=5) - if widget in self._nav_stack: cloudlog.warning("Widget already in stack, cannot push again!") return @@ -392,16 +389,16 @@ def pop_widget(self): cloudlog.warning("At least one widget should remain on the stack, ignoring pop!") return - # re-enable previous widget if exists - if len(self._nav_stack) > 1: - prev_widget = self._nav_stack[-2] - print('Re-enabling and show_event for', prev_widget.__class__.__name__) - # prev_widget.show_event() - prev_widget.set_enabled(True) - if len(self._nav_stack) > 1: - print('Popping and hide_event for', self._nav_stack[-1].__class__.__name__) - widget = self._nav_stack.pop() - widget.hide_event() + # re-enable previous widget and pop current + # TODO: switch to touch_valid + prev_widget = self._nav_stack[-2] + print('Re-enabling and show_event for', prev_widget.__class__.__name__) + # prev_widget.show_event() + prev_widget.set_enabled(True) + + print('Popping and hide_event for', self._nav_stack[-1].__class__.__name__) + widget = self._nav_stack.pop() + widget.hide_event() print() def pop_widgets_to(self, widget): @@ -571,7 +568,7 @@ def render(self): for widget in self._nav_stack[-self._nav_stack_widgets_to_render:]: widget.render(rl.Rectangle(0, 0, self.width, self.height)) - print('widget stack', len(self._nav_stack), [w.__class__.__name__ for w in self._nav_stack]) + # print('widget stack', len(self._nav_stack), [w.__class__.__name__ for w in self._nav_stack]) yield True diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 7e93fc353049f3..14278ec986b6f1 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -546,7 +546,6 @@ def _set_state(self, state: SetupState): self._network_setup_page.hide_event() def _render(self, rect: rl.Rectangle): - print(f"Rendering state: {self.state.name}") if self.state == SetupState.GETTING_STARTED: self._start_page.render(rect) elif self.state in (SetupState.NETWORK_SETUP, SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE): From 2edd93f9611fd8cdc97a3ccd6f2b9b5ae051a042 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 17:26:22 -0800 Subject: [PATCH 52/73] fix mici dialogs like tici, pop first incase call back pushes --- selfdrive/ui/mici/widgets/dialog.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 4fbbcf40f0d425..6453d28e7d13a6 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -86,10 +86,10 @@ def __init__(self, title: str, icon: str, red: bool = False, self._slider.set_enabled(lambda: not self._swiping_away) def _on_confirm(self): - if self._confirm_callback: - self._confirm_callback() if self._exit_on_confirm: gui_app.pop_widget() + if self._confirm_callback: + self._confirm_callback() def _update_state(self): super()._update_state() @@ -131,9 +131,10 @@ def __init__(self, self._top_right_button_rect = rl.Rectangle(0, 0, 0, 0) def confirm_callback_wrapper(): - if confirm_callback: - confirm_callback(self._keyboard.text()) + text = self._keyboard.text() gui_app.pop_widget() + if confirm_callback: + confirm_callback(text) self._confirm_callback = confirm_callback_wrapper def _update_state(self): From f69d106df61262f049df20cc1a9064ca1e6feeb7 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:01:13 -0800 Subject: [PATCH 53/73] clever way but not not --- system/ui/lib/application.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 63d6a1e96f5017..4129652821630a 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -564,9 +564,17 @@ def render(self): if self._nav_stack_tick is not None: self._nav_stack_tick() - # Only render top widgets - for widget in self._nav_stack[-self._nav_stack_widgets_to_render:]: - widget.render(rl.Rectangle(0, 0, self.width, self.height)) + # Only render top widgets; suppress mouse events for non-active (background) widgets + # so their children can't process touches either + widgets_to_render = self._nav_stack[-self._nav_stack_widgets_to_render:] + for i, widget in enumerate(widgets_to_render): + if i < len(widgets_to_render) - 1: + saved_events = self._mouse_events + self._mouse_events = [] + widget.render(rl.Rectangle(0, 0, self.width, self.height)) + self._mouse_events = saved_events + else: + widget.render(rl.Rectangle(0, 0, self.width, self.height)) # print('widget stack', len(self._nav_stack), [w.__class__.__name__ for w in self._nav_stack]) From 258befcb221b717cb4368a5eaa8053fec894c91e Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:01:19 -0800 Subject: [PATCH 54/73] Revert "clever way but not not" This reverts commit f69d106df61262f049df20cc1a9064ca1e6feeb7. --- system/ui/lib/application.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 4129652821630a..63d6a1e96f5017 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -564,17 +564,9 @@ def render(self): if self._nav_stack_tick is not None: self._nav_stack_tick() - # Only render top widgets; suppress mouse events for non-active (background) widgets - # so their children can't process touches either - widgets_to_render = self._nav_stack[-self._nav_stack_widgets_to_render:] - for i, widget in enumerate(widgets_to_render): - if i < len(widgets_to_render) - 1: - saved_events = self._mouse_events - self._mouse_events = [] - widget.render(rl.Rectangle(0, 0, self.width, self.height)) - self._mouse_events = saved_events - else: - widget.render(rl.Rectangle(0, 0, self.width, self.height)) + # Only render top widgets + for widget in self._nav_stack[-self._nav_stack_widgets_to_render:]: + widget.render(rl.Rectangle(0, 0, self.width, self.height)) # print('widget stack', len(self._nav_stack), [w.__class__.__name__ for w in self._nav_stack]) From 8cd715516369b0d0ce922a2ff4c36db2759d6283 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:01:37 -0800 Subject: [PATCH 55/73] more setup --- system/ui/mici_setup.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 14278ec986b6f1..aa6f54c508b058 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -127,7 +127,9 @@ def __init__(self, use_openpilot_callback: Callable, super().__init__() self._openpilot_slider = LargerSlider("slide to use\nopenpilot", use_openpilot_callback) + self._openpilot_slider.set_enabled(lambda: self.enabled) self._custom_software_slider = LargerSlider("slide to use\ncustom software", use_custom_software_callback, green=False) + self._custom_software_slider.set_enabled(lambda: self.enabled) def reset(self): self._openpilot_slider.reset() @@ -389,9 +391,11 @@ def __init__(self, reboot_callback: Callable, retry_callback: Callable, title: s self._reboot_button = SmallRedPillButton("reboot") self._reboot_button.set_click_callback(reboot_callback) + self._reboot_button.set_enabled(lambda: self.enabled) # for nav stack self._retry_button = WideRoundedButton("retry") self._retry_button.set_click_callback(retry_callback) + self._retry_button.set_enabled(lambda: self.enabled) # for nav stack def set_reason(self, reason: str): self._reason_label.set_text(reason) @@ -515,11 +519,14 @@ def __init__(self): self._software_selection_page = SoftwareSelectionPage(self._software_selection_continue_button_callback, self._software_selection_custom_software_button_callback) + self._software_selection_page.set_enabled(lambda: self.enabled) # for nav stack self._download_failed_page = FailedPage(HARDWARE.reboot, self._download_failed_startover_button_callback) + self._download_failed_page.set_enabled(lambda: self.enabled) # for nav stack self._custom_software_warning_page = CustomSoftwareWarningPage(self._software_selection_custom_software_continue, self._custom_software_warning_back_button_callback) + self._custom_software_warning_page.set_enabled(lambda: self.enabled) # for nav stack self._downloading_page = DownloadingPage() From 5f145b04e50980f037162db655d9170f32cf3bbd Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:09:47 -0800 Subject: [PATCH 56/73] mici keyboard: fix not disabling below --- selfdrive/ui/mici/widgets/dialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 6453d28e7d13a6..3b7241cd4cd3af 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -83,7 +83,7 @@ def __init__(self, title: str, icon: str, red: bool = False, self._slider = RedBigSlider(title, icon_txt, confirm_callback=self._on_confirm) else: self._slider = BigSlider(title, icon_txt, confirm_callback=self._on_confirm) - self._slider.set_enabled(lambda: not self._swiping_away) + self._slider.set_enabled(lambda: self.enabled and not self._swiping_away) def _on_confirm(self): if self._exit_on_confirm: @@ -115,6 +115,7 @@ def __init__(self, font_weight=FontWeight.MEDIUM) self._keyboard = MiciKeyboard() self._keyboard.set_text(default_text) + self._keyboard.set_enabled(lambda: self.enabled) # for nav stack self._minimum_length = minimum_length self._backspace_held_time: float | None = None From 78602b4e83d42a57f30d9ab139a180f5c56bbbdc Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:10:52 -0800 Subject: [PATCH 57/73] cmt --- selfdrive/ui/mici/widgets/dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 3b7241cd4cd3af..792b24c69f1729 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -83,7 +83,7 @@ def __init__(self, title: str, icon: str, red: bool = False, self._slider = RedBigSlider(title, icon_txt, confirm_callback=self._on_confirm) else: self._slider = BigSlider(title, icon_txt, confirm_callback=self._on_confirm) - self._slider.set_enabled(lambda: self.enabled and not self._swiping_away) + self._slider.set_enabled(lambda: self.enabled and not self._swiping_away) # self.enabled for nav stack def _on_confirm(self): if self._exit_on_confirm: From 62689246edd2ea085a8e85490a0a6f8dcd6b6773 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:23:45 -0800 Subject: [PATCH 58/73] fix wifi callbacks not running in rare case --- .../mici/layouts/settings/network/__init__.py | 27 ++++--------------- .../mici/layouts/settings/network/wifi_ui.py | 8 ------ 2 files changed, 5 insertions(+), 30 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index 547bd9bb10118a..9f49521c36dcbe 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -11,18 +11,10 @@ from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType, ConnectStatus, normalize_ssid -# class NetworkPanelType(IntEnum): -# NONE = 0 -# WIFI = 1 - - class NetworkLayoutMici(NavWidget): def __init__(self): super().__init__() - # self._current_panel = NetworkPanelType.WIFI - # self.set_back_enabled(lambda: self._current_panel == NetworkPanelType.NONE) - self._wifi_manager = WifiManager() self._wifi_manager.set_active(False) self._wifi_ui = WifiUIMici(self._wifi_manager) @@ -77,7 +69,6 @@ def network_metered_callback(value: str): self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 64, 47) self._wifi_button = BigButton("wi-fi", "not connected", self._wifi_slash_txt, scroll=True) - # self._wifi_button.set_click_callback(lambda: self._switch_to_panel(NetworkPanelType.WIFI)) self._wifi_button.set_click_callback(lambda: gui_app.push_widget(self._wifi_ui)) # ******** Advanced settings ******** @@ -148,10 +139,15 @@ def show_event(self): self._wifi_manager.set_active(True) self._scroller.show_event() + # Process wifi callbacks while at any point in the nav stack + gui_app.set_nav_stack_tick(self._wifi_manager.process_callbacks) + def hide_event(self): super().hide_event() self._wifi_manager.set_active(False) + gui_app.set_nav_stack_tick(None) + def _toggle_roaming(self, checked: bool): self._wifi_manager.update_gsm_settings(checked, ui_state.params.get("GsmApn") or "", ui_state.params.get_bool("GsmMetered")) @@ -189,18 +185,5 @@ def _on_network_updated(self, networks: list[Network]): MeteredType.NO: 'unmetered' }.get(self._wifi_manager.current_network_metered, 'default')) - # def _switch_to_panel(self, panel_type: NetworkPanelType): - # if panel_type == NetworkPanelType.WIFI: - # self._wifi_ui.show_event() - # elif self._current_panel == NetworkPanelType.WIFI: - # self._wifi_ui.hide_event() - # self._current_panel = panel_type - def _render(self, rect: rl.Rectangle): - # TODO: make sure process_callbacks still gets called with new nav stack - self._wifi_manager.process_callbacks() - - # if self._current_panel == NetworkPanelType.WIFI: - # self._wifi_ui.render(rect) - # else: self._scroller.render(rect) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 6c6cc6cb6c999f..6a41d805aeb027 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -437,14 +437,6 @@ def _on_need_auth(self, ssid, incorrect_password=True): confirm_callback=lambda _password: self._connect_with_password(ssid, _password)) gui_app.push_widget(dlg) - # def on_close(result=None): - # gui_app.set_modal_overlay_tick(None) - # self._open_network_manage_page(result) - # - # # Process wifi callbacks while the keyboard is shown so forgotten clears connecting state - # gui_app.set_modal_overlay_tick(self._wifi_manager.process_callbacks) - # gui_app.set_modal_overlay(dlg, on_close) - def _render(self, _): super()._render(_) From 34dda14971e14af7130626e034b6c65ef517cd6e Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:29:03 -0800 Subject: [PATCH 59/73] clean up network --- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 6a41d805aeb027..5296141c172e27 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -256,7 +256,7 @@ def update_networks(self, networks: dict[str, Network]): def _update_state(self): super()._update_state() - # Modal overlays stop main UI rendering, so we need to call here + # TODO: remove? only left for potential compatibility with setup/updater self._wifi_manager.process_callbacks() if self._network is None: @@ -412,8 +412,6 @@ def _on_option_selected(self, option: str): if option in self._networks: self._network_info_page.set_current_network(self._networks[option]) - # self._network_info_page.update_networks(self._networks) - # gui_app.set_modal_overlay(self._network_info_page) gui_app.push_widget(self._network_info_page) def _connect_to_network(self, ssid: str): From d0d275bca2d7311f1173e048a6a9e143201411af Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:30:40 -0800 Subject: [PATCH 60/73] clean up network --- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 5296141c172e27..9dd324f433250e 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -229,7 +229,6 @@ def __init__(self, wifi_manager, connect_callback: Callable, forget_callback: Ca self._subtitle = UnifiedLabel("", 36, FontWeight.ROMAN, rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)), alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) - # self.set_back_callback(lambda: gui_app.set_modal_overlay(None)) self.set_back_callback(gui_app.pop_widget) # State @@ -249,8 +248,6 @@ def update_networks(self, networks: dict[str, Network]): break else: # network disappeared, close page - # FIXME: this closes wifi page - # gui_app.set_modal_overlay(None) if gui_app.get_active_widget() == self: gui_app.pop_widget() @@ -369,11 +366,9 @@ def show_event(self): def hide_event(self): super().hide_event() - print('WifiUIMici->hide_event') self._scroller.hide_event() def _on_network_updated(self, networks: list[Network]): - print('WifiUIMici->_on_network_updated') self._networks = {network.ssid: network for network in networks} self._update_buttons() self._network_info_page.update_networks(self._networks) @@ -408,7 +403,6 @@ def _connect_with_password(self, ssid: str, password: str): def _on_option_selected(self, option: str): super()._on_option_selected(option) - print(f"WifiUIMici->_on_option_selected: {option}") if option in self._networks: self._network_info_page.set_current_network(self._networks[option]) From 3bd4d9d2be31ae90af07d518c3d7eaeddb991abf Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:33:23 -0800 Subject: [PATCH 61/73] clean up dialog --- selfdrive/ui/mici/widgets/dialog.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 792b24c69f1729..612f31b7537e5c 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -26,9 +26,6 @@ def __init__(self): self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) self.set_back_callback(gui_app.pop_widget) - # def _render(self, _): - # pass - class BigDialog(BigDialogBase): def __init__(self, From bbf87f280751538b0f4977dc2e8f71067629679b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:33:47 -0800 Subject: [PATCH 62/73] pairing --- selfdrive/ui/mici/widgets/pairing_dialog.py | 1 - 1 file changed, 1 deletion(-) diff --git a/selfdrive/ui/mici/widgets/pairing_dialog.py b/selfdrive/ui/mici/widgets/pairing_dialog.py index 0e3a34890351df..cfc291ab407af6 100644 --- a/selfdrive/ui/mici/widgets/pairing_dialog.py +++ b/selfdrive/ui/mici/widgets/pairing_dialog.py @@ -106,7 +106,6 @@ def __del__(self): gui_app.init_window("pairing device") pairing = PairingDialog() gui_app.push_widget(pairing) - # TODO: black screen? try: for _ in gui_app.render(): pass From cc73a63cfe0decbada269d7cf4765f899d5d60cc Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:35:31 -0800 Subject: [PATCH 63/73] rm --- system/ui/lib/wifi_manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 7f3893fc9d55b1..25c4548e945550 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -439,7 +439,6 @@ def _network_scanner(self): if time.monotonic() - self._last_network_scan > SCAN_PERIOD_SECONDS: self._request_scan() self._last_network_scan = time.monotonic() - # print('WifiManager active', self._active) time.sleep(1 / 2.) def _wait_for_wifi_device(self): From 453308050c8e193b431ab359fea4ca3e5c747b02 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:37:19 -0800 Subject: [PATCH 64/73] todo --- selfdrive/ui/mici/layouts/settings/network/wifi_ui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 9dd324f433250e..191836252756fd 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -248,6 +248,7 @@ def update_networks(self, networks: dict[str, Network]): break else: # network disappeared, close page + # TODO: pop_widgets_to, to close potentially open keyboard too if gui_app.get_active_widget() == self: gui_app.pop_widget() From d15c1011d89f70e3d755567659de1d01c6682b40 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:40:27 -0800 Subject: [PATCH 65/73] fix replay --- selfdrive/ui/tests/diff/replay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/tests/diff/replay.py b/selfdrive/ui/tests/diff/replay.py index bfeebad4b7599b..9be35f6abfbd5f 100755 --- a/selfdrive/ui/tests/diff/replay.py +++ b/selfdrive/ui/tests/diff/replay.py @@ -46,7 +46,7 @@ def run_replay(variant: LayoutVariant) -> None: else: from openpilot.selfdrive.ui.layouts.main import MainLayout main_layout = MainLayout() - main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + gui_app.push_widget(main_layout) pm = PubMaster(["deviceState", "pandaStates", "driverStateV2", "selfdriveState"]) script = build_script(pm, main_layout, variant) From 38aac9a34ab8d89404cfa289ea246156fcb1ea52 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:41:58 -0800 Subject: [PATCH 66/73] they push themselkves! --- selfdrive/ui/tests/diff/replay.py | 1 - 1 file changed, 1 deletion(-) diff --git a/selfdrive/ui/tests/diff/replay.py b/selfdrive/ui/tests/diff/replay.py index 9be35f6abfbd5f..7ed7ce936495f1 100755 --- a/selfdrive/ui/tests/diff/replay.py +++ b/selfdrive/ui/tests/diff/replay.py @@ -46,7 +46,6 @@ def run_replay(variant: LayoutVariant) -> None: else: from openpilot.selfdrive.ui.layouts.main import MainLayout main_layout = MainLayout() - gui_app.push_widget(main_layout) pm = PubMaster(["deviceState", "pandaStates", "driverStateV2", "selfdriveState"]) script = build_script(pm, main_layout, variant) From e380ef28244743ad2db6b0067541ebec06c4f241 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:43:04 -0800 Subject: [PATCH 67/73] clean up ui_state --- selfdrive/ui/ui_state.py | 1 - 1 file changed, 1 deletion(-) diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index 1874643a53b54e..30a656509551e0 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -208,7 +208,6 @@ def set_override_interactive_timeout(self, timeout: int | None) -> None: @property def interactive_timeout(self) -> int: - return 5 if self._override_interactive_timeout is not None: return self._override_interactive_timeout From 8a36526b5c2216b72f93844de94c5fbc66607e5e Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:46:09 -0800 Subject: [PATCH 68/73] clean up application --- system/ui/lib/application.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 63d6a1e96f5017..7c0bd2bf3cd776 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -220,8 +220,8 @@ def __init__(self, width: int | None = None, height: int | None = None): self._frame = 0 self._window_close_requested = False self._nav_stack: list[object] = [] - self._nav_stack_widgets_to_render = 1 if self.big_ui() else 2 self._nav_stack_tick: Callable[[], None] | None = None + self._nav_stack_widgets_to_render = 1 if self.big_ui() else 2 self._mouse = MouseState(self._scale) self._mouse_events: list[MouseEvent] = [] @@ -371,18 +371,14 @@ def push_widget(self, widget: object): cloudlog.warning("Widget already in stack, cannot push again!") return - # disable previous widget to prevent input processing, but keep rendering for smooth transitions + # disable previous widget to prevent input processing if len(self._nav_stack) > 0: prev_widget = self._nav_stack[-1] - print('Disabling and hide_event for', prev_widget.__class__.__name__) - # prev_widget.hide_event() # TODO: change these to touch_valid prev_widget.set_enabled(False) - print('Pushing and show_event for', widget.__class__.__name__) self._nav_stack.append(widget) widget.show_event() - print() def pop_widget(self): if len(self._nav_stack) < 2: @@ -392,14 +388,10 @@ def pop_widget(self): # re-enable previous widget and pop current # TODO: switch to touch_valid prev_widget = self._nav_stack[-2] - print('Re-enabling and show_event for', prev_widget.__class__.__name__) - # prev_widget.show_event() prev_widget.set_enabled(True) - print('Popping and hide_event for', self._nav_stack[-1].__class__.__name__) widget = self._nav_stack.pop() widget.hide_event() - print() def pop_widgets_to(self, widget): if widget not in self._nav_stack: @@ -568,8 +560,6 @@ def render(self): for widget in self._nav_stack[-self._nav_stack_widgets_to_render:]: widget.render(rl.Rectangle(0, 0, self.width, self.height)) - # print('widget stack', len(self._nav_stack), [w.__class__.__name__ for w in self._nav_stack]) - yield True if self._render_texture: From b26ca175b859bfcbf9f4d0bcae69127a151b53aa Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:49:10 -0800 Subject: [PATCH 69/73] clean up --- system/ui/widgets/__init__.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 49a8513e0900d6..25c908c72cc579 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -266,7 +266,7 @@ def set_back_callback(self, callback: Callable[[], None]) -> None: self._back_callback = callback def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: - # FIXME: disabling this widget on new push widget still causes this widget to track mouse events without mouse down + # FIXME: disabling this widget on new push_widget still causes this widget to track mouse events without mouse down super()._handle_mouse_event(mouse_event) if not self.back_enabled: @@ -326,6 +326,7 @@ def _update_state(self): if not self._set_up: self._set_up = True if hasattr(self, '_scroller'): + # TODO: use touch_valid original_enabled = self._scroller._enabled self._scroller.set_enabled(lambda: self.enabled and not self._swiping_away and (original_enabled() if callable(original_enabled) else original_enabled)) @@ -345,7 +346,7 @@ def _update_state(self): if not self.enabled: self._back_button_start_pos = None - # TODO: why not in handle_mouse_event? have to hack above + # TODO: why is this not in handle_mouse_event? have to hack above if self._back_button_start_pos is not None: last_mouse_event = gui_app.last_mouse_event # push entire widget as user drags it away @@ -397,10 +398,6 @@ def render(self, rect: rl.Rectangle | None = None) -> bool | int | None: else: self._nav_bar_y_filter.update(NAV_BAR_MARGIN) - # # draw black above widget when dismissing - # if self._rect.y > 0: - # rl.draw_rectangle(int(self._rect.x), 0, int(self._rect.width), int(self._rect.y), rl.BLACK) - self._nav_bar.set_position(bar_x, round(self._nav_bar_y_filter.x)) self._nav_bar.render() From 07d3f5f26c99ef891086b6fe03095d53a62b8631 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:49:46 -0800 Subject: [PATCH 70/73] stash --- tools/replay/replay.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/replay/replay.cc b/tools/replay/replay.cc index d8d59e41a43578..7a5cbf9f12e24a 100644 --- a/tools/replay/replay.cc +++ b/tools/replay/replay.cc @@ -32,6 +32,7 @@ Replay::Replay(const std::string &route, std::vector allow, std::ve if (!(flags_ & REPLAY_FLAG_ALL_SERVICES)) { block.insert(block.end(), {"bookmarkButton", "uiDebug", "userBookmark"}); } + allow = {"driverCameraState", "driverEncodeIdx", "driverStateV2", "driverMonitoringState"}; setupServices(allow, block); setupSegmentManager(!allow.empty() || !block.empty()); } From c33f1dc3bba5f57c651997f19139418c7747c49d Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:49:50 -0800 Subject: [PATCH 71/73] Revert "stash" This reverts commit 07d3f5f26c99ef891086b6fe03095d53a62b8631. --- tools/replay/replay.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/replay/replay.cc b/tools/replay/replay.cc index 7a5cbf9f12e24a..d8d59e41a43578 100644 --- a/tools/replay/replay.cc +++ b/tools/replay/replay.cc @@ -32,7 +32,6 @@ Replay::Replay(const std::string &route, std::vector allow, std::ve if (!(flags_ & REPLAY_FLAG_ALL_SERVICES)) { block.insert(block.end(), {"bookmarkButton", "uiDebug", "userBookmark"}); } - allow = {"driverCameraState", "driverEncodeIdx", "driverStateV2", "driverMonitoringState"}; setupServices(allow, block); setupSegmentManager(!allow.empty() || !block.empty()); } From a9e7da73c37684df6cfde17663abc337dc12cff9 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:50:44 -0800 Subject: [PATCH 72/73] typing --- selfdrive/ui/mici/widgets/pairing_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/mici/widgets/pairing_dialog.py b/selfdrive/ui/mici/widgets/pairing_dialog.py index cfc291ab407af6..088d2a0b6fdb3e 100644 --- a/selfdrive/ui/mici/widgets/pairing_dialog.py +++ b/selfdrive/ui/mici/widgets/pairing_dialog.py @@ -72,7 +72,7 @@ def _update_state(self): if ui_state.prime_state.is_paired(): self._playing_dismiss_animation = True - def _render(self, rect: rl.Rectangle) -> int: + def _render(self, rect: rl.Rectangle): self._check_qr_refresh() self._render_qr_code() From 0f3c8398a2f32ed94f2648dde446b4a94c31ec40 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 20 Feb 2026 18:52:33 -0800 Subject: [PATCH 73/73] lint --- selfdrive/ui/ui.py | 1 - 1 file changed, 1 deletion(-) diff --git a/selfdrive/ui/ui.py b/selfdrive/ui/ui.py index d62aebec8da695..e3cac2618e84e1 100755 --- a/selfdrive/ui/ui.py +++ b/selfdrive/ui/ui.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 import os -import pyray as rl from openpilot.system.hardware import TICI from openpilot.common.realtime import config_realtime_process, set_core_affinity