diff --git a/selfdrive/ui/layouts/settings/settings.py b/selfdrive/ui/layouts/settings/settings.py index 4a29f408ee..a3e0f967f8 100644 --- a/selfdrive/ui/layouts/settings/settings.py +++ b/selfdrive/ui/layouts/settings/settings.py @@ -8,9 +8,18 @@ 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, FontWeight +from openpilot.system.ui.lib.scroller import Scroller from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.selfdrive.ui.layouts.network import NetworkLayout from openpilot.system.ui.lib.widget 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 # Import individual panels @@ -37,6 +46,14 @@ class PanelType(IntEnum): SOFTWARE = 3 FIREHOSE = 4 DEVELOPER = 5 + SUNNYLINK = 6 + MODELS = 7 + STEERING = 8 + CRUISE = 9 + VISUALS = 10 + OSM = 11 + TRIPS = 12 + VEHICLE = 13 @dataclass @@ -50,13 +67,26 @@ class SettingsLayout(Widget): def __init__(self): super().__init__() self._current_panel = PanelType.DEVICE + self._nav_items: list[Widget] = [] + + # Create sidebar scroller + self._sidebar_scroller = Scroller([], spacing=0, line_separator = False, pad_end=False) + # Panel configuration self._panels = { PanelType.DEVICE: PanelInfo("Device", DeviceLayout()), PanelType.NETWORK: PanelInfo("Network", NetworkLayout()), + PanelType.SUNNYLINK: PanelInfo("sunnylink", SunnylinkLayout()), PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout()), PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout()), + PanelType.MODELS: PanelInfo("Models", ModelsLayout()), + PanelType.STEERING: PanelInfo("Steering", SteeringLayout()), + PanelType.CRUISE: PanelInfo("Cruise", CruiseLayout()), + PanelType.VISUALS: PanelInfo("Visuals", VisualsLayout()), + PanelType.OSM: PanelInfo("OSM", OSMLayout()), + PanelType.TRIPS: PanelInfo("Trips", TripsLayout()), + PanelType.VEHICLE: PanelInfo("Vehicle", VehicleLayout()), PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout()), PanelType.DEVELOPER: PanelInfo("Developer", DeveloperLayout()), } @@ -79,6 +109,31 @@ class SettingsLayout(Widget): self._draw_sidebar(sidebar_rect) self._draw_current_panel(panel_rect) + def _create_nav_button(self, panel_type: 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 = TEXT_SELECTED if is_selected else TEXT_NORMAL + + # Draw button text (right-aligned) + text_size = measure_text_cached(self.parent._font_medium, self.panel_info.name, 65) + text_pos = rl.Vector2( + rect.x + rect.width - text_size.x - 20, # 50px padding from right + rect.y + (NAV_BTN_HEIGHT - text_size.y) / 2 + ) + rl.draw_text_ex(self.parent._font_medium, self.panel_info.name, text_pos, 65, 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, SIDEBAR_COLOR) @@ -102,6 +157,27 @@ class SettingsLayout(Widget): # 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 = 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, + rect.y + 300, # 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 + # Navigation buttons y = rect.y + 300 for panel_type, panel_info in self._panels.items(): diff --git a/selfdrive/ui/sunnypilot/layouts/settings/cruise.py b/selfdrive/ui/sunnypilot/layouts/settings/cruise.py new file mode 100644 index 0000000000..c97dc247a7 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/cruise.py @@ -0,0 +1,22 @@ +from openpilot.system.ui.lib.list_view import ListItem, button_item, toggle_item +from openpilot.system.ui.lib.scroller import Scroller +from openpilot.system.ui.lib.widget import Widget +from openpilot.common.params import Params + + +class CruiseLayout(Widget): + def __init__(self): + super().__init__() + + self._params = Params() + items = self._init_items() + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _init_items(self): + items = [ + + ] + return items + + def _render(self, rect): + self._scroller.render(rect) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/models.py b/selfdrive/ui/sunnypilot/layouts/settings/models.py new file mode 100644 index 0000000000..60553eac16 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/models.py @@ -0,0 +1,26 @@ +from openpilot.system.ui.lib.list_view import ListItem, button_item, toggle_item +from openpilot.system.ui.lib.scroller import Scroller +from openpilot.system.ui.lib.widget import Widget +from openpilot.common.params import Params + + +class ModelsLayout(Widget): + def __init__(self): + super().__init__() + + self._params = Params() + items = self._init_items() + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _init_items(self): + items = [ + button_item("Current Model", "SELECT", callback=self._on_model_select), + toggle_item("Live Learning Steer Delay"), + ] + return items + + def _render(self, rect): + self._scroller.render(rect) + + def _on_model_select(self): + return \ No newline at end of file diff --git a/selfdrive/ui/sunnypilot/layouts/settings/osm.py b/selfdrive/ui/sunnypilot/layouts/settings/osm.py new file mode 100644 index 0000000000..2888a9ce07 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/osm.py @@ -0,0 +1,22 @@ +from openpilot.system.ui.lib.list_view import ListItem, button_item, toggle_item +from openpilot.system.ui.lib.scroller import Scroller +from openpilot.system.ui.lib.widget import Widget +from openpilot.common.params import Params + + +class OSMLayout(Widget): + def __init__(self): + super().__init__() + + self._params = Params() + items = self._init_items() + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _init_items(self): + items = [ + + ] + return items + + def _render(self, rect): + self._scroller.render(rect) \ No newline at end of file diff --git a/selfdrive/ui/sunnypilot/layouts/settings/steering.py b/selfdrive/ui/sunnypilot/layouts/settings/steering.py new file mode 100644 index 0000000000..3814671997 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/steering.py @@ -0,0 +1,22 @@ +from openpilot.system.ui.lib.list_view import ListItem, button_item, toggle_item +from openpilot.system.ui.lib.scroller import Scroller +from openpilot.system.ui.lib.widget import Widget +from openpilot.common.params import Params + + +class SteeringLayout(Widget): + def __init__(self): + super().__init__() + + self._params = Params() + items = self._init_items() + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _init_items(self): + items = [ + + ] + return items + + def _render(self, rect): + self._scroller.render(rect) \ No newline at end of file diff --git a/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py b/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py new file mode 100644 index 0000000000..78d9e140bb --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/sunnylink.py @@ -0,0 +1,22 @@ +from openpilot.system.ui.lib.list_view import ListItem, button_item, toggle_item +from openpilot.system.ui.lib.scroller import Scroller +from openpilot.system.ui.lib.widget import Widget +from openpilot.common.params import Params + + +class SunnylinkLayout(Widget): + def __init__(self): + super().__init__() + + self._params = Params() + items = self._init_items() + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _init_items(self): + items = [ + + ] + return items + + def _render(self, rect): + self._scroller.render(rect) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/trips.py b/selfdrive/ui/sunnypilot/layouts/settings/trips.py new file mode 100644 index 0000000000..b0067bd3b5 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/trips.py @@ -0,0 +1,22 @@ +from openpilot.system.ui.lib.list_view import ListItem, button_item, toggle_item +from openpilot.system.ui.lib.scroller import Scroller +from openpilot.system.ui.lib.widget import Widget +from openpilot.common.params import Params + + +class TripsLayout(Widget): + def __init__(self): + super().__init__() + + self._params = Params() + items = self._init_items() + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _init_items(self): + items = [ + + ] + return items + + def _render(self, rect): + self._scroller.render(rect) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/vehicle.py b/selfdrive/ui/sunnypilot/layouts/settings/vehicle.py new file mode 100644 index 0000000000..d3efb60ef7 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/vehicle.py @@ -0,0 +1,22 @@ +from openpilot.system.ui.lib.list_view import ListItem, button_item, toggle_item +from openpilot.system.ui.lib.scroller import Scroller +from openpilot.system.ui.lib.widget import Widget +from openpilot.common.params import Params + + +class VehicleLayout(Widget): + def __init__(self): + super().__init__() + + self._params = Params() + items = self._init_items() + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _init_items(self): + items = [ + + ] + return items + + def _render(self, rect): + self._scroller.render(rect) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/visuals.py b/selfdrive/ui/sunnypilot/layouts/settings/visuals.py new file mode 100644 index 0000000000..7322029b57 --- /dev/null +++ b/selfdrive/ui/sunnypilot/layouts/settings/visuals.py @@ -0,0 +1,22 @@ +from openpilot.system.ui.lib.list_view import ListItem, button_item, toggle_item +from openpilot.system.ui.lib.scroller import Scroller +from openpilot.system.ui.lib.widget import Widget +from openpilot.common.params import Params + + +class VisualsLayout(Widget): + def __init__(self): + super().__init__() + + self._params = Params() + items = self._init_items() + self._scroller = Scroller(items, line_separator=True, spacing=0) + + def _init_items(self): + items = [ + + ] + return items + + def _render(self, rect): + self._scroller.render(rect) \ No newline at end of file diff --git a/system/ui/lib/toggle.py b/system/ui/lib/toggle.py index 8ed9a655ec..2fe7ed3a8a 100644 --- a/system/ui/lib/toggle.py +++ b/system/ui/lib/toggle.py @@ -1,5 +1,6 @@ import pyray as rl from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.sunnypilot.lib.toggle import ToggleSP ON_COLOR = rl.Color(51, 171, 76, 255) OFF_COLOR = rl.Color(0x39, 0x39, 0x39, 255) @@ -12,13 +13,14 @@ BG_HEIGHT = 60 ANIMATION_SPEED = 8.0 -class Toggle(Widget): +class Toggle(Widget, ToggleSP): def __init__(self, initial_state=False): super().__init__() self._state = initial_state self._enabled = True self._progress = 1.0 if initial_state else 0.0 self._target = self._progress + ToggleSP.__init__(self) def set_rect(self, rect: rl.Rectangle): self._rect = rl.Rectangle(rect.x, rect.y, WIDTH, HEIGHT) @@ -52,6 +54,9 @@ class Toggle(Widget): def _render(self, rect: rl.Rectangle): self.update() + if ToggleSP._render(self, self._rect): + return + if self._enabled: bg_color = self._blend_color(OFF_COLOR, ON_COLOR, self._progress) knob_color = KNOB_COLOR diff --git a/system/ui/sunnypilot/lib/list_view.py b/system/ui/sunnypilot/lib/list_view.py new file mode 100644 index 0000000000..08f68b8fed --- /dev/null +++ b/system/ui/sunnypilot/lib/list_view.py @@ -0,0 +1,59 @@ +import pyray as rl + +import openpilot.system.ui.lib.list_view as ListItem +from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.lib.text_measure import measure_text_cached + +LINE_PADDING = 40 +ITEM_BASE_HEIGHT = 170 +ITEM_PADDING = 20 +ITEM_TEXT_FONT_SIZE = 50 +ITEM_TEXT_COLOR = rl.WHITE +ITEM_DESC_TEXT_COLOR = rl.Color(128, 128, 128, 255) +ITEM_DESC_FONT_SIZE = 40 +ITEM_DESC_V_OFFSET = 140 +ICON_SIZE = 80 +BUTTON_WIDTH = 250 +BUTTON_HEIGHT = 100 +BUTTON_FONT_SIZE = 35 + +class ListItemSP(Widget): + + def __init__(self): + super().__init__() + + def _render(self, rect: rl.Rectangle): + # Handle click on title/description area for toggling description + if self.description and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT): + mouse_pos = rl.get_mouse_position() + + text_area_width = rect.width - self.get_action_width() - ITEM_PADDING + text_area_x = rect.x + if isinstance(self, ListItem.ToggleItem): + text_area_x = text_area_x + self.get_action_width() + ITEM_PADDING + text_area = rl.Rectangle(text_area_x, rect.y, text_area_width, rect.height) + + if rl.check_collision_point_rec(mouse_pos, text_area): + self.show_desc = not self.show_desc + + # Render title and description + x = rect.x + ITEM_PADDING + + # Draw description if visible + if self.show_desc and self._wrapped_description: + rl.draw_text_ex(self._font, self._wrapped_description, (x, rect.y + ITEM_DESC_V_OFFSET), + ITEM_DESC_FONT_SIZE, 0, ITEM_DESC_TEXT_COLOR) + + # Render action if needed + action_width = self.get_action_width() + if isinstance(self, ListItem.ToggleItem): + action_rect = rl.Rectangle(rect.x + ITEM_PADDING, rect.y, action_width, ITEM_BASE_HEIGHT) + x += action_width + ITEM_PADDING + else: + action_rect = rl.Rectangle(rect.x + rect.width - action_width, rect.y, action_width, ITEM_BASE_HEIGHT) + + text_size = measure_text_cached(self._font, self.title, ITEM_TEXT_FONT_SIZE) + title_y = rect.y + (ITEM_BASE_HEIGHT - text_size.y) // 2 + rl.draw_text_ex(self._font, self.title, (x, title_y), ITEM_TEXT_FONT_SIZE, 0, ITEM_TEXT_COLOR) + + return action_rect diff --git a/system/ui/sunnypilot/lib/toggle.py b/system/ui/sunnypilot/lib/toggle.py new file mode 100644 index 0000000000..c709691ace --- /dev/null +++ b/system/ui/sunnypilot/lib/toggle.py @@ -0,0 +1,93 @@ +import pyray as rl +import openpilot.system.ui.lib.toggle as ToggleOP + +ON_COLOR = rl.Color(28, 101, 186, 255) +OFF_COLOR = rl.Color(0x39, 0x39, 0x39, 255) +KNOB_COLOR = rl.WHITE +DISABLED_ON_COLOR = rl.Color(0x22, 0x77, 0x22, 255) # Dark green when disabled + on +DISABLED_OFF_COLOR = rl.Color(0x39, 0x39, 0x39, 255) +DISABLED_KNOB_COLOR = rl.Color(0x88, 0x88, 0x88, 255) +WIDTH, HEIGHT = 130, 80 +BG_HEIGHT = 60 + +class ToggleSP: + def _render(self, rect: rl.Rectangle): + + if self._enabled: + bg_color = ToggleOP.Toggle._blend_color(self, OFF_COLOR, ON_COLOR, self._progress) + knob_color = KNOB_COLOR + else: + bg_color = ToggleOP.Toggle._blend_color(self, DISABLED_OFF_COLOR, DISABLED_ON_COLOR, self._progress) + knob_color = DISABLED_KNOB_COLOR + + # Draw background + bg_rect = rl.Rectangle(self._rect.x + 5, self._rect.y + 10, WIDTH - 10, BG_HEIGHT) + + # Draw outline first + outline_color = 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 = 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 + 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, + ON_COLOR + ) + rl.draw_line_ex( + rl.Vector2(int(mid_x), int(mid_y)), + rl.Vector2(int(end_x), int(end_y)), + 3, + 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, + 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, + OFF_COLOR + ) + return True