diff --git a/selfdrive/ui/layouts/settings/toggles.py b/selfdrive/ui/layouts/settings/toggles.py index cd233aa3a..f5f3a4e9c 100644 --- a/selfdrive/ui/layouts/settings/toggles.py +++ b/selfdrive/ui/layouts/settings/toggles.py @@ -11,6 +11,7 @@ from openpilot.selfdrive.ui.ui_state import ui_state if gui_app.sunnypilot_ui(): from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp as toggle_item + from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp as multiple_button_item PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants @@ -99,7 +100,7 @@ class TogglesLayout(Widget): lambda: tr("Driving Personality"), lambda: tr(DESCRIPTIONS["LongitudinalPersonality"]), buttons=[lambda: tr("Aggressive"), lambda: tr("Standard"), lambda: tr("Relaxed")], - button_width=255, + button_width=300, callback=self._set_longitudinal_personality, selected_index=self._params.get("LongitudinalPersonality", return_default=True), icon="speed_limit.png" diff --git a/system/ui/sunnypilot/lib/styles.py b/system/ui/sunnypilot/lib/styles.py index 4880ad58d..b942f79d3 100644 --- a/system/ui/sunnypilot/lib/styles.py +++ b/system/ui/sunnypilot/lib/styles.py @@ -24,6 +24,10 @@ class Base: TOGGLE_WIDTH = int(TOGGLE_HEIGHT * 1.75) TOGGLE_BG_HEIGHT = TOGGLE_HEIGHT - 20 + # Button Control + BUTTON_WIDTH = 300 + BUTTON_HEIGHT = 120 + @dataclass class DefaultStyleSP(Base): @@ -47,5 +51,10 @@ class DefaultStyleSP(Base): TOGGLE_DISABLED_OFF_COLOR = DISABLED_OFF_BG_COLOR TOGGLE_DISABLED_KNOB_COLOR = rl.Color(88, 88, 88, 255) # Lighter Grey + # Multi Button Control + MBC_TRANSPARENT = rl.Color(255, 255, 255, 0) + MBC_BG_CHECKED_ENABLED = rl.Color(0x69, 0x68, 0x68, 0xFF) + MBC_DISABLED = rl.Color(0xFF, 0xFF, 0xFF, 0x33) + style = DefaultStyleSP diff --git a/system/ui/sunnypilot/widgets/list_view.py b/system/ui/sunnypilot/widgets/list_view.py index dcf8f6019..4ecb9a488 100644 --- a/system/ui/sunnypilot/widgets/list_view.py +++ b/system/ui/sunnypilot/widgets/list_view.py @@ -7,9 +7,11 @@ See the LICENSE.md file in the root directory for more details. from collections.abc import Callable import pyray as rl +from openpilot.common.params import Params +from openpilot.system.ui.lib.application import MousePos 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.widgets.list_view import ListItem, ToggleAction, ItemAction, MultipleButtonAction, _resolve_value from openpilot.system.ui.sunnypilot.lib.styles import style @@ -20,11 +22,74 @@ class ToggleActionSP(ToggleAction): self.toggle = ToggleSP(initial_state=initial_state, callback=callback, param=param) +class MultipleButtonActionSP(MultipleButtonAction): + def __init__(self, buttons: list[str | Callable[[], str]], button_width: int, selected_index: int = 0, callback: Callable = None, + param: str | None = None): + MultipleButtonAction.__init__(self, buttons, button_width, selected_index, callback) + self.param_key = param + self.params = Params() + if self.param_key: + self.selected_button = int(self.params.get(self.param_key, return_default=True)) + self._anim_x: float | None = None + + def _render(self, rect: rl.Rectangle): + + button_y = rect.y + (rect.height - style.BUTTON_HEIGHT) / 2 + + total_width = len(self.buttons) * self.button_width + track_rect = rl.Rectangle(rect.x, button_y, total_width, style.BUTTON_HEIGHT) + + bg_color = style.MBC_TRANSPARENT + text_color = style.ITEM_TEXT_COLOR if self.enabled else style.MBC_DISABLED + highlight_color = style.MBC_BG_CHECKED_ENABLED if self.enabled else style.MBC_DISABLED + + # background + rl.draw_rectangle_rounded(track_rect, 0.2, 20, bg_color) + + # border + border_color = style.MBC_BG_CHECKED_ENABLED if self.enabled else style.MBC_DISABLED + rl.draw_rectangle_rounded_lines_ex(track_rect, 0.2, 20, 2, border_color) + + # highlight with animation + target_x = rect.x + self.selected_button * self.button_width + if not self._anim_x: + self._anim_x = target_x + self._anim_x += (target_x - self._anim_x) * 0.2 + + highlight_rect = rl.Rectangle(self._anim_x, button_y, self.button_width, style.BUTTON_HEIGHT) + rl.draw_rectangle_rounded(highlight_rect, 0.2, 20, highlight_color) + + # text + for i, _text in enumerate(self.buttons): + button_x = rect.x + i * self.button_width + + text = _resolve_value(_text, "") + text_size = measure_text_cached(self._font, text, 40) + text_x = button_x + (self.button_width - text_size.x) / 2 + text_y = button_y + (style.BUTTON_HEIGHT - text_size.y) / 2 + + rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, text_color) + + def _handle_mouse_release(self, mouse_pos: MousePos): + MultipleButtonAction._handle_mouse_release(self, mouse_pos) + if self.param_key: + self.params.put(self.param_key, self.selected_button) + + 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, - action_item: ItemAction | None = None): + action_item: ItemAction | None = None, inline: bool = True): ListItem.__init__(self, title, icon, description, description_visible, callback, action_item) + self.inline = inline + if not self.inline: + self._rect.height += style.ITEM_BASE_HEIGHT/1.75 + + def get_item_height(self, font: rl.Font, max_width: int) -> float: + height = super().get_item_height(font, max_width) + if not self.inline: + height = height + style.ITEM_BASE_HEIGHT/1.75 + return height def show_description(self, show: bool): self._set_description_visible(show) @@ -33,6 +98,10 @@ class ListItemSP(ListItem): if not self.action_item: return rl.Rectangle(0, 0, 0, 0) + if not self.inline: + action_y = item_rect.y + self._text_size.y + style.ITEM_PADDING * 3 + return rl.Rectangle(item_rect.x + style.ITEM_PADDING, action_y, item_rect.width - (style.ITEM_PADDING * 2), style.BUTTON_HEIGHT) + 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, @@ -47,6 +116,13 @@ class ListItemSP(ListItem): return rl.Rectangle(action_x, action_y, action_width, style.ITEM_BASE_HEIGHT) def _render(self, _): + if not self.is_visible: + return + + # Don't draw items that are not in parent's viewport + if (self._rect.y + self.rect.height) <= self._parent_rect.y or self._rect.y >= (self._parent_rect.y + self._parent_rect.height): + return + content_x = self._rect.x + style.ITEM_PADDING text_x = content_x left_action_item = isinstance(self.action_item, ToggleAction) @@ -62,8 +138,8 @@ class ListItemSP(ListItem): # 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 + self._text_size = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE) + item_y = self._rect.y + (style.ITEM_BASE_HEIGHT - self._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 @@ -74,14 +150,13 @@ class ListItemSP(ListItem): 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 + self._text_size = measure_text_cached(self._font, self.title, style.ITEM_TEXT_FONT_SIZE) + item_y = self._rect.y + (style.ITEM_BASE_HEIGHT - self._text_size.y) // 2 if self.inline else self._rect.y + style.ITEM_PADDING * 1.5 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: @@ -91,12 +166,12 @@ class ListItemSP(ListItem): 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 - ) + + desc_y = self._rect.y + style.ITEM_DESC_V_OFFSET + if not self.inline and self.action_item: + desc_y = self.action_item.rect.y + style.ITEM_DESC_V_OFFSET - style.ITEM_PADDING * 1.75 + + description_rect = rl.Rectangle(self._rect.x + style.ITEM_PADDING, desc_y, content_width, description_height) self._html_renderer.render(description_rect) @@ -104,3 +179,10 @@ 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) -> 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) + + +def multiple_button_item_sp(title: str | Callable[[], str], description: str | Callable[[], str], buttons: list[str | Callable[[], str]], + selected_index: int = 0, button_width: int = style.BUTTON_WIDTH, callback: Callable = None, + icon: str = "", param: str | None = None, inline: bool = False) -> ListItemSP: + action = MultipleButtonActionSP(buttons, button_width, selected_index, callback=callback, param=param) + return ListItemSP(title=title, description=description, icon=icon, action_item=action, inline=inline) diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index fa47d3553..71755cc28 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -16,7 +16,9 @@ from openpilot.system.ui.widgets.scroller_tici import Scroller from openpilot.system.ui.widgets.list_view import ButtonAction, ListItem, MultipleButtonAction, ToggleAction, button_item, text_item if gui_app.sunnypilot_ui(): + from openpilot.system.ui.sunnypilot.widgets.list_view import ListItemSP as ListItem from openpilot.system.ui.sunnypilot.widgets.list_view import ToggleActionSP as ToggleAction + from openpilot.system.ui.sunnypilot.widgets.list_view import MultipleButtonActionSP as MultipleButtonAction # These are only used for AdvancedNetworkSettings, standalone apps just need WifiManagerUI try: