Compare commits

...

59 Commits

Author SHA1 Message Date
discountchubbs ee863481d6 fuzzy search helper 2025-11-23 11:27:03 -08:00
discountchubbs 65042f6091 Merge remote-tracking branch 'origin/rl-sp-panels' into fuzzy-search-dialog 2025-11-23 08:41:06 -08:00
nayan 60da05f428 fix scroller. yay 2025-11-23 00:02:06 -05:00
nayan 79394ff91c Merge remote-tracking branch 'origin/rl-sp-panels' into rl-sp-panels 2025-11-21 23:51:31 -05:00
nayan f6fc098d16 size adjustments 2025-11-21 23:51:16 -05:00
Jason Wen f81d864434 more 2025-11-21 23:44:23 -05:00
Jason Wen a4ce6f1ca7 no 2025-11-21 23:40:45 -05:00
Jason Wen b7524418f3 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into rl-sp-panels
# Conflicts:
#	selfdrive/ui/tests/test_ui/raylib_screenshots.py
#	system/ui/sunnypilot/lib/styles.py
#	system/ui/sunnypilot/widgets/list_view.py
#	system/ui/sunnypilot/widgets/toggle.py
2025-11-21 23:39:54 -05:00
nayan b0caef0ece Merge remote-tracking branch 'origin/master' into rl-sp-panels
# Conflicts:
#	selfdrive/ui/tests/test_ui/raylib_screenshots.py
2025-11-21 23:38:43 -05:00
nayan 16e7f3095c Merge branch 'rl-sp-toggles' into rl-sp-panels 2025-11-21 23:37:21 -05:00
nayan 6dfd075ea8 mici scroller - no touchy 2025-11-21 23:27:10 -05:00
Jason Wen bd55d151d7 match them 2025-11-21 23:21:13 -05:00
nayan 554239013c no fancy toggles :( 2025-11-21 23:20:08 -05:00
Jason Wen 70dcab68a4 Merge remote-tracking branch 'sunnypilot/sunnypilot/master' into rl-sp-panels 2025-11-21 23:11:03 -05:00
nayan b787a838ac Merge remote-tracking branch 'origin/rl-sp-toggles' into rl-sp-toggles 2025-11-21 23:02:32 -05:00
nayan 21855e8aad Merge remote-tracking branch 'origin/master' into rl-sp-toggles 2025-11-21 23:02:25 -05:00
Jason Wen f5adcb4082 lint 2025-11-21 23:01:15 -05:00
James Vecellio-Grant 924e5a3211 Merge branch 'master' into input-dialog 2025-11-21 19:33:18 -08:00
Jason Wen 66d41e606b Merge branch 'master' into rl-sp-toggles 2025-11-21 22:29:15 -05:00
James Vecellio-Grant a4ee4ba76d Merge branch 'master' into input-dialog 2025-11-21 16:24:31 -08:00
nayan 5d3f95d420 use gui_app.sunnypilot_ui() 2025-11-21 18:08:19 -05:00
nayan f8d19fe9dd Merge branch 'rl-sp-toggles' into rl-sp-panels 2025-11-21 18:07:19 -05:00
nayan 9d711350c2 Merge remote-tracking branch 'origin/ui-gui-app-ext' into rl-sp-panels 2025-11-21 18:07:03 -05:00
nayan ed775185f2 use gui_app.sunnypilot_ui() 2025-11-21 17:49:27 -05:00
nayan 7bbbc6588e Merge remote-tracking branch 'origin/ui-gui-app-ext' into rl-sp-toggles 2025-11-21 17:42:23 -05:00
nayan e68c65d15d Merge remote-tracking branch 'origin/master' into rl-sp-toggles 2025-11-21 17:40:10 -05:00
Jason Wen 0db8722221 Merge branch 'master' into ui-gui-app-ext 2025-11-21 17:24:14 -05:00
James Vecellio-Grant e911de5968 Merge branch 'master' into input-dialog 2025-11-21 13:50:33 -08:00
Jason Wen a33497ed19 add to readme 2025-11-21 16:42:59 -05:00
Jason Wen 91f2bf3459 ui: GuiApplicationExt 2025-11-21 16:23:01 -05:00
Jason Wen 7fad2fc189 Merge branch 'master' into rl-sp-toggles 2025-11-21 15:55:34 -05:00
nayan 48d33e98e7 scroller -> scroller_tici 2025-11-21 15:27:51 -05:00
nayan 2717d97350 scroller -> scroller_tici 2025-11-21 15:20:31 -05:00
nayan 64232397ed Merge remote-tracking branch 'origin/master' into rl-sp-panels 2025-11-21 15:16:50 -05:00
James Vecellio-Grant cea6e00819 Merge branch 'master' into input-dialog 2025-11-21 12:01:45 -08:00
Jason Wen 0613442ac9 Merge branch 'master' into rl-sp-toggles 2025-11-21 15:00:14 -05:00
nayan e6f5aae246 remove padding from line separator.
like, WHY? 😩😩
2025-11-20 18:05:12 -05:00
nayan 7032e4a972 add show_description method 2025-11-20 18:00:44 -05:00
nayan 5b03369a8f listitem -> listitemsp 2025-11-20 17:56:26 -05:00
nayan 1e0564b484 this 2025-11-20 08:05:20 -05:00
nayan eb94abaa14 better padding 2025-11-19 23:44:05 -05:00
discountchubbs d7b8ce86ed compare vs what used to be done before InputDialog 2025-11-17 20:21:33 -08:00
James Vecellio-Grant 2d3d104658 Merge branch 'master' into input-dialog 2025-11-17 19:24:20 -08:00
discountchubbs ded02895f4 dialog txt 2025-11-17 19:22:01 -08:00
Jason Wen 9778a925b0 input dialog 2025-11-17 19:05:54 -08:00
nayan a9e57f0a76 add ui previews 2025-11-16 12:47:37 -05:00
nayan 712a358c94 Merge branch 'rl-sp-toggles' into rl-sp-panels 2025-11-16 11:23:31 -05:00
nayan 423a7d2ed0 fix ui preview 2025-11-16 11:15:28 -05:00
nayan e4e10d4b87 fix callback 2025-11-16 11:15:22 -05:00
nayan 632e9d13b2 Merge branch 'rl-sp-toggles' into rl-sp-panels 2025-11-16 09:55:25 -05:00
nayan 362e9ce04b sp raylib preview 2025-11-16 09:53:28 -05:00
nayan 51d0666c85 more patience, grasshopper 2025-11-16 09:33:35 -05:00
nayan deda1329a2 patience, grasshopper 2025-11-16 09:33:35 -05:00
nayan 4110749cb0 Panels. With Icons. And Scroller. 2025-11-16 09:33:35 -05:00
nayan 3946e643f6 optimizations 2025-11-16 09:29:58 -05:00
nayan 0c37a38596 Lint 2025-11-16 09:29:58 -05:00
nayan 9c5acf61c0 SP Toggles 2025-11-16 09:29:58 -05:00
nayan 121b304fe0 init styles 2025-11-16 09:29:58 -05:00
nayan 47d848293b param to control stock vs sp ui 2025-11-16 09:29:58 -05:00
23 changed files with 753 additions and 42 deletions
+3
View File
@@ -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
if gui_app.sunnypilot_ui():
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.settings import SettingsLayoutSP as SettingsLayout
class MainState(IntEnum):
HOME = 0
@@ -0,0 +1,30 @@
"""
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 openpilot.common.params import Params
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
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()
@@ -0,0 +1,12 @@
"""
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 openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout
class DeviceLayoutSP(DeviceLayout):
def __init__(self):
DeviceLayout.__init__(self)
@@ -0,0 +1,30 @@
"""
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 openpilot.common.params import Params
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
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()
@@ -0,0 +1,30 @@
"""
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 openpilot.common.params import Params
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
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()
@@ -0,0 +1,30 @@
"""
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 openpilot.common.params import Params
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
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()
@@ -0,0 +1,30 @@
"""
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 openpilot.common.params import Params
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
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()
@@ -0,0 +1,198 @@
"""
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
from enum import IntEnum
import pyray as rl
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.sunnypilot.lib.styles import style
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 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 - 50, rect.y, OP.SIDEBAR_WIDTH - 50, 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 + style.ITEM_PADDING * 3, rect.y + style.ITEM_PADDING * 2, style.CLOSE_BTN_SIZE, style.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,
self._close_btn_rect.height + style.ITEM_PADDING * 4, # 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
@@ -0,0 +1,30 @@
"""
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 openpilot.common.params import Params
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
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()
@@ -0,0 +1,30 @@
"""
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 openpilot.common.params import Params
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
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()
@@ -0,0 +1,30 @@
"""
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 openpilot.common.params import Params
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
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()
@@ -0,0 +1,30 @@
"""
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 openpilot.common.params import Params
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
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()
@@ -0,0 +1,30 @@
"""
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 openpilot.common.params import Params
from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.system.ui.widgets import Widget
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()
@@ -41,47 +41,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")
@@ -89,13 +89,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
@@ -103,30 +103,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')
@@ -137,22 +139,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
@@ -173,7 +216,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
@@ -192,23 +235,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)
@@ -243,6 +286,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):
@@ -276,11 +332,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)
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a258a022f0ab90368327d899ed4fb85b47dd0e35c93508e35851d9c3184528c0
size 36382
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8623dbc5c7dd043a91d98777cb423cfd116014ed6390af6d7d00c1f8dea3c6e8
size 1181
+6 -2
View File
@@ -41,8 +41,12 @@ class GuiScrollPanel:
if DEBUG:
rl.draw_rectangle_lines(0, 0, abs(int(self._velocity_filter_y.x)), 10, rl.RED)
# Handle mouse wheel
self._offset_filter_y.x += rl.get_mouse_wheel_move() * MOUSE_WHEEL_SCROLL_SPEED
# Handle mouse wheel only when the mouse cursor is over this panel
mouse_wheel = rl.get_mouse_wheel_move()
if mouse_wheel != 0:
mouse_pos = rl.get_mouse_position()
if rl.check_collision_point_rec(mouse_pos, bounds):
self._offset_filter_y.x += mouse_wheel * MOUSE_WHEEL_SCROLL_SPEED
max_scroll_distance = max(0, content.height - bounds.height)
if self._scroll_state == ScrollState.IDLE:
+2 -1
View File
@@ -17,6 +17,7 @@ class Base:
ITEM_TEXT_FONT_SIZE = 50
ITEM_DESC_FONT_SIZE = 40
ITEM_DESC_V_OFFSET = 150
CLOSE_BTN_SIZE = 160
# Toggle Control
TOGGLE_HEIGHT = 120
@@ -29,7 +30,7 @@ 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
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
@@ -0,0 +1,34 @@
import re
import unicodedata
def normalize(text: str) -> str:
return unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode('utf-8').lower()
def search_from_list(query: str, items: list[str]) -> list[str]:
if not query:
return items
normalized_query = normalize(query)
search_terms = [re.sub(r'[^a-z0-9]', '', term) for term in normalized_query.split() if term.strip()]
results = []
for item in items:
normalized_item = normalize(item)
item_with_spaces = re.sub(r'[^a-z0-9\s]', ' ', normalized_item)
item_stripped = re.sub(r'[^a-z0-9]', '', normalized_item)
all_terms_match = True
for term in search_terms:
if not term:
continue
if term not in item_with_spaces and term not in item_stripped:
all_terms_match = False
break
if all_terms_match:
results.append(item)
return results
@@ -0,0 +1,35 @@
from collections.abc import Callable
from openpilot.common.params import Params
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.keyboard import Keyboard
class InputDialogSP:
def __init__(self, title: str, sub_title: str | None = None, current_text: str = "", param: str | None = None,
callback: Callable[[DialogResult, str], None] | None = None,
min_text_size: int = 0, password_mode: bool = False):
self.callback = callback
self.current_text = current_text
self.keyboard = Keyboard(max_text_size=255, min_text_size=min_text_size, password_mode=password_mode)
self.param = param
self._params = Params()
self.sub_title = sub_title
self.title = title
def show(self):
self.keyboard.reset(min_text_size=self.keyboard._min_text_size)
self.keyboard.set_title(tr(self.title), *(tr(self.sub_title),) if self.sub_title else ())
self.keyboard.set_text(self.current_text)
def internal_callback(result: DialogResult):
text = self.keyboard.text if result == DialogResult.CONFIRM else ""
if result == DialogResult.CONFIRM:
if self.param:
self._params.put(self.param, text)
if self.callback:
self.callback(result, text)
gui_app.set_modal_overlay(self.keyboard, internal_callback)
@@ -0,0 +1,58 @@
import os
import pytest
from openpilot.common.params import Params
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets.keyboard import Keyboard
from openpilot.system.ui.sunnypilot.widgets.input_dialog import InputDialogSP
os.environ['SDL_VIDEODRIVER'] = 'dummy'
if not os.environ.get('CI'):
pytest.skip("Test in CI environment, or comment out this flag to test locally", allow_module_level=True)
class TestInputDialog:
def setup_method(self):
self.params = Params()
def test_input_dialog_int(self):
gui_app.init_window("test window")
dialog = InputDialogSP("title", current_text="current_text", param="MapTargetVelocities")
dialog.show()
dialog.keyboard._render_return_status = 1
gui_app._handle_modal_overlay()
assert self.params.get("MapTargetVelocities") == "current_text"
def test_before_input_dialog(self):
gui_app.init_window("test window")
current_apn = "gsmapn"
# This tests the pre InputDialogSP, where keyboard setup had to be done for every single dialog box you want to use.
self.keyboard = Keyboard()
self.keyboard.reset(min_text_size=0)
self.keyboard.set_title(("Enter APN"), ("networking"))
self.keyboard.set_text(current_apn)
def pre_input_dialog_callback(result):
if result == 1:
apn = self.keyboard.text.strip()
self.params.put("GsmApn", apn)
gui_app.set_modal_overlay(self.keyboard, pre_input_dialog_callback)
self.keyboard.set_text("new_apn")
self.keyboard._render_return_status = 1
gui_app._handle_modal_overlay()
pre_input_dialog_result = self.params.get("GsmApn")
assert pre_input_dialog_result == "new_apn"
dialog = InputDialogSP(title="Enter APN", sub_title="networking", current_text=current_apn, param="GsmApn")
dialog.show()
dialog.keyboard.set_text("new_apn")
dialog.keyboard._render_return_status = 1
gui_app._handle_modal_overlay()
input_dialog_result = self.params.get("GsmApn")
assert input_dialog_result == "new_apn"
assert pre_input_dialog_result == input_dialog_result