diff --git a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py index 969db6623..8430f959c 100644 --- a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py +++ b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py @@ -450,6 +450,108 @@ class AetherInteractiveMixin: self._can_click = True +class PanelManagerView(AetherInteractiveMixin, Widget): + """Shared base for list-panel ManagerViews used by some panels. + + Encapsulates scroll infrastructure, the standard ``_render`` pipeline, + shared two-column layout helpers, and a convenience toggle-tile factory. + Subclasses declare panel-specific metrics via ``METRICS`` and override + ``_draw_header``, ``_measure_content_height``, and + ``_draw_scroll_content`` (at minimum). + """ + + METRICS: AetherListMetrics = AETHER_LIST_METRICS + PANEL_STYLE: PanelStyle = DEFAULT_PANEL_STYLE + TWO_COLUMN_BREAKPOINT: int = 1180 + COLUMN_GAP: float = 22.0 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._scroll_panel = GuiScrollPanel2(horizontal=False) + self._scrollbar = AetherScrollbar() + self._content_height = 0.0 + self._scroll_offset = 0.0 + self._scroll_rect = rl.Rectangle(0, 0, 0, 0) + + # ── hooks ───────────────────────────────────────────────── + + def _on_frame_created(self, frame: AetherListFrame) -> None: + """Called after ``init_list_panel`` builds the panel frame. + Override to store ``frame.shell`` or perform per-frame setup.""" + + def _draw_header(self, header_rect: rl.Rectangle) -> None: + """Override to render the panel header.""" + + def _measure_content_height(self, content_width: float) -> float: + """Override to compute total scrollable content height.""" + + def _draw_scroll_content(self, scroll_rect: rl.Rectangle, content_width: float) -> None: + """Override to render visible content rows inside the scissor region.""" + + # ── render pipeline ─────────────────────────────────────── + + def _render(self, rect: rl.Rectangle) -> None: + self._interactive_rects.clear() + self.set_rect(rect) + + frame, scroll_rect, content_width = init_list_panel(rect, self.PANEL_STYLE, self.METRICS) + self._scroll_rect = scroll_rect + self._on_frame_created(frame) + + self._draw_header(frame.header) + + self._content_height = self._measure_content_height(content_width) + self._scroll_panel.set_enabled(self.is_visible) + self._scroll_offset = self._scroll_panel.update( + scroll_rect, max(self._content_height, scroll_rect.height)) + + rl.begin_scissor_mode( + int(scroll_rect.x), int(scroll_rect.y), + int(scroll_rect.width), int(scroll_rect.height)) + self._draw_scroll_content(scroll_rect, content_width) + rl.end_scissor_mode() + + if self._content_height > scroll_rect.height: + self._scrollbar.render(scroll_rect, self._content_height, self._scroll_offset) + + draw_list_scroll_fades(scroll_rect, self._content_height, self._scroll_offset, + AetherListColors.PANEL_BG) + + # ── shared layout helpers ───────────────────────────────── + + def _uses_two_columns(self, width: float) -> bool: + return width >= self.TWO_COLUMN_BREAKPOINT + + def _column_width(self, width: float) -> float: + return (width - self.COLUMN_GAP) / 2 if self._uses_two_columns(width) else width + + def _section_height(self, count: int, row_height: float) -> float: + return 0.0 if count <= 0 else count * row_height + + def _section_block_height(self, content_height: float) -> float: + if content_height <= 0: + return 0.0 + return self.METRICS.section_header_height + self.METRICS.section_header_gap + content_height + + def _stacked_section_height(self, sections: list[float]) -> float: + if not sections: + return 0.0 + return sum(sections) + self.METRICS.section_gap * (len(sections) - 1) + + # ── convenience builders ────────────────────────────────── + + def _make_toggle_tile(self, d: dict) -> ToggleTile: + return ToggleTile( + title=d["title"], + get_state=d.get("get_state", d.get("get")), + set_state=d.get("set_state", d.get("set")), + bg_color=self.PANEL_STYLE.accent, + desc=d.get("subtitle", d.get("desc", "")), + is_enabled=d.get("is_enabled"), + disabled_label=d.get("disabled_label", ""), + ) + + PANEL_HEADER_TITLE_Y: int = 4 PANEL_HEADER_SUBTITLE_Y: int = 48 PANEL_HEADER_TITLE_FONT_SIZE: int = 40 diff --git a/selfdrive/ui/layouts/settings/starpilot/sounds.py b/selfdrive/ui/layouts/settings/starpilot/sounds.py index 325d2a81e..b29f9c783 100644 --- a/selfdrive/ui/layouts/settings/starpilot/sounds.py +++ b/selfdrive/ui/layouts/settings/starpilot/sounds.py @@ -9,7 +9,6 @@ from openpilot.common.basedir import BASEDIR from openpilot.starpilot.common.starpilot_variables import ACTIVE_THEME_PATH from openpilot.system.ui.lib.application import gui_app, FontWeight, MouseEvent, MousePos from openpilot.system.ui.lib.multilang import tr, tr_noop -from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets.label import gui_label from openpilot.selfdrive.ui.ui_state import ui_state @@ -20,17 +19,15 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( COMPACT_PANEL_METRICS, AetherAdjustorRow, AetherListColors, - AetherScrollbar, + PanelManagerView, TileGrid, ToggleTile, DEFAULT_PANEL_STYLE, _point_hits, draw_action_pill, draw_list_group_shell, - draw_list_scroll_fades, draw_section_header, draw_settings_panel_header, - init_list_panel, AetherSliderDialog, ) @@ -53,7 +50,9 @@ SOUNDS_PANEL_METRICS = replace( ) -class SoundsManagerView(Widget): +class SoundsManagerView(PanelManagerView): + METRICS = SOUNDS_PANEL_METRICS + def __init__(self, controller: StarPilotSoundsLayout): super().__init__() self._controller = controller @@ -62,11 +61,6 @@ class SoundsManagerView(Widget): self._can_click = True self._reset_rect = rl.Rectangle(0, 0, 0, 0) - self._scroll_panel = GuiScrollPanel2(horizontal=False) - self._scrollbar = AetherScrollbar() - self._content_height = 0.0 - self._scroll_offset = 0.0 - self._scroll_rect = rl.Rectangle(0, 0, 0, 0) self._tile_grid_h = 0.0 self._init_adjustors() @@ -245,33 +239,6 @@ class SoundsManagerView(Widget): self._pressed_target = None self._can_click = True - def _render(self, rect: rl.Rectangle): - self.set_rect(rect) - - frame, scroll_rect, content_width = init_list_panel(rect, PANEL_STYLE, metrics=SOUNDS_PANEL_METRICS) - self._scroll_rect = scroll_rect - - self._draw_header(frame.header) - - self._content_height = self._measure_content_height(content_width) - - self._scroll_panel.set_enabled(self.is_visible) - self._scroll_offset = self._scroll_panel.update( - scroll_rect, max(self._content_height, scroll_rect.height) - ) - - rl.begin_scissor_mode( - int(scroll_rect.x), int(scroll_rect.y), - int(scroll_rect.width), int(scroll_rect.height) - ) - self._draw_scroll_content(scroll_rect, content_width) - rl.end_scissor_mode() - - if self._content_height > scroll_rect.height: - self._scrollbar.render(scroll_rect, self._content_height, self._scroll_offset) - draw_list_scroll_fades(scroll_rect, self._content_height, self._scroll_offset, - AetherListColors.PANEL_BG) - def _measure_content_height(self, content_width: float) -> float: col_width = (content_width - SECTION_GAP) / 2 left_h = 0.0 diff --git a/selfdrive/ui/layouts/settings/starpilot/system_settings.py b/selfdrive/ui/layouts/settings/starpilot/system_settings.py index 72e3e5300..003081e80 100644 --- a/selfdrive/ui/layouts/settings/starpilot/system_settings.py +++ b/selfdrive/ui/layouts/settings/starpilot/system_settings.py @@ -15,7 +15,6 @@ import pyray as rl from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr, tr_noop -from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import DialogResult, Widget from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog @@ -29,16 +28,13 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.scribble import draw_cust from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( AETHER_LIST_METRICS, AetherAdjustorRow, - AetherInteractiveMixin, - AetherScrollbar, AetherSegmentedControl, AetherListColors, + DEFAULT_PANEL_STYLE, + PanelManagerView, TileGrid, ToggleTile, - DEFAULT_PANEL_STYLE, - init_list_panel, draw_list_group_shell, - draw_list_scroll_fades, draw_section_header, draw_selection_list_row, draw_settings_panel_header, @@ -103,25 +99,20 @@ SYSTEM_PANEL_METRICS = replace( ) -class SystemSettingsManagerView(AetherInteractiveMixin, Widget): +class SystemSettingsManagerView(PanelManagerView): HEADER_SUBTITLE_HEIGHT = 24 HEADER_SUMMARY_GAP = 6 HEADER_CARD_HEIGHT = 140 TAB_HEIGHT = 68 TAB_GAP = 10 TAB_BOTTOM_GAP = 18 - COLUMN_GAP = 22 - TWO_COLUMN_BREAKPOINT = 1180 ACTION_PILL_WIDTH = 132 DANGER_PILL_WIDTH = 112 + METRICS = SYSTEM_PANEL_METRICS def __init__(self, controller: StarPilotSystemLayout): super().__init__() self._controller = controller - self._scroll_panel = GuiScrollPanel2(horizontal=False) - self._scrollbar = AetherScrollbar() - self._content_height = 0.0 - self._scroll_offset = 0.0 self._adjustor_rows: dict[str, AetherAdjustorRow] = {} self._display_slider_keys = ["ScreenBrightness", "ScreenBrightnessOnroad", "ScreenTimeout", "ScreenTimeoutOnroad"] self._power_slider_keys = ["DeviceShutdown", "LowVoltageShutdown"] @@ -330,22 +321,6 @@ class SystemSettingsManagerView(AetherInteractiveMixin, Widget): ) ) - self._scroll_rect = rl.Rectangle(0, 0, 0, 0) - - def _section_height(self, count: int, row_height: float) -> float: - return 0.0 if count <= 0 else count * row_height - - def _stacked_section_height(self, sections: list[float]) -> float: - if not sections: - return 0.0 - return sum(sections) + SECTION_GAP * (len(sections) - 1) - - def _uses_two_columns(self, width: float) -> bool: - return width >= self.TWO_COLUMN_BREAKPOINT - - def _column_width(self, width: float) -> float: - return (width - self.COLUMN_GAP) / 2 if self._uses_two_columns(width) else width - def _tab_subtitle(self, tab_id: str) -> str: if tab_id == "basics": return tr("{} controls + {} toggles").format( @@ -456,30 +431,9 @@ class SystemSettingsManagerView(AetherInteractiveMixin, Widget): if target_id == "static:first_aid": gui_app.push_widget(AetherBackupsCareDialog(self._controller)) - - def _render(self, rect: rl.Rectangle): - self._interactive_rects.clear() - self.set_rect(rect) - - frame, scroll_rect, content_width = init_list_panel(rect, PANEL_STYLE, metrics=SYSTEM_PANEL_METRICS) - self._scroll_rect = scroll_rect - + def _on_frame_created(self, frame) -> None: self._drive_mode_control.set_parent_rect(frame.header) - self._draw_header(frame.header) - self._content_height = self._measure_content_height(content_width) - self._scroll_panel.set_enabled(self.is_visible) - self._scroll_offset = self._scroll_panel.update(scroll_rect, max(self._content_height, scroll_rect.height)) - - rl.begin_scissor_mode(int(scroll_rect.x), int(scroll_rect.y), int(scroll_rect.width), int(scroll_rect.height)) - self._draw_scroll_content(scroll_rect, content_width) - rl.end_scissor_mode() - - if self._content_height > scroll_rect.height: - self._scrollbar.render(scroll_rect, self._content_height, self._scroll_offset) - - draw_list_scroll_fades(scroll_rect, self._content_height, self._scroll_offset, AetherListColors.PANEL_BG, fade_height=FADE_HEIGHT) - def _draw_header(self, rect: rl.Rectangle): draw_settings_panel_header(rect, tr("System Settings"), tr("Manage display, backups, connectivity, and device maintenance from one touch-first panel."), @@ -562,9 +516,6 @@ class SystemSettingsManagerView(AetherInteractiveMixin, Widget): else: return self._stacked_section_height([display_h, power_h, tiles_content_h + 24]) - def _section_block_height(self, content_height: float) -> float: - return SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP + content_height - def _slider_section_height(self, keys: list[str], width: float) -> float: total = 0.0 for key in keys: diff --git a/selfdrive/ui/layouts/settings/starpilot/vehicle.py b/selfdrive/ui/layouts/settings/starpilot/vehicle.py index 319eeb048..5a7b1c48d 100644 --- a/selfdrive/ui/layouts/settings/starpilot/vehicle.py +++ b/selfdrive/ui/layouts/settings/starpilot/vehicle.py @@ -5,7 +5,6 @@ import pyray as rl from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.lib.multilang import tr, tr_noop -from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.system.ui.widgets import DialogResult, Widget from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets.label import gui_label @@ -14,19 +13,16 @@ from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import _SettingsPage from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( AetherListMetrics, - AetherInteractiveMixin, AetherListColors, - AetherScrollbar, AetherSliderDialog, DEFAULT_PANEL_STYLE, + PanelManagerView, draw_list_group_shell, - draw_list_scroll_fades, draw_section_header, draw_selection_list_row, draw_settings_list_row, draw_settings_panel_header, draw_soft_card, - init_list_panel, TileGrid, ToggleTile, _with_alpha, @@ -92,22 +88,16 @@ FADE_HEIGHT = CUSTOM_METRICS.fade_height PANEL_STYLE = DEFAULT_PANEL_STYLE -class VehicleSettingsManagerView(AetherInteractiveMixin, Widget): +class VehicleSettingsManagerView(PanelManagerView): HEADER_SUBTITLE_HEIGHT = 22 HEADER_SUMMARY_GAP = 10 HEADER_CARD_HEIGHT = 100 - TWO_COLUMN_BREAKPOINT = 1180 - COLUMN_GAP = 22 + METRICS = CUSTOM_METRICS def __init__(self, controller: "StarPilotVehicleSettingsLayout"): super().__init__() self._controller = controller - self._scroll_panel = GuiScrollPanel2(horizontal=False) - self._scrollbar = AetherScrollbar() - self._content_height = 0.0 - self._scroll_offset = 0.0 self._shell_rect = rl.Rectangle(0, 0, 0, 0) - self._scroll_rect = rl.Rectangle(0, 0, 0, 0) self._toggle_grid = TileGrid(columns=2, padding=12, min_tile_width=100) self._toggle_grid.set_touch_valid_callback(lambda: self._scroll_panel.is_touch_valid()) @@ -242,24 +232,8 @@ class VehicleSettingsManagerView(AetherInteractiveMixin, Widget): self._last_model = current_model self._rebuild_toggle_grid() - def _uses_two_columns(self, width: float) -> bool: - return width >= self.TWO_COLUMN_BREAKPOINT - - def _column_width(self, width: float) -> float: - return (width - self.COLUMN_GAP) / 2 if self._uses_two_columns(width) else width - - def _section_height(self, count: int, row_height: float) -> float: - return 0.0 if count <= 0 else count * row_height - - def _section_block_height(self, content_height: float) -> float: - if content_height <= 0: - return 0.0 - return SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP + content_height - - def _stacked_section_height(self, sections: list[float]) -> float: - if not sections: - return 0.0 - return sum(sections) + SECTION_GAP * (len(sections) - 1) + def _on_frame_created(self, frame) -> None: + self._shell_rect = frame.shell def _activate_target(self, target_id: str | None): if not target_id: @@ -270,28 +244,6 @@ class VehicleSettingsManagerView(AetherInteractiveMixin, Widget): elif prefix == "select": self._controller._on_select(value) - def _render(self, rect: rl.Rectangle): - self.set_rect(rect) - - frame, scroll_rect, content_width = init_list_panel(rect, PANEL_STYLE, CUSTOM_METRICS) - self._shell_rect = frame.shell - self._scroll_rect = scroll_rect - - self._draw_header(frame.header) - - self._content_height = self._measure_content_height(content_width) - self._scroll_panel.set_enabled(self.is_visible) - self._scroll_offset = self._scroll_panel.update(scroll_rect, max(self._content_height, scroll_rect.height)) - - rl.begin_scissor_mode(int(scroll_rect.x), int(scroll_rect.y), int(scroll_rect.width), int(scroll_rect.height)) - self._draw_scroll_content(scroll_rect, content_width) - rl.end_scissor_mode() - - if self._content_height > scroll_rect.height: - self._scrollbar.render(scroll_rect, self._content_height, self._scroll_offset) - - draw_list_scroll_fades(scroll_rect, self._content_height, self._scroll_offset, AetherListColors.PANEL_BG, fade_height=FADE_HEIGHT) - def _draw_header(self, rect: rl.Rectangle): draw_settings_panel_header(rect, tr("Vehicle Settings"), tr("Configure vehicle fingerprint, driving features, and steering controls."),