ui: sunnypilot toggle style (#1475)

* param to control stock vs sp ui

* init styles

* SP Toggles

* Lint

* optimizations

* sp raylib preview

* fix callback

* fix ui preview

* better padding

* this

* listitem -> listitemsp

* add show_description method

* remove padding from line separator.
like, WHY? 😩😩

* ui: `GuiApplicationExt`

* add to readme

* use gui_app.sunnypilot_ui()

* lint

* no fancy toggles :(

* mici scroller - no touchy

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
This commit is contained in:
Nayan
2025-11-21 23:37:03 -05:00
committed by GitHub
parent d92d2cb683
commit 457b6634fd
7 changed files with 222 additions and 1 deletions

View File

@@ -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 gui_app.sunnypilot_ui():
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp as toggle_item
# Description constants
DESCRIPTIONS = {
'enable_adb': tr_noop(

View File

@@ -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 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
# Description constants

View File

@@ -149,7 +149,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):

View File

@@ -0,0 +1,50 @@
"""
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.
"""
from dataclasses import dataclass
import pyray as rl
@dataclass
class Base:
# 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 = 120
TOGGLE_WIDTH = int(TOGGLE_HEIGHT * 1.75)
TOGGLE_BG_HEIGHT = TOGGLE_HEIGHT - 20
@dataclass
class DefaultStyleSP(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 = rl.Color(70, 70, 70, 255) # Lighter Grey
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 = DefaultStyleSP

View File

@@ -0,0 +1,106 @@
"""
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.
"""
from collections.abc import Callable
import pyray as rl
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.lib.styles import style
class ToggleActionSP(ToggleAction):
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,
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 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)
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 * 1.5
# 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 | 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) -> 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)

View File

@@ -0,0 +1,56 @@
"""
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.
"""
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.widgets.toggle import Toggle
from openpilot.system.ui.sunnypilot.lib.styles import style
KNOB_PADDING = 5
KNOB_RADIUS = style.TOGGLE_BG_HEIGHT / 2 - KNOB_PADDING
class ToggleSP(Toggle):
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, callback)
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()
self._rect.y -= style.ITEM_PADDING / 2
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 actual background
rl.draw_rectangle_rounded(bg_rect, 1.0, 10, bg_color)
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)

View File

@@ -15,6 +15,9 @@ from openpilot.system.ui.widgets.label import gui_label
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 ToggleActionSP as ToggleAction
# These are only used for AdvancedNetworkSettings, standalone apps just need WifiManagerUI
try:
from openpilot.common.params import Params