BigUI WIP: Spacing

This commit is contained in:
firestarsdog
2026-04-09 21:44:09 -04:00
parent 3cb5792926
commit e91d0bef87
5 changed files with 287 additions and 265 deletions
@@ -16,6 +16,23 @@ TILE_PADDING = 20
SLIDER_BUTTON_SIZE = 60
class SPACING:
xs: int = 4
sm: int = 8
md: int = 12
lg: int = 16
xl: int = 24
xxl: int = 32
xxxl: int = 48
tile_gap: int = 16
tile_content: int = 16
line_gap: int = 8
section_gap: int = 24
tab_height: int = 96
tab_panel_gap: int = 16
def hex_to_color(hex_str: str) -> rl.Color:
hex_str = hex_str.lstrip('#')
return rl.Color(int(hex_str[0:2], 16), int(hex_str[2:4], 16), int(hex_str[4:6], 16), 255)
@@ -141,7 +158,7 @@ class AetherTile(Widget):
rl.draw_text_ex(font, text, rl.Vector2(round(draw_x), round(pos.y + nudge_y)), actual_font_size, spacing, rl.WHITE)
def _centered_content(self, face: rl.Rectangle, icon: rl.Texture2D | None, icon_scale: float, title_font_size: float, text_lines: int, line_heights: list[float]):
line_spacing = 8
line_spacing = SPACING.line_gap
total_h = sum(line_heights) + line_spacing * (text_lines - 1)
icon_w = icon.width * icon_scale if icon else 0
icon_h = icon.height * icon_scale if icon else 0
@@ -197,20 +214,21 @@ class HubTile(AetherTile):
def _render(self, rect: rl.Rectangle):
face = self._render_layers(rect)
max_w = face.width - 40
content_pad = SPACING.tile_content
max_w = face.width - (content_pad * 2)
lines = self._wrap_text(self._font_title, self.title, max_w, 30)
line_heights = [30] * len(lines)
_, ty = self._centered_content(face, self._icon, 0.75, 30, len(line_heights), line_heights)
line_h = 30
line_spacing = 8
line_spacing = SPACING.line_gap
for i, line in enumerate(lines):
self._draw_text_fit(self._font_title, line, rl.Vector2(face.x + 20, ty + i * (line_h + line_spacing)), max_w, line_h, align_center=True)
self._draw_text_fit(self._font_title, line, rl.Vector2(face.x + content_pad, ty + i * (line_h + line_spacing)), max_w, line_h, align_center=True)
if self.desc:
desc_lines = self._wrap_text(self._font_desc, self.desc, max_w, 18, max_lines=3)
desc_y = ty + len(lines) * (line_h + line_spacing) + 18
desc_y = ty + len(lines) * (line_h + line_spacing) + SPACING.lg
for i, line in enumerate(desc_lines):
self._draw_text_fit(self._font_desc, line, rl.Vector2(face.x + 20, desc_y + i * 20), max_w, 18, align_center=True)
self._draw_text_fit(self._font_desc, line, rl.Vector2(face.x + content_pad, desc_y + i * 20), max_w, 18, align_center=True)
class ToggleTile(AetherTile):
@@ -250,16 +268,17 @@ class ToggleTile(AetherTile):
face = self._render_layers(rect)
line_heights = [28, 30]
_, ty = self._centered_content(face, self._icon, 0.75, 28, len(line_heights), line_heights)
max_w = face.width - 40
self._draw_text_fit(self._font, self.title, rl.Vector2(face.x + 20, ty), max_w, 28, align_center=True, uppercase=True)
content_pad = SPACING.tile_content
max_w = face.width - (content_pad * 2)
self._draw_text_fit(self._font, self.title, rl.Vector2(face.x + content_pad, ty), max_w, 28, align_center=True, uppercase=True)
if enabled:
state_text = tr("ON") if active else tr("OFF")
else:
state_text = tr(self._disabled_label) if self._disabled_label else tr("LOCKED")
self._draw_text_fit(self._font, state_text, rl.Vector2(face.x + 20, ty + 28 + 8), max_w, 30, align_center=True, uppercase=True)
self._draw_text_fit(self._font, state_text, rl.Vector2(face.x + content_pad, ty + 28 + SPACING.line_gap), max_w, 30, align_center=True, uppercase=True)
if self.desc:
self._draw_text_fit(self._font_desc, self.desc, rl.Vector2(face.x + 20, ty + 28 + 8 + 34), max_w, 18, align_center=True)
self._draw_text_fit(self._font_desc, self.desc, rl.Vector2(face.x + content_pad, ty + 28 + SPACING.line_gap + 34), max_w, 18, align_center=True)
class ValueTile(AetherTile):
@@ -285,13 +304,14 @@ class ValueTile(AetherTile):
face = self._render_layers(rect)
line_heights = [28, 28]
_, ty = self._centered_content(face, self._icon, 0.75, 28, len(line_heights), line_heights)
max_w = face.width - 40
self._draw_text_fit(self._font, self.title, rl.Vector2(face.x + 20, ty), max_w, 28, align_center=True, uppercase=True)
content_pad = SPACING.tile_content
max_w = face.width - (content_pad * 2)
self._draw_text_fit(self._font, self.title, rl.Vector2(face.x + content_pad, ty), max_w, 28, align_center=True, uppercase=True)
val_text = self.get_value()
self._draw_text_fit(self._font, val_text, rl.Vector2(face.x + 20, ty + 28 + 8), max_w, 28, align_center=True, uppercase=True)
self._draw_text_fit(self._font, val_text, rl.Vector2(face.x + content_pad, ty + 28 + SPACING.line_gap), max_w, 28, align_center=True, uppercase=True)
if self.desc:
self._draw_text_fit(self._font_desc, self.desc, rl.Vector2(face.x + 20, ty + 28 + 8 + 34), max_w, 18, align_center=True)
self._draw_text_fit(self._font_desc, self.desc, rl.Vector2(face.x + content_pad, ty + 28 + SPACING.line_gap + 34), max_w, 18, align_center=True)
class AetherSlider(Widget):
@@ -488,38 +508,41 @@ class AetherSliderDialog(Widget):
self._cancel_offset += (self._cancel_target - self._cancel_offset) * (1 - math.exp(-dt / PLATE_TAU))
rl.draw_rectangle(0, 0, gui_app.width, gui_app.height, rl.Color(0, 0, 0, 160))
dialog_w, dialog_h = 1000, 500
dialog_margin = SPACING.xxl
button_height = 80
button_width = 350
dx, dy = rect.x + (rect.width - dialog_w) / 2, rect.y + (rect.height - dialog_h) / 2
self._ok_rect = rl.Rectangle(dx + dialog_w - 450, dy + dialog_h - 120, 350, 80)
self._cancel_rect = rl.Rectangle(dx + 100, dy + dialog_h - 120, 350, 80)
self._ok_rect = rl.Rectangle(dx + dialog_w - button_width - SPACING.lg, dy + dialog_h - button_height - SPACING.lg, button_width, button_height)
self._cancel_rect = rl.Rectangle(dx + SPACING.lg, dy + dialog_h - button_height - SPACING.lg, button_width, button_height)
d_rect = rl.Rectangle(dx, dy, dialog_w, dialog_h)
rl.draw_rectangle_rounded(d_rect, 0.05, 10, rl.Color(30, 30, 30, 255))
rl.draw_rectangle_rounded_lines_ex(d_rect, 0.05, 10, 2, self._color)
ts = measure_text_cached(self._font_title, self.title, 50)
rl.draw_text_ex(self._font_title, self.title, rl.Vector2(round(dx + (dialog_w - ts.x) / 2), round(dy + 40)), 50, 0, rl.WHITE)
slider_rect = rl.Rectangle(dx + 100, dy + 200, dialog_w - 200, 100)
rl.draw_text_ex(self._font_title, self.title, rl.Vector2(round(dx + (dialog_w - ts.x) / 2), round(dy + SPACING.xxl)), 50, 0, rl.WHITE)
slider_rect = rl.Rectangle(dx + SPACING.xxl, dy + 200, dialog_w - (SPACING.xxl * 2), 100)
self._slider.render(slider_rect)
c_shadow_alpha = int(255 * (1.0 - 0.9 * self._cancel_offset))
rl.draw_rectangle_rounded(rl.Rectangle(self._cancel_rect.x + GEOMETRY_OFFSET, self._cancel_rect.y + GEOMETRY_OFFSET, 350, 80), 0.2, 10, rl.Color(30, 30, 30, c_shadow_alpha))
rl.draw_rectangle_rounded(rl.Rectangle(self._cancel_rect.x + GEOMETRY_OFFSET, self._cancel_rect.y + GEOMETRY_OFFSET, button_width, button_height), 0.2, 10, rl.Color(30, 30, 30, c_shadow_alpha))
c_face_x = self._cancel_rect.x + GEOMETRY_OFFSET * self._cancel_offset
c_face_y = self._cancel_rect.y + GEOMETRY_OFFSET * self._cancel_offset
c_face = rl.Rectangle(c_face_x, c_face_y, 350, 80)
c_face = rl.Rectangle(c_face_x, c_face_y, button_width, button_height)
rl.draw_rectangle_rounded(c_face, 0.2, 10, rl.Color(80, 80, 80, 255))
rl.draw_rectangle_rounded(rl.Rectangle(c_face.x + 1, c_face.y + 1, c_face.width - 2, c_face.height - 2), 0.2, 10, rl.Color(0, 0, 0, 80))
rl.draw_rectangle_rounded(rl.Rectangle(c_face.x, c_face.y, c_face.width - 1.5, c_face.height - 1.5), 0.2, 10, rl.Color(255, 255, 255, 110))
cts = measure_text_cached(self._font_btn, tr("CANCEL"), 35)
cancel_text_pos = rl.Vector2(c_face_x + (350 - cts.x) / 2, c_face_y + (80 - cts.y) / 2)
cancel_text_pos = rl.Vector2(c_face_x + (button_width - cts.x) / 2, c_face_y + (button_height - cts.y) / 2)
rl.draw_text_ex(self._font_btn, tr("CANCEL"), rl.Vector2(round(cancel_text_pos.x + 1), round(cancel_text_pos.y + 2)), 35, 0, rl.Color(0, 0, 0, 90))
rl.draw_text_ex(self._font_btn, tr("CANCEL"), rl.Vector2(round(cancel_text_pos.x), round(cancel_text_pos.y)), 35, 0, rl.WHITE)
o_shadow_alpha = int(255 * (1.0 - 0.9 * self._ok_offset))
rl.draw_rectangle_rounded(rl.Rectangle(self._ok_rect.x + GEOMETRY_OFFSET, self._ok_rect.y + GEOMETRY_OFFSET, 350, 80), 0.2, 10, rl.Color(self._color.r, self._color.g, self._color.b, int(o_shadow_alpha * 0.4)))
rl.draw_rectangle_rounded(rl.Rectangle(self._ok_rect.x + GEOMETRY_OFFSET, self._ok_rect.y + GEOMETRY_OFFSET, button_width, button_height), 0.2, 10, rl.Color(self._color.r, self._color.g, self._color.b, int(o_shadow_alpha * 0.4)))
o_face_x = self._ok_rect.x + GEOMETRY_OFFSET * self._ok_offset
o_face_y = self._ok_rect.y + GEOMETRY_OFFSET * self._ok_offset
o_face = rl.Rectangle(o_face_x, o_face_y, 350, 80)
o_face = rl.Rectangle(o_face_x, o_face_y, button_width, button_height)
rl.draw_rectangle_rounded(o_face, 0.2, 10, self._color)
rl.draw_rectangle_rounded(rl.Rectangle(o_face.x + 1, o_face.y + 1, o_face.width - 2, o_face.height - 2), 0.2, 10, rl.Color(0, 0, 0, 80))
rl.draw_rectangle_rounded(rl.Rectangle(o_face.x, o_face.y, o_face.width - 1.5, o_face.height - 1.5), 0.2, 10, rl.Color(255, 255, 255, 110))
ots = measure_text_cached(self._font_btn, tr("OK"), 35)
ok_text_pos = rl.Vector2(o_face_x + (350 - ots.x) / 2, o_face_y + (80 - ots.y) / 2)
ok_text_pos = rl.Vector2(o_face_x + (button_width - ots.x) / 2, o_face_y + (button_height - ots.y) / 2)
rl.draw_text_ex(self._font_btn, tr("OK"), rl.Vector2(round(ok_text_pos.x + 1), round(ok_text_pos.y + 2)), 35, 0, rl.Color(0, 0, 0, 90))
rl.draw_text_ex(self._font_btn, tr("OK"), rl.Vector2(round(ok_text_pos.x), round(ok_text_pos.y)), 35, 0, rl.WHITE)
return DialogResult.NO_ACTION
@@ -568,9 +591,9 @@ class RadioTileGroup(Widget):
self._option_targets.append(0.0)
for i in range(len(self._option_offsets)):
self._option_offsets[i] += (self._option_targets[i] - self._option_offsets[i]) * (1 - math.exp(-dt / PLATE_TAU))
padding = 16
gap = SPACING.lg
option_w = 240 if len(self.options) <= 3 else 188
total_width = len(self.options) * option_w + max(0, len(self.options) - 1) * padding
total_width = len(self.options) * option_w + max(0, len(self.options) - 1) * gap
if self.title:
title_size = measure_text_cached(self._font_title, self.title, 40)
rl.draw_text_ex(self._font_title, self.title, rl.Vector2(round(rect.x), round(rect.y + (rect.height - title_size.y) / 2)), 40, 0, rl.WHITE)
@@ -578,7 +601,7 @@ class RadioTileGroup(Widget):
else:
start_x = rect.x + (rect.width - total_width) / 2
for i, opt in enumerate(self.options):
r = rl.Rectangle(start_x + i * (option_w + padding), rect.y, option_w, rect.height)
r = rl.Rectangle(start_x + i * (option_w + gap), rect.y, option_w, rect.height)
self._option_rects.append(r)
is_active = i == self.current_index
color = self._active_color if is_active else self._inactive_color
@@ -593,7 +616,7 @@ class RadioTileGroup(Widget):
rl.draw_rectangle_rounded(rl.Rectangle(face_rect.x, face_rect.y, face_rect.width - 1.5, face_rect.height - 1.5), TILE_RADIUS, 10, rl.Color(255, 255, 255, 110))
font_size = 30
spacing = round(font_size * 0.08)
max_width = r.width - 28
max_width = r.width - (SPACING.lg + SPACING.xs)
ts = measure_text_cached(self._font, opt, font_size, spacing=spacing)
while font_size > 22 and ts.x > max_width:
font_size -= 1
@@ -605,9 +628,11 @@ class RadioTileGroup(Widget):
class TileGrid(Widget):
def __init__(self, columns: int | None = None, padding: int = 20, uniform_width: bool = False):
def __init__(self, columns: int | None = None, padding: int | None = None, uniform_width: bool = False):
super().__init__()
self._columns, self.padding, self.tiles = columns, padding, []
self._columns = columns
self._gap = padding if padding is not None else SPACING.tile_gap
self.tiles = []
self._uniform_width = uniform_width
def add_tile(self, tile: Widget): self.tiles.append(tile)
@@ -630,8 +655,8 @@ class TileGrid(Widget):
elif count <= 6: cols = 3
else: cols = 4
rows = (count + cols - 1) // cols
tile_h = (rect.height - (self.padding * (rows - 1))) / rows
uniform_tile_w = (rect.width - (self.padding * (cols - 1))) / cols if self._uniform_width else 0
tile_h = (rect.height - (self._gap * (rows - 1))) / rows
uniform_tile_w = (rect.width - (self._gap * (cols - 1))) / cols if self._uniform_width else 0
tile_idx = 0
for r in range(rows):
remaining = count - tile_idx
@@ -639,12 +664,12 @@ class TileGrid(Widget):
items_in_row = min(cols, remaining)
if self._uniform_width:
row_tile_w = uniform_tile_w
row_width = (row_tile_w * items_in_row) + (self.padding * (items_in_row - 1))
row_width = (row_tile_w * items_in_row) + (self._gap * (items_in_row - 1))
row_x = rect.x + (rect.width - row_width) / 2
else:
row_tile_w = (rect.width - (self.padding * (items_in_row - 1))) / items_in_row
row_tile_w = (rect.width - (self._gap * (items_in_row - 1))) / items_in_row
row_x = rect.x
for c in range(items_in_row):
tile = tiles_to_render[tile_idx]
tile.render(rl.Rectangle(row_x + c * (row_tile_w + self.padding), rect.y + r * (tile_h + self.padding), row_tile_w, tile_h))
tile.render(rl.Rectangle(row_x + c * (row_tile_w + self._gap), rect.y + r * (tile_h + self._gap), row_tile_w, tile_h))
tile_idx += 1
@@ -23,7 +23,7 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.themes import StarPilotTh
from openpilot.selfdrive.ui.layouts.settings.starpilot.vehicle import StarPilotVehicleSettingsLayout
from openpilot.selfdrive.ui.layouts.settings.starpilot.wheel import StarPilotWheelLayout
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, HubTile, RadioTileGroup
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, HubTile, RadioTileGroup, SPACING
STARPILOT_ICONS_DIR = "toggle_icons"
@@ -115,7 +115,7 @@ class StarPilotLayout(Widget):
StarPilotPanelType.VEHICLE,
)
self._main_grid = TileGrid(columns=None, padding=20)
self._main_grid = TileGrid(columns=None, padding=SPACING.tile_gap)
self._rebuild_grid()
def set_depth_callback(self, callback: Callable):
+133 -136
View File
@@ -8,177 +8,174 @@ import pyray as rl
from openpilot.common.params import Params
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, HubTile, ToggleTile, ValueTile, SPACING
class StarPilotPanelType(IntEnum):
MAIN = 0
SOUNDS = 1
DRIVING_MODEL = 2
LONGITUDINAL = 3
LATERAL = 4
MAPS = 5
NAVIGATION = 6
DATA = 7
DEVICE = 8
UTILITIES = 9
VISUALS = 10
THEMES = 11
VEHICLE = 12
WHEEL = 13
SYSTEM = 14
MAIN = 0
SOUNDS = 1
DRIVING_MODEL = 2
LONGITUDINAL = 3
LATERAL = 4
MAPS = 5
NAVIGATION = 6
DATA = 7
DEVICE = 8
UTILITIES = 9
VISUALS = 10
THEMES = 11
VEHICLE = 12
WHEEL = 13
SYSTEM = 14
@dataclass
class StarPilotPanelInfo:
name: str
instance: Widget
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, HubTile, ToggleTile, ValueTile
name: str
instance: Widget
class StarPilotPanel(Widget):
def __init__(self):
super().__init__()
self._params = Params()
self._params_memory = Params(memory=True)
self._navigate_callback: Callable | None = None
self._back_callback: Callable | None = None
self._current_sub_panel = ""
self._sub_panels: dict[str, Widget] = {}
self._scroller = None
self._tile_grid = None
self.CATEGORIES = []
def __init__(self):
super().__init__()
self._params = Params()
self._params_memory = Params(memory=True)
self._navigate_callback: Callable | None = None
self._back_callback: Callable | None = None
self._current_sub_panel = ""
self._sub_panels: dict[str, Widget] = {}
self._scroller = None
self._tile_grid = None
self.CATEGORIES = []
def set_navigate_callback(self, callback: Callable):
self._navigate_callback = callback
def set_navigate_callback(self, callback: Callable):
self._navigate_callback = callback
def set_back_callback(self, callback: Callable):
self._back_callback = callback
def set_back_callback(self, callback: Callable):
self._back_callback = callback
def set_current_sub_panel(self, sub_panel: str):
self._current_sub_panel = sub_panel
def set_current_sub_panel(self, sub_panel: str):
self._current_sub_panel = sub_panel
def _rebuild_grid(self):
if not self.CATEGORIES:
return
def _rebuild_grid(self):
if not self.CATEGORIES:
return
if self._tile_grid is None:
self._tile_grid = TileGrid(columns=None, padding=20)
if self._tile_grid is None:
self._tile_grid = TileGrid(columns=None, padding=SPACING.tile_gap)
self._tile_grid.clear()
self._tile_grid.clear()
for cat in self.CATEGORIES:
visible_fn = cat.get("visible")
if visible_fn is not None and not visible_fn():
continue
for cat in self.CATEGORIES:
visible_fn = cat.get("visible")
if visible_fn is not None and not visible_fn():
continue
tile_type = cat.get("type", "hub")
if tile_type == "hub":
on_click = cat.get("on_click")
if on_click is None:
on_click = lambda c=cat: self._navigate_to(c["panel"])
tile_type = cat.get("type", "hub")
if tile_type == "hub":
on_click = cat.get("on_click")
if on_click is None:
on_click = lambda c=cat: self._navigate_to(c["panel"])
tile = HubTile(
title=tr(cat["title"]),
desc=tr(cat.get("desc", "")),
icon_path=cat.get("icon"),
on_click=on_click,
starpilot_icon=cat.get("starpilot_icon", True),
bg_color=cat.get("color"),
)
elif tile_type == "toggle":
raw_set_state = cat["set_state"]
tile = HubTile(
title=tr(cat["title"]),
desc=tr(cat.get("desc", "")),
icon_path=cat.get("icon"),
on_click=on_click,
starpilot_icon=cat.get("starpilot_icon", True),
bg_color=cat.get("color"),
)
elif tile_type == "toggle":
raw_set_state = cat["set_state"]
def on_toggle(state: bool, setter=raw_set_state):
setter(state)
self._rebuild_grid()
def on_toggle(state: bool, setter=raw_set_state):
setter(state)
self._rebuild_grid()
tile = ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=on_toggle, icon_path=cat.get("icon"), bg_color=cat.get("color"), desc=tr(cat.get("desc", "")), is_enabled=cat.get("is_enabled"), disabled_label=cat.get("disabled_label", ""))
elif tile_type == "value":
tile = ValueTile(title=tr(cat["title"]), get_value=cat["get_value"], on_click=cat["on_click"], icon_path=cat.get("icon"), bg_color=cat.get("color"), is_enabled=cat.get("is_enabled"), desc=tr(cat.get("desc", "")))
else:
continue
tile = ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=on_toggle, icon_path=cat.get("icon"), bg_color=cat.get("color"), desc=tr(cat.get("desc", "")), is_enabled=cat.get("is_enabled"), disabled_label=cat.get("disabled_label", ""))
elif tile_type == "value":
tile = ValueTile(title=tr(cat["title"]), get_value=cat["get_value"], on_click=cat["on_click"], icon_path=cat.get("icon"), bg_color=cat.get("color"), is_enabled=cat.get("is_enabled"), desc=tr(cat.get("desc", "")))
else:
continue
self._tile_grid.add_tile(tile)
self._tile_grid.add_tile(tile)
def _navigate_to(self, sub_panel: str):
self._current_sub_panel = sub_panel
if self._navigate_callback:
self._navigate_callback(sub_panel)
def _navigate_to(self, sub_panel: str):
self._current_sub_panel = sub_panel
if self._navigate_callback:
self._navigate_callback(sub_panel)
def _go_back(self):
self._current_sub_panel = ""
if self._back_callback:
self._back_callback()
def _go_back(self):
self._current_sub_panel = ""
if self._back_callback:
self._back_callback()
def _render(self, rect: rl.Rectangle):
if self._current_sub_panel and self._current_sub_panel in self._sub_panels:
self._sub_panels[self._current_sub_panel].render(rect)
elif self.CATEGORIES and self._tile_grid:
self._tile_grid.render(rect)
elif self._scroller:
self._scroller.render(rect)
def _render(self, rect: rl.Rectangle):
if self._current_sub_panel and self._current_sub_panel in self._sub_panels:
self._sub_panels[self._current_sub_panel].render(rect)
elif self.CATEGORIES and self._tile_grid:
self._tile_grid.render(rect)
elif self._scroller:
self._scroller.render(rect)
def show_event(self):
super().show_event()
self._rebuild_grid()
if self._current_sub_panel and self._current_sub_panel in self._sub_panels:
self._sub_panels[self._current_sub_panel].show_event()
elif self._scroller:
self._scroller.show_event()
def show_event(self):
super().show_event()
self._rebuild_grid()
if self._current_sub_panel and self._current_sub_panel in self._sub_panels:
self._sub_panels[self._current_sub_panel].show_event()
elif self._scroller:
self._scroller.show_event()
def create_tile_panel(categories: list[dict], sub_panels: dict[str, Widget] | None = None) -> StarPilotPanel:
panel = StarPilotPanel()
panel.CATEGORIES = categories
panel._sub_panels = sub_panels or {}
panel._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True)
panel = StarPilotPanel()
panel.CATEGORIES = categories
panel._sub_panels = sub_panels or {}
panel._tile_grid = TileGrid(columns=2, padding=SPACING.tile_gap, uniform_width=True)
for name, child in panel._sub_panels.items():
if hasattr(child, 'set_navigate_callback'):
child.set_navigate_callback(panel._navigate_to)
if hasattr(child, 'set_back_callback'):
child.set_back_callback(panel._go_back)
for name, child in panel._sub_panels.items():
if hasattr(child, 'set_navigate_callback'):
child.set_navigate_callback(panel._navigate_to)
if hasattr(child, 'set_back_callback'):
child.set_back_callback(panel._go_back)
panel._rebuild_grid()
return panel
panel._rebuild_grid()
return panel
def create_master_toggle_panel(toggle_specs: list[dict], sub_panels: dict[str, Widget] | None = None,
extra_categories: list[dict] | None = None) -> StarPilotPanel:
panel = create_tile_panel([], sub_panels)
categories: list[dict] = []
extra_categories: list[dict] | None = None) -> StarPilotPanel:
panel = create_tile_panel([], sub_panels)
categories: list[dict] = []
for spec in toggle_specs:
get_state = spec["get_state"]
visible = spec.get("visible")
manage_enabled = spec.get("manage_enabled", get_state)
for spec in toggle_specs:
get_state = spec["get_state"]
visible = spec.get("visible")
manage_enabled = spec.get("manage_enabled", get_state)
categories.append({
"title": spec["title"],
"desc": spec.get("desc", ""),
"type": "toggle",
"get_state": get_state,
"set_state": spec["set_state"],
"icon": spec.get("icon"),
"color": spec.get("color"),
"visible": visible,
})
categories.append({
"title": spec["title"],
"desc": spec.get("desc", ""),
"type": "toggle",
"get_state": get_state,
"set_state": spec["set_state"],
"icon": spec.get("icon"),
"color": spec.get("color"),
"visible": visible,
})
categories.append({
"title": spec.get("manage_title", "Settings"),
"desc": spec.get("manage_desc", ""),
"type": "value",
"get_value": lambda enabled=get_state, active_label=spec.get("manage_label", "Manage"), inactive_label=spec.get("disabled_label", "Enable First"): tr(active_label) if enabled() else tr(inactive_label),
"on_click": lambda sub_panel=spec["panel"]: panel._navigate_to(sub_panel),
"is_enabled": manage_enabled,
"icon": spec.get("manage_icon", spec.get("icon")),
"color": spec.get("color"),
"visible": visible,
})
categories.append({
"title": spec.get("manage_title", "Settings"),
"desc": spec.get("manage_desc", ""),
"type": "value",
"get_value": lambda enabled=get_state, active_label=spec.get("manage_label", "Manage"), inactive_label=spec.get("disabled_label", "Enable First"): tr(active_label) if enabled() else tr(inactive_label),
"on_click": lambda sub_panel=spec["panel"]: panel._navigate_to(sub_panel),
"is_enabled": manage_enabled,
"icon": spec.get("manage_icon", spec.get("icon")),
"color": spec.get("color"),
"visible": visible,
})
panel.CATEGORIES = categories + list(extra_categories or [])
panel._rebuild_grid()
return panel
panel.CATEGORIES = categories + list(extra_categories or [])
panel._rebuild_grid()
return panel
@@ -12,7 +12,7 @@ from openpilot.system.ui.widgets import DialogResult
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, ToggleTile, AetherSliderDialog, RadioTileGroup
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, ToggleTile, AetherSliderDialog, RadioTileGroup, SPACING
class StarPilotSoundsLayout(StarPilotPanel):
COOLDOWN_KEY = "SwitchbackModeCooldown"
@@ -98,8 +98,8 @@ class StarPilotSoundsLayout(StarPilotPanel):
self._set_active_section(sub_panel)
def _render(self, rect):
tab_rect = rl.Rectangle(rect.x, rect.y, rect.width, 110)
panel_rect = rl.Rectangle(rect.x, rect.y + 140, rect.width, rect.height - 140)
tab_rect = rl.Rectangle(rect.x, rect.y, rect.width, SPACING.tab_height)
panel_rect = rl.Rectangle(rect.x, rect.y + SPACING.tab_height + SPACING.tab_panel_gap, rect.width, rect.height - SPACING.tab_height - SPACING.tab_panel_gap)
self._section_tabs.render(tab_rect)
self._sub_panels[self._active_section].render(panel_rect)
@@ -129,7 +129,7 @@ class StarPilotVolumeControlLayout(StarPilotPanel):
def __init__(self):
super().__init__()
self._init_sound_player()
self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True)
self._tile_grid = TileGrid(columns=2, padding=SPACING.tile_gap, uniform_width=True)
self.CATEGORIES = []
for key in StarPilotSoundsLayout.VOLUME_KEYS:
@@ -257,7 +257,7 @@ class StarPilotCustomAlertsLayout(StarPilotPanel):
def __init__(self):
super().__init__()
self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True)
self._tile_grid = TileGrid(columns=2, padding=SPACING.tile_gap, uniform_width=True)
self.CATEGORIES = []
for key in StarPilotSoundsLayout.CUSTOM_ALERTS_KEYS:
info = self.ALERT_INFO[key]
@@ -8,112 +8,112 @@ import pyray as rl
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import RadioTileGroup
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import RadioTileGroup, SPACING
@dataclass(frozen=True)
class TabSectionSpec:
key: str
label: str
panel: Widget
key: str
label: str
panel: Widget
class TabbedSectionHost(Widget):
def __init__(self, sections: list[TabSectionSpec]):
super().__init__()
if not sections:
raise ValueError("TabbedSectionHost requires at least one section")
def __init__(self, sections: list[TabSectionSpec]):
super().__init__()
if not sections:
raise ValueError("TabbedSectionHost requires at least one section")
self._sections = {spec.key: spec.panel for spec in sections}
self._section_order = [spec.key for spec in sections]
self._active_section = self._section_order[0]
self._navigate_callback: Callable | None = None
self._back_callback: Callable | None = None
self._current_sub_panel = ""
self._tab_height = 110
self._panel_top = 140
self._section_tabs = RadioTileGroup("", [tr(spec.label) for spec in sections], 0, self._on_tab_change)
self._sections = {spec.key: spec.panel for spec in sections}
self._section_order = [spec.key for spec in sections]
self._active_section = self._section_order[0]
self._navigate_callback: Callable | None = None
self._back_callback: Callable | None = None
self._current_sub_panel = ""
self._tab_height = SPACING.tab_height
self._panel_top = self._tab_height + SPACING.tab_panel_gap
self._section_tabs = RadioTileGroup("", [tr(spec.label) for spec in sections], 0, self._on_tab_change)
for key, panel in self._sections.items():
if hasattr(panel, "set_navigate_callback"):
panel.set_navigate_callback(lambda sub_panel, section_key=key: self._on_child_navigate(section_key, sub_panel))
if hasattr(panel, "set_back_callback"):
panel.set_back_callback(self._go_back)
for key, panel in self._sections.items():
if hasattr(panel, "set_navigate_callback"):
panel.set_navigate_callback(lambda sub_panel, section_key=key: self._on_child_navigate(section_key, sub_panel))
if hasattr(panel, "set_back_callback"):
panel.set_back_callback(self._go_back)
def set_navigate_callback(self, callback: Callable):
self._navigate_callback = callback
def set_navigate_callback(self, callback: Callable):
self._navigate_callback = callback
def set_back_callback(self, callback: Callable):
self._back_callback = callback
def set_back_callback(self, callback: Callable):
self._back_callback = callback
def set_current_sub_panel(self, sub_panel: str):
self._current_sub_panel = sub_panel
if not sub_panel:
panel = self._sections[self._active_section]
if hasattr(panel, "set_current_sub_panel"):
panel.set_current_sub_panel("")
return
def set_current_sub_panel(self, sub_panel: str):
self._current_sub_panel = sub_panel
if not sub_panel:
panel = self._sections[self._active_section]
if hasattr(panel, "set_current_sub_panel"):
panel.set_current_sub_panel("")
return
if ":" in sub_panel:
section_key, child_panel = sub_panel.split(":", 1)
self._activate_section(section_key, child_panel)
elif sub_panel in self._sections:
self._activate_section(sub_panel)
else:
panel = self._sections[self._active_section]
if hasattr(panel, "set_current_sub_panel"):
panel.set_current_sub_panel(sub_panel)
if ":" in sub_panel:
section_key, child_panel = sub_panel.split(":", 1)
self._activate_section(section_key, child_panel)
elif sub_panel in self._sections:
self._activate_section(sub_panel)
else:
panel = self._sections[self._active_section]
if hasattr(panel, "set_current_sub_panel"):
panel.set_current_sub_panel(sub_panel)
def _on_tab_change(self, index: int):
if 0 <= index < len(self._section_order):
self._current_sub_panel = ""
self._activate_section(self._section_order[index], "")
if self._navigate_callback:
self._navigate_callback("")
def _on_tab_change(self, index: int):
if 0 <= index < len(self._section_order):
self._current_sub_panel = ""
self._activate_section(self._section_order[index], "")
if self._navigate_callback:
self._navigate_callback("")
def _activate_section(self, section_key: str, child_panel: str = ""):
if section_key not in self._sections:
return
def _activate_section(self, section_key: str, child_panel: str = ""):
if section_key not in self._sections:
return
previous = self._active_section
if section_key != previous:
previous_panel = self._sections[previous]
if hasattr(previous_panel, "set_current_sub_panel"):
previous_panel.set_current_sub_panel("")
self._sections[previous].hide_event()
self._active_section = section_key
self._sections[section_key].show_event()
previous = self._active_section
if section_key != previous:
previous_panel = self._sections[previous]
if hasattr(previous_panel, "set_current_sub_panel"):
previous_panel.set_current_sub_panel("")
self._sections[previous].hide_event()
self._active_section = section_key
self._sections[section_key].show_event()
self._section_tabs.set_index(self._section_order.index(section_key))
panel = self._sections[section_key]
if hasattr(panel, "set_current_sub_panel"):
panel.set_current_sub_panel(child_panel)
self._section_tabs.set_index(self._section_order.index(section_key))
panel = self._sections[section_key]
if hasattr(panel, "set_current_sub_panel"):
panel.set_current_sub_panel(child_panel)
def _on_child_navigate(self, section_key: str, sub_panel: str):
self._current_sub_panel = f"{section_key}:{sub_panel}" if sub_panel else section_key
if self._navigate_callback:
self._navigate_callback(self._current_sub_panel)
def _on_child_navigate(self, section_key: str, sub_panel: str):
self._current_sub_panel = f"{section_key}:{sub_panel}" if sub_panel else section_key
if self._navigate_callback:
self._navigate_callback(self._current_sub_panel)
def _go_back(self):
self._current_sub_panel = ""
panel = self._sections[self._active_section]
if hasattr(panel, "set_current_sub_panel"):
panel.set_current_sub_panel("")
if self._back_callback:
self._back_callback()
def _go_back(self):
self._current_sub_panel = ""
panel = self._sections[self._active_section]
if hasattr(panel, "set_current_sub_panel"):
panel.set_current_sub_panel("")
if self._back_callback:
self._back_callback()
def _render(self, rect: rl.Rectangle):
tab_rect = rl.Rectangle(rect.x, rect.y, rect.width, self._tab_height)
panel_rect = rl.Rectangle(rect.x, rect.y + self._panel_top, rect.width, rect.height - self._panel_top)
self._section_tabs.render(tab_rect)
self._sections[self._active_section].render(panel_rect)
def _render(self, rect: rl.Rectangle):
tab_rect = rl.Rectangle(rect.x, rect.y, rect.width, self._tab_height)
panel_rect = rl.Rectangle(rect.x, rect.y + self._panel_top, rect.width, rect.height - self._panel_top)
self._section_tabs.render(tab_rect)
self._sections[self._active_section].render(panel_rect)
def show_event(self):
super().show_event()
self._section_tabs.show_event()
self._sections[self._active_section].show_event()
def show_event(self):
super().show_event()
self._section_tabs.show_event()
self._sections[self._active_section].show_event()
def hide_event(self):
super().hide_event()
self._section_tabs.hide_event()
self._sections[self._active_section].hide_event()
def hide_event(self):
super().hide_event()
self._section_tabs.hide_event()
self._sections[self._active_section].hide_event()