diff --git a/.run/Build_BIG_UI.run.xml b/.run/Build_BIG_UI.run.xml
new file mode 100644
index 000000000..b58b1bf1b
--- /dev/null
+++ b/.run/Build_BIG_UI.run.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/Build_SMALL_UI.run.xml b/.run/Build_SMALL_UI.run.xml
new file mode 100644
index 000000000..6c231c83f
--- /dev/null
+++ b/.run/Build_SMALL_UI.run.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SConstruct b/SConstruct
index 48dd04470..f5d5ec6fb 100644
--- a/SConstruct
+++ b/SConstruct
@@ -178,7 +178,8 @@ Export('envCython', 'np_version')
Export('env', 'arch')
# Setup cache dir
-cache_dir = '/data/scons_cache' if arch == "larch64" else '/tmp/scons_cache'
+default_cache_dir = '/data/scons_cache' if arch == "larch64" else '/tmp/scons_cache'
+cache_dir = ARGUMENTS.get('cache_dir', default_cache_dir)
CacheDir(cache_dir)
Clean(["."], cache_dir)
diff --git a/selfdrive/ui/layouts/settings/developer.py b/selfdrive/ui/layouts/settings/developer.py
index 646c81750..f47cd310c 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 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(
diff --git a/selfdrive/ui/layouts/settings/toggles.py b/selfdrive/ui/layouts/settings/toggles.py
index 7fae2dfd2..b7f176e24 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 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
diff --git a/selfdrive/ui/tests/test_ui/raylib_screenshots.py b/selfdrive/ui/tests/test_ui/raylib_screenshots.py
index 481ac111b..d3de616cc 100755
--- a/selfdrive/ui/tests/test_ui/raylib_screenshots.py
+++ b/selfdrive/ui/tests/test_ui/raylib_screenshots.py
@@ -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):
diff --git a/system/ui/README.md b/system/ui/README.md
index f81cb5573..796d29862 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
diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py
index e3370a5f7..727112783 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 000000000..b39bf31e9
--- /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
diff --git a/system/ui/sunnypilot/lib/styles.py b/system/ui/sunnypilot/lib/styles.py
new file mode 100644
index 000000000..a232ac192
--- /dev/null
+++ b/system/ui/sunnypilot/lib/styles.py
@@ -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
diff --git a/system/ui/sunnypilot/widgets/list_view.py b/system/ui/sunnypilot/widgets/list_view.py
new file mode 100644
index 000000000..dcf8f6019
--- /dev/null
+++ b/system/ui/sunnypilot/widgets/list_view.py
@@ -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)
diff --git a/system/ui/sunnypilot/widgets/toggle.py b/system/ui/sunnypilot/widgets/toggle.py
new file mode 100644
index 000000000..46b390f77
--- /dev/null
+++ b/system/ui/sunnypilot/widgets/toggle.py
@@ -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)
diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py
index f41a04c24..fa47d3553 100644
--- a/system/ui/widgets/network.py
+++ b/system/ui/widgets/network.py
@@ -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