From 6aa58a623f30b219b1f5a03c3f8b96bd18fe7af9 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 16 Dec 2025 15:58:03 -0800 Subject: [PATCH 1/9] generic long press --- selfdrive/ui/mici/layouts/home.py | 31 ++++++++----------------------- system/ui/widgets/__init__.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/selfdrive/ui/mici/layouts/home.py b/selfdrive/ui/mici/layouts/home.py index 9152bdc7fa789e..39a9ba4c4f1424 100644 --- a/selfdrive/ui/mici/layouts/home.py +++ b/selfdrive/ui/mici/layouts/home.py @@ -83,9 +83,6 @@ def __init__(self): self._on_settings_click: Callable | None = None self._last_refresh = 0 - self._mouse_down_t: None | float = None - self._did_long_press = False - self._is_pressed_prev = False self._version_text = None self._experimental_mode = False @@ -125,22 +122,6 @@ def _update_params(self): self._experimental_mode = ui_state.params.get_bool("ExperimentalMode") def _update_state(self): - if self.is_pressed and not self._is_pressed_prev: - self._mouse_down_t = time.monotonic() - elif not self.is_pressed and self._is_pressed_prev: - self._mouse_down_t = None - self._did_long_press = False - self._is_pressed_prev = self.is_pressed - - if self._mouse_down_t is not None: - if time.monotonic() - self._mouse_down_t > 0.5: - # long gating for experimental mode - only allow toggle if longitudinal control is available - if ui_state.has_longitudinal_control: - self._experimental_mode = not self._experimental_mode - ui_state.params.put("ExperimentalMode", self._experimental_mode) - self._mouse_down_t = None - self._did_long_press = True - if rl.get_time() - self._last_refresh > 5.0: device_state = ui_state.sm['deviceState'] self._update_network_status(device_state) @@ -158,11 +139,15 @@ def _update_network_status(self, device_state): def set_callbacks(self, on_settings: Callable | None = None): self._on_settings_click = on_settings + def _handle_long_press(self, mouse_pos: MousePos) -> None: + # long gating for experimental mode - only allow toggle if longitudinal control is available + if ui_state.has_longitudinal_control: + self._experimental_mode = not self._experimental_mode + ui_state.params.put("ExperimentalMode", self._experimental_mode) + def _handle_mouse_release(self, mouse_pos: MousePos): - if not self._did_long_press: - if self._on_settings_click: - self._on_settings_click() - self._did_long_press = False + if self._on_settings_click: + self._on_settings_click() def _get_version_text(self) -> tuple[str, str, str, str] | None: description = ui_state.params.get("UpdaterCurrentDescription") diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index a3fed6d96217fa..68af1f70a69c45 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -20,6 +20,8 @@ class DialogResult(IntEnum): class Widget(abc.ABC): + LONG_PRESS_THRESHOLD_S = 0.5 + def __init__(self): self._rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) self._parent_rect: rl.Rectangle | None = None @@ -33,6 +35,10 @@ def __init__(self): self._multi_touch = False self.__was_awake = True + # Long press state (single touch only, slot 0) + self._long_press_start_t: float | None = None + self._long_press_fired: bool = False + @property def rect(self) -> rl.Rectangle: return self._rect @@ -127,19 +133,28 @@ def _process_mouse_events(self) -> None: self._handle_mouse_press(mouse_event.pos) self.__is_pressed[mouse_event.slot] = True self.__tracking_is_pressed[mouse_event.slot] = True + if mouse_event.slot == 0: + self._long_press_start_t = mouse_event.t + self._long_press_fired = False self._handle_mouse_event(mouse_event) # Callback such as scroll panel signifies user is scrolling elif not touch_valid: self.__is_pressed[mouse_event.slot] = False self.__tracking_is_pressed[mouse_event.slot] = False + if mouse_event.slot == 0: + self._long_press_start_t = None + self._long_press_fired = False elif mouse_event.left_released: self._handle_mouse_event(mouse_event) - if self.__is_pressed[mouse_event.slot] and mouse_in_rect: + if self.__is_pressed[mouse_event.slot] and mouse_in_rect and not (mouse_event.slot == 0 and self._long_press_fired): self._handle_mouse_release(mouse_event.pos) self.__is_pressed[mouse_event.slot] = False self.__tracking_is_pressed[mouse_event.slot] = False + if mouse_event.slot == 0: + self._long_press_start_t = None + self._long_press_fired = False # Mouse/touch is still within our rect elif mouse_in_rect: @@ -151,6 +166,15 @@ def _process_mouse_events(self) -> None: elif not mouse_in_rect: self.__is_pressed[mouse_event.slot] = False self._handle_mouse_event(mouse_event) + if mouse_event.slot == 0: + self._long_press_start_t = None + self._long_press_fired = False + + # Long press detection (fire once per press). Note: long presses can occur without new mouse events. + if self._long_press_start_t is not None and not self._long_press_fired: + if (gui_app.last_mouse_event.t - self._long_press_start_t) >= self.LONG_PRESS_THRESHOLD_S: + self._long_press_fired = True + self._handle_long_press(gui_app.last_mouse_event.pos) def _layout(self) -> None: """Optionally lay out child widgets separately. This is called before rendering.""" @@ -175,6 +199,9 @@ def _handle_mouse_release(self, mouse_pos: MousePos) -> bool: self._click_callback() return False + def _handle_long_press(self, mouse_pos: MousePos) -> None: + """Optionally handle a long-press (press-and-hold) gesture.""" + def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: """Optionally handle mouse events. This is called before rendering.""" # Default implementation does nothing, can be overridden by subclasses From 82f49496e9b7514354d23ccb5a5c34bb51de034d Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 16 Dec 2025 16:03:50 -0800 Subject: [PATCH 2/9] bad --- selfdrive/ui/mici/layouts/settings/toggles.py | 6 +++- selfdrive/ui/mici/widgets/button.py | 36 ++++++++++++++----- system/ui/widgets/__init__.py | 3 +- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/toggles.py b/selfdrive/ui/mici/layouts/settings/toggles.py index 8efb516a42d175..5fdad34d291277 100644 --- a/selfdrive/ui/mici/layouts/settings/toggles.py +++ b/selfdrive/ui/mici/layouts/settings/toggles.py @@ -17,6 +17,9 @@ def __init__(self, back_callback: Callable): super().__init__() self.set_back_callback(back_callback) + enable_openpilot_desc = ("Use the openpilot system for adaptive cruise control and lane keep driver assistance. " + "Your attention is required at all times to use this feature.") + self._personality_toggle = BigMultiParamToggle("driving personality", "LongitudinalPersonality", ["aggressive", "standard", "relaxed"]) self._experimental_btn = BigParamControl("experimental mode", "ExperimentalMode") is_metric_toggle = BigParamControl("use metric units", "IsMetric") @@ -24,7 +27,8 @@ def __init__(self, back_callback: Callable): always_on_dm_toggle = BigParamControl("always-on driver monitor", "AlwaysOnDM") record_front = BigParamControl("record & upload driver camera", "RecordFront", toggle_callback=restart_needed_callback) record_mic = BigParamControl("record & upload mic audio", "RecordAudio", toggle_callback=restart_needed_callback) - enable_openpilot = BigParamControl("enable openpilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback) + enable_openpilot = BigParamControl("enable openpilot", "OpenpilotEnabledToggle", + toggle_callback=restart_needed_callback, description=enable_openpilot_desc) self._scroller = Scroller([ self._personality_toggle, diff --git a/selfdrive/ui/mici/widgets/button.py b/selfdrive/ui/mici/widgets/button.py index be08e0fee3768f..7b5064cf0eeaf3 100644 --- a/selfdrive/ui/mici/widgets/button.py +++ b/selfdrive/ui/mici/widgets/button.py @@ -104,12 +104,13 @@ def _render(self, _): class BigButton(Widget): """A lightweight stand-in for the Qt BigButton, drawn & updated each frame.""" - def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = ""): + def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", description: str | Callable[[], str] | None = None): super().__init__() self.set_rect(rl.Rectangle(0, 0, 402, 180)) self.text = text self.value = value self.set_icon(icon) + self._description = description self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps) @@ -136,6 +137,22 @@ def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "" def set_icon(self, icon: Union[str, rl.Texture]): self._txt_icon = gui_app.texture(icon, 64, 64) if isinstance(icon, str) and len(icon) else icon + def set_description(self, description: str | Callable[[], str] | None): + self._description = description + + def _get_description(self) -> str: + if callable(self._description): + return self._description() + return self._description or "" + + def _handle_long_press(self, mouse_pos: MousePos) -> None: + desc = self._get_description() + if not desc: + return + # Import inside to avoid circular imports + from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog + gui_app.set_modal_overlay(BigDialog(self.text, desc)) + def set_rotate_icon(self, rotate: bool): if rotate and self._rotate_icon_t is not None: return @@ -251,8 +268,9 @@ def _render(self, _): class BigToggle(BigButton): - def __init__(self, text: str, value: str = "", initial_state: bool = False, toggle_callback: Callable = None): - super().__init__(text, value, "") + def __init__(self, text: str, value: str = "", initial_state: bool = False, toggle_callback: Callable = None, + description: str | Callable[[], str] | None = None): + super().__init__(text, value, "", description=description) self._checked = initial_state self._toggle_callback = toggle_callback @@ -289,8 +307,8 @@ def _render(self, _): class BigMultiToggle(BigToggle): def __init__(self, text: str, options: list[str], toggle_callback: Callable = None, - select_callback: Callable = None): - super().__init__(text, "", toggle_callback=toggle_callback) + select_callback: Callable = None, description: str | Callable[[], str] | None = None): + super().__init__(text, "", toggle_callback=toggle_callback, description=description) assert len(options) > 0 self._options = options self._select_callback = select_callback @@ -328,8 +346,8 @@ def _render(self, _): class BigMultiParamToggle(BigMultiToggle): def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable = None, - select_callback: Callable = None): - super().__init__(text, options, toggle_callback, select_callback) + select_callback: Callable = None, description: str | Callable[[], str] | None = None): + super().__init__(text, options, toggle_callback, select_callback, description=description) self._param = param self._params = Params() @@ -345,8 +363,8 @@ def _handle_mouse_release(self, mouse_pos: MousePos): class BigParamControl(BigToggle): - def __init__(self, text: str, param: str, toggle_callback: Callable = None): - super().__init__(text, "", toggle_callback=toggle_callback) + def __init__(self, text: str, param: str, toggle_callback: Callable = None, description: str | Callable[[], str] | None = None): + super().__init__(text, "", toggle_callback=toggle_callback, description=description) self.param = param self.params = Params() self.set_checked(self.params.get_bool(self.param, False)) diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 68af1f70a69c45..ad279d58cb6af5 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -1,5 +1,6 @@ import abc import pyray as rl +import time from enum import IntEnum from collections.abc import Callable from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter @@ -172,7 +173,7 @@ def _process_mouse_events(self) -> None: # Long press detection (fire once per press). Note: long presses can occur without new mouse events. if self._long_press_start_t is not None and not self._long_press_fired: - if (gui_app.last_mouse_event.t - self._long_press_start_t) >= self.LONG_PRESS_THRESHOLD_S: + if (time.monotonic() - self._long_press_start_t) >= self.LONG_PRESS_THRESHOLD_S: self._long_press_fired = True self._handle_long_press(gui_app.last_mouse_event.pos) From 9bee2fe8a4d804ad73d9e9e96a1bb263cd34e1b6 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 16 Dec 2025 16:21:49 -0800 Subject: [PATCH 3/9] move --- selfdrive/ui/mici/layouts/home.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/selfdrive/ui/mici/layouts/home.py b/selfdrive/ui/mici/layouts/home.py index 7bc166d838e739..cecad25dde6019 100644 --- a/selfdrive/ui/mici/layouts/home.py +++ b/selfdrive/ui/mici/layouts/home.py @@ -119,12 +119,6 @@ def show_event(self): def _update_params(self): self._experimental_mode = ui_state.params.get_bool("ExperimentalMode") - def _handle_long_press(self, _): - # long gating for experimental mode - only allow toggle if longitudinal control is available - if ui_state.has_longitudinal_control: - self._experimental_mode = not self._experimental_mode - ui_state.params.put("ExperimentalMode", self._experimental_mode) - def _update_state(self): if rl.get_time() - self._last_refresh > 5.0: device_state = ui_state.sm['deviceState'] @@ -143,7 +137,7 @@ def _update_network_status(self, device_state): def set_callbacks(self, on_settings: Callable | None = None): self._on_settings_click = on_settings - def _handle_long_press(self, mouse_pos: MousePos) -> None: + def _handle_long_press(self, _): # long gating for experimental mode - only allow toggle if longitudinal control is available if ui_state.has_longitudinal_control: self._experimental_mode = not self._experimental_mode From 640e050048ed0fcb061a504502484dc923d7c98b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 16 Dec 2025 16:23:55 -0800 Subject: [PATCH 4/9] clean up --- system/ui/widgets/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 216e5b2ca9b845..098785664a9452 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -170,9 +170,6 @@ def _process_mouse_events(self) -> None: self._long_press_start_t = None self._long_press_fired = False self._handle_mouse_event(mouse_event) - if mouse_event.slot == 0: - self._long_press_start_t = None - self._long_press_fired = False # Long press detection (fire once per press). Note: long presses can occur without new mouse events. if self._long_press_start_t is not None and not self._long_press_fired: From 0e682bcda198f4aad8d7b0c10eba13a06575cb83 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 16 Dec 2025 16:24:01 -0800 Subject: [PATCH 5/9] clean up --- system/ui/widgets/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 098785664a9452..7b021f2e5ad657 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -171,12 +171,6 @@ def _process_mouse_events(self) -> None: self._long_press_fired = False self._handle_mouse_event(mouse_event) - # Long press detection (fire once per press). Note: long presses can occur without new mouse events. - if self._long_press_start_t is not None and not self._long_press_fired: - if (time.monotonic() - self._long_press_start_t) >= self.LONG_PRESS_THRESHOLD_S: - self._long_press_fired = True - self._handle_long_press(gui_app.last_mouse_event.pos) - # Long press detection if self._long_press_start_t is not None and not self._long_press_fired: if (rl.get_time() - self._long_press_start_t) >= self.LONG_PRESS_TIME: From d63bb952f1b90a8b330cec8a3a35353a5a924820 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 16 Dec 2025 16:33:53 -0800 Subject: [PATCH 6/9] simple --- selfdrive/ui/mici/layouts/settings/toggles.py | 6 ++--- selfdrive/ui/mici/widgets/button.py | 25 ++++++------------- selfdrive/ui/mici/widgets/dialog.py | 5 ++-- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/toggles.py b/selfdrive/ui/mici/layouts/settings/toggles.py index 38836f2c782892..2237d955b83cbf 100644 --- a/selfdrive/ui/mici/layouts/settings/toggles.py +++ b/selfdrive/ui/mici/layouts/settings/toggles.py @@ -6,6 +6,7 @@ from openpilot.selfdrive.ui.mici.widgets.button import BigParamControl, BigMultiParamToggle from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import NavWidget +from openpilot.selfdrive.ui.layouts.settings.toggles import DESCRIPTIONS from openpilot.selfdrive.ui.layouts.settings.common import restart_needed_callback from openpilot.selfdrive.ui.ui_state import ui_state @@ -17,9 +18,6 @@ def __init__(self, back_callback: Callable): super().__init__() self.set_back_callback(back_callback) - enable_openpilot_desc = ("Use the openpilot system for adaptive cruise control and lane keep driver assistance. " - "Your attention is required at all times to use this feature.") - self._personality_toggle = BigMultiParamToggle("driving personality", "LongitudinalPersonality", ["aggressive", "standard", "relaxed"]) self._experimental_btn = BigParamControl("experimental mode", "ExperimentalMode") is_metric_toggle = BigParamControl("use metric units", "IsMetric") @@ -28,7 +26,7 @@ def __init__(self, back_callback: Callable): record_front = BigParamControl("record & upload driver camera", "RecordFront", toggle_callback=restart_needed_callback) record_mic = BigParamControl("record & upload mic audio", "RecordAudio", toggle_callback=restart_needed_callback) enable_openpilot = BigParamControl("enable openpilot", "OpenpilotEnabledToggle", - toggle_callback=restart_needed_callback, description=enable_openpilot_desc) + toggle_callback=restart_needed_callback, description=DESCRIPTIONS["OpenpilotEnabledToggle"]) self._scroller = Scroller([ self._personality_toggle, diff --git a/selfdrive/ui/mici/widgets/button.py b/selfdrive/ui/mici/widgets/button.py index 7b5064cf0eeaf3..a0bd4c7981e5cc 100644 --- a/selfdrive/ui/mici/widgets/button.py +++ b/selfdrive/ui/mici/widgets/button.py @@ -104,13 +104,13 @@ def _render(self, _): class BigButton(Widget): """A lightweight stand-in for the Qt BigButton, drawn & updated each frame.""" - def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", description: str | Callable[[], str] | None = None): + def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", description: str | None = None): super().__init__() self.set_rect(rl.Rectangle(0, 0, 402, 180)) self.text = text self.value = value self.set_icon(icon) - self._description = description + self.description = description self._scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps) @@ -137,21 +137,12 @@ def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "" def set_icon(self, icon: Union[str, rl.Texture]): self._txt_icon = gui_app.texture(icon, 64, 64) if isinstance(icon, str) and len(icon) else icon - def set_description(self, description: str | Callable[[], str] | None): - self._description = description - - def _get_description(self) -> str: - if callable(self._description): - return self._description() - return self._description or "" - def _handle_long_press(self, mouse_pos: MousePos) -> None: - desc = self._get_description() - if not desc: + if not self.description: return # Import inside to avoid circular imports from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog - gui_app.set_modal_overlay(BigDialog(self.text, desc)) + gui_app.set_modal_overlay(BigDialog(self.text, self.description)) def set_rotate_icon(self, rotate: bool): if rotate and self._rotate_icon_t is not None: @@ -269,7 +260,7 @@ def _render(self, _): class BigToggle(BigButton): def __init__(self, text: str, value: str = "", initial_state: bool = False, toggle_callback: Callable = None, - description: str | Callable[[], str] | None = None): + description: str | None = None): super().__init__(text, value, "", description=description) self._checked = initial_state self._toggle_callback = toggle_callback @@ -307,7 +298,7 @@ def _render(self, _): class BigMultiToggle(BigToggle): def __init__(self, text: str, options: list[str], toggle_callback: Callable = None, - select_callback: Callable = None, description: str | Callable[[], str] | None = None): + select_callback: Callable = None, description: str | None = None): super().__init__(text, "", toggle_callback=toggle_callback, description=description) assert len(options) > 0 self._options = options @@ -346,7 +337,7 @@ def _render(self, _): class BigMultiParamToggle(BigMultiToggle): def __init__(self, text: str, param: str, options: list[str], toggle_callback: Callable = None, - select_callback: Callable = None, description: str | Callable[[], str] | None = None): + select_callback: Callable = None, description: str | None = None): super().__init__(text, options, toggle_callback, select_callback, description=description) self._param = param @@ -363,7 +354,7 @@ def _handle_mouse_release(self, mouse_pos: MousePos): class BigParamControl(BigToggle): - def __init__(self, text: str, param: str, toggle_callback: Callable = None, description: str | Callable[[], str] | None = None): + def __init__(self, text: str, param: str, toggle_callback: Callable = None, description: str | None = None): super().__init__(text, "", toggle_callback=toggle_callback, description=description) self.param = param self.params = Params() diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 3d9aa3f9e247a3..e4cc9f58dbaa8a 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -400,11 +400,10 @@ def _render(self, _): class BigDialogButton(BigButton): def __init__(self, text: str, value: str = "", icon: Union[str, rl.Texture] = "", description: str = ""): - super().__init__(text, value, icon) - self._description = description + super().__init__(text, value, icon, description=description) def _handle_mouse_release(self, mouse_pos: MousePos): super()._handle_mouse_release(mouse_pos) - dlg = BigDialog(self.text, self._description) + dlg = BigDialog(self.text, self.description) gui_app.set_modal_overlay(dlg) From 2c638a434b93ab02bd8d2ca9c5bc11d40cf06864 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 16 Dec 2025 16:34:53 -0800 Subject: [PATCH 7/9] rm --- selfdrive/ui/mici/layouts/settings/toggles.py | 12 ++++++------ system/ui/widgets/__init__.py | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/toggles.py b/selfdrive/ui/mici/layouts/settings/toggles.py index 2237d955b83cbf..5c2afc2626f3ca 100644 --- a/selfdrive/ui/mici/layouts/settings/toggles.py +++ b/selfdrive/ui/mici/layouts/settings/toggles.py @@ -18,13 +18,13 @@ def __init__(self, back_callback: Callable): super().__init__() self.set_back_callback(back_callback) - self._personality_toggle = BigMultiParamToggle("driving personality", "LongitudinalPersonality", ["aggressive", "standard", "relaxed"]) + self._personality_toggle = BigMultiParamToggle("driving personality", "LongitudinalPersonality", ["aggressive", "standard", "relaxed"], description=DESCRIPTIONS["LongitudinalPersonality"]) self._experimental_btn = BigParamControl("experimental mode", "ExperimentalMode") - is_metric_toggle = BigParamControl("use metric units", "IsMetric") - ldw_toggle = BigParamControl("lane departure warnings", "IsLdwEnabled") - always_on_dm_toggle = BigParamControl("always-on driver monitor", "AlwaysOnDM") - record_front = BigParamControl("record & upload driver camera", "RecordFront", toggle_callback=restart_needed_callback) - record_mic = BigParamControl("record & upload mic audio", "RecordAudio", toggle_callback=restart_needed_callback) + is_metric_toggle = BigParamControl("use metric units", "IsMetric", description=DESCRIPTIONS["IsMetric"]) + ldw_toggle = BigParamControl("lane departure warnings", "IsLdwEnabled", description=DESCRIPTIONS["IsLdwEnabled"]) + always_on_dm_toggle = BigParamControl("always-on driver monitor", "AlwaysOnDM", description=DESCRIPTIONS["AlwaysOnDM"]) + record_front = BigParamControl("record & upload driver camera", "RecordFront", toggle_callback=restart_needed_callback, description=DESCRIPTIONS["RecordFront"]) + record_mic = BigParamControl("record & upload mic audio", "RecordAudio", toggle_callback=restart_needed_callback, description=DESCRIPTIONS["RecordAudio"]) enable_openpilot = BigParamControl("enable openpilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback, description=DESCRIPTIONS["OpenpilotEnabledToggle"]) diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 7b021f2e5ad657..5b3b4a691cf1c5 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -1,6 +1,5 @@ import abc import pyray as rl -import time from enum import IntEnum from collections.abc import Callable from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter From 5b1f0aecc5c737d14c18fc0158d7e63ff34b4fb4 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 16 Dec 2025 16:35:13 -0800 Subject: [PATCH 8/9] lint --- selfdrive/ui/mici/layouts/settings/toggles.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/toggles.py b/selfdrive/ui/mici/layouts/settings/toggles.py index 5c2afc2626f3ca..ffcee9fb4907c7 100644 --- a/selfdrive/ui/mici/layouts/settings/toggles.py +++ b/selfdrive/ui/mici/layouts/settings/toggles.py @@ -18,15 +18,18 @@ def __init__(self, back_callback: Callable): super().__init__() self.set_back_callback(back_callback) - self._personality_toggle = BigMultiParamToggle("driving personality", "LongitudinalPersonality", ["aggressive", "standard", "relaxed"], description=DESCRIPTIONS["LongitudinalPersonality"]) + self._personality_toggle = BigMultiParamToggle("driving personality", "LongitudinalPersonality", ["aggressive", "standard", "relaxed"], + description=DESCRIPTIONS["LongitudinalPersonality"]) self._experimental_btn = BigParamControl("experimental mode", "ExperimentalMode") is_metric_toggle = BigParamControl("use metric units", "IsMetric", description=DESCRIPTIONS["IsMetric"]) ldw_toggle = BigParamControl("lane departure warnings", "IsLdwEnabled", description=DESCRIPTIONS["IsLdwEnabled"]) always_on_dm_toggle = BigParamControl("always-on driver monitor", "AlwaysOnDM", description=DESCRIPTIONS["AlwaysOnDM"]) - record_front = BigParamControl("record & upload driver camera", "RecordFront", toggle_callback=restart_needed_callback, description=DESCRIPTIONS["RecordFront"]) - record_mic = BigParamControl("record & upload mic audio", "RecordAudio", toggle_callback=restart_needed_callback, description=DESCRIPTIONS["RecordAudio"]) - enable_openpilot = BigParamControl("enable openpilot", "OpenpilotEnabledToggle", - toggle_callback=restart_needed_callback, description=DESCRIPTIONS["OpenpilotEnabledToggle"]) + record_front = BigParamControl("record & upload driver camera", "RecordFront", toggle_callback=restart_needed_callback, + description=DESCRIPTIONS["RecordFront"]) + record_mic = BigParamControl("record & upload mic audio", "RecordAudio", toggle_callback=restart_needed_callback, + description=DESCRIPTIONS["RecordAudio"]) + enable_openpilot = BigParamControl("enable openpilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback, + description=DESCRIPTIONS["OpenpilotEnabledToggle"]) self._scroller = Scroller([ self._personality_toggle, From 88b5d19a105c0833c418ab24dd0d64f1eb7ab1d4 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 16 Dec 2025 16:40:29 -0800 Subject: [PATCH 9/9] hmm why no work --- selfdrive/ui/mici/widgets/dialog.py | 55 +++++++++++++++++------------ 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index e4cc9f58dbaa8a..785b58b02b3e6b 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -59,8 +59,12 @@ def __init__(self, right_btn: str | None = None, right_btn_callback: Callable | None = None): super().__init__(right_btn, right_btn_callback) - self._title = title - self._description = description + self._title_label = UnifiedLabel(title, font_size=50, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.BOLD) + self._description_label = UnifiedLabel(description, font_size=30, text_color=rl.Color(255, 255, 255, int(255 * 0.58)), ) + self._scroller = Scroller([ + self._title_label, + self._description_label + ], horizontal=False, snap_items=False) def _render(self, _) -> DialogResult: super()._render(_) @@ -73,26 +77,33 @@ def _render(self, _) -> DialogResult: if self._right_btn: max_width -= self._right_btn._rect.width - title_wrapped = '\n'.join(wrap_text(gui_app.font(FontWeight.BOLD), self._title, 50, int(max_width))) - title_size = measure_text_cached(gui_app.font(FontWeight.BOLD), title_wrapped, 50) - text_x_offset = 0 - title_rect = rl.Rectangle(int(self._rect.x + text_x_offset + PADDING), - int(self._rect.y + PADDING), - int(max_width), - int(title_size.y)) - gui_label(title_rect, title_wrapped, 50, font_weight=FontWeight.BOLD, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) - - # draw description - desc_wrapped = '\n'.join(wrap_text(gui_app.font(FontWeight.MEDIUM), self._description, 30, int(max_width))) - desc_size = measure_text_cached(gui_app.font(FontWeight.MEDIUM), desc_wrapped, 30) - desc_rect = rl.Rectangle(int(self._rect.x + text_x_offset + PADDING), - int(self._rect.y + self._rect.height / 3), - int(max_width), - int(desc_size.y)) - # TODO: text align doesn't seem to work properly with newlines - gui_label(desc_rect, desc_wrapped, 30, font_weight=FontWeight.MEDIUM, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + self._scroller.render(self._rect) + # title_height = self._title_label.get_text_height(int(max_width)) + # self._title_label.render(rl.Rectangle( + # self._rect.x, + # self._rect.y + # )) + + # title_wrapped = '\n'.join(wrap_text(gui_app.font(FontWeight.BOLD), self._title, 50, int(max_width))) + # title_size = measure_text_cached(gui_app.font(FontWeight.BOLD), title_wrapped, 50) + # text_x_offset = 0 + # title_rect = rl.Rectangle(int(self._rect.x + text_x_offset + PADDING), + # int(self._rect.y + PADDING), + # int(max_width), + # int(title_size.y)) + # gui_label(title_rect, title_wrapped, 50, font_weight=FontWeight.BOLD, + # alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + # + # # draw description + # desc_wrapped = '\n'.join(wrap_text(gui_app.font(FontWeight.MEDIUM), self._description, 30, int(max_width))) + # desc_size = measure_text_cached(gui_app.font(FontWeight.MEDIUM), desc_wrapped, 30) + # desc_rect = rl.Rectangle(int(self._rect.x + text_x_offset + PADDING), + # int(self._rect.y + self._rect.height / 3), + # int(max_width), + # int(desc_size.y)) + # # TODO: text align doesn't seem to work properly with newlines + # gui_label(desc_rect, desc_wrapped, 30, font_weight=FontWeight.MEDIUM, + # alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) return self._ret