From 79319d24475f1f006d023406340703f299b52ba4 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Thu, 12 Jun 2025 12:18:07 +0800 Subject: [PATCH] ui: add driving personality selector to settings (#35524) * Add driving personality selector to settings * icon * format * type --------- Co-authored-by: Shane Smiskol --- selfdrive/ui/layouts/settings/toggles.py | 19 +++++++- system/ui/lib/list_view.py | 59 ++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/layouts/settings/toggles.py b/selfdrive/ui/layouts/settings/toggles.py index 233186be2..2c18e44ef 100644 --- a/selfdrive/ui/layouts/settings/toggles.py +++ b/selfdrive/ui/layouts/settings/toggles.py @@ -1,4 +1,4 @@ -from openpilot.system.ui.lib.list_view import ListView, toggle_item +from openpilot.system.ui.lib.list_view import ListView, multiple_button_item, toggle_item from openpilot.system.ui.lib.widget import Widget from openpilot.common.params import Params @@ -9,6 +9,11 @@ DESCRIPTIONS = { "Your attention is required at all times to use this feature." ), "DisengageOnAccelerator": "When enabled, pressing the accelerator pedal will disengage openpilot.", + "LongitudinalPersonality": ( + "Standard is recommended. In aggressive mode, openpilot will follow lead cars closer and be more aggressive with the gas and brake. " + + "In relaxed mode openpilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with " + + "your steering wheel distance button." + ), "IsLdwEnabled": ( "Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line " + "without a turn signal activated while driving over 31 mph (50 km/h)." @@ -41,6 +46,15 @@ class TogglesLayout(Widget): self._params.get_bool("DisengageOnAccelerator"), icon="disengage_on_accelerator.png", ), + multiple_button_item( + "Driving Personality", + DESCRIPTIONS["LongitudinalPersonality"], + buttons=["Aggressive", "Standard", "Relaxed"], + button_width=255, + callback=self._set_longitudinal_personality, + selected_index=int(self._params.get("LongitudinalPersonality") or 0), + icon="speed_limit.png" + ), toggle_item( "Enable Lane Departure Warnings", DESCRIPTIONS["IsLdwEnabled"], @@ -68,3 +82,6 @@ class TogglesLayout(Widget): def _render(self, rect): self._list_widget.render(rect) + + def _set_longitudinal_personality(self, button_index: int): + self._params.put("LongitudinalPersonality", str(button_index)) diff --git a/system/ui/lib/list_view.py b/system/ui/lib/list_view.py index 2a1880522..99690e600 100644 --- a/system/ui/lib/list_view.py +++ b/system/ui/lib/list_view.py @@ -149,6 +149,59 @@ class DualButtonAction(ItemAction): return False +class MultipleButtonAction(ItemAction): + def __init__(self, buttons: list[str], button_width: int, selected_index: int = 0, callback: Callable = None): + super().__init__(width=len(buttons) * (button_width + 20), enabled=True) + self.buttons = buttons + self.button_width = button_width + self.selected_button = selected_index + self.callback = callback + self._font = gui_app.font(FontWeight.MEDIUM) + + def _render(self, rect: rl.Rectangle) -> bool: + spacing = 20 + button_y = rect.y + (rect.height - 100) / 2 + clicked = -1 + + for i, text in enumerate(self.buttons): + button_x = rect.x + i * (self.button_width + spacing) + button_rect = rl.Rectangle(button_x, button_y, self.button_width, 100) + + # Check button state + mouse_pos = rl.get_mouse_position() + is_hovered = rl.check_collision_point_rec(mouse_pos, button_rect) + is_pressed = is_hovered and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) + is_selected = i == self.selected_button + + # Button colors + if is_selected: + bg_color = rl.Color(51, 171, 76, 255) # Green + elif is_pressed: + bg_color = rl.Color(74, 74, 74, 255) # Dark gray + else: + bg_color = rl.Color(57, 57, 57, 255) # Gray + + # Draw button + rl.draw_rectangle_rounded(button_rect, 1.0, 20, bg_color) + + # Draw text + text_size = measure_text_cached(self._font, text, 40) + text_x = button_x + (self.button_width - text_size.x) / 2 + text_y = button_y + (100 - text_size.y) / 2 + rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, rl.Color(228, 228, 228, 255)) + + # Handle click + if is_hovered and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT): + clicked = i + + if clicked >= 0: + self.selected_button = clicked + if self.callback: + self.callback(clicked) + return True + return False + + @dataclass class ListItem: title: str @@ -391,3 +444,9 @@ def dual_button_item(left_text: str, right_text: str, left_callback: Callable = visible: bool | Callable[[], bool] = True) -> ListItem: action = DualButtonAction(left_text, right_text, left_callback, right_callback, enabled) return ListItem(title="", description=description, action_item=action, visible=visible) + + +def multiple_button_item(title: str, description: str, buttons: list[str], selected_index: int, + button_width: int = BUTTON_WIDTH, callback: Callable = None, icon: str = ""): + action = MultipleButtonAction(buttons, button_width, selected_index, callback=callback) + return ListItem(title=title, description=description, icon=icon, action_item=action)