From 47d848293b54a6adf83c964120ed74dfbdf58359 Mon Sep 17 00:00:00 2001 From: nayan Date: Sat, 15 Nov 2025 09:28:43 -0500 Subject: [PATCH 01/41] param to control stock vs sp ui --- common/params_keys.h | 1 + 1 file changed, 1 insertion(+) diff --git a/common/params_keys.h b/common/params_keys.h index 38578d7ddc..fe08062987 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -173,6 +173,7 @@ inline static std::unordered_map keys = { {"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}}, {"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"sunnypilot_ui", {PERSISTENT, BOOL, "1"}}, {"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}}, // MADS params From 121b304fe0dcd8bfad02a05aaba6d3e0c9d3029e Mon Sep 17 00:00:00 2001 From: nayan Date: Sat, 15 Nov 2025 09:28:58 -0500 Subject: [PATCH 02/41] init styles --- system/ui/sunnypilot/lib/styles.py | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 system/ui/sunnypilot/lib/styles.py diff --git a/system/ui/sunnypilot/lib/styles.py b/system/ui/sunnypilot/lib/styles.py new file mode 100644 index 0000000000..851a52817d --- /dev/null +++ b/system/ui/sunnypilot/lib/styles.py @@ -0,0 +1,35 @@ +import pyray as rl +from dataclasses import dataclass + +@dataclass +class Default: + # Base Colors + BASE_BG_COLOR = rl.Color(57, 57, 57, 255) # Grey + ON_BG_COLOR = rl.Color(28, 101, 186, 255) # Blue + OFF_BG_COLOR = BASE_BG_COLOR + ON_HOVER_BG_COLOR = rl.Color(17, 78, 150, 255) # Dark Blue + OFF_HOVER_BG_COLOR = rl.Color(21, 21, 21, 255) # Dark gray + DISABLED_ON_BG_COLOR = rl.Color(37, 70, 107, 255) # Dull Blue + DISABLED_OFF_BG_COLOR = rl.Color(39, 39, 39, 255) # Grey + ITEM_TEXT_COLOR = rl.WHITE + ITEM_DISABLED_TEXT_COLOR = rl.Color(88, 88, 88, 255) + ITEM_DESC_TEXT_COLOR = rl.Color(128, 128, 128, 255) + + # Widget/Control Base Dimensions + ITEM_BASE_HEIGHT = 170 + ITEM_PADDING = 20 + ITEM_TEXT_FONT_SIZE = 50 + ITEM_DESC_FONT_SIZE = 40 + ITEM_DESC_V_OFFSET = 150 + + # Toggle Control + TOGGLE_HEIGHT = 100 + TOGGLE_WIDTH = int(TOGGLE_HEIGHT * 1.75) + TOGGLE_BG_HEIGHT = TOGGLE_HEIGHT - 20 + + TOGGLE_ON_COLOR = ON_BG_COLOR + TOGGLE_OFF_COLOR = OFF_BG_COLOR + TOGGLE_KNOB_COLOR = rl.WHITE + TOGGLE_DISABLED_ON_COLOR = DISABLED_ON_BG_COLOR + TOGGLE_DISABLED_OFF_COLOR = DISABLED_OFF_BG_COLOR + TOGGLE_DISABLED_KNOB_COLOR = rl.Color(88, 88, 88, 255) # Lighter Grey From 9c5acf61c047c81fd17bd02e9354225aabf5f7e8 Mon Sep 17 00:00:00 2001 From: nayan Date: Sat, 15 Nov 2025 09:29:07 -0500 Subject: [PATCH 03/41] SP Toggles --- selfdrive/ui/layouts/settings/developer.py | 3 + selfdrive/ui/layouts/settings/toggles.py | 3 + system/ui/sunnypilot/widgets/list_view.py | 97 ++++++++++++++++++++ system/ui/sunnypilot/widgets/toggle.py | 100 +++++++++++++++++++++ 4 files changed, 203 insertions(+) create mode 100644 system/ui/sunnypilot/widgets/list_view.py create mode 100644 system/ui/sunnypilot/widgets/toggle.py diff --git a/selfdrive/ui/layouts/settings/developer.py b/selfdrive/ui/layouts/settings/developer.py index 9ea1019f54..af00d98e73 100644 --- a/selfdrive/ui/layouts/settings/developer.py +++ b/selfdrive/ui/layouts/settings/developer.py @@ -9,6 +9,9 @@ from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.multilang import tr, tr_noop from openpilot.system.ui.widgets import DialogResult +if Params().get_bool("sunnypilot_ui"): + from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp as toggle_item + # Description constants DESCRIPTIONS = { 'enable_adb': tr_noop( diff --git a/selfdrive/ui/layouts/settings/toggles.py b/selfdrive/ui/layouts/settings/toggles.py index 3a1265e0fe..06005c5f84 100644 --- a/selfdrive/ui/layouts/settings/toggles.py +++ b/selfdrive/ui/layouts/settings/toggles.py @@ -9,6 +9,9 @@ from openpilot.system.ui.lib.multilang import tr, tr_noop from openpilot.system.ui.widgets import DialogResult from openpilot.selfdrive.ui.ui_state import ui_state +if Params().get_bool("sunnypilot_ui"): + from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp as toggle_item + PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants # Description constants diff --git a/system/ui/sunnypilot/widgets/list_view.py b/system/ui/sunnypilot/widgets/list_view.py new file mode 100644 index 0000000000..9d3e973c63 --- /dev/null +++ b/system/ui/sunnypilot/widgets/list_view.py @@ -0,0 +1,97 @@ +import pyray as rl + +from collections.abc import Callable +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.sunnypilot.widgets.toggle import ToggleSP +from openpilot.system.ui.widgets.list_view import ListItem, ToggleAction, ItemAction + + +import openpilot.system.ui.sunnypilot.lib.styles as styles +style = styles.Default + + +class ToggleActionSP(ToggleAction): + def __init__(self, initial_state: bool = False, width: int = style.TOGGLE_WIDTH, enabled: bool | Callable[[], bool] = True, param: str | None = None): + ToggleAction.__init__(self, initial_state, width, enabled) + self.toggle = ToggleSP(initial_state=initial_state, param=param) + +class ListItemSP(ListItem): + def __init__(self, title: str = "", icon: str | None = None, description: str | Callable[[], str] | None = None, + description_visible: bool = False, callback: Callable | None = None, + action_item: ItemAction | None = None): + ListItem.__init__(self, title, icon, description, description_visible, callback, action_item) + + def get_right_item_rect(self, item_rect: rl.Rectangle) -> rl.Rectangle: + if not self.action_item: + return rl.Rectangle(0, 0, 0, 0) + + right_width = self.action_item.rect.width + if right_width == 0: # Full width action (like DualButtonAction) + return rl.Rectangle(item_rect.x + style.ITEM_PADDING, item_rect.y, + item_rect.width - (style.ITEM_PADDING * 2), style.ITEM_BASE_HEIGHT) + + action_width = self.action_item.rect.width + if isinstance(self.action_item, ToggleAction): + action_x = item_rect.x + else: + action_x = item_rect.x + item_rect.width - action_width + action_y = item_rect.y + return rl.Rectangle(action_x, action_y, action_width, style.ITEM_BASE_HEIGHT) + + def _render(self, _): + content_x = self._rect.x + style.ITEM_PADDING + text_x = content_x + left_action_item = isinstance(self.action_item, ToggleAction) + + if left_action_item: + left_rect = rl.Rectangle( + content_x, + self._rect.y + (style.ITEM_BASE_HEIGHT - style.TOGGLE_HEIGHT) // 2, + style.TOGGLE_WIDTH, + style.TOGGLE_HEIGHT + ) + text_x = left_rect.x + left_rect.width + style.ITEM_PADDING + + # Draw title + if self.title: + text_size = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE) + item_y = self._rect.y + (style.ITEM_BASE_HEIGHT - text_size.y) // 2 + rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), style.ITEM_TEXT_FONT_SIZE, 0, style.ITEM_TEXT_COLOR) + + # Render toggle and handle callback + if self.action_item.render(left_rect) and self.action_item.enabled: + if self.callback: + self.callback() + + else: + if self.title: + # Draw main text + text_size = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE) + item_y = self._rect.y + (style.ITEM_BASE_HEIGHT - text_size.y) // 2 + rl.draw_text_ex(self._font, self.title, rl.Vector2(text_x, item_y), style.ITEM_TEXT_FONT_SIZE, 0, style.ITEM_TEXT_COLOR) + + # Draw right item if present + if self.action_item: + right_rect = self.get_right_item_rect(self._rect) + right_rect.y = self._rect.y + if self.action_item.render(right_rect) and self.action_item.enabled: + # Right item was clicked/activated + if self.callback: + self.callback() + + # Draw description if visible + if self.description_visible: + content_width = int(self._rect.width - style.ITEM_PADDING * 2) + description_height = self._html_renderer.get_total_height(content_width) + description_rect = rl.Rectangle( + self._rect.x + style.ITEM_PADDING, + self._rect.y + style.ITEM_DESC_V_OFFSET, + content_width, + description_height + ) + self._html_renderer.render(description_rect) + +def toggle_item_sp(title: str, description: str | Callable[[], str] | None = None, initial_state: bool = False, + callback: Callable | None = None, icon: str = "", enabled: bool | Callable[[], bool] = True, param: str | None = None) -> ListItem: + action = ToggleActionSP(initial_state=initial_state, enabled=enabled, param=param) + return ListItemSP(title=title, description=description, action_item=action, icon=icon, callback=callback) diff --git a/system/ui/sunnypilot/widgets/toggle.py b/system/ui/sunnypilot/widgets/toggle.py new file mode 100644 index 0000000000..a62e51d758 --- /dev/null +++ b/system/ui/sunnypilot/widgets/toggle.py @@ -0,0 +1,100 @@ +import pyray as rl +from openpilot.common.params import Params +from openpilot.system.ui.lib.application import MousePos +from openpilot.system.ui.widgets.toggle import Toggle +import openpilot.system.ui.sunnypilot.lib.styles as styles + +style = styles.Default + +class ToggleSP(Toggle): + def __init__(self, initial_state=False, param: str | None = None): + self.param_key = param + self.params = Params() + if self.param_key: + initial_state = self.params.get_bool(self.param_key) + Toggle.__init__(self, initial_state) + + def _handle_mouse_release(self, mouse_pos: MousePos): + super()._handle_mouse_release(mouse_pos) + if self._enabled and self.param_key: + self.params.put_bool(self.param_key, self._state) + + def _render(self, rect: rl.Rectangle): + self.update() + if self._enabled: + bg_color = self._blend_color(style.TOGGLE_OFF_COLOR, style.TOGGLE_ON_COLOR, self._progress) + knob_color = style.TOGGLE_KNOB_COLOR + else: + bg_color = self._blend_color(style.TOGGLE_DISABLED_OFF_COLOR, style.TOGGLE_DISABLED_ON_COLOR, self._progress) + knob_color = style.TOGGLE_DISABLED_KNOB_COLOR + + # Draw background + bg_rect = rl.Rectangle(self._rect.x, self._rect.y, style.TOGGLE_WIDTH, style.TOGGLE_BG_HEIGHT) + + # Draw outline first + outline_color = style.TOGGLE_ON_COLOR + if not self._enabled: + # Use a more subtle color for disabled state + outline_color = rl.Color(outline_color.r // 2, outline_color.g // 2, outline_color.b // 2, 255) + + # Draw outline by drawing a slightly larger rounded rectangle behind the background + outline_rect = rl.Rectangle(bg_rect.x - 2, bg_rect.y - 2, bg_rect.width + 4, bg_rect.height + 4) + rl.draw_rectangle_rounded(outline_rect, 1.0, 10, outline_color) + + # Draw actual background + rl.draw_rectangle_rounded(bg_rect, 1.0, 10, bg_color) + + # Draw knob to sit inside the background + knob_padding = 5 + knob_radius = style.TOGGLE_BG_HEIGHT / 2 - knob_padding + + left_edge = bg_rect.x + knob_padding + right_edge = bg_rect.x + bg_rect.width - knob_padding + + knob_travel_distance = right_edge - left_edge - 2 * knob_radius + min_knob_x = left_edge + knob_radius + knob_x = min_knob_x + knob_travel_distance * self._progress + knob_y = self._rect.y + style.TOGGLE_BG_HEIGHT / 2 + + rl.draw_circle(int(knob_x), int(knob_y), knob_radius, knob_color) + + symbol_size = knob_radius / 2 + + if self._state and (self._enabled or self._progress > 0.5): + # Draw checkmark when toggle is ON + start_x = knob_x - symbol_size * 0.8 + start_y = knob_y + mid_x = knob_x - symbol_size * 0.1 + mid_y = knob_y + symbol_size * 0.6 + end_x = knob_x + symbol_size * 0.8 + end_y = knob_y - symbol_size * 0.5 + + rl.draw_line_ex( + rl.Vector2(int(start_x), int(start_y)), + rl.Vector2(int(mid_x), int(mid_y)), + 3, + style.TOGGLE_ON_COLOR + ) + rl.draw_line_ex( + rl.Vector2(int(mid_x), int(mid_y)), + rl.Vector2(int(end_x), int(end_y)), + 3, + style.TOGGLE_ON_COLOR + ) + else: + # Draw X when toggle is OFF + x_size_factor = 0.65 + x_offset = symbol_size * x_size_factor + + rl.draw_line_ex( + rl.Vector2(int(knob_x - x_offset), int(knob_y - x_offset)), + rl.Vector2(int(knob_x + x_offset), int(knob_y + x_offset)), + 3, + style.TOGGLE_OFF_COLOR + ) + rl.draw_line_ex( + rl.Vector2(int(knob_x + x_offset), int(knob_y - x_offset)), + rl.Vector2(int(knob_x - x_offset), int(knob_y + x_offset)), + 3, + style.TOGGLE_OFF_COLOR + ) From 0c37a385967c201d7db6e23ca3506bc21a22d698 Mon Sep 17 00:00:00 2001 From: nayan Date: Sat, 15 Nov 2025 09:42:12 -0500 Subject: [PATCH 04/41] Lint --- system/ui/sunnypilot/widgets/list_view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/ui/sunnypilot/widgets/list_view.py b/system/ui/sunnypilot/widgets/list_view.py index 9d3e973c63..c7043db531 100644 --- a/system/ui/sunnypilot/widgets/list_view.py +++ b/system/ui/sunnypilot/widgets/list_view.py @@ -16,7 +16,7 @@ class ToggleActionSP(ToggleAction): self.toggle = ToggleSP(initial_state=initial_state, param=param) class ListItemSP(ListItem): - def __init__(self, title: str = "", icon: str | None = None, description: str | Callable[[], str] | None = None, + def __init__(self, title: str | Callable[[], str] = "", icon: str | None = None, description: str | Callable[[], str] | None = None, description_visible: bool = False, callback: Callable | None = None, action_item: ItemAction | None = None): ListItem.__init__(self, title, icon, description, description_visible, callback, action_item) @@ -91,7 +91,7 @@ class ListItemSP(ListItem): ) self._html_renderer.render(description_rect) -def toggle_item_sp(title: str, description: str | Callable[[], str] | None = None, initial_state: bool = False, +def toggle_item_sp(title: str | Callable[[], str], description: str | Callable[[], str] | None = None, initial_state: bool = False, callback: Callable | None = None, icon: str = "", enabled: bool | Callable[[], bool] = True, param: str | None = None) -> ListItem: action = ToggleActionSP(initial_state=initial_state, enabled=enabled, param=param) return ListItemSP(title=title, description=description, action_item=action, icon=icon, callback=callback) From 3946e643f68fdb5784a2b42839162903a5f4a592 Mon Sep 17 00:00:00 2001 From: nayan Date: Sat, 15 Nov 2025 20:24:20 -0500 Subject: [PATCH 05/41] optimizations --- system/ui/sunnypilot/lib/styles.py | 31 +++++++++++--------- system/ui/sunnypilot/widgets/list_view.py | 5 +--- system/ui/sunnypilot/widgets/toggle.py | 35 ++++++++++------------- 3 files changed, 34 insertions(+), 37 deletions(-) diff --git a/system/ui/sunnypilot/lib/styles.py b/system/ui/sunnypilot/lib/styles.py index 851a52817d..28d50f48d7 100644 --- a/system/ui/sunnypilot/lib/styles.py +++ b/system/ui/sunnypilot/lib/styles.py @@ -2,19 +2,7 @@ import pyray as rl from dataclasses import dataclass @dataclass -class Default: - # Base Colors - BASE_BG_COLOR = rl.Color(57, 57, 57, 255) # Grey - ON_BG_COLOR = rl.Color(28, 101, 186, 255) # Blue - OFF_BG_COLOR = BASE_BG_COLOR - ON_HOVER_BG_COLOR = rl.Color(17, 78, 150, 255) # Dark Blue - OFF_HOVER_BG_COLOR = rl.Color(21, 21, 21, 255) # Dark gray - DISABLED_ON_BG_COLOR = rl.Color(37, 70, 107, 255) # Dull Blue - DISABLED_OFF_BG_COLOR = rl.Color(39, 39, 39, 255) # Grey - ITEM_TEXT_COLOR = rl.WHITE - ITEM_DISABLED_TEXT_COLOR = rl.Color(88, 88, 88, 255) - ITEM_DESC_TEXT_COLOR = rl.Color(128, 128, 128, 255) - +class Base: # Widget/Control Base Dimensions ITEM_BASE_HEIGHT = 170 ITEM_PADDING = 20 @@ -27,9 +15,26 @@ class Default: TOGGLE_WIDTH = int(TOGGLE_HEIGHT * 1.75) TOGGLE_BG_HEIGHT = TOGGLE_HEIGHT - 20 +@dataclass +class SP_Default(Base): + # Base Colors + BASE_BG_COLOR = rl.Color(57, 57, 57, 255) # Grey + ON_BG_COLOR = rl.Color(28, 101, 186, 255) # Blue + OFF_BG_COLOR = BASE_BG_COLOR + ON_HOVER_BG_COLOR = rl.Color(17, 78, 150, 255) # Dark Blue + OFF_HOVER_BG_COLOR = rl.Color(21, 21, 21, 255) # Dark gray + DISABLED_ON_BG_COLOR = rl.Color(37, 70, 107, 255) # Dull Blue + DISABLED_OFF_BG_COLOR = rl.Color(39, 39, 39, 255) # Grey + ITEM_TEXT_COLOR = rl.WHITE + ITEM_DISABLED_TEXT_COLOR = rl.Color(88, 88, 88, 255) + ITEM_DESC_TEXT_COLOR = rl.Color(128, 128, 128, 255) + + # Toggle Control TOGGLE_ON_COLOR = ON_BG_COLOR TOGGLE_OFF_COLOR = OFF_BG_COLOR TOGGLE_KNOB_COLOR = rl.WHITE TOGGLE_DISABLED_ON_COLOR = DISABLED_ON_BG_COLOR TOGGLE_DISABLED_OFF_COLOR = DISABLED_OFF_BG_COLOR TOGGLE_DISABLED_KNOB_COLOR = rl.Color(88, 88, 88, 255) # Lighter Grey + +style = SP_Default diff --git a/system/ui/sunnypilot/widgets/list_view.py b/system/ui/sunnypilot/widgets/list_view.py index c7043db531..6020ab71cb 100644 --- a/system/ui/sunnypilot/widgets/list_view.py +++ b/system/ui/sunnypilot/widgets/list_view.py @@ -4,10 +4,7 @@ from collections.abc import Callable from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.sunnypilot.widgets.toggle import ToggleSP from openpilot.system.ui.widgets.list_view import ListItem, ToggleAction, ItemAction - - -import openpilot.system.ui.sunnypilot.lib.styles as styles -style = styles.Default +from openpilot.system.ui.sunnypilot.lib.styles import style class ToggleActionSP(ToggleAction): diff --git a/system/ui/sunnypilot/widgets/toggle.py b/system/ui/sunnypilot/widgets/toggle.py index a62e51d758..7d3847d3f0 100644 --- a/system/ui/sunnypilot/widgets/toggle.py +++ b/system/ui/sunnypilot/widgets/toggle.py @@ -2,9 +2,11 @@ import pyray as rl from openpilot.common.params import Params from openpilot.system.ui.lib.application import MousePos from openpilot.system.ui.widgets.toggle import Toggle -import openpilot.system.ui.sunnypilot.lib.styles as styles +from openpilot.system.ui.sunnypilot.lib.styles import style -style = styles.Default +KNOB_PADDING = 5 +KNOB_RADIUS = style.TOGGLE_BG_HEIGHT / 2 - KNOB_PADDING +SYMBOL_SIZE = KNOB_RADIUS / 2 class ToggleSP(Toggle): def __init__(self, initial_state=False, param: str | None = None): @@ -44,30 +46,24 @@ class ToggleSP(Toggle): # Draw actual background rl.draw_rectangle_rounded(bg_rect, 1.0, 10, bg_color) - # Draw knob to sit inside the background - knob_padding = 5 - knob_radius = style.TOGGLE_BG_HEIGHT / 2 - knob_padding + left_edge = bg_rect.x + KNOB_PADDING + right_edge = bg_rect.x + bg_rect.width - KNOB_PADDING - left_edge = bg_rect.x + knob_padding - right_edge = bg_rect.x + bg_rect.width - knob_padding - - knob_travel_distance = right_edge - left_edge - 2 * knob_radius - min_knob_x = left_edge + knob_radius + knob_travel_distance = right_edge - left_edge - 2 * KNOB_RADIUS + min_knob_x = left_edge + KNOB_RADIUS knob_x = min_knob_x + knob_travel_distance * self._progress knob_y = self._rect.y + style.TOGGLE_BG_HEIGHT / 2 - rl.draw_circle(int(knob_x), int(knob_y), knob_radius, knob_color) - - symbol_size = knob_radius / 2 + rl.draw_circle(int(knob_x), int(knob_y), KNOB_RADIUS, knob_color) if self._state and (self._enabled or self._progress > 0.5): # Draw checkmark when toggle is ON - start_x = knob_x - symbol_size * 0.8 + start_x = knob_x - SYMBOL_SIZE * 0.8 start_y = knob_y - mid_x = knob_x - symbol_size * 0.1 - mid_y = knob_y + symbol_size * 0.6 - end_x = knob_x + symbol_size * 0.8 - end_y = knob_y - symbol_size * 0.5 + mid_x = knob_x - SYMBOL_SIZE * 0.1 + mid_y = knob_y + SYMBOL_SIZE * 0.6 + end_x = knob_x + SYMBOL_SIZE * 0.8 + end_y = knob_y - SYMBOL_SIZE * 0.5 rl.draw_line_ex( rl.Vector2(int(start_x), int(start_y)), @@ -83,8 +79,7 @@ class ToggleSP(Toggle): ) else: # Draw X when toggle is OFF - x_size_factor = 0.65 - x_offset = symbol_size * x_size_factor + x_offset = SYMBOL_SIZE * 0.65 rl.draw_line_ex( rl.Vector2(int(knob_x - x_offset), int(knob_y - x_offset)), From 4110749cb01bdd48c59725337216e4c5be9c11d3 Mon Sep 17 00:00:00 2001 From: nayan Date: Sat, 15 Nov 2025 11:29:04 -0500 Subject: [PATCH 06/41] Panels. With Icons. And Scroller. --- selfdrive/ui/layouts/main.py | 3 + .../ui/sunnypilot/layouts/settings/cruise.py | 24 +++ .../ui/sunnypilot/layouts/settings/device.py | 5 + .../ui/sunnypilot/layouts/settings/display.py | 24 +++ .../ui/sunnypilot/layouts/settings/models.py | 24 +++ .../sunnypilot/layouts/settings/navigation.py | 24 +++ .../ui/sunnypilot/layouts/settings/osm.py | 24 +++ .../sunnypilot/layouts/settings/settings.py | 189 ++++++++++++++++++ .../sunnypilot/layouts/settings/steering.py | 24 +++ .../sunnypilot/layouts/settings/sunnylink.py | 24 +++ .../ui/sunnypilot/layouts/settings/trips.py | 24 +++ .../ui/sunnypilot/layouts/settings/vehicle.py | 24 +++ .../ui/sunnypilot/layouts/settings/visuals.py | 24 +++ .../assets/offroad/icon_firehose.png | 3 + .../selfdrive/assets/offroad/icon_home.png | 3 + 15 files changed, 443 insertions(+) create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/cruise.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/device.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/display.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/models.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/navigation.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/osm.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/settings.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/steering.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/trips.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/vehicle.py create mode 100644 selfdrive/ui/sunnypilot/layouts/settings/visuals.py create mode 100644 sunnypilot/selfdrive/assets/offroad/icon_firehose.png create mode 100644 sunnypilot/selfdrive/assets/offroad/icon_home.png diff --git a/selfdrive/ui/layouts/main.py b/selfdrive/ui/layouts/main.py index 702854f98a..e965888fde 100644 --- a/selfdrive/ui/layouts/main.py +++ b/selfdrive/ui/layouts/main.py @@ -10,6 +10,9 @@ from openpilot.selfdrive.ui.ui_state import device, ui_state from openpilot.system.ui.widgets import Widget from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow +from openpilot.common.params import Params +if Params().get_bool("sunnypilot_ui"): + from openpilot.selfdrive.ui.sunnypilot.layouts.settings.settings import SettingsLayoutSP as SettingsLayout class MainState(IntEnum): HOME = 0 diff --git a/selfdrive/ui/sunnypilot/layouts/settings/cruise.py b/selfdrive/ui/sunnypilot/layouts/settings/cruise.py new file mode 100644 index 0000000000..b8e2a1344b --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/cruise.py @@ -0,0 +1,24 @@ +from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets import Widget +from openpilot.common.params import Params + + +class CruiseLayout(Widget): + def __init__(self): + super().__init__() + + self._params = Params() + items = self._initialize_items() + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _initialize_items(self): + + items = [ + ] + return items + + def _render(self, rect): + self._scroller.render(rect) + + def show_event(self): + self._scroller.show_event() diff --git a/selfdrive/ui/sunnypilot/layouts/settings/device.py b/selfdrive/ui/sunnypilot/layouts/settings/device.py new file mode 100644 index 0000000000..85aa79c8e8 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/device.py @@ -0,0 +1,5 @@ +from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout + +class DeviceLayoutSP(DeviceLayout): + def __init__(self): + DeviceLayout.__init__(self) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/display.py b/selfdrive/ui/sunnypilot/layouts/settings/display.py new file mode 100644 index 0000000000..1cbea6c9fa --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/display.py @@ -0,0 +1,24 @@ +from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets import Widget +from openpilot.common.params import Params + + +class DisplayLayout(Widget): + def __init__(self): + super().__init__() + + self._params = Params() + items = self._initialize_items() + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _initialize_items(self): + items = [ + + ] + return items + + def _render(self, rect): + self._scroller.render(rect) + + def show_event(self): + self._scroller.show_event() diff --git a/selfdrive/ui/sunnypilot/layouts/settings/models.py b/selfdrive/ui/sunnypilot/layouts/settings/models.py new file mode 100644 index 0000000000..1f0107aa9b --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/models.py @@ -0,0 +1,24 @@ +from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets import Widget +from openpilot.common.params import Params + + +class ModelsLayout(Widget): + def __init__(self): + super().__init__() + + self._params = Params() + items = self._initialize_items() + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _initialize_items(self): + items = [ + + ] + return items + + def _render(self, rect): + self._scroller.render(rect) + + def show_event(self): + self._scroller.show_event() diff --git a/selfdrive/ui/sunnypilot/layouts/settings/navigation.py b/selfdrive/ui/sunnypilot/layouts/settings/navigation.py new file mode 100644 index 0000000000..eec1367b69 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/navigation.py @@ -0,0 +1,24 @@ +from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets import Widget +from openpilot.common.params import Params + + +class NavigationLayout(Widget): + def __init__(self): + super().__init__() + + self._params = Params() + items = self._initialize_items() + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _initialize_items(self): + items = [ + + ] + return items + + def _render(self, rect): + self._scroller.render(rect) + + def show_event(self): + self._scroller.show_event() diff --git a/selfdrive/ui/sunnypilot/layouts/settings/osm.py b/selfdrive/ui/sunnypilot/layouts/settings/osm.py new file mode 100644 index 0000000000..62f9b6d858 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/osm.py @@ -0,0 +1,24 @@ +from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets import Widget +from openpilot.common.params import Params + + +class OSMLayout(Widget): + def __init__(self): + super().__init__() + + self._params = Params() + items = self._initialize_items() + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _initialize_items(self): + items = [ + + ] + return items + + def _render(self, rect): + self._scroller.render(rect) + + def show_event(self): + self._scroller.show_event() diff --git a/selfdrive/ui/sunnypilot/layouts/settings/settings.py b/selfdrive/ui/sunnypilot/layouts/settings/settings.py new file mode 100644 index 0000000000..adca958ff8 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/settings.py @@ -0,0 +1,189 @@ +import pyray as rl +from dataclasses import dataclass +from enum import IntEnum +from openpilot.selfdrive.ui.layouts.settings import settings as OP +from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.device import DeviceLayoutSP +from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout +from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout +from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout +from openpilot.system.ui.lib.application import gui_app,MousePos +from openpilot.system.ui.lib.multilang import tr_noop +from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.widgets.network import NetworkUI +from openpilot.system.ui.lib.wifi_manager import WifiManager +from openpilot.system.ui.widgets import Widget +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.sunnylink import SunnylinkLayout +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.osm import OSMLayout +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.trips import TripsLayout +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle import VehicleLayout +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.steering import SteeringLayout +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise import CruiseLayout +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.visuals import VisualsLayout +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import DisplayLayout +from openpilot.selfdrive.ui.sunnypilot.layouts.settings.navigation import NavigationLayout + +OP.PANEL_COLOR = rl.Color(10, 10, 10, 255) +ICON_SIZE = 70 + +OP.PanelType = IntEnum( # type: ignore + "PanelType", + [es.name for es in OP.PanelType] + [ + "SUNNYLINK", + "MODELS", + "STEERING", + "CRUISE", + "VISUALS", + "DISPLAY", + "OSM", + "NAVIGATION", + "TRIPS", + "VEHICLE", + ], + start=0, + ) + +@dataclass +class PanelInfo(OP.PanelInfo): + icon: str = "" + + +class SettingsLayoutSP(OP.SettingsLayout): + def __init__(self): + OP.SettingsLayout.__init__(self) + self._nav_items: list[Widget] = [] + + # Create sidebar scroller + self._sidebar_scroller = Scroller([], spacing=0, line_separator = False, pad_end=False) + + # Panel configuration + wifi_manager = WifiManager() + wifi_manager.set_active(False) + + self._panels = { + OP.PanelType.DEVICE: PanelInfo(tr_noop("Device"), DeviceLayoutSP(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_home.png"), + OP.PanelType.NETWORK: PanelInfo(tr_noop("Network"), NetworkUI(wifi_manager), icon="icons/network.png"), + OP.PanelType.SUNNYLINK: PanelInfo(tr_noop("sunnylink"), SunnylinkLayout(), icon="icons/shell.png"), + OP.PanelType.TOGGLES: PanelInfo(tr_noop("Toggles"), TogglesLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_toggle.png"), + OP.PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_software.png"), + OP.PanelType.MODELS: PanelInfo(tr_noop("Models"), ModelsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_models.png"), + OP.PanelType.STEERING: PanelInfo(tr_noop("Steering"), SteeringLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_lateral.png"), + OP.PanelType.CRUISE: PanelInfo(tr_noop("Cruise"), CruiseLayout(), icon="icons/speed_limit.png"), + OP.PanelType.VISUALS: PanelInfo(tr_noop("Visuals"), VisualsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_visuals.png"), + OP.PanelType.DISPLAY: PanelInfo(tr_noop("Display"), DisplayLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_display.png"), + OP.PanelType.OSM: PanelInfo(tr_noop("OSM"), OSMLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_map.png"), + OP.PanelType.NAVIGATION: PanelInfo(tr_noop("Navigation"), NavigationLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_map.png"), + OP.PanelType.TRIPS: PanelInfo(tr_noop("Trips"), TripsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"), + OP.PanelType.VEHICLE: PanelInfo(tr_noop("Vehicle"), VehicleLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"), + OP.PanelType.FIREHOSE: PanelInfo(tr_noop("Firehose"), FirehoseLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_firehose.png"), + OP.PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayout(), icon="icons/shell.png"), + } + + def _create_nav_button(self, panel_type: OP.PanelType, panel_info: PanelInfo) -> Widget: + class NavButton(Widget): + def __init__(self, parent, p_type, p_info): + super().__init__() + self.parent = parent + self.panel_type = p_type + self.panel_info = p_info + + def _render(self, rect): + is_selected = self.panel_type == self.parent._current_panel + text_color = OP.TEXT_SELECTED if is_selected else OP.TEXT_NORMAL + content_x = rect.x + 90 + text_size = measure_text_cached(self.parent._font_medium, self.panel_info.name, 65) + + # Draw background if selected + if is_selected: + self.container_rect = rl.Rectangle( + content_x - 20, rect.y, OP.SIDEBAR_WIDTH - 70, OP.NAV_BTN_HEIGHT + ) + rl.draw_rectangle_rounded(self.container_rect, 0.2, 5, OP.CLOSE_BTN_COLOR) + + if self.panel_info.icon: + icon_texture = gui_app.texture(self.panel_info.icon, ICON_SIZE, ICON_SIZE, keep_aspect_ratio=True) + rl.draw_texture(icon_texture, int(content_x), int(rect.y + (OP.NAV_BTN_HEIGHT - icon_texture.height) / 2), rl.WHITE) + content_x += ICON_SIZE + 20 + + # Draw button text (right-aligned) + text_pos = rl.Vector2( + content_x, + rect.y + (OP.NAV_BTN_HEIGHT - text_size.y) / 2 + ) + rl.draw_text_ex(self.parent._font_medium, self.panel_info.name, text_pos, 55, 0, text_color) + + # Store button rect for click detection + self.panel_info.button_rect = rect + + return NavButton(self, panel_type, panel_info) + + def _draw_sidebar(self, rect: rl.Rectangle): + rl.draw_rectangle_rec(rect, OP.SIDEBAR_COLOR) + + # Close button + close_btn_rect = rl.Rectangle( + rect.x + (rect.width - OP.CLOSE_BTN_SIZE) / 2, rect.y + 60, OP.CLOSE_BTN_SIZE, OP.CLOSE_BTN_SIZE + ) + + pressed = (rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and + rl.check_collision_point_rec(rl.get_mouse_position(), close_btn_rect)) + close_color = OP.CLOSE_BTN_PRESSED if pressed else OP.CLOSE_BTN_COLOR + rl.draw_rectangle_rounded(close_btn_rect, 1.0, 20, close_color) + + icon_color = rl.Color(255, 255, 255, 255) if not pressed else rl.Color(220, 220, 220, 255) + icon_dest = rl.Rectangle( + close_btn_rect.x + (close_btn_rect.width - self._close_icon.width) / 2, + close_btn_rect.y + (close_btn_rect.height - self._close_icon.height) / 2, + self._close_icon.width, + self._close_icon.height, + ) + rl.draw_texture_pro( + self._close_icon, + rl.Rectangle(0, 0, self._close_icon.width, self._close_icon.height), + icon_dest, + rl.Vector2(0, 0), + 0, + icon_color, + ) + + # Store close button rect for click detection + self._close_btn_rect = close_btn_rect + + # Navigation buttons with scroller + if not self._nav_items: + for panel_type, panel_info in self._panels.items(): + nav_button = self._create_nav_button(panel_type, panel_info) + nav_button.rect.width = rect.width - 100 # Full width minus padding + nav_button.rect.height = OP.NAV_BTN_HEIGHT + self._nav_items.append(nav_button) + self._sidebar_scroller.add_widget(nav_button) + + # Draw navigation section with scroller + nav_rect = rl.Rectangle( + rect.x, + rect.y + 300, # Starting Y position for nav items + rect.width, + rect.height - 300 # Remaining height after close button + ) + + if self._nav_items: + self._sidebar_scroller.render(nav_rect) + return + + + def _handle_mouse_release(self, mouse_pos: MousePos) -> bool: + # Check close button + if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect): + if self._close_callback: + self._close_callback() + return True + + # Check navigation buttons + for panel_type, panel_info in self._panels.items(): + if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect) and self._sidebar_scroller.scroll_panel.is_touch_valid(): + self.set_current_panel(panel_type) + return True + + return False diff --git a/selfdrive/ui/sunnypilot/layouts/settings/steering.py b/selfdrive/ui/sunnypilot/layouts/settings/steering.py new file mode 100644 index 0000000000..f8ae00fcca --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/steering.py @@ -0,0 +1,24 @@ +from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets import Widget +from openpilot.common.params import Params + + +class SteeringLayout(Widget): + def __init__(self): + super().__init__() + + self._params = Params() + items = self._initialize_items() + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _initialize_items(self): + items = [ + + ] + return items + + def _render(self, rect): + self._scroller.render(rect) + + def show_event(self): + self._scroller.show_event() diff --git a/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py b/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py new file mode 100644 index 0000000000..068679ec67 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py @@ -0,0 +1,24 @@ +from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets import Widget +from openpilot.common.params import Params + + +class SunnylinkLayout(Widget): + def __init__(self): + super().__init__() + + self._params = Params() + items = self._initialize_items() + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _initialize_items(self): + items = [ + + ] + return items + + def _render(self, rect): + self._scroller.render(rect) + + def show_event(self): + self._scroller.show_event() diff --git a/selfdrive/ui/sunnypilot/layouts/settings/trips.py b/selfdrive/ui/sunnypilot/layouts/settings/trips.py new file mode 100644 index 0000000000..091bd8f987 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/trips.py @@ -0,0 +1,24 @@ +from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets import Widget +from openpilot.common.params import Params + + +class TripsLayout(Widget): + def __init__(self): + super().__init__() + + self._params = Params() + items = self._initialize_items() + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _initialize_items(self): + items = [ + + ] + return items + + def _render(self, rect): + self._scroller.render(rect) + + def show_event(self): + self._scroller.show_event() diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle.py new file mode 100644 index 0000000000..7d7a6f5fe8 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle.py @@ -0,0 +1,24 @@ +from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets import Widget +from openpilot.common.params import Params + + +class VehicleLayout(Widget): + def __init__(self): + super().__init__() + + self._params = Params() + items = self._initialize_items() + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _initialize_items(self): + items = [ + + ] + return items + + def _render(self, rect): + self._scroller.render(rect) + + def show_event(self): + self._scroller.show_event() diff --git a/selfdrive/ui/sunnypilot/layouts/settings/visuals.py b/selfdrive/ui/sunnypilot/layouts/settings/visuals.py new file mode 100644 index 0000000000..dba68c912f --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/visuals.py @@ -0,0 +1,24 @@ +from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets import Widget +from openpilot.common.params import Params + + +class VisualsLayout(Widget): + def __init__(self): + super().__init__() + + self._params = Params() + items = self._initialize_items() + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _initialize_items(self): + items = [ + + ] + return items + + def _render(self, rect): + self._scroller.render(rect) + + def show_event(self): + self._scroller.show_event() diff --git a/sunnypilot/selfdrive/assets/offroad/icon_firehose.png b/sunnypilot/selfdrive/assets/offroad/icon_firehose.png new file mode 100644 index 0000000000..d579937f3f --- /dev/null +++ b/sunnypilot/selfdrive/assets/offroad/icon_firehose.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a258a022f0ab90368327d899ed4fb85b47dd0e35c93508e35851d9c3184528c0 +size 36382 diff --git a/sunnypilot/selfdrive/assets/offroad/icon_home.png b/sunnypilot/selfdrive/assets/offroad/icon_home.png new file mode 100644 index 0000000000..1229af846d --- /dev/null +++ b/sunnypilot/selfdrive/assets/offroad/icon_home.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8623dbc5c7dd043a91d98777cb423cfd116014ed6390af6d7d00c1f8dea3c6e8 +size 1181 From deda1329a2df3bc5fcab503d000956e37f67f85e Mon Sep 17 00:00:00 2001 From: nayan Date: Sat, 15 Nov 2025 21:36:22 -0500 Subject: [PATCH 07/41] patience, grasshopper --- selfdrive/ui/sunnypilot/layouts/settings/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/settings.py b/selfdrive/ui/sunnypilot/layouts/settings/settings.py index adca958ff8..329608d577 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/settings.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/settings.py @@ -74,7 +74,7 @@ class SettingsLayoutSP(OP.SettingsLayout): OP.PanelType.VISUALS: PanelInfo(tr_noop("Visuals"), VisualsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_visuals.png"), OP.PanelType.DISPLAY: PanelInfo(tr_noop("Display"), DisplayLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_display.png"), OP.PanelType.OSM: PanelInfo(tr_noop("OSM"), OSMLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_map.png"), - OP.PanelType.NAVIGATION: PanelInfo(tr_noop("Navigation"), NavigationLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_map.png"), + #OP.PanelType.NAVIGATION: PanelInfo(tr_noop("Navigation"), NavigationLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_map.png"), OP.PanelType.TRIPS: PanelInfo(tr_noop("Trips"), TripsLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"), OP.PanelType.VEHICLE: PanelInfo(tr_noop("Vehicle"), VehicleLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"), OP.PanelType.FIREHOSE: PanelInfo(tr_noop("Firehose"), FirehoseLayout(), icon="../../sunnypilot/selfdrive/assets/offroad/icon_firehose.png"), From 51d0666c85a801b2731d120dd7c098aae2f4f9ea Mon Sep 17 00:00:00 2001 From: nayan Date: Sat, 15 Nov 2025 22:49:39 -0500 Subject: [PATCH 08/41] more patience, grasshopper --- selfdrive/ui/sunnypilot/layouts/settings/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/settings.py b/selfdrive/ui/sunnypilot/layouts/settings/settings.py index 329608d577..6bf8218bf0 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/settings.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/settings.py @@ -23,7 +23,7 @@ from openpilot.selfdrive.ui.sunnypilot.layouts.settings.steering import Steering from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise import CruiseLayout from openpilot.selfdrive.ui.sunnypilot.layouts.settings.visuals import VisualsLayout from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import DisplayLayout -from openpilot.selfdrive.ui.sunnypilot.layouts.settings.navigation import NavigationLayout +#from openpilot.selfdrive.ui.sunnypilot.layouts.settings.navigation import NavigationLayout OP.PANEL_COLOR = rl.Color(10, 10, 10, 255) ICON_SIZE = 70 From 362e9ce04becc1ef1d16d2f23daa01cb2d5303c4 Mon Sep 17 00:00:00 2001 From: nayan Date: Sun, 16 Nov 2025 09:53:28 -0500 Subject: [PATCH 09/41] sp raylib preview --- selfdrive/ui/tests/test_ui/raylib_screenshots.py | 1 + 1 file changed, 1 insertion(+) diff --git a/selfdrive/ui/tests/test_ui/raylib_screenshots.py b/selfdrive/ui/tests/test_ui/raylib_screenshots.py index f36ad1badb..009c940d1e 100755 --- a/selfdrive/ui/tests/test_ui/raylib_screenshots.py +++ b/selfdrive/ui/tests/test_ui/raylib_screenshots.py @@ -292,6 +292,7 @@ def create_screenshots(): with OpenpilotPrefix(): params = Params() params.put("DongleId", "123456789012345") + params.put_bool("sunnypilot_ui", True) # Set branch name params.put("UpdaterCurrentDescription", VERSION) From e4e10d4b87d0bdf4fe2e766122935e5ee3706a5b Mon Sep 17 00:00:00 2001 From: nayan Date: Sun, 16 Nov 2025 11:15:22 -0500 Subject: [PATCH 10/41] fix callback --- system/ui/sunnypilot/widgets/list_view.py | 9 +++++---- system/ui/sunnypilot/widgets/toggle.py | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/system/ui/sunnypilot/widgets/list_view.py b/system/ui/sunnypilot/widgets/list_view.py index 6020ab71cb..2260a0ba4c 100644 --- a/system/ui/sunnypilot/widgets/list_view.py +++ b/system/ui/sunnypilot/widgets/list_view.py @@ -8,9 +8,10 @@ from openpilot.system.ui.sunnypilot.lib.styles import style class ToggleActionSP(ToggleAction): - def __init__(self, initial_state: bool = False, width: int = style.TOGGLE_WIDTH, enabled: bool | Callable[[], bool] = True, param: str | None = None): - ToggleAction.__init__(self, initial_state, width, enabled) - self.toggle = ToggleSP(initial_state=initial_state, param=param) + def __init__(self, initial_state: bool = False, width: int = style.TOGGLE_WIDTH, enabled: bool | Callable[[], bool] = True, + callback: Callable[[bool], None] | None = None, param: str | None = None): + ToggleAction.__init__(self, initial_state, width, enabled, callback) + self.toggle = ToggleSP(initial_state=initial_state, callback=callback, param=param) class ListItemSP(ListItem): def __init__(self, title: str | Callable[[], str] = "", icon: str | None = None, description: str | Callable[[], str] | None = None, @@ -90,5 +91,5 @@ class ListItemSP(ListItem): def toggle_item_sp(title: str | Callable[[], str], description: str | Callable[[], str] | None = None, initial_state: bool = False, callback: Callable | None = None, icon: str = "", enabled: bool | Callable[[], bool] = True, param: str | None = None) -> ListItem: - action = ToggleActionSP(initial_state=initial_state, enabled=enabled, param=param) + action = ToggleActionSP(initial_state=initial_state, enabled=enabled, callback=callback, param=param) return ListItemSP(title=title, description=description, action_item=action, icon=icon, callback=callback) diff --git a/system/ui/sunnypilot/widgets/toggle.py b/system/ui/sunnypilot/widgets/toggle.py index 7d3847d3f0..84e3f9b4f2 100644 --- a/system/ui/sunnypilot/widgets/toggle.py +++ b/system/ui/sunnypilot/widgets/toggle.py @@ -1,4 +1,5 @@ import pyray as rl +from collections.abc import Callable from openpilot.common.params import Params from openpilot.system.ui.lib.application import MousePos from openpilot.system.ui.widgets.toggle import Toggle @@ -9,12 +10,12 @@ KNOB_RADIUS = style.TOGGLE_BG_HEIGHT / 2 - KNOB_PADDING SYMBOL_SIZE = KNOB_RADIUS / 2 class ToggleSP(Toggle): - def __init__(self, initial_state=False, param: str | None = None): + def __init__(self, initial_state=False, callback: Callable[[bool], None] | None = None, param: str | None = None): self.param_key = param self.params = Params() if self.param_key: initial_state = self.params.get_bool(self.param_key) - Toggle.__init__(self, initial_state) + Toggle.__init__(self, initial_state, callback) def _handle_mouse_release(self, mouse_pos: MousePos): super()._handle_mouse_release(mouse_pos) From 423a7d2ed0249e12743c2b36e1053d5a446f8d33 Mon Sep 17 00:00:00 2001 From: nayan Date: Sun, 16 Nov 2025 11:15:28 -0500 Subject: [PATCH 11/41] fix ui preview --- selfdrive/ui/tests/test_ui/raylib_screenshots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/tests/test_ui/raylib_screenshots.py b/selfdrive/ui/tests/test_ui/raylib_screenshots.py index 009c940d1e..a26c27bcb9 100755 --- a/selfdrive/ui/tests/test_ui/raylib_screenshots.py +++ b/selfdrive/ui/tests/test_ui/raylib_screenshots.py @@ -148,7 +148,7 @@ def setup_experimental_mode_description(click, pm: PubMaster): def setup_openpilot_long_confirmation_dialog(click, pm: PubMaster): setup_settings_developer(click, pm) - click(2000, 960) # toggle openpilot longitudinal control + click(650, 960) # toggle openpilot longitudinal control def setup_onroad(click, pm: PubMaster): From a9e57f0a769f97f985fcf6fa5a468da4ff05d2d1 Mon Sep 17 00:00:00 2001 From: nayan Date: Sun, 16 Nov 2025 12:47:37 -0500 Subject: [PATCH 12/41] add ui previews --- .../ui/tests/test_ui/raylib_screenshots.py | 138 +++++++++++++----- 1 file changed, 99 insertions(+), 39 deletions(-) diff --git a/selfdrive/ui/tests/test_ui/raylib_screenshots.py b/selfdrive/ui/tests/test_ui/raylib_screenshots.py index a26c27bcb9..673d3d4947 100755 --- a/selfdrive/ui/tests/test_ui/raylib_screenshots.py +++ b/selfdrive/ui/tests/test_ui/raylib_screenshots.py @@ -40,47 +40,47 @@ def put_update_params(params: Params): params.put("UpdaterTargetBranch", BRANCH_NAME) -def setup_homescreen(click, pm: PubMaster): +def setup_homescreen(click, pm: PubMaster, scroll=None): pass -def setup_homescreen_update_available(click, pm: PubMaster): +def setup_homescreen_update_available(click, pm: PubMaster, scroll=None): params = Params() params.put_bool("UpdateAvailable", True) put_update_params(params) setup_offroad_alert(click, pm) -def setup_settings(click, pm: PubMaster): +def setup_settings(click, pm: PubMaster, scroll=None): click(100, 100) -def close_settings(click, pm: PubMaster): +def close_settings(click, pm: PubMaster, scroll=None): click(240, 216) -def setup_settings_network(click, pm: PubMaster): +def setup_settings_network(click, pm: PubMaster, scroll=None): setup_settings(click, pm) click(278, 450) -def setup_settings_network_advanced(click, pm: PubMaster): - setup_settings_network(click, pm) +def setup_settings_network_advanced(click, pm: PubMaster, scroll=None): + setup_settings_network(click, pm, scroll=scroll) click(1880, 100) -def setup_settings_toggles(click, pm: PubMaster): - setup_settings(click, pm) - click(278, 600) - - -def setup_settings_software(click, pm: PubMaster): - put_update_params(Params()) +def setup_settings_toggles(click, pm: PubMaster, scroll=None): setup_settings(click, pm) click(278, 720) -def setup_settings_software_download(click, pm: PubMaster): +def setup_settings_software(click, pm: PubMaster, scroll=None): + put_update_params(Params()) + setup_settings(click, pm) + click(278, 845) + + +def setup_settings_software_download(click, pm: PubMaster, scroll=None): params = Params() # setup_settings_software but with "DOWNLOAD" button to test long text params.put("UpdaterState", "idle") @@ -88,13 +88,13 @@ def setup_settings_software_download(click, pm: PubMaster): setup_settings_software(click, pm) -def setup_settings_software_release_notes(click, pm: PubMaster): - setup_settings_software(click, pm) +def setup_settings_software_release_notes(click, pm: PubMaster, scroll=None): + setup_settings_software(click, pm, scroll=scroll) click(588, 110) # expand description for current version -def setup_settings_software_branch_switcher(click, pm: PubMaster): - setup_settings_software(click, pm) +def setup_settings_software_branch_switcher(click, pm: PubMaster, scroll=None): + setup_settings_software(click, pm, scroll=scroll) params = Params() params.put("UpdaterAvailableBranches", f"master,nightly,release,{BRANCH_NAME}") params.put("GitBranch", BRANCH_NAME) # should be on top @@ -102,30 +102,32 @@ def setup_settings_software_branch_switcher(click, pm: PubMaster): click(1984, 449) -def setup_settings_firehose(click, pm: PubMaster): +def setup_settings_firehose(click, pm: PubMaster, scroll=None): setup_settings(click, pm) - click(278, 845) + scroll(-1000, 278, 950) + click(278, 950) -def setup_settings_developer(click, pm: PubMaster): +def setup_settings_developer(click, pm: PubMaster, scroll=None): CP = car.CarParams() CP.alphaLongitudinalAvailable = True # show alpha long control toggle Params().put("CarParamsPersistent", CP.to_bytes()) setup_settings(click, pm) - click(278, 950) + scroll(-1000, 278, 950) + click(278, 1040) -def setup_keyboard(click, pm: PubMaster): - setup_settings_developer(click, pm) +def setup_keyboard(click, pm: PubMaster, scroll=None): + setup_settings_developer(click, pm, scroll=scroll) click(1930, 470) -def setup_pair_device(click, pm: PubMaster): +def setup_pair_device(click, pm: PubMaster, scroll=None): click(1950, 800) -def setup_offroad_alert(click, pm: PubMaster): +def setup_offroad_alert(click, pm: PubMaster, scroll=None): put_update_params(Params()) set_offroad_alert("Offroad_TemperatureTooHigh", True, extra_text='99C') set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text='longitudinal') @@ -136,22 +138,63 @@ def setup_offroad_alert(click, pm: PubMaster): close_settings(click, pm) -def setup_confirmation_dialog(click, pm: PubMaster): +def setup_confirmation_dialog(click, pm: PubMaster, scroll=None): setup_settings(click, pm) click(1985, 791) # reset calibration -def setup_experimental_mode_description(click, pm: PubMaster): +def setup_experimental_mode_description(click, pm: PubMaster, scroll=None): setup_settings_toggles(click, pm) click(1200, 280) # expand description for experimental mode -def setup_openpilot_long_confirmation_dialog(click, pm: PubMaster): - setup_settings_developer(click, pm) +def setup_openpilot_long_confirmation_dialog(click, pm: PubMaster, scroll=None): + setup_settings_developer(click, pm, scroll=scroll) click(650, 960) # toggle openpilot longitudinal control +def setup_settings_sunnylink(click, pm: PubMaster, scroll=None): + setup_settings(click, pm) + click(278, 600) -def setup_onroad(click, pm: PubMaster): +def setup_settings_models(click, pm: PubMaster, scroll=None): + setup_settings(click, pm) + click(278, 950) + +def setup_settings_steering(click, pm: PubMaster, scroll=None): + setup_settings(click, pm) + click(278, 1040) + +def setup_settings_cruise(click, pm: PubMaster, scroll=None): + setup_settings(click, pm) + scroll(-40, 278, 950) + click(278, 1040) + +def setup_settings_visuals(click, pm: PubMaster, scroll=None): + setup_settings(click, pm) + scroll(-1000, 278, 950) + click(278, 330) + +def setup_settings_display(click, pm: PubMaster, scroll=None): + setup_settings(click, pm) + scroll(-1000, 278, 950) + click(278, 450) + +def setup_settings_osm(click, pm: PubMaster, scroll=None): + setup_settings(click, pm) + scroll(-1000, 278, 950) + click(278, 600) + +def setup_settings_trips(click, pm: PubMaster, scroll=None): + setup_settings(click, pm) + scroll(-1000, 278, 950) + click(278, 720) + +def setup_settings_vehicle(click, pm: PubMaster, scroll=None): + setup_settings(click, pm) + scroll(-1000, 278, 950) + click(278, 845) + +def setup_onroad(click, pm: PubMaster, scroll=None): ds = messaging.new_message('deviceState') ds.deviceState.started = True @@ -172,7 +215,7 @@ def setup_onroad(click, pm: PubMaster): time.sleep(0.05) -def setup_onroad_sidebar(click, pm: PubMaster): +def setup_onroad_sidebar(click, pm: PubMaster, scroll=None): setup_onroad(click, pm) click(100, 100) # open sidebar @@ -191,23 +234,23 @@ def setup_onroad_alert(click, pm: PubMaster, size: log.SelfdriveState.AlertSize, time.sleep(0.05) -def setup_onroad_small_alert(click, pm: PubMaster): +def setup_onroad_small_alert(click, pm: PubMaster, scroll=None): setup_onroad_alert(click, pm, AlertSize.small, "Small Alert", "This is a small alert", AlertStatus.normal) -def setup_onroad_medium_alert(click, pm: PubMaster): +def setup_onroad_medium_alert(click, pm: PubMaster, scroll=None): setup_onroad_alert(click, pm, AlertSize.mid, "Medium Alert", "This is a medium alert", AlertStatus.userPrompt) -def setup_onroad_full_alert(click, pm: PubMaster): +def setup_onroad_full_alert(click, pm: PubMaster, scroll=None): setup_onroad_alert(click, pm, AlertSize.full, "DISENGAGE IMMEDIATELY", "Driver Distracted", AlertStatus.critical) -def setup_onroad_full_alert_multiline(click, pm: PubMaster): +def setup_onroad_full_alert_multiline(click, pm: PubMaster, scroll=None): setup_onroad_alert(click, pm, AlertSize.full, "Reverse\nGear", "", AlertStatus.normal) -def setup_onroad_full_alert_long_text(click, pm: PubMaster): +def setup_onroad_full_alert_long_text(click, pm: PubMaster, scroll=None): setup_onroad_alert(click, pm, AlertSize.full, "TAKE CONTROL IMMEDIATELY", "Calibration Invalid: Remount Device & Recalibrate", AlertStatus.userPrompt) @@ -242,6 +285,19 @@ CASES = { "onroad_full_alert_long_text": setup_onroad_full_alert_long_text, } +# sunnypilot cases +CASES.update({ + "settings_sunnylink": setup_settings_sunnylink, + "settings_models": setup_settings_models, + "settings_steering": setup_settings_steering, + "settings_cruise": setup_settings_cruise, + "settings_visuals": setup_settings_visuals, + "settings_display": setup_settings_display, + "settings_osm": setup_settings_osm, + "settings_trips": setup_settings_trips, + "settings_vehicle": setup_settings_vehicle, +}) + class TestUI: def __init__(self): @@ -274,11 +330,15 @@ class TestUI: time.sleep(0.01) pyautogui.mouseUp(self.ui.left + x, self.ui.top + y, *args, **kwargs) + def scroll(self, clicks, x, y, *args, **kwargs): + pyautogui.scroll(clicks, self.ui.left + x, self.ui.top + y, *args, **kwargs) + time.sleep(UI_DELAY) + @with_processes(["ui"]) def test_ui(self, name, setup_case): self.setup() time.sleep(UI_DELAY) # wait for UI to start - setup_case(self.click, self.pm) + setup_case(self.click, self.pm, self.scroll) self.screenshot(name) From 5369b6880fa3cbaaeb34ade916b98208fbf3e148 Mon Sep 17 00:00:00 2001 From: nayan Date: Sat, 15 Nov 2025 23:46:49 -0500 Subject: [PATCH 13/41] Option Control --- system/ui/sunnypilot/lib/styles.py | 9 + system/ui/sunnypilot/widgets/list_view.py | 34 +++ .../ui/sunnypilot/widgets/option_control.py | 228 ++++++++++++++++++ 3 files changed, 271 insertions(+) create mode 100644 system/ui/sunnypilot/widgets/option_control.py diff --git a/system/ui/sunnypilot/lib/styles.py b/system/ui/sunnypilot/lib/styles.py index 28d50f48d7..2438185c97 100644 --- a/system/ui/sunnypilot/lib/styles.py +++ b/system/ui/sunnypilot/lib/styles.py @@ -37,4 +37,13 @@ class SP_Default(Base): TOGGLE_DISABLED_OFF_COLOR = DISABLED_OFF_BG_COLOR TOGGLE_DISABLED_KNOB_COLOR = rl.Color(88, 88, 88, 255) # Lighter Grey + # Option Control + OPTIONCONTROL_CONTAINER_BG = OFF_BG_COLOR + OPTIONCONTROL_BTN_ENABLED = rl.Color(88, 88, 88, 255) + OPTIONCONTROL_BTN_PRESSED = ON_BG_COLOR + OPTIONCONTROL_BTN_DISABLED = DISABLED_OFF_BG_COLOR + OPTIONCONTROL_TEXT_ENABLED = rl.WHITE + OPTIONCONTROL_TEXT_PRESSED = rl.WHITE + OPTIONCONTROL_TEXT_DISABLED = ITEM_DISABLED_TEXT_COLOR + style = SP_Default diff --git a/system/ui/sunnypilot/widgets/list_view.py b/system/ui/sunnypilot/widgets/list_view.py index 2260a0ba4c..93576893e9 100644 --- a/system/ui/sunnypilot/widgets/list_view.py +++ b/system/ui/sunnypilot/widgets/list_view.py @@ -4,6 +4,7 @@ from collections.abc import Callable from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.sunnypilot.widgets.toggle import ToggleSP from openpilot.system.ui.widgets.list_view import ListItem, ToggleAction, ItemAction +from openpilot.system.ui.sunnypilot.widgets.option_control import OptionControlSP from openpilot.system.ui.sunnypilot.lib.styles import style @@ -13,6 +14,27 @@ class ToggleActionSP(ToggleAction): ToggleAction.__init__(self, initial_state, width, enabled, callback) self.toggle = ToggleSP(initial_state=initial_state, callback=callback, param=param) +class OptionControlActionSP(ItemAction): + def __init__(self, param: str, min_value: int, max_value: int, + value_change_step: int = 1, enabled: bool | Callable[[], bool] = True, + on_value_changed: Callable[[int], None] | None = None, + value_map: dict[int, tuple[int, str]] | None = None, + label_width: int = style.BUTTON_WIDTH, + use_float_scaling: bool = False, + label_callback: Callable[[int], str] | None = None): + # Initialize with zero width - the component will size itself + super().__init__() + + # Create the option control + self.option_control = OptionControlSP( + param, min_value, max_value, value_change_step, + enabled, on_value_changed, value_map, label_width, use_float_scaling, + label_callback + ) + + def _render(self, rect: rl.Rectangle): + self.option_control.render(rect) + class ListItemSP(ListItem): def __init__(self, title: str | Callable[[], str] = "", icon: str | None = None, description: str | Callable[[], str] | None = None, description_visible: bool = False, callback: Callable | None = None, @@ -93,3 +115,15 @@ def toggle_item_sp(title: str | Callable[[], str], description: str | Callable[[ callback: Callable | None = None, icon: str = "", enabled: bool | Callable[[], bool] = True, param: str | None = None) -> ListItem: action = ToggleActionSP(initial_state=initial_state, enabled=enabled, callback=callback, param=param) return ListItemSP(title=title, description=description, action_item=action, icon=icon, callback=callback) + +def option_item_sp(title: str, param: str, + min_value: int, max_value: int, description: str | Callable[[], str] | None = None, + value_change_step: int = 1, on_value_changed: Callable[[int], None] | None = None, + enabled: bool | Callable[[], bool] = True, + icon: str = "", label_width: int = style.BUTTON_WIDTH, value_map: dict[int, tuple[int, str]] | None = None, + use_float_scaling: bool = False, label_callback: Callable[[int], str] | None = None) -> ListItem: + action = OptionControlActionSP( + param, min_value, max_value, value_change_step, + enabled, on_value_changed, value_map, label_width, use_float_scaling, label_callback + ) + return ListItemSP(title=title, description=description, action_item=action, icon=icon) diff --git a/system/ui/sunnypilot/widgets/option_control.py b/system/ui/sunnypilot/widgets/option_control.py new file mode 100644 index 0000000000..2f485e8113 --- /dev/null +++ b/system/ui/sunnypilot/widgets/option_control.py @@ -0,0 +1,228 @@ +import pyray as rl +from collections.abc import Callable +from openpilot.common.params import Params +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.sunnypilot.lib.styles import style + +# Dimensions and styling constants +BUTTON_WIDTH = 80 +BUTTON_HEIGHT = 80 +LABEL_WIDTH = 200 +BUTTON_SPACING = 25 +VALUE_FONT_SIZE = 50 +BUTTON_FONT_SIZE = 60 +BUTTON_CORNER_RADIUS = 20 +CONTAINER_PADDING = 20 +INNER_PADDING = 10 +TOP_PADDING = 25 + +class OptionControlSP(Widget): + def __init__(self, param: str, min_value: int, max_value: int, + value_change_step: int = 1, enabled: bool | Callable[[], bool] = True, + on_value_changed: Callable[[int], None] | None = None, + value_map: dict[int, tuple[int, str]] | None = None, + label_width: int = LABEL_WIDTH, + use_float_scaling: bool = False, label_callback: Callable[[int], str] | None = None): + + super().__init__() + self.params = Params() + self.param_key = param + self.min_value = min_value + self.max_value = max_value + self.value_change_step = value_change_step + self._enabled = enabled + self._minus_enabled = enabled + self._plus_enabled = enabled + self.on_value_changed = on_value_changed + self.value_map = value_map + self.label_width = label_width + self.use_float_scaling = use_float_scaling + self.current_value = min_value + self.label_callback = label_callback + if self.value_map: + for key in self.value_map: + if self.value_map[key][0] == self.params.get(self.param_key, return_default = True): + self.current_value = int(key) + break + else: + self.current_value = int(self.params.get(self.param_key, return_default = True)) + + # Initialize font and button styles + self._font = gui_app.font(FontWeight.MEDIUM) + + # Layout rectangles for components + self.minus_btn_rect = rl.Rectangle(0, 0, BUTTON_WIDTH, BUTTON_HEIGHT) + self.label_rect = rl.Rectangle(0, 0, self.label_width, BUTTON_HEIGHT) + self.plus_btn_rect = rl.Rectangle(0, 0, BUTTON_WIDTH, BUTTON_HEIGHT) + self.container_rect = rl.Rectangle(0, 0, 0, 0) + + def set_enabled(self, enabled: bool | Callable[[], bool]): + """Set whether the control is enabled""" + self._enabled = enabled + + def get_value(self) -> int: + """Get the current value of the control""" + return self.current_value + + def set_value(self, value: int): + """Set the control to a specific value""" + if self.min_value <= value <= self.max_value: + self.current_value = value + if self.value_map: + self.params.put(self.param_key, self.value_map[value][0]) + else: + self.params.put(self.param_key, value) + if self.on_value_changed: + self.on_value_changed(value) + + def _update_layout_rects(self): + """Update the layout rectangles when the widget rect changes""" + # Calculate total control width + control_width = (BUTTON_WIDTH * 2) + self.label_width + (BUTTON_SPACING * 2) + total_width = control_width + (CONTAINER_PADDING * 2) + + # Update the widget's width to match the control + self._rect.width = total_width + + # Position the control in the parent rectangle + start_x = self._rect.x + self._rect.width - control_width - (CONTAINER_PADDING * 2) + + # Set container rectangle + self.container_rect = rl.Rectangle( + start_x, + self._rect.y + TOP_PADDING, + total_width, + BUTTON_HEIGHT + (CONTAINER_PADDING * 2) + ) + + # Set component rectangles + component_y = self._rect.y + TOP_PADDING + CONTAINER_PADDING + start_x = self.container_rect.x + CONTAINER_PADDING + + # Minus button + self.minus_btn_rect = rl.Rectangle( + start_x, + component_y, + BUTTON_WIDTH, + BUTTON_HEIGHT + ) + + # Value label + label_x = start_x + BUTTON_WIDTH + BUTTON_SPACING + self.label_rect = rl.Rectangle( + label_x, + component_y, + self.label_width, + BUTTON_HEIGHT + ) + + # Plus button + plus_x = label_x + self.label_width + BUTTON_SPACING + self.plus_btn_rect = rl.Rectangle( + plus_x, + component_y, + BUTTON_WIDTH, + BUTTON_HEIGHT + ) + + def is_enabled(self) -> bool: + """Check if the control is enabled""" + return self._enabled() if callable(self._enabled) else self._enabled + + def get_displayed_value(self) -> str: + """Get the displayed value, handling value mapping if present""" + value = self.current_value + + if callable(self.label_callback): + return self.label_callback(value) + + if self.value_map: + # Use the value map to get the display string + if value in self.value_map: + return self.value_map[value][1] # Return the display string + + # If using float scaling, format as float + if self.use_float_scaling: + return f"{value / 100.0:.2f}" + + return str(value) + + def _render(self, rect: rl.Rectangle): + """Render the widget and handle input""" + if self._rect.width == 0 or self._rect.height == 0: + return False + + # Ensure layout rectangles are updated + if self.container_rect.width == 0: + self._update_layout_rects() + + # Get enabled state + enabled = self.is_enabled() + + # Draw container background + rl.draw_rectangle_rounded(self.container_rect, 1, BUTTON_CORNER_RADIUS, style.OPTIONCONTROL_CONTAINER_BG) + + # Determine button states + self._minus_enabled = enabled and self.current_value > self.min_value + self._plus_enabled = enabled and self.current_value < self.max_value + + # Render buttons and label + self._render_button(self.minus_btn_rect, "-", self._minus_enabled) + self._render_value_label() + self._render_button(self.plus_btn_rect, "+", self._plus_enabled) + + def _render_button(self, rect: rl.Rectangle, text: str, enabled: bool): + """Render a button and return True if it was clicked""" + mouse_pos = rl.get_mouse_position() + is_hovered = rl.check_collision_point_rec(mouse_pos, rect) and self._touch_valid() + is_pressed = is_hovered and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) + + # Determine button colors based on state + if not enabled: + bg_color = style.OPTIONCONTROL_BTN_DISABLED + text_color = style.OPTIONCONTROL_TEXT_DISABLED + elif is_pressed: + bg_color = style.OPTIONCONTROL_BTN_PRESSED + text_color = style.OPTIONCONTROL_TEXT_PRESSED + else: + bg_color = style.OPTIONCONTROL_BTN_ENABLED + text_color = style.OPTIONCONTROL_TEXT_ENABLED + + # Draw button background + rl.draw_rectangle_rounded(rect, 1, BUTTON_CORNER_RADIUS, bg_color) + + # Draw button text + text_size = measure_text_cached(self._font, text, BUTTON_FONT_SIZE) + text_x = rect.x + (rect.width - text_size.x) / 2 + text_y = rect.y + (rect.height - text_size.y) / 2 + rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), BUTTON_FONT_SIZE, 0, text_color) + + def _render_value_label(self): + """Render the current value label""" + text = self.get_displayed_value() + text_color = style.OPTIONCONTROL_TEXT_ENABLED if self.is_enabled() else style.OPTIONCONTROL_TEXT_DISABLED + + # Calculate text position centered in the label area + text_size = measure_text_cached(self._font, text, VALUE_FONT_SIZE) + text_x = self.label_rect.x + (self.label_rect.width - text_size.x) / 2 + text_y = self.label_rect.y + (self.label_rect.height - text_size.y) / 2 + + # Draw the text + rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), VALUE_FONT_SIZE, 0, text_color) + + def _handle_mouse_release(self, mouse_pos: MousePos): + value_changed = False + if self._minus_enabled and rl.check_collision_point_rec(mouse_pos, self.minus_btn_rect): + self.current_value -= self.value_change_step + self.current_value = max(self.min_value, self.current_value) + value_changed = True + elif self._plus_enabled and rl.check_collision_point_rec(mouse_pos, self.plus_btn_rect): + self.current_value += self.value_change_step + self.current_value = min(self.max_value, self.current_value) + value_changed = True + + # Call the value changed callback + if value_changed: + self.set_value(self.current_value) From 98c479830c9a329c89b598f430a67a91a76dd411 Mon Sep 17 00:00:00 2001 From: nayan Date: Sun, 16 Nov 2025 14:00:37 -0500 Subject: [PATCH 14/41] Need this --- system/ui/sunnypilot/lib/styles.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/system/ui/sunnypilot/lib/styles.py b/system/ui/sunnypilot/lib/styles.py index 2438185c97..6fcaf55506 100644 --- a/system/ui/sunnypilot/lib/styles.py +++ b/system/ui/sunnypilot/lib/styles.py @@ -37,6 +37,10 @@ class SP_Default(Base): TOGGLE_DISABLED_OFF_COLOR = DISABLED_OFF_BG_COLOR TOGGLE_DISABLED_KNOB_COLOR = rl.Color(88, 88, 88, 255) # Lighter Grey + # Button Control + BUTTON_WIDTH = 250 + BUTTON_HEIGHT = 100 + # Option Control OPTIONCONTROL_CONTAINER_BG = OFF_BG_COLOR OPTIONCONTROL_BTN_ENABLED = rl.Color(88, 88, 88, 255) From 21beea51ecc86522433290b70975d0138f6fcfa0 Mon Sep 17 00:00:00 2001 From: nayan Date: Tue, 18 Nov 2025 16:26:48 -0500 Subject: [PATCH 15/41] introducing ui_state_sp for py --- selfdrive/ui/sunnypilot/ui_state.py | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 selfdrive/ui/sunnypilot/ui_state.py diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py new file mode 100644 index 0000000000..718e5f706c --- /dev/null +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -0,0 +1,32 @@ +from openpilot.selfdrive.ui.ui_state import UIState +from cereal import messaging +from openpilot.common.params import Params + + +class UIStateSP(UIState): + _instance: 'UIStateSP | None' = None + + def _initialize(self): + UIState._initialize(self) + self.params = Params() + self.sm = messaging.SubMaster( + [ + "modelV2", "controlsState", "liveCalibration", "deviceState", "carParams", + "driverMonitoringState", "carState", "roadCameraState", "managerState", + "selfdriveState", "longitudinalPlan", "rawAudioData", + "modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP", + "carControl", "gpsLocationExternal", "gpsLocation", "liveTorqueParameters", + "carStateSP", "liveParameters", "liveMapDataSP", "carParamsSP" + ]) + + def update(self) -> None: + UIState.update(self) + + def _update_status(self) -> None: + UIState._update_status(self) + + def update_params(self) -> None: + UIState.update_params(self) + +# Global instance +ui_state_sp = UIStateSP() From 01aa6c4204cf270d8d23ffde607ee3aa82e61a4a Mon Sep 17 00:00:00 2001 From: nayan Date: Sat, 15 Nov 2025 09:28:43 -0500 Subject: [PATCH 16/41] param to control stock vs sp ui --- common/params_keys.h | 1 + 1 file changed, 1 insertion(+) diff --git a/common/params_keys.h b/common/params_keys.h index 38578d7ddc..fe08062987 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -173,6 +173,7 @@ inline static std::unordered_map keys = { {"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}}, {"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"sunnypilot_ui", {PERSISTENT, BOOL, "1"}}, {"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}}, // MADS params From 48202652688a8d72b198973e16ab33780bfafe5b Mon Sep 17 00:00:00 2001 From: nayan Date: Tue, 18 Nov 2025 19:09:46 -0500 Subject: [PATCH 17/41] better --- selfdrive/ui/sunnypilot/ui_state.py | 16 +++++++--------- selfdrive/ui/ui.py | 3 +++ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index 718e5f706c..1c350a0e6f 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -9,15 +9,13 @@ class UIStateSP(UIState): def _initialize(self): UIState._initialize(self) self.params = Params() - self.sm = messaging.SubMaster( - [ - "modelV2", "controlsState", "liveCalibration", "deviceState", "carParams", - "driverMonitoringState", "carState", "roadCameraState", "managerState", - "selfdriveState", "longitudinalPlan", "rawAudioData", - "modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP", - "carControl", "gpsLocationExternal", "gpsLocation", "liveTorqueParameters", - "carStateSP", "liveParameters", "liveMapDataSP", "carParamsSP" - ]) + op_services = self.sm.services + sp_services = [ + "modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP", + "carControl", "gpsLocationExternal", "gpsLocation", "liveTorqueParameters", + "carStateSP", "liveParameters", "liveMapDataSP", "carParamsSP" + ] + self.sm = messaging.SubMaster(op_services + sp_services) def update(self) -> None: UIState.update(self) diff --git a/selfdrive/ui/ui.py b/selfdrive/ui/ui.py index a4b99825e9..e68442aba1 100755 --- a/selfdrive/ui/ui.py +++ b/selfdrive/ui/ui.py @@ -8,6 +8,9 @@ from openpilot.system.ui.lib.application import gui_app from openpilot.selfdrive.ui.layouts.main import MainLayout from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.common.params import Params +if Params().get_bool("sunnypilot_ui"): + from openpilot.selfdrive.ui.sunnypilot.ui_state import ui_state_sp as ui_state def main(): cores = {5, } From 4da32cc0097434aab0aa6a3c35465eabb23c8958 Mon Sep 17 00:00:00 2001 From: nayan Date: Wed, 19 Nov 2025 23:03:56 -0500 Subject: [PATCH 18/41] add ui_update callback --- selfdrive/ui/sunnypilot/ui_state.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index 1c350a0e6f..3f7e87ab9a 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -1,3 +1,5 @@ +from collections.abc import Callable + from openpilot.selfdrive.ui.ui_state import UIState from cereal import messaging from openpilot.common.params import Params @@ -17,8 +19,16 @@ class UIStateSP(UIState): ] self.sm = messaging.SubMaster(op_services + sp_services) + # Callbacks + self._ui_update_callbacks: list[Callable[[], None]] = [] + + def add_ui_update_callback(self, callback: Callable[[], None]): + self._ui_update_callbacks.append(callback) + def update(self) -> None: UIState.update(self) + for callback in self._ui_update_callbacks: + callback() def _update_status(self) -> None: UIState._update_status(self) From eb94abaa14a355d65a281b7225180c144677d5fd Mon Sep 17 00:00:00 2001 From: nayan Date: Wed, 19 Nov 2025 23:44:05 -0500 Subject: [PATCH 19/41] better padding --- system/ui/sunnypilot/lib/styles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/ui/sunnypilot/lib/styles.py b/system/ui/sunnypilot/lib/styles.py index 28d50f48d7..2c694a1f38 100644 --- a/system/ui/sunnypilot/lib/styles.py +++ b/system/ui/sunnypilot/lib/styles.py @@ -5,7 +5,7 @@ from dataclasses import dataclass class Base: # Widget/Control Base Dimensions ITEM_BASE_HEIGHT = 170 - ITEM_PADDING = 20 + ITEM_PADDING = 30 ITEM_TEXT_FONT_SIZE = 50 ITEM_DESC_FONT_SIZE = 40 ITEM_DESC_V_OFFSET = 150 From 1e0564b4844acafbb733bfb7ae55b4ed0c93dd07 Mon Sep 17 00:00:00 2001 From: nayan Date: Thu, 20 Nov 2025 08:05:20 -0500 Subject: [PATCH 20/41] this --- system/ui/sunnypilot/lib/styles.py | 2 +- system/ui/sunnypilot/widgets/list_view.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/system/ui/sunnypilot/lib/styles.py b/system/ui/sunnypilot/lib/styles.py index 2c694a1f38..28d50f48d7 100644 --- a/system/ui/sunnypilot/lib/styles.py +++ b/system/ui/sunnypilot/lib/styles.py @@ -5,7 +5,7 @@ from dataclasses import dataclass class Base: # Widget/Control Base Dimensions ITEM_BASE_HEIGHT = 170 - ITEM_PADDING = 30 + ITEM_PADDING = 20 ITEM_TEXT_FONT_SIZE = 50 ITEM_DESC_FONT_SIZE = 40 ITEM_DESC_V_OFFSET = 150 diff --git a/system/ui/sunnypilot/widgets/list_view.py b/system/ui/sunnypilot/widgets/list_view.py index 2260a0ba4c..7e118f7506 100644 --- a/system/ui/sunnypilot/widgets/list_view.py +++ b/system/ui/sunnypilot/widgets/list_view.py @@ -48,7 +48,7 @@ class ListItemSP(ListItem): style.TOGGLE_WIDTH, style.TOGGLE_HEIGHT ) - text_x = left_rect.x + left_rect.width + style.ITEM_PADDING + text_x = left_rect.x + left_rect.width + style.ITEM_PADDING * 1.5 # Draw title if self.title: From 5b03369a8f1f3450a82097da369302bdddf10fc9 Mon Sep 17 00:00:00 2001 From: nayan Date: Thu, 20 Nov 2025 17:56:26 -0500 Subject: [PATCH 21/41] listitem -> listitemsp --- system/ui/sunnypilot/widgets/list_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/ui/sunnypilot/widgets/list_view.py b/system/ui/sunnypilot/widgets/list_view.py index 7e118f7506..219118298e 100644 --- a/system/ui/sunnypilot/widgets/list_view.py +++ b/system/ui/sunnypilot/widgets/list_view.py @@ -90,6 +90,6 @@ class ListItemSP(ListItem): self._html_renderer.render(description_rect) def toggle_item_sp(title: str | Callable[[], str], description: str | Callable[[], str] | None = None, initial_state: bool = False, - callback: Callable | None = None, icon: str = "", enabled: bool | Callable[[], bool] = True, param: str | None = None) -> ListItem: + callback: Callable | None = None, icon: str = "", enabled: bool | Callable[[], bool] = True, param: str | None = None) -> ListItemSP: action = ToggleActionSP(initial_state=initial_state, enabled=enabled, callback=callback, param=param) return ListItemSP(title=title, description=description, action_item=action, icon=icon, callback=callback) From ffa78eabaa7e8489075425b7f4fc6a9492f2e65b Mon Sep 17 00:00:00 2001 From: nayan Date: Thu, 20 Nov 2025 17:58:19 -0500 Subject: [PATCH 22/41] Revert "add ui_update callback" This reverts commit 4da32cc0097434aab0aa6a3c35465eabb23c8958. --- selfdrive/ui/sunnypilot/ui_state.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index 3f7e87ab9a..1c350a0e6f 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -1,5 +1,3 @@ -from collections.abc import Callable - from openpilot.selfdrive.ui.ui_state import UIState from cereal import messaging from openpilot.common.params import Params @@ -19,16 +17,8 @@ class UIStateSP(UIState): ] self.sm = messaging.SubMaster(op_services + sp_services) - # Callbacks - self._ui_update_callbacks: list[Callable[[], None]] = [] - - def add_ui_update_callback(self, callback: Callable[[], None]): - self._ui_update_callbacks.append(callback) - def update(self) -> None: UIState.update(self) - for callback in self._ui_update_callbacks: - callback() def _update_status(self) -> None: UIState._update_status(self) From 7032e4a9727783136c14c7c38ea11092e25a19be Mon Sep 17 00:00:00 2001 From: nayan Date: Thu, 20 Nov 2025 18:00:44 -0500 Subject: [PATCH 23/41] add show_description method --- system/ui/sunnypilot/widgets/list_view.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/ui/sunnypilot/widgets/list_view.py b/system/ui/sunnypilot/widgets/list_view.py index 219118298e..86dbde2797 100644 --- a/system/ui/sunnypilot/widgets/list_view.py +++ b/system/ui/sunnypilot/widgets/list_view.py @@ -19,6 +19,9 @@ class ListItemSP(ListItem): action_item: ItemAction | None = None): ListItem.__init__(self, title, icon, description, description_visible, callback, action_item) + def show_description(self, show: bool): + self._set_description_visible(show) + def get_right_item_rect(self, item_rect: rl.Rectangle) -> rl.Rectangle: if not self.action_item: return rl.Rectangle(0, 0, 0, 0) From e6f5aae2464cea37804eeef87c0aa7793bc6fa89 Mon Sep 17 00:00:00 2001 From: nayan Date: Thu, 20 Nov 2025 18:05:12 -0500 Subject: [PATCH 24/41] =?UTF-8?q?remove=20padding=20from=20line=20separato?= =?UTF-8?q?r.=20like,=20WHY=3F=20=F0=9F=98=A9=F0=9F=98=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- system/ui/widgets/scroller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index a843010d56..eec75c8592 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -17,8 +17,8 @@ class LineSeparator(Widget): self._rect.width = parent_rect.width def _render(self, _): - rl.draw_line(int(self._rect.x) + LINE_PADDING, int(self._rect.y), - int(self._rect.x + self._rect.width) - LINE_PADDING, int(self._rect.y), + rl.draw_line(int(self._rect.x), int(self._rect.y), + int(self._rect.x + self._rect.width), int(self._rect.y), LINE_COLOR) From 942ccb90dd167086a00fea5122b380f50a0e516a Mon Sep 17 00:00:00 2001 From: nayan Date: Thu, 20 Nov 2025 22:08:06 -0500 Subject: [PATCH 25/41] simplify --- system/ui/sunnypilot/widgets/list_view.py | 29 +---- .../ui/sunnypilot/widgets/option_control.py | 119 ++++++------------ 2 files changed, 39 insertions(+), 109 deletions(-) diff --git a/system/ui/sunnypilot/widgets/list_view.py b/system/ui/sunnypilot/widgets/list_view.py index 93576893e9..6f6954c838 100644 --- a/system/ui/sunnypilot/widgets/list_view.py +++ b/system/ui/sunnypilot/widgets/list_view.py @@ -14,27 +14,6 @@ class ToggleActionSP(ToggleAction): ToggleAction.__init__(self, initial_state, width, enabled, callback) self.toggle = ToggleSP(initial_state=initial_state, callback=callback, param=param) -class OptionControlActionSP(ItemAction): - def __init__(self, param: str, min_value: int, max_value: int, - value_change_step: int = 1, enabled: bool | Callable[[], bool] = True, - on_value_changed: Callable[[int], None] | None = None, - value_map: dict[int, tuple[int, str]] | None = None, - label_width: int = style.BUTTON_WIDTH, - use_float_scaling: bool = False, - label_callback: Callable[[int], str] | None = None): - # Initialize with zero width - the component will size itself - super().__init__() - - # Create the option control - self.option_control = OptionControlSP( - param, min_value, max_value, value_change_step, - enabled, on_value_changed, value_map, label_width, use_float_scaling, - label_callback - ) - - def _render(self, rect: rl.Rectangle): - self.option_control.render(rect) - class ListItemSP(ListItem): def __init__(self, title: str | Callable[[], str] = "", icon: str | None = None, description: str | Callable[[], str] | None = None, description_visible: bool = False, callback: Callable | None = None, @@ -116,13 +95,13 @@ def toggle_item_sp(title: str | Callable[[], str], description: str | Callable[[ action = ToggleActionSP(initial_state=initial_state, enabled=enabled, callback=callback, param=param) return ListItemSP(title=title, description=description, action_item=action, icon=icon, callback=callback) -def option_item_sp(title: str, param: str, +def option_item_sp(title: str | Callable[[], str], param: str, min_value: int, max_value: int, description: str | Callable[[], str] | None = None, value_change_step: int = 1, on_value_changed: Callable[[int], None] | None = None, enabled: bool | Callable[[], bool] = True, - icon: str = "", label_width: int = style.BUTTON_WIDTH, value_map: dict[int, tuple[int, str]] | None = None, - use_float_scaling: bool = False, label_callback: Callable[[int], str] | None = None) -> ListItem: - action = OptionControlActionSP( + icon: str = "", label_width: int = style.BUTTON_WIDTH, value_map: dict[int, int] | None = None, + use_float_scaling: bool = False, label_callback: Callable[[int], str] | None = None) -> ListItemSP: + action = OptionControlSP( param, min_value, max_value, value_change_step, enabled, on_value_changed, value_map, label_width, use_float_scaling, label_callback ) diff --git a/system/ui/sunnypilot/widgets/option_control.py b/system/ui/sunnypilot/widgets/option_control.py index 2f485e8113..fe13d9b4e3 100644 --- a/system/ui/sunnypilot/widgets/option_control.py +++ b/system/ui/sunnypilot/widgets/option_control.py @@ -1,10 +1,10 @@ import pyray as rl from collections.abc import Callable from openpilot.common.params import Params -from openpilot.system.ui.widgets import Widget from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.sunnypilot.lib.styles import style +from openpilot.system.ui.widgets.list_view import ItemAction # Dimensions and styling constants BUTTON_WIDTH = 80 @@ -18,21 +18,20 @@ CONTAINER_PADDING = 20 INNER_PADDING = 10 TOP_PADDING = 25 -class OptionControlSP(Widget): +class OptionControlSP(ItemAction): def __init__(self, param: str, min_value: int, max_value: int, value_change_step: int = 1, enabled: bool | Callable[[], bool] = True, on_value_changed: Callable[[int], None] | None = None, - value_map: dict[int, tuple[int, str]] | None = None, + value_map: dict[int, int] | None = None, label_width: int = LABEL_WIDTH, use_float_scaling: bool = False, label_callback: Callable[[int], str] | None = None): - super().__init__() + super().__init__(enabled=enabled) self.params = Params() self.param_key = param self.min_value = min_value self.max_value = max_value self.value_change_step = value_change_step - self._enabled = enabled self._minus_enabled = enabled self._plus_enabled = enabled self.on_value_changed = on_value_changed @@ -43,7 +42,7 @@ class OptionControlSP(Widget): self.label_callback = label_callback if self.value_map: for key in self.value_map: - if self.value_map[key][0] == self.params.get(self.param_key, return_default = True): + if self.value_map[key] == self.params.get(self.param_key, return_default = True): self.current_value = int(key) break else: @@ -58,10 +57,6 @@ class OptionControlSP(Widget): self.plus_btn_rect = rl.Rectangle(0, 0, BUTTON_WIDTH, BUTTON_HEIGHT) self.container_rect = rl.Rectangle(0, 0, 0, 0) - def set_enabled(self, enabled: bool | Callable[[], bool]): - """Set whether the control is enabled""" - self._enabled = enabled - def get_value(self) -> int: """Get the current value of the control""" return self.current_value @@ -71,66 +66,12 @@ class OptionControlSP(Widget): if self.min_value <= value <= self.max_value: self.current_value = value if self.value_map: - self.params.put(self.param_key, self.value_map[value][0]) + self.params.put(self.param_key, self.value_map[value]) else: self.params.put(self.param_key, value) if self.on_value_changed: self.on_value_changed(value) - def _update_layout_rects(self): - """Update the layout rectangles when the widget rect changes""" - # Calculate total control width - control_width = (BUTTON_WIDTH * 2) + self.label_width + (BUTTON_SPACING * 2) - total_width = control_width + (CONTAINER_PADDING * 2) - - # Update the widget's width to match the control - self._rect.width = total_width - - # Position the control in the parent rectangle - start_x = self._rect.x + self._rect.width - control_width - (CONTAINER_PADDING * 2) - - # Set container rectangle - self.container_rect = rl.Rectangle( - start_x, - self._rect.y + TOP_PADDING, - total_width, - BUTTON_HEIGHT + (CONTAINER_PADDING * 2) - ) - - # Set component rectangles - component_y = self._rect.y + TOP_PADDING + CONTAINER_PADDING - start_x = self.container_rect.x + CONTAINER_PADDING - - # Minus button - self.minus_btn_rect = rl.Rectangle( - start_x, - component_y, - BUTTON_WIDTH, - BUTTON_HEIGHT - ) - - # Value label - label_x = start_x + BUTTON_WIDTH + BUTTON_SPACING - self.label_rect = rl.Rectangle( - label_x, - component_y, - self.label_width, - BUTTON_HEIGHT - ) - - # Plus button - plus_x = label_x + self.label_width + BUTTON_SPACING - self.plus_btn_rect = rl.Rectangle( - plus_x, - component_y, - BUTTON_WIDTH, - BUTTON_HEIGHT - ) - - def is_enabled(self) -> bool: - """Check if the control is enabled""" - return self._enabled() if callable(self._enabled) else self._enabled - def get_displayed_value(self) -> str: """Get the displayed value, handling value mapping if present""" value = self.current_value @@ -141,7 +82,7 @@ class OptionControlSP(Widget): if self.value_map: # Use the value map to get the display string if value in self.value_map: - return self.value_map[value][1] # Return the display string + return str(self.value_map[value]) # Return the display string # If using float scaling, format as float if self.use_float_scaling: @@ -150,31 +91,43 @@ class OptionControlSP(Widget): return str(value) def _render(self, rect: rl.Rectangle): - """Render the widget and handle input""" - if self._rect.width == 0 or self._rect.height == 0: - return False + if self._rect.width == 0 or self._rect.height == 0 or not self.is_visible: + return - # Ensure layout rectangles are updated - if self.container_rect.width == 0: - self._update_layout_rects() + # Calculate total control width + control_width = (BUTTON_WIDTH * 2) + self.label_width + (BUTTON_SPACING * 2) + total_width = control_width + (CONTAINER_PADDING * 2) + self._rect.width = total_width - # Get enabled state - enabled = self.is_enabled() + # Position the control in the parent rectangle + start_x = self._rect.x + self._rect.width - control_width - (CONTAINER_PADDING * 2) + + self.container_rect = rl.Rectangle(start_x, self._rect.y + TOP_PADDING, total_width, BUTTON_HEIGHT + (CONTAINER_PADDING * 2)) + + component_y = self._rect.y + TOP_PADDING + CONTAINER_PADDING + start_x = self.container_rect.x + CONTAINER_PADDING + + # Minus button + self.minus_btn_rect = rl.Rectangle(start_x, component_y, BUTTON_WIDTH, BUTTON_HEIGHT) + + # Value label + label_x = start_x + BUTTON_WIDTH + BUTTON_SPACING + self.label_rect = rl.Rectangle(label_x, component_y, self.label_width, BUTTON_HEIGHT) + + # Plus button + plus_x = label_x + self.label_width + BUTTON_SPACING + self.plus_btn_rect = rl.Rectangle(plus_x, component_y, BUTTON_WIDTH, BUTTON_HEIGHT) - # Draw container background rl.draw_rectangle_rounded(self.container_rect, 1, BUTTON_CORNER_RADIUS, style.OPTIONCONTROL_CONTAINER_BG) - # Determine button states - self._minus_enabled = enabled and self.current_value > self.min_value - self._plus_enabled = enabled and self.current_value < self.max_value + self._minus_enabled = self.enabled and self.current_value > self.min_value + self._plus_enabled = self.enabled and self.current_value < self.max_value - # Render buttons and label self._render_button(self.minus_btn_rect, "-", self._minus_enabled) self._render_value_label() self._render_button(self.plus_btn_rect, "+", self._plus_enabled) def _render_button(self, rect: rl.Rectangle, text: str, enabled: bool): - """Render a button and return True if it was clicked""" mouse_pos = rl.get_mouse_position() is_hovered = rl.check_collision_point_rec(mouse_pos, rect) and self._touch_valid() is_pressed = is_hovered and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) @@ -202,7 +155,7 @@ class OptionControlSP(Widget): def _render_value_label(self): """Render the current value label""" text = self.get_displayed_value() - text_color = style.OPTIONCONTROL_TEXT_ENABLED if self.is_enabled() else style.OPTIONCONTROL_TEXT_DISABLED + text_color = style.OPTIONCONTROL_TEXT_ENABLED if self.enabled else style.OPTIONCONTROL_TEXT_DISABLED # Calculate text position centered in the label area text_size = measure_text_cached(self._font, text, VALUE_FONT_SIZE) @@ -223,6 +176,4 @@ class OptionControlSP(Widget): self.current_value = min(self.max_value, self.current_value) value_changed = True - # Call the value changed callback - if value_changed: - self.set_value(self.current_value) + self.set_value(self.current_value) From 1db71aff571934472f4b87c12fa6471a8cad7494 Mon Sep 17 00:00:00 2001 From: nayan Date: Thu, 20 Nov 2025 22:15:43 -0500 Subject: [PATCH 26/41] I. SAID. SIMPLIFY. --- system/ui/sunnypilot/widgets/option_control.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/system/ui/sunnypilot/widgets/option_control.py b/system/ui/sunnypilot/widgets/option_control.py index fe13d9b4e3..f281f843a2 100644 --- a/system/ui/sunnypilot/widgets/option_control.py +++ b/system/ui/sunnypilot/widgets/option_control.py @@ -170,10 +170,8 @@ class OptionControlSP(ItemAction): if self._minus_enabled and rl.check_collision_point_rec(mouse_pos, self.minus_btn_rect): self.current_value -= self.value_change_step self.current_value = max(self.min_value, self.current_value) - value_changed = True elif self._plus_enabled and rl.check_collision_point_rec(mouse_pos, self.plus_btn_rect): self.current_value += self.value_change_step self.current_value = min(self.max_value, self.current_value) - value_changed = True self.set_value(self.current_value) From 68c7f6943915c4c7276d6ad0dd2a8864d5aee3d2 Mon Sep 17 00:00:00 2001 From: nayan Date: Thu, 20 Nov 2025 22:18:04 -0500 Subject: [PATCH 27/41] AAARGGGGGG..... --- system/ui/sunnypilot/widgets/option_control.py | 1 - 1 file changed, 1 deletion(-) diff --git a/system/ui/sunnypilot/widgets/option_control.py b/system/ui/sunnypilot/widgets/option_control.py index f281f843a2..bbe95dde62 100644 --- a/system/ui/sunnypilot/widgets/option_control.py +++ b/system/ui/sunnypilot/widgets/option_control.py @@ -166,7 +166,6 @@ class OptionControlSP(ItemAction): rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), VALUE_FONT_SIZE, 0, text_color) def _handle_mouse_release(self, mouse_pos: MousePos): - value_changed = False if self._minus_enabled and rl.check_collision_point_rec(mouse_pos, self.minus_btn_rect): self.current_value -= self.value_change_step self.current_value = max(self.min_value, self.current_value) From dd9d4bedf42619c58cb3af9a5747e7dd34c734d4 Mon Sep 17 00:00:00 2001 From: nayan Date: Thu, 20 Nov 2025 22:58:55 -0500 Subject: [PATCH 28/41] init --- selfdrive/ui/sunnypilot/layouts/settings/display.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/display.py b/selfdrive/ui/sunnypilot/layouts/settings/display.py index 1cbea6c9fa..77d2b25fd1 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/display.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/display.py @@ -1,3 +1,5 @@ +from openpilot.system.ui.lib.multilang import tr +from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp from openpilot.system.ui.widgets.scroller import Scroller from openpilot.system.ui.widgets import Widget from openpilot.common.params import Params @@ -12,8 +14,14 @@ class DisplayLayout(Widget): self._scroller = Scroller(items, line_separator=True, spacing=0) def _initialize_items(self): + self._onroad_brightness_toggle = toggle_item_sp( + param="OnroadScreenOffControl", + title=tr("Onroad Screen: Reduced Brightness"), + description=tr("Turn off device screen or reduce brightness after driving starts. " + + "It automatically brightens again when screen is touched or a visible alert is displayed."), + ) items = [ - + self._onroad_brightness_toggle, ] return items From f8e8c59dd5600478342dbf8893dcf594b931500d Mon Sep 17 00:00:00 2001 From: nayan Date: Thu, 20 Nov 2025 23:34:24 -0500 Subject: [PATCH 29/41] option control value fix --- system/ui/sunnypilot/widgets/option_control.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system/ui/sunnypilot/widgets/option_control.py b/system/ui/sunnypilot/widgets/option_control.py index bbe95dde62..58fb778cf0 100644 --- a/system/ui/sunnypilot/widgets/option_control.py +++ b/system/ui/sunnypilot/widgets/option_control.py @@ -77,6 +77,9 @@ class OptionControlSP(ItemAction): value = self.current_value if callable(self.label_callback): + if self.value_map: + return self.label_callback(self.value_map[value]) + else: return self.label_callback(value) if self.value_map: From 2be95225b137a2e6e80a25103ba55bb32af83f14 Mon Sep 17 00:00:00 2001 From: nayan Date: Thu, 20 Nov 2025 23:41:15 -0500 Subject: [PATCH 30/41] add all controls --- .../ui/sunnypilot/layouts/settings/display.py | 71 +++++++++++++++++-- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/display.py b/selfdrive/ui/sunnypilot/layouts/settings/display.py index 77d2b25fd1..6e06cab022 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/display.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/display.py @@ -1,9 +1,23 @@ from openpilot.system.ui.lib.multilang import tr -from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp -from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, option_item_sp +from openpilot.system.ui.widgets.scroller import Scroller, LineSeparator from openpilot.system.ui.widgets import Widget from openpilot.common.params import Params +onroad_brightness_timer_values = { + 0: 15, + 1: 30, + 2: 60, + 3: 120, + 4: 180, + 5: 240, + 6: 300, + 7: 360, + 8: 420, + 9: 480, + 10: 540, + 11: 600 +} class DisplayLayout(Widget): def __init__(self): @@ -16,15 +30,64 @@ class DisplayLayout(Widget): def _initialize_items(self): self._onroad_brightness_toggle = toggle_item_sp( param="OnroadScreenOffControl", - title=tr("Onroad Screen: Reduced Brightness"), - description=tr("Turn off device screen or reduce brightness after driving starts. " + + title=lambda: tr("Onroad Screen: Reduced Brightness"), + description=lambda: tr("Turn off device screen or reduce brightness after driving starts. " + "It automatically brightens again when screen is touched or a visible alert is displayed."), ) + self._onroad_brightness_timer = option_item_sp( + param="OnroadScreenOffTimer", + title=lambda: tr("Onroad Brightness Delay"), + description="", + min_value=0, + max_value=11, + value_change_step=1, + value_map=onroad_brightness_timer_values, + label_callback=lambda value: f"{value} s" if value < 60 else f"{int(value/60)} m", + ) + self._onroad_brightness = option_item_sp( + param="OnroadScreenOffBrightness", + title=lambda: tr("Onroad Brightness"), + description="", + min_value=0, + max_value=90, + value_change_step=5, + label_callback=lambda value: f"{value} %" if value > 0 else tr("Screen Off"), + ) + self._global_brightness = option_item_sp( + param="Brightness", + title=tr("Global Brightness"), + description=lambda: tr("Overrides the brightness of the device. This applies to both onroad and offroad screens."), + min_value=-5, + max_value=100, + value_change_step=5, + label_callback=lambda value: f"{value}" if value > 0 else tr("Auto") if value == 0 else tr("Auto(Dark)") + ) + self._interactivity_timeout = option_item_sp( + param="InteractivityTimeout", + title=lambda: tr("Interactivity Timeout"), + description=lambda: tr("Apply a custom timeout for settings UI." + + "
This is the time after which settings UI closes automatically " + + "if user is not interacting with the screen."), + min_value=0, + max_value=120, + value_change_step=10, + label_callback=lambda value: (tr("Default") if not value or value == 0 else + f"{value} s" if value < 60 else f"{int(value/60)} m") + ) items = [ self._onroad_brightness_toggle, + self._onroad_brightness_timer, + self._onroad_brightness, + self._global_brightness, + self._interactivity_timeout, ] return items + def _update_state(self): + super()._update_state() + self._onroad_brightness_timer.set_visible(self._onroad_brightness_toggle.action_item.get_state()) + self._onroad_brightness.set_visible(self._onroad_brightness_toggle.action_item.get_state()) + def _render(self, rect): self._scroller.render(rect) From 4baa170cfa1d47ed7b2cd49b5aaf83f91f25f63f Mon Sep 17 00:00:00 2001 From: nayan Date: Thu, 20 Nov 2025 23:46:16 -0500 Subject: [PATCH 31/41] hide all controls --- selfdrive/ui/sunnypilot/layouts/settings/display.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/display.py b/selfdrive/ui/sunnypilot/layouts/settings/display.py index 6e06cab022..8f2cd74297 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/display.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/display.py @@ -88,6 +88,15 @@ class DisplayLayout(Widget): self._onroad_brightness_timer.set_visible(self._onroad_brightness_toggle.action_item.get_state()) self._onroad_brightness.set_visible(self._onroad_brightness_toggle.action_item.get_state()) + self.hide_for_now() + + def hide_for_now(self): + self._onroad_brightness_toggle.set_visible(False) + self._onroad_brightness_timer.set_visible(False) + self._onroad_brightness.set_visible(False) + self._global_brightness.set_visible(False) + self._interactivity_timeout.set_visible(False) + def _render(self, rect): self._scroller.render(rect) From 93ab6ee06b952f2297fe5ceccff910d7f99b42f6 Mon Sep 17 00:00:00 2001 From: nayan Date: Thu, 20 Nov 2025 23:49:57 -0500 Subject: [PATCH 32/41] lint --- selfdrive/ui/sunnypilot/layouts/settings/display.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/display.py b/selfdrive/ui/sunnypilot/layouts/settings/display.py index 8f2cd74297..69e8d17ec4 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/display.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/display.py @@ -1,6 +1,6 @@ from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, option_item_sp -from openpilot.system.ui.widgets.scroller import Scroller, LineSeparator +from openpilot.system.ui.widgets.scroller import Scroller from openpilot.system.ui.widgets import Widget from openpilot.common.params import Params From 2717d9735017409be0a77d936d5853fb16b39f19 Mon Sep 17 00:00:00 2001 From: nayan Date: Fri, 21 Nov 2025 15:20:31 -0500 Subject: [PATCH 33/41] scroller -> scroller_tici --- selfdrive/ui/sunnypilot/layouts/settings/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/settings.py b/selfdrive/ui/sunnypilot/layouts/settings/settings.py index 6bf8218bf0..7460219530 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/settings.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/settings.py @@ -9,7 +9,7 @@ from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout from openpilot.system.ui.lib.application import gui_app,MousePos from openpilot.system.ui.lib.multilang import tr_noop -from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets.network import NetworkUI from openpilot.system.ui.lib.wifi_manager import WifiManager From 48d33e98e7d4cbc4b616a58faa8a70bb883eb714 Mon Sep 17 00:00:00 2001 From: nayan Date: Fri, 21 Nov 2025 15:27:51 -0500 Subject: [PATCH 34/41] scroller -> scroller_tici --- selfdrive/ui/sunnypilot/layouts/settings/cruise.py | 2 +- selfdrive/ui/sunnypilot/layouts/settings/display.py | 2 +- selfdrive/ui/sunnypilot/layouts/settings/models.py | 2 +- selfdrive/ui/sunnypilot/layouts/settings/navigation.py | 2 +- selfdrive/ui/sunnypilot/layouts/settings/osm.py | 2 +- selfdrive/ui/sunnypilot/layouts/settings/steering.py | 2 +- selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py | 2 +- selfdrive/ui/sunnypilot/layouts/settings/trips.py | 2 +- selfdrive/ui/sunnypilot/layouts/settings/vehicle.py | 2 +- selfdrive/ui/sunnypilot/layouts/settings/visuals.py | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/cruise.py b/selfdrive/ui/sunnypilot/layouts/settings/cruise.py index b8e2a1344b..049f0e0de4 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/cruise.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/cruise.py @@ -1,4 +1,4 @@ -from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.widgets import Widget from openpilot.common.params import Params diff --git a/selfdrive/ui/sunnypilot/layouts/settings/display.py b/selfdrive/ui/sunnypilot/layouts/settings/display.py index 1cbea6c9fa..706410dbfa 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/display.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/display.py @@ -1,4 +1,4 @@ -from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.widgets import Widget from openpilot.common.params import Params diff --git a/selfdrive/ui/sunnypilot/layouts/settings/models.py b/selfdrive/ui/sunnypilot/layouts/settings/models.py index 1f0107aa9b..ca064bcae9 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/models.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/models.py @@ -1,4 +1,4 @@ -from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.widgets import Widget from openpilot.common.params import Params diff --git a/selfdrive/ui/sunnypilot/layouts/settings/navigation.py b/selfdrive/ui/sunnypilot/layouts/settings/navigation.py index eec1367b69..a1654f0e32 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/navigation.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/navigation.py @@ -1,4 +1,4 @@ -from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.widgets import Widget from openpilot.common.params import Params diff --git a/selfdrive/ui/sunnypilot/layouts/settings/osm.py b/selfdrive/ui/sunnypilot/layouts/settings/osm.py index 62f9b6d858..a8c18b3d59 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/osm.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/osm.py @@ -1,4 +1,4 @@ -from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.widgets import Widget from openpilot.common.params import Params diff --git a/selfdrive/ui/sunnypilot/layouts/settings/steering.py b/selfdrive/ui/sunnypilot/layouts/settings/steering.py index f8ae00fcca..d76b802fbf 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/steering.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/steering.py @@ -1,4 +1,4 @@ -from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.widgets import Widget from openpilot.common.params import Params diff --git a/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py b/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py index 068679ec67..ae44acd2bd 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py @@ -1,4 +1,4 @@ -from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.widgets import Widget from openpilot.common.params import Params diff --git a/selfdrive/ui/sunnypilot/layouts/settings/trips.py b/selfdrive/ui/sunnypilot/layouts/settings/trips.py index 091bd8f987..60cb25872b 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/trips.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/trips.py @@ -1,4 +1,4 @@ -from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.widgets import Widget from openpilot.common.params import Params diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle.py index 7d7a6f5fe8..fe2e150ea9 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/vehicle.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle.py @@ -1,4 +1,4 @@ -from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.widgets import Widget from openpilot.common.params import Params diff --git a/selfdrive/ui/sunnypilot/layouts/settings/visuals.py b/selfdrive/ui/sunnypilot/layouts/settings/visuals.py index dba68c912f..fe43c6cb58 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/visuals.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/visuals.py @@ -1,4 +1,4 @@ -from openpilot.system.ui.widgets.scroller import Scroller +from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.widgets import Widget from openpilot.common.params import Params From 91f2bf345956e0710e75a4f94a440d0986a0dc1d Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 21 Nov 2025 16:23:01 -0500 Subject: [PATCH 35/41] ui: `GuiApplicationExt` --- system/ui/lib/application.py | 4 +++- system/ui/sunnypilot/lib/application.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 system/ui/sunnypilot/lib/application.py diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index e3370a5f74..7271127838 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -19,6 +19,8 @@ from openpilot.system.hardware import HARDWARE, PC from openpilot.system.ui.lib.multilang import multilang from openpilot.common.realtime import Ratekeeper +from openpilot.system.ui.sunnypilot.lib.application import GuiApplicationExt + _DEFAULT_FPS = int(os.getenv("FPS", {'tizi': 20}.get(HARDWARE.get_device_type(), 60))) FPS_LOG_INTERVAL = 5 # Seconds between logging FPS drops FPS_DROP_THRESHOLD = 0.9 # FPS drop threshold for triggering a warning @@ -186,7 +188,7 @@ class MouseState: self._prev_mouse_event[slot] = ev -class GuiApplication: +class GuiApplication(GuiApplicationExt): def __init__(self, width: int | None = None, height: int | None = None): self._fonts: dict[FontWeight, rl.Font] = {} self._width = width if width is not None else GuiApplication._default_width() diff --git a/system/ui/sunnypilot/lib/application.py b/system/ui/sunnypilot/lib/application.py new file mode 100644 index 0000000000..b39bf31e9d --- /dev/null +++ b/system/ui/sunnypilot/lib/application.py @@ -0,0 +1,15 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import os + +SUNNYPILOT_UI = os.getenv("SUNNYPILOT_UI", "1") == "1" + + +class GuiApplicationExt: + @staticmethod + def sunnypilot_ui() -> bool: + return SUNNYPILOT_UI From a33497ed19a413931ea4053c4903f15f2ae71d50 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 21 Nov 2025 16:42:49 -0500 Subject: [PATCH 36/41] add to readme --- system/ui/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/system/ui/README.md b/system/ui/README.md index f81cb5573a..796d298626 100644 --- a/system/ui/README.md +++ b/system/ui/README.md @@ -10,6 +10,7 @@ Quick start: * set `BURN_IN=1` to get a burn-in heatmap version of the UI * set `GRID=50` to show a 50-pixel alignment grid overlay * set `MAGIC_DEBUG=1` to show every dropped frames (only on device) +* set `SUNNYPILOT_UI=0` to run the stock UI instead of the sunnypilot UI * https://www.raylib.com/cheatsheet/cheatsheet.html * https://electronstudio.github.io/raylib-python-cffi/README.html#quickstart From ed775185f2434f2383bf68887805be3260c24eef Mon Sep 17 00:00:00 2001 From: nayan Date: Fri, 21 Nov 2025 17:49:27 -0500 Subject: [PATCH 37/41] use gui_app.sunnypilot_ui() --- common/params_keys.h | 1 - selfdrive/ui/layouts/settings/developer.py | 2 +- selfdrive/ui/layouts/settings/toggles.py | 2 +- selfdrive/ui/tests/test_ui/raylib_screenshots.py | 1 - 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/common/params_keys.h b/common/params_keys.h index fe08062987..38578d7ddc 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -173,7 +173,6 @@ inline static std::unordered_map keys = { {"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}}, {"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}}, - {"sunnypilot_ui", {PERSISTENT, BOOL, "1"}}, {"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}}, // MADS params diff --git a/selfdrive/ui/layouts/settings/developer.py b/selfdrive/ui/layouts/settings/developer.py index d133a992ad..f47cd310ca 100644 --- a/selfdrive/ui/layouts/settings/developer.py +++ b/selfdrive/ui/layouts/settings/developer.py @@ -9,7 +9,7 @@ from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.multilang import tr, tr_noop from openpilot.system.ui.widgets import DialogResult -if Params().get_bool("sunnypilot_ui"): +if gui_app.sunnypilot_ui(): from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp as toggle_item # Description constants diff --git a/selfdrive/ui/layouts/settings/toggles.py b/selfdrive/ui/layouts/settings/toggles.py index 86c27aa7c8..b7f176e24f 100644 --- a/selfdrive/ui/layouts/settings/toggles.py +++ b/selfdrive/ui/layouts/settings/toggles.py @@ -9,7 +9,7 @@ from openpilot.system.ui.lib.multilang import tr, tr_noop from openpilot.system.ui.widgets import DialogResult from openpilot.selfdrive.ui.ui_state import ui_state -if Params().get_bool("sunnypilot_ui"): +if gui_app.sunnypilot_ui(): from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp as toggle_item PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants diff --git a/selfdrive/ui/tests/test_ui/raylib_screenshots.py b/selfdrive/ui/tests/test_ui/raylib_screenshots.py index 3cc5b01541..d3de616cc0 100755 --- a/selfdrive/ui/tests/test_ui/raylib_screenshots.py +++ b/selfdrive/ui/tests/test_ui/raylib_screenshots.py @@ -294,7 +294,6 @@ def create_screenshots(): with OpenpilotPrefix(): params = Params() params.put("DongleId", "123456789012345") - params.put_bool("sunnypilot_ui", True) # Set branch name params.put("UpdaterCurrentDescription", VERSION) From e8ab9d812d2be1bdbe5ac27af43707e16e9829e4 Mon Sep 17 00:00:00 2001 From: nayan Date: Fri, 21 Nov 2025 17:55:00 -0500 Subject: [PATCH 38/41] use gui_app.sunnypilot_ui() --- common/params_keys.h | 1 - selfdrive/ui/ui.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/common/params_keys.h b/common/params_keys.h index fe08062987..38578d7ddc 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -173,7 +173,6 @@ inline static std::unordered_map keys = { {"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}}, {"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}}, - {"sunnypilot_ui", {PERSISTENT, BOOL, "1"}}, {"TrueVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}}, // MADS params diff --git a/selfdrive/ui/ui.py b/selfdrive/ui/ui.py index 11fc4e3b95..5e0ba6d1ae 100755 --- a/selfdrive/ui/ui.py +++ b/selfdrive/ui/ui.py @@ -9,8 +9,7 @@ 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 -from openpilot.common.params import Params -if Params().get_bool("sunnypilot_ui"): +if gui_app.sunnypilot_ui(): from openpilot.selfdrive.ui.sunnypilot.ui_state import ui_state_sp as ui_state def main(): From 5d3f95d420e97e9ea2d128d6f17564eb194611db Mon Sep 17 00:00:00 2001 From: nayan Date: Fri, 21 Nov 2025 18:08:19 -0500 Subject: [PATCH 39/41] use gui_app.sunnypilot_ui() --- selfdrive/ui/layouts/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/selfdrive/ui/layouts/main.py b/selfdrive/ui/layouts/main.py index e965888fde..11b9965d78 100644 --- a/selfdrive/ui/layouts/main.py +++ b/selfdrive/ui/layouts/main.py @@ -10,8 +10,7 @@ from openpilot.selfdrive.ui.ui_state import device, ui_state from openpilot.system.ui.widgets import Widget from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow -from openpilot.common.params import Params -if Params().get_bool("sunnypilot_ui"): +if gui_app.sunnypilot_ui(): from openpilot.selfdrive.ui.sunnypilot.layouts.settings.settings import SettingsLayoutSP as SettingsLayout class MainState(IntEnum): From a3be1edde7e2f74b3d3d9720d4fa8f1422ec6e8a Mon Sep 17 00:00:00 2001 From: nayan Date: Sat, 22 Nov 2025 23:48:06 -0500 Subject: [PATCH 40/41] optimizations --- selfdrive/ui/sunnypilot/layouts/settings/display.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/display.py b/selfdrive/ui/sunnypilot/layouts/settings/display.py index f55cfc1188..dd0af8521a 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/display.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/display.py @@ -2,7 +2,6 @@ from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, option_item_sp from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.widgets import Widget -from openpilot.common.params import Params onroad_brightness_timer_values = { 0: 15, @@ -23,7 +22,6 @@ class DisplayLayout(Widget): def __init__(self): super().__init__() - self._params = Params() items = self._initialize_items() self._scroller = Scroller(items, line_separator=True, spacing=0) From 76101207cbb5f740fff1bb397d26b8f8284adf8e Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 30 Nov 2025 12:51:16 +0100 Subject: [PATCH 41/41] Removed hide for now --- selfdrive/ui/sunnypilot/layouts/settings/display.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/display.py b/selfdrive/ui/sunnypilot/layouts/settings/display.py index dbabd1c891..8714108c61 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/display.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/display.py @@ -94,15 +94,6 @@ class DisplayLayout(Widget): self._onroad_brightness_timer.set_visible(self._onroad_brightness_toggle.action_item.get_state()) self._onroad_brightness.set_visible(self._onroad_brightness_toggle.action_item.get_state()) - self.hide_for_now() - - def hide_for_now(self): - self._onroad_brightness_toggle.set_visible(False) - self._onroad_brightness_timer.set_visible(False) - self._onroad_brightness.set_visible(False) - self._global_brightness.set_visible(False) - self._interactivity_timeout.set_visible(False) - def _render(self, rect): self._scroller.render(rect)