ui: add navigation stack for tici (#37275)

* initial

* start to support nav stack in settings panels + fix some navwidget bugs

* add deprecation warning and move more to new nav stack

* fix overriding NavWidget enabled and do developer panel

* fix interactive timeout and do main

* more device, not done yet

* minor network fixes

* dcam dialog

* start onboarding

* fix onboarding

* do mici setup

* remove now useless CUSTOM_SOFTWARE

* support big ui with old modal overlay

* reset can be old modal overlay, but updater needs new since it uses wifiui

* flip name truthiness to inspire excitement

* all *should* work, but will do pass later

* clean up main

* clean up settiings

* clean up dialog and developer

* cleanup mici setup some

* rm one more

* fix keyboard

* revert

* might as well but clarify

* fix networkinfopage buttons

* lint

* nice clean up from cursor

* animate background fade with position

* fix device overlays

* cursor fix pt1

cursor fix pt2

* rm print

* capital

* temp fix from cursor for onboarding not freeing space after reviewing training guide

* fix home screen scroller snap not resetting

* stash

* nice gradient on top

* 40

* 20

* no gradient

* return unused returns and always show regulatory btn

* nice!

* revert selfdrive/ui

* let's do tici first

* bring back ui

* not sure why __del__, SetupWidget was never deleted?

* device "done"

* network "done!!"

* toggles "done"

* software "done"

* developer "done"

* fix onboarding

* use new modal for debug windows

* and aug

* setup "done"

* clean up

* updater "done"

* reset "done"

* pop first before callbacks in case callbacks push

* fix cmt

* not needed

* remove two commented functions for mici

* clean up application

* typing

* static

* not sure what this means

* fix big

* more static

* actually great catch

* fix cmt
This commit is contained in:
Shane Smiskol
2026-02-20 02:43:11 -08:00
committed by GitHub
parent f829c90de6
commit cefddf4b9b
22 changed files with 231 additions and 188 deletions
+4 -2
View File
@@ -36,10 +36,12 @@ class MainLayout(Widget):
# Set callbacks
self._setup_callbacks()
# Start onboarding if terms or training not completed
gui_app.push_widget(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 _render(self, _):
self._handle_onroad_transition()
+4 -2
View File
@@ -81,6 +81,9 @@ class TrainingGuide(Widget):
if self._completed_callback:
self._completed_callback()
# NOTE: this pops OnboardingWindow during real onboarding
gui_app.pop_widget()
def _update_state(self):
if len(self._image_objs):
self._textures.append(gui_app._load_texture_from_image(self._image_objs.pop(0)))
@@ -194,11 +197,10 @@ class OnboardingWindow(Widget):
ui_state.params.put("HasAcceptedTerms", terms_version)
self._state = OnboardingState.ONBOARDING
if self._training_done:
gui_app.set_modal_overlay(None)
gui_app.pop_widget()
def _on_completed_training(self):
ui_state.params.put("CompletedTrainingVersion", training_version)
gui_app.set_modal_overlay(None)
def _render(self, _):
if self._training_guide is None:
+3 -3
View File
@@ -164,7 +164,7 @@ class DeveloperLayout(Widget):
def _on_alpha_long_enabled(self, state: bool):
if state:
def confirm_callback(result: int):
def confirm_callback(result: DialogResult):
if result == DialogResult.CONFIRM:
self._params.put_bool("AlphaLongitudinalEnabled", True)
self._params.put_bool("OnroadCycleRequested", True)
@@ -176,8 +176,8 @@ class DeveloperLayout(Widget):
content = (f"<h1>{self._alpha_long_toggle.title}</h1><br>" +
f"<p>{self._alpha_long_toggle.description}</p>")
dlg = ConfirmDialog(content, tr("Enable"), rich=True)
gui_app.set_modal_overlay(dlg, callback=confirm_callback)
dlg = ConfirmDialog(content, tr("Enable"), rich=True, callback=confirm_callback)
gui_app.push_widget(dlg)
else:
self._params.put_bool("AlphaLongitudinalEnabled", False)
+26 -41
View File
@@ -33,8 +33,6 @@ class DeviceLayout(Widget):
self._params = Params()
self._select_language_dialog: MultiOptionDialog | None = None
self._driver_camera: DriverCameraDialog | None = None
self._pair_device_dialog: PairingDialog | None = None
self._fcc_dialog: HtmlModal | None = None
self._training_guide: TrainingGuide | None = None
@@ -44,7 +42,8 @@ class DeviceLayout(Widget):
ui_state.add_offroad_transition_callback(self._offroad_transition)
def _initialize_items(self):
self._pair_device_btn = button_item(lambda: tr("Pair Device"), lambda: tr("PAIR"), lambda: tr(DESCRIPTIONS['pair_device']), callback=self._pair_device)
self._pair_device_btn = button_item(lambda: tr("Pair Device"), lambda: tr("PAIR"), lambda: tr(DESCRIPTIONS['pair_device']),
callback=lambda: gui_app.push_widget(PairingDialog()))
self._pair_device_btn.set_visible(lambda: not ui_state.prime_state.is_paired())
self._reset_calib_btn = button_item(lambda: tr("Reset Calibration"), lambda: tr("RESET"), lambda: tr(DESCRIPTIONS['reset_calibration']),
@@ -59,7 +58,7 @@ class DeviceLayout(Widget):
text_item(lambda: tr("Serial"), self._params.get("HardwareSerial") or (lambda: tr("N/A"))),
self._pair_device_btn,
button_item(lambda: tr("Driver Camera"), lambda: tr("PREVIEW"), lambda: tr(DESCRIPTIONS['driver_camera']),
callback=self._show_driver_camera, enabled=ui_state.is_offroad),
callback=lambda: gui_app.push_widget(DriverCameraDialog()), enabled=ui_state.is_offroad),
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),
@@ -79,29 +78,23 @@ class DeviceLayout(Widget):
self._scroller.render(rect)
def _show_language_dialog(self):
def handle_language_selection(result: int):
if result == 1 and self._select_language_dialog:
def handle_language_selection(result: DialogResult):
if result == DialogResult.CONFIRM and self._select_language_dialog:
selected_language = multilang.languages[self._select_language_dialog.selection]
multilang.change_language(selected_language)
self._update_calib_description()
self._select_language_dialog = None
self._select_language_dialog = MultiOptionDialog(tr("Select a language"), multilang.languages, multilang.codes[multilang.language],
option_font_weight=FontWeight.UNIFONT)
gui_app.set_modal_overlay(self._select_language_dialog, callback=handle_language_selection)
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))
option_font_weight=FontWeight.UNIFONT, callback=handle_language_selection)
gui_app.push_widget(self._select_language_dialog)
def _reset_calibration_prompt(self):
if ui_state.engaged:
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Reset Calibration")))
gui_app.push_widget(alert_dialog(tr("Disengage to Reset Calibration")))
return
def reset_calibration(result: int):
def reset_calibration(result: DialogResult):
# Check engaged again in case it changed while the dialog was open
if ui_state.engaged or result != DialogResult.CONFIRM:
return
@@ -114,8 +107,8 @@ class DeviceLayout(Widget):
self._params.put_bool("OnroadCycleRequested", True)
self._update_calib_description()
dialog = ConfirmDialog(tr("Are you sure you want to reset calibration?"), tr("Reset"))
gui_app.set_modal_overlay(dialog, callback=reset_calibration)
dialog = ConfirmDialog(tr("Are you sure you want to reset calibration?"), tr("Reset"), callback=reset_calibration)
gui_app.push_widget(dialog)
def _update_calib_description(self):
desc = tr(DESCRIPTIONS['reset_calibration'])
@@ -167,42 +160,34 @@ class DeviceLayout(Widget):
def _reboot_prompt(self):
if ui_state.engaged:
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Reboot")))
gui_app.push_widget(alert_dialog(tr("Disengage to Reboot")))
return
dialog = ConfirmDialog(tr("Are you sure you want to reboot?"), tr("Reboot"))
gui_app.set_modal_overlay(dialog, callback=self._perform_reboot)
def perform_reboot(result: DialogResult):
if not ui_state.engaged and result == DialogResult.CONFIRM:
self._params.put_bool_nonblocking("DoReboot", True)
def _perform_reboot(self, result: int):
if not ui_state.engaged and result == DialogResult.CONFIRM:
self._params.put_bool_nonblocking("DoReboot", True)
dialog = ConfirmDialog(tr("Are you sure you want to reboot?"), tr("Reboot"), callback=perform_reboot)
gui_app.push_widget(dialog)
def _power_off_prompt(self):
if ui_state.engaged:
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Power Off")))
gui_app.push_widget(alert_dialog(tr("Disengage to Power Off")))
return
dialog = ConfirmDialog(tr("Are you sure you want to power off?"), tr("Power Off"))
gui_app.set_modal_overlay(dialog, callback=self._perform_power_off)
def perform_power_off(result: DialogResult):
if not ui_state.engaged and result == DialogResult.CONFIRM:
self._params.put_bool_nonblocking("DoShutdown", True)
def _perform_power_off(self, result: int):
if not ui_state.engaged and result == DialogResult.CONFIRM:
self._params.put_bool_nonblocking("DoShutdown", True)
def _pair_device(self):
if not self._pair_device_dialog:
self._pair_device_dialog = PairingDialog()
gui_app.set_modal_overlay(self._pair_device_dialog, callback=lambda result: setattr(self, '_pair_device_dialog', None))
dialog = ConfirmDialog(tr("Are you sure you want to power off?"), tr("Power Off"), callback=perform_power_off)
gui_app.push_widget(dialog)
def _on_regulatory(self):
if not self._fcc_dialog:
self._fcc_dialog = HtmlModal(os.path.join(BASEDIR, "selfdrive/assets/offroad/fcc.html"))
gui_app.set_modal_overlay(self._fcc_dialog)
gui_app.push_widget(self._fcc_dialog)
def _on_review_training_guide(self):
if not self._training_guide:
def completed_callback():
gui_app.set_modal_overlay(None)
self._training_guide = TrainingGuide(completed_callback=completed_callback)
gui_app.set_modal_overlay(self._training_guide)
self._training_guide = TrainingGuide()
gui_app.push_widget(self._training_guide)
+6 -6
View File
@@ -165,12 +165,12 @@ class SoftwareLayout(Widget):
os.system("pkill -SIGHUP -f system.updated.updated")
def _on_uninstall(self):
def handle_uninstall_confirmation(result):
def handle_uninstall_confirmation(result: DialogResult):
if result == DialogResult.CONFIRM:
ui_state.params.put_bool("DoUninstall", True)
dialog = ConfirmDialog(tr("Are you sure you want to uninstall?"), tr("Uninstall"))
gui_app.set_modal_overlay(dialog, callback=handle_uninstall_confirmation)
dialog = ConfirmDialog(tr("Are you sure you want to uninstall?"), tr("Uninstall"), callback=handle_uninstall_confirmation)
gui_app.push_widget(dialog)
def _on_install_update(self):
# Trigger reboot to install update
@@ -189,9 +189,8 @@ class SoftwareLayout(Widget):
branches.insert(0, b)
current_target = ui_state.params.get("UpdaterTargetBranch") or ""
self._branch_dialog = MultiOptionDialog(tr("Select a branch"), branches, current_target)
def handle_selection(result):
def handle_selection(result: DialogResult):
# Confirmed selection
if result == DialogResult.CONFIRM and self._branch_dialog is not None and self._branch_dialog.selection:
selection = self._branch_dialog.selection
@@ -200,4 +199,5 @@ class SoftwareLayout(Widget):
os.system("pkill -SIGUSR1 -f system.updated.updated")
self._branch_dialog = None
gui_app.set_modal_overlay(self._branch_dialog, callback=handle_selection)
self._branch_dialog = MultiOptionDialog(tr("Select a branch"), branches, current_target, callback=handle_selection)
gui_app.push_widget(self._branch_dialog)
+3 -3
View File
@@ -214,7 +214,7 @@ class TogglesLayout(Widget):
def _handle_experimental_mode_toggle(self, state: bool):
confirmed = self._params.get_bool("ExperimentalModeConfirmed")
if state and not confirmed:
def confirm_callback(result: int):
def confirm_callback(result: DialogResult):
if result == DialogResult.CONFIRM:
self._params.put_bool("ExperimentalMode", True)
self._params.put_bool("ExperimentalModeConfirmed", True)
@@ -225,8 +225,8 @@ class TogglesLayout(Widget):
# show confirmation dialog
content = (f"<h1>{self._toggles['ExperimentalMode'].title}</h1><br>" +
f"<p>{self._toggles['ExperimentalMode'].description}</p>")
dlg = ConfirmDialog(content, tr("Enable"), rich=True)
gui_app.set_modal_overlay(dlg, callback=confirm_callback)
dlg = ConfirmDialog(content, tr("Enable"), rich=True, callback=confirm_callback)
gui_app.push_widget(dlg)
else:
self._update_experimental_mode_icon()
self._params.put_bool("ExperimentalMode", state)
+2 -2
View File
@@ -219,8 +219,9 @@ class AugmentedRoadView(CameraView):
if __name__ == "__main__":
gui_app.init_window("OnRoad Camera View")
gui_app.init_window("OnRoad Camera View", new_modal=True)
road_camera_view = AugmentedRoadView(ROAD_CAM)
gui_app.push_widget(road_camera_view)
print("***press space to switch camera view***")
try:
for _ in gui_app.render():
@@ -229,6 +230,5 @@ if __name__ == "__main__":
if WIDE_CAM in road_camera_view.available_streams:
stream = ROAD_CAM if road_camera_view.stream_type == WIDE_CAM else WIDE_CAM
road_camera_view.switch_stream(stream)
road_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
finally:
road_camera_view.close()
+4 -4
View File
@@ -14,7 +14,7 @@ class DriverCameraDialog(CameraView):
super().__init__("camerad", VisionStreamType.VISION_STREAM_DRIVER)
self.driver_state_renderer = DriverStateRenderer()
# TODO: this can grow unbounded, should be given some thought
device.add_interactive_timeout_callback(lambda: gui_app.set_modal_overlay(None))
device.add_interactive_timeout_callback(gui_app.pop_widget)
ui_state.params.put_bool("IsDriverViewEnabled", True)
def hide_event(self):
@@ -24,7 +24,7 @@ class DriverCameraDialog(CameraView):
def _handle_mouse_release(self, _):
super()._handle_mouse_release(_)
gui_app.set_modal_overlay(None)
gui_app.pop_widget()
def __del__(self):
self.close()
@@ -100,12 +100,12 @@ class DriverCameraDialog(CameraView):
if __name__ == "__main__":
gui_app.init_window("Driver Camera View")
gui_app.init_window("Driver Camera View", 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))
finally:
driver_camera_view.close()
+1 -1
View File
@@ -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)
gui_app.init_window("ui diff test", fps=FPS, new_modal=variant == "tizi")
# Dynamically import main layout based on variant
if variant == "mici":
+9 -4
View File
@@ -9,21 +9,26 @@ from openpilot.selfdrive.ui.layouts.main import MainLayout
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", new_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))
main_layout.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
for should_render in gui_app.render():
ui_state.update()
if should_render:
main_layout.render()
if not BIG_UI:
main_layout.render()
# reaffine after power save offlines our core
if TICI and os.sched_getaffinity(0) != cores:
+5 -6
View File
@@ -26,7 +26,7 @@ class PairingDialog(Widget):
self.qr_texture: rl.Texture | None = None
self.last_qr_generation = float('-inf')
self._close_btn = IconButton(gui_app.texture("icons/close.png", 80, 80))
self._close_btn.set_click_callback(lambda: gui_app.set_modal_overlay(None))
self._close_btn.set_click_callback(gui_app.pop_widget)
def _get_pairing_url(self) -> str:
try:
@@ -69,7 +69,7 @@ class PairingDialog(Widget):
def _update_state(self):
if ui_state.prime_state.is_paired():
gui_app.set_modal_overlay(None)
gui_app.pop_widget()
def _render(self, rect: rl.Rectangle) -> int:
rl.clear_background(rl.Color(224, 224, 224, 255))
@@ -160,12 +160,11 @@ class PairingDialog(Widget):
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)
try:
for _ in gui_app.render():
result = pairing.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
if result != -1:
break
pass
finally:
del pairing
+4 -10
View File
@@ -15,7 +15,6 @@ class SetupWidget(Widget):
def __init__(self):
super().__init__()
self._open_settings_callback = None
self._pairing_dialog: PairingDialog | None = None
self._pair_device_btn = Button(lambda: tr("Pair device"), self._show_pairing, button_style=ButtonStyle.PRIMARY)
self._open_settings_btn = Button(lambda: tr("Open"), lambda: self._open_settings_callback() if self._open_settings_callback else None,
button_style=ButtonStyle.PRIMARY)
@@ -86,16 +85,11 @@ class SetupWidget(Widget):
button_rect = rl.Rectangle(x, y, w, button_height)
self._open_settings_btn.render(button_rect)
def _show_pairing(self):
@staticmethod
def _show_pairing():
if not system_time_valid():
dlg = alert_dialog(tr("Please connect to Wi-Fi to complete initial pairing"))
gui_app.set_modal_overlay(dlg)
gui_app.push_widget(dlg)
return
if not self._pairing_dialog:
self._pairing_dialog = PairingDialog()
gui_app.set_modal_overlay(self._pairing_dialog, lambda result: setattr(self, '_pairing_dialog', None))
def __del__(self):
if self._pairing_dialog:
del self._pairing_dialog
gui_app.push_widget(PairingDialog())
+3 -2
View File
@@ -59,7 +59,7 @@ class SshKeyAction(ItemAction):
# Show error dialog if there's an error
if self._error_message:
message = copy.copy(self._error_message)
gui_app.set_modal_overlay(alert_dialog(message))
gui_app.push_widget(alert_dialog(message))
self._username = ""
self._error_message = ""
@@ -87,7 +87,8 @@ class SshKeyAction(ItemAction):
if self._state == SshKeyActionState.ADD:
self._keyboard.reset()
self._keyboard.set_title(tr("Enter your GitHub username"))
gui_app.set_modal_overlay(self._keyboard, callback=self._on_username_submit)
self._keyboard.set_callback(self._on_username_submit)
gui_app.push_widget(self._keyboard)
elif self._state == SshKeyActionState.REMOVE:
self._params.remove("GithubUsername")
self._params.remove("GithubSshKeys")