BigUI WIP: Some panel standarization

This commit is contained in:
firestarsdog
2026-06-09 00:00:33 -04:00
parent a11d064aeb
commit 8cc9c59330
4 changed files with 116 additions and 144 deletions
@@ -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
@@ -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
@@ -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:
@@ -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."),