Files
StarPilot/selfdrive/ui/layouts/settings/starpilot/sectioned_panel.py
T
2026-05-19 01:16:53 -04:00

176 lines
7.8 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
import pyray as rl
from openpilot.system.ui.lib.application import FontWeight, gui_app
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 Widget
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import (
SPACING, TileGrid, AetherScrollbar, AetherListColors, draw_list_scroll_fades,
)
@dataclass(frozen=True)
class TileSection:
title: str
grid: TileGrid
class SectionedTileLayout(Widget):
def __init__(self, section_gap: int = 28, title_height: int = 34, title_gap: int = 12,
min_row_height: int = 150, max_row_height: int = 280, top_padding: int = 0,
horizontal_padding: int = SPACING.xl, max_content_width: int | None = 1440):
super().__init__()
self._sections: list[TileSection] = []
self._section_gap = section_gap
self._title_height = title_height
self._title_gap = title_gap
self._min_row_height = min_row_height
self._max_row_height = max_row_height
self._top_padding = top_padding
self._horizontal_padding = horizontal_padding
self._max_content_width = max_content_width
self._title_font_size = 26
self._font_title = gui_app.font(FontWeight.BOLD)
self._is_active = False
self._scroll_panel = GuiScrollPanel2(horizontal=False)
self._scrollbar = AetherScrollbar()
self._scroll_offset = 0.0
self._content_height = 0.0
def set_sections(self, sections: list[TileSection]):
if self._is_active:
for section in self._sections:
section.grid.hide_event()
self._sections = list(sections)
if self._is_active:
for section in self._sections:
section.grid.show_event()
def clear(self):
self._sections.clear()
def show_event(self):
self._is_active = True
super().show_event()
self._scroll_offset = 0.0
for section in self._sections:
section.grid.show_event()
def hide_event(self):
self._is_active = False
super().hide_event()
for section in self._sections:
section.grid.hide_event()
def _content_height_for_layout(self, rect: rl.Rectangle, sections: list[TileSection], title_height: int, title_gap: int, section_gap: int, row_height: float) -> float:
total = self._top_padding
for index, section in enumerate(sections):
total += self._title_block_height(section, title_height, title_gap)
total += (section.grid.get_row_count(available_width=rect.width) * row_height) + section.grid.get_internal_gap_height(available_width=rect.width)
if index < len(sections) - 1:
total += section_gap
return total
def _title_block_height(self, section: TileSection, title_height: int | None = None, title_gap: int | None = None) -> int:
resolved_title_height = self._title_height if title_height is None else title_height
resolved_title_gap = self._title_gap if title_gap is None else title_gap
return (resolved_title_height + resolved_title_gap) if section.title else 0
def _section_band_height(self, section: TileSection, row_height: float) -> float:
rows = section.grid.get_row_count(available_width=self._content_rect(self._rect).width if self._rect.width else None)
if rows <= 0:
return 0.0
return (rows * row_height) + (section.grid.gap * max(0, rows - 1))
def _content_rect(self, rect: rl.Rectangle) -> rl.Rectangle:
content_x = rect.x + self._horizontal_padding
content_w = max(0.0, rect.width - (self._horizontal_padding * 2))
if self._max_content_width is not None and content_w > self._max_content_width:
content_w = float(self._max_content_width)
content_x = rect.x + (rect.width - content_w) / 2
return rl.Rectangle(content_x, rect.y, content_w, rect.height)
def _layout_profile(self, rect: rl.Rectangle, sections: list[TileSection]) -> tuple[int, int, int, float]:
total_rows = sum(section.grid.get_row_count(available_width=rect.width) for section in sections)
if total_rows <= 0:
return self._title_height, self._title_gap, self._section_gap, 0.0
total_internal_gaps = sum(section.grid.get_internal_gap_height(available_width=rect.width) for section in sections)
title_height = self._title_height
title_gap = self._title_gap
section_gap = self._section_gap
for compact_pass in range(2):
total_title_height = sum(self._title_block_height(section, title_height, title_gap) for section in sections)
total_section_gaps = section_gap * max(0, len(sections) - 1)
fit_row_height = max(0.0, (rect.height - self._top_padding - total_title_height - total_section_gaps - total_internal_gaps) / total_rows)
min_row_height = min(self._min_row_height, 124) if rect.width < 960 or compact_pass else self._min_row_height
if fit_row_height >= min_row_height:
return title_height, title_gap, section_gap, min(fit_row_height, self._max_row_height)
title_height = max(24, min(title_height, 28))
title_gap = max(4, min(title_gap, 6))
section_gap = max(12, min(section_gap, 16))
return title_height, title_gap, section_gap, fit_row_height
def _draw_section_title(self, rect: rl.Rectangle, title: str):
title_text = title
spacing = round(self._title_font_size * 0.08)
size = measure_text_cached(self._font_title, title_text, self._title_font_size, spacing=spacing)
text_y = rect.y + (rect.height - size.y) / 2
text_pos = rl.Vector2(round(rect.x), round(text_y))
rl.draw_text_ex(self._font_title, title_text, text_pos, self._title_font_size, spacing, rl.Color(236, 242, 250, 215))
line_x = rect.x + size.x + SPACING.lg
line_w = rect.width - (line_x - rect.x)
if line_w <= 0:
return
line_y = int(rect.y + rect.height / 2)
rl.draw_rectangle(int(line_x), line_y, int(line_w), 2, rl.Color(255, 255, 255, 36))
def _render(self, rect: rl.Rectangle):
self.set_rect(rect)
sections = [section for section in self._sections if section.grid.tiles]
if not sections:
return
content_rect = self._content_rect(rect)
title_height, title_gap, section_gap, row_height = self._layout_profile(content_rect, sections)
if row_height <= 0:
return
self._content_height = self._content_height_for_layout(content_rect, sections, title_height, title_gap, section_gap, row_height)
self._scroll_panel.set_enabled(self.is_visible)
self._scroll_offset = self._scroll_panel.update(content_rect, max(self._content_height, content_rect.height)) if self._content_height > content_rect.height else 0.0
rl.begin_scissor_mode(int(content_rect.x), int(content_rect.y), int(content_rect.width), int(content_rect.height))
y = content_rect.y + self._top_padding + self._scroll_offset
for index, section in enumerate(sections):
if section.title:
title_rect = rl.Rectangle(content_rect.x, y, content_rect.width, title_height)
self._draw_section_title(title_rect, section.title)
y += title_height + title_gap
active_grid_height = (section.grid.get_row_count(available_width=content_rect.width) * row_height) + section.grid.get_internal_gap_height(available_width=content_rect.width)
if hasattr(section.grid, "set_touch_valid_callback"):
section.grid.set_touch_valid_callback(lambda: self._scroll_panel.is_touch_valid())
section.grid.set_parent_rect(content_rect)
section.grid.render(rl.Rectangle(content_rect.x, y, content_rect.width, active_grid_height))
y += self._section_band_height(section, row_height)
if index < len(sections) - 1:
y += section_gap
rl.end_scissor_mode()
if self._content_height > content_rect.height:
self._scrollbar.render(content_rect, self._content_height, self._scroll_offset)
draw_list_scroll_fades(content_rect, self._content_height, self._scroll_offset,
AetherListColors.PANEL_BG)