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 + )