import pyray as rl from dataclasses import dataclass from enum import IntEnum from collections.abc import Callable from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout from openpilot.selfdrive.ui.layouts.settings.starpilot.main_panel import StarPilotLayout 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, MousePos from openpilot.system.ui.lib.multilang import tr, tr_noop from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.network import NetworkUI # Constants SIDEBAR_WIDTH = 500 CLOSE_BTN_SIZE = 200 CLOSE_ICON_SIZE = 70 NAV_BTN_HEIGHT = 110 PANEL_MARGIN = 50 # Colors SIDEBAR_COLOR = rl.BLACK PANEL_COLOR = rl.BLACK CLOSE_BTN_COLOR = rl.Color(41, 41, 41, 255) CLOSE_BTN_PRESSED = rl.Color(59, 59, 59, 255) TEXT_NORMAL = rl.Color(128, 128, 128, 255) TEXT_SELECTED = rl.WHITE class PanelType(IntEnum): STARPILOT = 0 DEVICE = 1 NETWORK = 2 TOGGLES = 3 SOFTWARE = 4 DEVELOPER = 5 @dataclass class PanelInfo: name: str instance: Widget button_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) class SettingsLayout(Widget): def __init__(self): super().__init__() self._current_panel = PanelType.STARPILOT # Panel depth tracking for hierarchical back navigation # 0 = top level (settings main), 1+ = nested custom panels self._panel_depth = 0 # Panel configuration wifi_manager = WifiManager() wifi_manager.set_active(False) self._panels = { PanelType.STARPILOT: PanelInfo(tr_noop("StarPilot"), StarPilotLayout()), PanelType.DEVICE: PanelInfo(tr_noop("Device"), DeviceLayout()), PanelType.NETWORK: PanelInfo(tr_noop("Network"), NetworkUI(wifi_manager)), PanelType.TOGGLES: PanelInfo(tr_noop("Toggles"), TogglesLayout()), PanelType.SOFTWARE: PanelInfo(tr_noop("Software"), SoftwareLayout()), PanelType.DEVELOPER: PanelInfo(tr_noop("Developer"), DeveloperLayout()), } # Connect the custom-panel depth callback for hierarchical back navigation self._panels[PanelType.STARPILOT].instance.set_depth_callback(self.set_panel_depth) self._panels[PanelType.STARPILOT].instance.set_settings_layout(self) self._font_medium = gui_app.font(FontWeight.MEDIUM) self._close_icon = gui_app.texture("icons/backspace.png", CLOSE_ICON_SIZE, CLOSE_ICON_SIZE) # Callbacks self._close_callback: Callable | None = None def set_callbacks(self, on_close: Callable): self._close_callback = on_close def set_panel_depth(self, depth: int): self._panel_depth = depth def refresh_developer_visibility(self): pass def get_panel_depth(self) -> int: return self._panel_depth def _handle_back_navigation(self): if self._panel_depth > 0: self._panel_depth -= 1 if hasattr(self._panels[PanelType.STARPILOT].instance, 'navigate_back'): self._panels[PanelType.STARPILOT].instance.navigate_back() else: if self._close_callback: self._close_callback() def _render(self, rect: rl.Rectangle): # Calculate layout sidebar_rect = rl.Rectangle(rect.x, rect.y, SIDEBAR_WIDTH, rect.height) panel_rect = rl.Rectangle(rect.x + SIDEBAR_WIDTH, rect.y, rect.width - SIDEBAR_WIDTH, rect.height) # Draw components self._draw_sidebar(sidebar_rect) self._draw_current_panel(panel_rect) def _draw_sidebar(self, rect: rl.Rectangle): rl.draw_rectangle_rec(rect, SIDEBAR_COLOR) # Back/Close button - hierarchical navigation back_btn_rect = rl.Rectangle(rect.x + (rect.width - CLOSE_BTN_SIZE) / 2, rect.y + 60, CLOSE_BTN_SIZE, CLOSE_BTN_SIZE) pressed = gui_app.last_mouse_event.left_down and rl.check_collision_point_rec(gui_app.last_mouse_event.pos, back_btn_rect) close_color = CLOSE_BTN_PRESSED if pressed else CLOSE_BTN_COLOR rl.draw_rectangle_rounded(back_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( back_btn_rect.x + (back_btn_rect.width - self._close_icon.width) / 2, back_btn_rect.y + (back_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 back button rect for click detection self._back_btn_rect = back_btn_rect # Navigation buttons y = rect.y + 300 for panel_type, panel_info in self._panels.items(): button_rect = rl.Rectangle(rect.x + 50, y, rect.width - 150, NAV_BTN_HEIGHT) # Button styling is_selected = panel_type == self._current_panel text_color = TEXT_SELECTED if is_selected else TEXT_NORMAL # Draw button text (right-aligned) panel_name = tr(panel_info.name) text_size = measure_text_cached(self._font_medium, panel_name, 65) text_pos = rl.Vector2(button_rect.x + button_rect.width - text_size.x, button_rect.y + (button_rect.height - text_size.y) / 2) rl.draw_text_ex(self._font_medium, panel_name, rl.Vector2(round(text_pos.x), round(text_pos.y)), 65, 0, text_color) # Store button rect for click detection panel_info.button_rect = button_rect y += NAV_BTN_HEIGHT def _draw_current_panel(self, rect: rl.Rectangle): rl.draw_rectangle_rounded(rl.Rectangle(rect.x + 10, rect.y + 10, rect.width - 20, rect.height - 20), 0.04, 30, PANEL_COLOR) content_rect = rl.Rectangle(rect.x + PANEL_MARGIN, rect.y + 25, rect.width - (PANEL_MARGIN * 2), rect.height - 50) # rl.draw_rectangle_rounded(content_rect, 0.03, 30, PANEL_COLOR) panel = self._panels[self._current_panel] if panel.instance: panel.instance.render(content_rect) def _handle_mouse_release(self, mouse_pos: MousePos) -> None: # Check back/close button - hierarchical navigation if rl.check_collision_point_rec(mouse_pos, self._back_btn_rect): self._handle_back_navigation() return # Check navigation buttons for panel_type, panel_info in self._panels.items(): if rl.check_collision_point_rec(mouse_pos, panel_info.button_rect): self.set_current_panel(panel_type) return def set_current_panel(self, panel_type: PanelType): if panel_type != self._current_panel: self._panels[self._current_panel].instance.hide_event() self._current_panel = panel_type self._panels[self._current_panel].instance.show_event() def show_event(self): super().show_event() self._panels[self._current_panel].instance.show_event() def hide_event(self): super().hide_event() self._panels[self._current_panel].instance.hide_event()