mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-08-23 01:04:01 +08:00
BigUI WIP: K, we'll start simpler
This commit is contained in:
@@ -36,6 +36,31 @@ _CONST_TERTIARY = rl.Color(145, 155, 175, 255)
|
||||
_NODE_NUM_MIN = 3
|
||||
_NODE_NUM_MAX = 5
|
||||
|
||||
|
||||
_GLOBAL_SCISSOR_LIMIT: rl.Rectangle | None = None
|
||||
|
||||
def aether_begin_scissor_mode(x: int, y: int, w: int, h: int) -> None:
|
||||
global _GLOBAL_SCISSOR_LIMIT
|
||||
if _GLOBAL_SCISSOR_LIMIT is not None:
|
||||
limit = _GLOBAL_SCISSOR_LIMIT
|
||||
ix1 = max(x, int(limit.x))
|
||||
iy1 = max(y, int(limit.y))
|
||||
ix2 = min(x + w, int(limit.x + limit.width))
|
||||
iy2 = min(y + h, int(limit.y + limit.height))
|
||||
iw = max(0, ix2 - ix1)
|
||||
ih = max(0, iy2 - iy1)
|
||||
rl.begin_scissor_mode(ix1, iy1, iw, ih)
|
||||
else:
|
||||
rl.begin_scissor_mode(x, y, w, h)
|
||||
|
||||
def aether_end_scissor_mode() -> None:
|
||||
global _GLOBAL_SCISSOR_LIMIT
|
||||
if _GLOBAL_SCISSOR_LIMIT is not None:
|
||||
limit = _GLOBAL_SCISSOR_LIMIT
|
||||
rl.begin_scissor_mode(int(limit.x), int(limit.y), int(limit.width), int(limit.height))
|
||||
else:
|
||||
rl.end_scissor_mode()
|
||||
|
||||
# Custom vector icon layout constants (scribble.py coordinate system)
|
||||
CUSTOM_ICON_BASE_SIZE = 100.0
|
||||
CUSTOM_ICON_SCALE_MULT = 1.25
|
||||
@@ -637,11 +662,11 @@ class PanelManagerView(AetherInteractiveMixin, Widget):
|
||||
if scroll_disabled:
|
||||
self._scroll_offset = 0.0
|
||||
|
||||
rl.begin_scissor_mode(
|
||||
aether_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()
|
||||
aether_end_scissor_mode()
|
||||
|
||||
self._draw_static_elements(scroll_rect, content_width)
|
||||
|
||||
@@ -769,13 +794,13 @@ class PanelManagerView(AetherInteractiveMixin, Widget):
|
||||
def _page_scissor_push(self, rect: rl.Rectangle | None) -> None:
|
||||
if rect is None:
|
||||
return
|
||||
rl.end_scissor_mode()
|
||||
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
||||
aether_end_scissor_mode()
|
||||
aether_begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
||||
|
||||
def _page_scissor_pop(self) -> None:
|
||||
rl.end_scissor_mode()
|
||||
rl.begin_scissor_mode(int(self._scroll_rect.x), int(self._scroll_rect.y),
|
||||
int(self._scroll_rect.width), int(self._scroll_rect.height))
|
||||
aether_end_scissor_mode()
|
||||
aether_begin_scissor_mode(int(self._scroll_rect.x), int(self._scroll_rect.y),
|
||||
int(self._scroll_rect.width), int(self._scroll_rect.height))
|
||||
|
||||
# ── drag + animation ───────────────────────────────────────
|
||||
|
||||
@@ -2834,10 +2859,10 @@ class AetherSettingsView(PanelManagerView):
|
||||
if scroll_disabled:
|
||||
self._scroll_offset = 0.0
|
||||
|
||||
rl.begin_scissor_mode(int(self._scroll_rect.x), int(self._scroll_rect.y),
|
||||
int(self._scroll_rect.width), int(self._scroll_rect.height))
|
||||
aether_begin_scissor_mode(int(self._scroll_rect.x), int(self._scroll_rect.y),
|
||||
int(self._scroll_rect.width), int(self._scroll_rect.height))
|
||||
self._draw_scroll_content(self._scroll_rect, content_width)
|
||||
rl.end_scissor_mode()
|
||||
aether_end_scissor_mode()
|
||||
|
||||
if self._content_height > self._scroll_rect.height and not scroll_disabled:
|
||||
self._scrollbar.render(self._scroll_rect, self._content_height, self._scroll_offset)
|
||||
@@ -3073,169 +3098,121 @@ def draw_back_button(pill_rect: rl.Rectangle, center_y: float, pressed: bool, ho
|
||||
BACK_BTN = "__back__"
|
||||
|
||||
|
||||
# ── AetherCategoryTileView — dynamic nesting doll tile view ──
|
||||
# ── AetherCategoryDrawer — touch-optimized sliding drawer ──
|
||||
|
||||
class AetherCategoryTileView(AetherSettingsView):
|
||||
"""Reusable nested tile view that maps SettingRows to interactive tiles."""
|
||||
class AetherCategoryDrawer(AetherSettingsView):
|
||||
"""Reusable nested drawer view that maps SettingRows to a slide-out vertical settings list."""
|
||||
|
||||
def __init__(self, controller, title: str, rows: list[SettingRow],
|
||||
*, color: rl.Color | str = "#8B5CF6", subtitle: str = "",
|
||||
panel_style=None):
|
||||
super().__init__(controller, [], header_title=title, header_subtitle=subtitle, panel_style=panel_style)
|
||||
# Group the rows in a single SettingSection with an empty title
|
||||
sections = [SettingSection(title="", rows=rows)]
|
||||
super().__init__(controller, sections, header_title=title, header_subtitle=subtitle, panel_style=panel_style)
|
||||
self._color = hex_to_color(color) if isinstance(color, str) else color
|
||||
self._rows = rows
|
||||
|
||||
self._scroll_panel = GuiScrollPanel2(horizontal=True)
|
||||
self._scroll_panel.snap_interval = 364.0
|
||||
self._tile_grid = TileGrid(padding=16, tile_height=178.0, carousel_rows=3, carousel_tile_width=348.0)
|
||||
self._tile_grid.set_touch_valid_callback(lambda: self._scroll_panel.is_touch_valid())
|
||||
self._child(self._tile_grid)
|
||||
|
||||
self._row_to_tile_map = {}
|
||||
for row in self._rows:
|
||||
tile = self._map_row_to_tile(row)
|
||||
if tile is not None:
|
||||
self._row_to_tile_map[row.id] = tile
|
||||
|
||||
self._slide_progress = 0.0
|
||||
self._back_btn_rect = None
|
||||
|
||||
def _map_row_to_tile(self, row: SettingRow) -> Widget | None:
|
||||
enabled_fn = row.enabled if row.enabled is not None else (lambda: True)
|
||||
subtitle_text = row.subtitle
|
||||
self._scroll_panel = GuiScrollPanel2(horizontal=False)
|
||||
self._scrollbar = AetherScrollbar()
|
||||
|
||||
if row.type == "toggle":
|
||||
return RowToggleTile(
|
||||
title=tr(row.title),
|
||||
get_state=row.get_state,
|
||||
set_state=row.set_state,
|
||||
bg_color=self._color,
|
||||
desc=tr(subtitle_text),
|
||||
is_enabled=enabled_fn,
|
||||
disabled_label=tr(row.disabled_label) if row.disabled_label else "",
|
||||
)
|
||||
elif row.type == "value":
|
||||
return RowPanelTile(
|
||||
title=tr(row.title),
|
||||
get_status=row.get_value,
|
||||
on_click=row.on_click,
|
||||
bg_color=self._color,
|
||||
desc=tr(subtitle_text),
|
||||
)
|
||||
elif row.type == "action":
|
||||
return RowPanelTile(
|
||||
title=tr(row.title),
|
||||
get_status=lambda: tr(row.action_text) if hasattr(row, 'action_text') and row.action_text else "",
|
||||
on_click=row.on_click,
|
||||
bg_color=self._color,
|
||||
desc=tr(subtitle_text),
|
||||
)
|
||||
return None
|
||||
# Read driving side dynamically for ergonomic layout (LHD vs RHD)
|
||||
try:
|
||||
from openpilot.common.params import Params
|
||||
self._is_rhd = Params().get_bool("IsRHD")
|
||||
except Exception:
|
||||
self._is_rhd = False
|
||||
|
||||
def _visible_rows(self) -> list[SettingRow]:
|
||||
def _visible_rows(self, section: SettingSection) -> list[SettingRow]:
|
||||
return [row for row in self._rows if row.visible is None or row.visible()]
|
||||
|
||||
def _update_visible_tiles(self):
|
||||
visible_rows = self._visible_rows()
|
||||
visible_ids = [row.id for row in visible_rows]
|
||||
if getattr(self, "_last_visible_ids", None) == visible_ids:
|
||||
return
|
||||
self._last_visible_ids = visible_ids
|
||||
self._tile_grid.clear()
|
||||
for row in visible_rows:
|
||||
tile = self._row_to_tile_map.get(row.id)
|
||||
if tile is not None:
|
||||
self._tile_grid.add_tile(tile)
|
||||
|
||||
def _measure_content_height(self, width: float) -> float:
|
||||
self._update_visible_tiles()
|
||||
return self._tile_grid.measure_height(width)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self.set_rect(rect)
|
||||
self._interactive_rects.clear()
|
||||
|
||||
# Dim background outside the dialog
|
||||
# Dim the screen area outside the drawer
|
||||
rl.draw_rectangle(int(rect.x), int(rect.y), int(rect.width), int(rect.height), rl.Color(0, 0, 0, 160))
|
||||
|
||||
dialog_w = 1600
|
||||
dialog_h = 750
|
||||
dx = rect.x + (rect.width - dialog_w) / 2
|
||||
dy = rect.y + (rect.height - dialog_h) / 2
|
||||
# Drawer slide-in animation (exponential smoothing)
|
||||
dt = rl.get_frame_time()
|
||||
self._slide_progress += (1.0 - self._slide_progress) * (1.0 - math.exp(-dt / 0.12))
|
||||
|
||||
# Draw custom dialog background and top color band
|
||||
d_rect = snap_rect(rl.Rectangle(dx, dy, dialog_w, dialog_h))
|
||||
draw_rounded_fill(d_rect, rl.Color(10, 12, 16, 255), radius_px=24)
|
||||
draw_rounded_stroke(d_rect, rl.Color(255, 255, 255, 16), radius_px=24)
|
||||
rl.draw_rectangle_rec(rl.Rectangle(d_rect.x, d_rect.y, d_rect.width, 3), self._color)
|
||||
drawer_w = 850
|
||||
if self._is_rhd:
|
||||
# RHD: slide out from the right (driver side)
|
||||
drawer_x = rect.x + rect.width - drawer_w * self._slide_progress
|
||||
drawer_rect = rl.Rectangle(drawer_x, rect.y + 12, drawer_w - 12, rect.height - 24)
|
||||
else:
|
||||
# LHD: slide out from the left (driver side)
|
||||
drawer_x = rect.x - drawer_w + drawer_w * self._slide_progress
|
||||
drawer_rect = rl.Rectangle(drawer_x + 12, rect.y + 12, drawer_w - 12, rect.height - 24)
|
||||
|
||||
header_rect = rl.Rectangle(dx + 60, dy + 24, dialog_w - 120, 100)
|
||||
# Draw drawer background and border
|
||||
draw_rounded_fill(drawer_rect, rl.Color(12, 10, 18, 250), radius_px=24)
|
||||
draw_rounded_stroke(drawer_rect, rl.Color(255, 255, 255, 18), radius_px=24)
|
||||
|
||||
# Draw accent stripe on the side separating it from the dimmed area
|
||||
if self._is_rhd:
|
||||
rl.draw_rectangle_rec(rl.Rectangle(drawer_rect.x, drawer_rect.y, 4, drawer_rect.height), self._color)
|
||||
else:
|
||||
rl.draw_rectangle_rec(rl.Rectangle(drawer_rect.x + drawer_rect.width - 4, drawer_rect.y, 4, drawer_rect.height), self._color)
|
||||
|
||||
# Draw header with navigation breadcrumbs
|
||||
header_rect = rl.Rectangle(drawer_rect.x + 36, drawer_rect.y + 24, drawer_rect.width - 72, 80)
|
||||
if self._has_header:
|
||||
self._draw_header(header_rect)
|
||||
|
||||
# Configure precise margins for the scroll area (80px sides)
|
||||
self._scroll_rect = rl.Rectangle(dx + 80, dy + 140, dialog_w - 160, dialog_h - 180)
|
||||
# Set up scroll area (vertical scroll list)
|
||||
self._scroll_rect = rl.Rectangle(
|
||||
drawer_rect.x + 36,
|
||||
drawer_rect.y + 120,
|
||||
drawer_rect.width - 72,
|
||||
drawer_rect.height - 144
|
||||
)
|
||||
|
||||
self._update_visible_tiles()
|
||||
content_width_needed = self._tile_grid.measure_width()
|
||||
content_height_needed = self._tile_grid.measure_height(self._scroll_rect.width)
|
||||
|
||||
scrolling_enabled = self.is_visible and (content_width_needed > self._scroll_rect.width)
|
||||
self._scroll_panel.set_enabled(scrolling_enabled)
|
||||
content_width = self._scroll_rect.width - AETHER_LIST_METRICS.content_right_gutter
|
||||
self._content_height = self._measure_content_height(content_width)
|
||||
self._scroll_panel.set_enabled(self.is_visible)
|
||||
|
||||
self._scroll_offset = self._scroll_panel.update(
|
||||
self._scroll_rect, max(content_width_needed, self._scroll_rect.width))
|
||||
|
||||
x_pad = 12
|
||||
y_pad = 24
|
||||
rl.begin_scissor_mode(int(self._scroll_rect.x - x_pad), int(self._scroll_rect.y - y_pad),
|
||||
int(self._scroll_rect.width + x_pad * 2), int(self._scroll_rect.height + y_pad * 2))
|
||||
|
||||
self._tile_grid._parent_rect = self._scroll_rect
|
||||
|
||||
y_margin = max(0, (self._scroll_rect.height - content_height_needed) / 2)
|
||||
x_margin = max(0, (self._scroll_rect.width - content_width_needed) / 2)
|
||||
|
||||
grid_rect = rl.Rectangle(
|
||||
self._scroll_rect.x + self._scroll_offset + x_margin,
|
||||
self._scroll_rect.y + y_margin,
|
||||
max(content_width_needed, self._scroll_rect.width),
|
||||
self._scroll_rect.height
|
||||
self._scroll_rect, max(self._content_height, self._scroll_rect.height)
|
||||
)
|
||||
self._tile_grid.render(grid_rect)
|
||||
|
||||
rl.end_scissor_mode()
|
||||
|
||||
# Draw horizontal scroll indicator glows on the sides using the thematic color
|
||||
if scrolling_enabled:
|
||||
glow_w = 120
|
||||
fade_dist = 100.0
|
||||
left_remaining = -self._scroll_offset
|
||||
right_remaining = (content_width_needed - self._scroll_rect.width) + self._scroll_offset
|
||||
|
||||
left_alpha = int(max(0.0, min(1.0, left_remaining / fade_dist)) * 60)
|
||||
right_alpha = int(max(0.0, min(1.0, right_remaining / fade_dist)) * 60)
|
||||
|
||||
glow_y = int(d_rect.y)
|
||||
glow_h = int(d_rect.height)
|
||||
|
||||
if left_alpha > 0:
|
||||
rl.draw_rectangle_gradient_h(
|
||||
int(d_rect.x + 2), glow_y, glow_w, glow_h,
|
||||
with_alpha(self._color, left_alpha), with_alpha(self._color, 0)
|
||||
)
|
||||
if right_alpha > 0:
|
||||
rl.draw_rectangle_gradient_h(
|
||||
int(d_rect.x + d_rect.width - glow_w - 2), glow_y, glow_w, glow_h,
|
||||
with_alpha(self._color, 0), with_alpha(self._color, right_alpha)
|
||||
)
|
||||
# Scissor and render settings rows
|
||||
aether_begin_scissor_mode(
|
||||
int(self._scroll_rect.x), int(self._scroll_rect.y),
|
||||
int(self._scroll_rect.width), int(self._scroll_rect.height)
|
||||
)
|
||||
self._draw_scroll_content(self._scroll_rect, content_width)
|
||||
aether_end_scissor_mode()
|
||||
|
||||
if self._content_height > self._scroll_rect.height:
|
||||
self._scrollbar.render(self._scroll_rect, self._content_height, self._scroll_offset)
|
||||
|
||||
draw_list_scroll_fades(
|
||||
self._scroll_rect, self._content_height, self._scroll_offset,
|
||||
rl.Color(12, 10, 18, 250), fade_height=self._fade_height
|
||||
)
|
||||
|
||||
def _target_at(self, mouse_pos: MousePos) -> str | None:
|
||||
if self._back_btn_rect and point_hits(mouse_pos, self._back_btn_rect, None, pad_x=8, pad_y=8):
|
||||
return BACK_BTN
|
||||
|
||||
# Tap outside the drawer to dismiss
|
||||
drawer_w = 850
|
||||
if self._is_rhd:
|
||||
drawer_x = self._rect.x + self._rect.width - drawer_w * self._slide_progress
|
||||
if mouse_pos.x < drawer_x:
|
||||
return "__dismiss__"
|
||||
else:
|
||||
drawer_x = self._rect.x - drawer_w + drawer_w * self._slide_progress
|
||||
if mouse_pos.x > drawer_x + drawer_w:
|
||||
return "__dismiss__"
|
||||
|
||||
return super()._target_at(mouse_pos)
|
||||
|
||||
def _activate_target(self, target_id: str | None):
|
||||
if target_id == BACK_BTN:
|
||||
if target_id == BACK_BTN or target_id == "__dismiss__":
|
||||
gui_app.pop_widget()
|
||||
else:
|
||||
super()._activate_target(target_id)
|
||||
@@ -3260,6 +3237,120 @@ class AetherCategoryTileView(AetherSettingsView):
|
||||
self._breadcrumbs.draw(crumb_rect)
|
||||
|
||||
|
||||
# Compatibility Alias
|
||||
AetherCategoryTileView = AetherCategoryDrawer
|
||||
|
||||
|
||||
# ── AetherTransitionManager — Spatial Parallax Page Transitions ──
|
||||
|
||||
class AetherTransitionManager:
|
||||
def __init__(self, duration: float = 0.24):
|
||||
self.duration = duration
|
||||
self._time = 0.0
|
||||
self._progress = 1.0
|
||||
self._direction = 1 # 1 = forward (right to left), -1 = backward (left to right)
|
||||
self._active = False
|
||||
self._outgoing_render_fn = None
|
||||
self._incoming_render_fn = None
|
||||
|
||||
def start(self, outgoing_render_fn, incoming_render_fn, direction: int):
|
||||
self._outgoing_render_fn = outgoing_render_fn
|
||||
self._incoming_render_fn = incoming_render_fn
|
||||
self._direction = direction
|
||||
self._time = 0.0
|
||||
self._progress = 0.0
|
||||
self._active = True
|
||||
|
||||
def is_animating(self) -> bool:
|
||||
return self._active
|
||||
|
||||
def update(self, dt: float):
|
||||
if not self._active:
|
||||
return
|
||||
# Cap dt to avoid large visual jumps on frame spikes
|
||||
dt = min(0.016, dt)
|
||||
self._time += dt
|
||||
|
||||
t = self._time / self.duration
|
||||
if t >= 1.0:
|
||||
t = 1.0
|
||||
self._progress = 1.0
|
||||
self._active = False
|
||||
self._outgoing_render_fn = None
|
||||
self._incoming_render_fn = None
|
||||
else:
|
||||
# Ease-in-out cubic curve (zero initial velocity, smooth acceleration & deceleration)
|
||||
if t < 0.5:
|
||||
self._progress = 4.0 * t * t * t
|
||||
else:
|
||||
self._progress = 1.0 - (-2.0 * t + 2.0) ** 3 / 2.0
|
||||
|
||||
def render(self, rect: rl.Rectangle):
|
||||
if not self._active:
|
||||
return
|
||||
|
||||
global _GLOBAL_SCISSOR_LIMIT
|
||||
_GLOBAL_SCISSOR_LIMIT = rect
|
||||
|
||||
# 1. Enforce strict content boundary clipping to block drawing over the sidebar on the left
|
||||
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
||||
|
||||
# Clear the transition area with solid background to prevent ghosting/bleeding of transparent areas
|
||||
rl.draw_rectangle_rec(rect, rl.Color(12, 10, 18, 255))
|
||||
|
||||
# Use progress directly since the exponential decay is already eased
|
||||
eased = self._progress
|
||||
|
||||
# Outgoing and incoming rect calculations
|
||||
if self._direction == 1:
|
||||
# Forward: incoming slides right to left, outgoing slides left slightly (parallax)
|
||||
out_x = rect.x - 0.25 * rect.width * eased
|
||||
in_x = rect.x + rect.width * (1.0 - eased)
|
||||
else:
|
||||
# Backward: incoming slides left to right, outgoing slides right slightly (parallax)
|
||||
out_x = rect.x + 0.25 * rect.width * eased
|
||||
in_x = rect.x - rect.width * (1.0 - eased)
|
||||
|
||||
out_rect = rl.Rectangle(out_x, rect.y, rect.width, rect.height)
|
||||
in_rect = rl.Rectangle(in_x, rect.y, rect.width, rect.height)
|
||||
|
||||
# 2. Render outgoing content
|
||||
if self._outgoing_render_fn:
|
||||
self._outgoing_render_fn(out_rect)
|
||||
|
||||
# Re-assert content scissor clip after child finishes rendering to override any nested disable calls
|
||||
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
||||
|
||||
# 3. Draw dimming overlay on outgoing content
|
||||
dim_alpha = int(120 * (1.0 - eased))
|
||||
if dim_alpha > 0:
|
||||
rl.draw_rectangle_rec(out_rect, rl.Color(0, 0, 0, dim_alpha))
|
||||
|
||||
# 4. Render incoming content
|
||||
if self._incoming_render_fn:
|
||||
self._incoming_render_fn(in_rect)
|
||||
|
||||
# Re-assert content scissor clip after child finishes rendering
|
||||
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
|
||||
|
||||
# 5. Draw edge shadow/glow on incoming edge to separate layers
|
||||
shadow_w = 40
|
||||
if self._direction == 1:
|
||||
rl.draw_rectangle_gradient_h(
|
||||
int(in_rect.x - shadow_w), int(in_rect.y), shadow_w, int(in_rect.height),
|
||||
rl.Color(0, 0, 0, 0), rl.Color(0, 0, 0, 100)
|
||||
)
|
||||
else:
|
||||
rl.draw_rectangle_gradient_h(
|
||||
int(in_rect.x + in_rect.width), int(in_rect.y), shadow_w, int(in_rect.height),
|
||||
rl.Color(0, 0, 0, 100), rl.Color(0, 0, 0, 0)
|
||||
)
|
||||
|
||||
# 6. Disable scissor globally when exiting transition rendering and clear global limit
|
||||
rl.end_scissor_mode()
|
||||
_GLOBAL_SCISSOR_LIMIT = None
|
||||
|
||||
|
||||
class AetherTile(Widget):
|
||||
def __init__(self, surface_color: rl.Color | str | None = None, substrate_color: rl.Color | str | None = None, on_click: Callable | None = None):
|
||||
super().__init__()
|
||||
|
||||
@@ -18,7 +18,7 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import (
|
||||
SettingRow,
|
||||
SettingSection,
|
||||
AetherSettingsView,
|
||||
AetherCategoryTileView,
|
||||
AetherCategoryDrawer,
|
||||
TileGrid,
|
||||
HubTile,
|
||||
draw_list_group_shell,
|
||||
@@ -86,48 +86,21 @@ class AppearanceManagerView(AetherSettingsView):
|
||||
"desc": tr("Customize dynamic lane paths, road edges, and colors."),
|
||||
"icon": "steering",
|
||||
"color": "#8B5CF6",
|
||||
"on_click": lambda: gui_app.push_widget(
|
||||
AetherCategoryTileView(
|
||||
self._controller,
|
||||
tr("Model & Path Visualization"),
|
||||
self._controller._model_rows,
|
||||
color="#8B5CF6",
|
||||
subtitle=tr("Customize dynamic lane paths, road edges, and colors."),
|
||||
panel_style=self._panel_style,
|
||||
)
|
||||
)
|
||||
"on_click": lambda: self._controller._navigate_to("model")
|
||||
},
|
||||
{
|
||||
"title": tr("Driving Widgets & HUD"),
|
||||
"desc": tr("Configure compass, dynamic pedals, signals, and screen borders."),
|
||||
"icon": "display",
|
||||
"color": "#8B5CF6",
|
||||
"on_click": lambda: gui_app.push_widget(
|
||||
AetherCategoryTileView(
|
||||
self._controller,
|
||||
tr("Driving Widgets & HUD"),
|
||||
self._controller._hud_rows,
|
||||
color="#8B5CF6",
|
||||
subtitle=tr("Configure compass, dynamic pedals, signals, and screen borders."),
|
||||
panel_style=self._panel_style,
|
||||
)
|
||||
)
|
||||
"on_click": lambda: self._controller._navigate_to("hud")
|
||||
},
|
||||
{
|
||||
"title": tr("Screen Declutter & Visibility"),
|
||||
"desc": tr("Toggle speed limits, alert banners, and driver monitoring icon."),
|
||||
"icon": "system",
|
||||
"color": "#8B5CF6",
|
||||
"on_click": lambda: gui_app.push_widget(
|
||||
AetherCategoryTileView(
|
||||
self._controller,
|
||||
tr("Screen Declutter & Visibility"),
|
||||
self._controller._declutter_rows,
|
||||
color="#8B5CF6",
|
||||
subtitle=tr("Toggle speed limits, alert banners, and driver monitoring icon."),
|
||||
panel_style=self._panel_style,
|
||||
)
|
||||
)
|
||||
"on_click": lambda: self._controller._navigate_to("declutter")
|
||||
},
|
||||
]
|
||||
|
||||
@@ -137,48 +110,21 @@ class AppearanceManagerView(AetherSettingsView):
|
||||
"desc": tr("Configure road names, Vienna signs, and offroad routes."),
|
||||
"icon": "navigate",
|
||||
"color": "#8B5CF6",
|
||||
"on_click": lambda: gui_app.push_widget(
|
||||
AetherCategoryTileView(
|
||||
self._controller,
|
||||
tr("Navigation & Mapping"),
|
||||
self._controller._nav_rows,
|
||||
color="#8B5CF6",
|
||||
subtitle=tr("Configure road names, Vienna signs, and offroad routes."),
|
||||
panel_style=self._panel_style,
|
||||
)
|
||||
)
|
||||
"on_click": lambda: self._controller._navigate_to("nav")
|
||||
},
|
||||
{
|
||||
"title": tr("Camera & System Startup"),
|
||||
"desc": tr("Manage driver monitoring cameras, boot logos, and startup sounds."),
|
||||
"icon": "vehicle",
|
||||
"color": "#8B5CF6",
|
||||
"on_click": lambda: gui_app.push_widget(
|
||||
AetherCategoryTileView(
|
||||
self._controller,
|
||||
tr("Camera & System Startup"),
|
||||
self._controller._system_rows,
|
||||
color="#8B5CF6",
|
||||
subtitle=tr("Manage driver monitoring cameras, boot logos, and startup sounds."),
|
||||
panel_style=self._panel_style,
|
||||
)
|
||||
)
|
||||
"on_click": lambda: self._controller._navigate_to("system")
|
||||
},
|
||||
{
|
||||
"title": tr("Advanced Metrics"),
|
||||
"desc": tr("Adjust radar plots, lead vehicle info, and stop sign metrics."),
|
||||
"icon": "sound",
|
||||
"color": "#8B5CF6",
|
||||
"on_click": lambda: gui_app.push_widget(
|
||||
AetherCategoryTileView(
|
||||
self._controller,
|
||||
tr("Advanced Metrics"),
|
||||
self._controller._dev_rows,
|
||||
color="#8B5CF6",
|
||||
subtitle=tr("Adjust radar plots, lead vehicle info, and stop sign metrics."),
|
||||
panel_style=self._panel_style,
|
||||
)
|
||||
)
|
||||
"on_click": lambda: self._controller._navigate_to("dev")
|
||||
},
|
||||
]
|
||||
|
||||
@@ -515,6 +461,51 @@ class StarPilotAppearanceLayout(_SettingsPage):
|
||||
panel_style=PANEL_STYLE,
|
||||
)
|
||||
|
||||
# Register subpanels for Level 2 slide transitions
|
||||
self._sub_panels["model"] = AetherSettingsView(
|
||||
self,
|
||||
[SettingSection(title="", rows=self._model_rows)],
|
||||
header_title=tr_noop("Model & Path Visualization"),
|
||||
header_subtitle=tr_noop("Customize dynamic lane paths, road edges, and colors."),
|
||||
panel_style=PANEL_STYLE,
|
||||
)
|
||||
self._sub_panels["hud"] = AetherSettingsView(
|
||||
self,
|
||||
[SettingSection(title="", rows=self._hud_rows)],
|
||||
header_title=tr_noop("Driving Widgets & HUD"),
|
||||
header_subtitle=tr_noop("Configure compass, dynamic pedals, signals, and screen borders."),
|
||||
panel_style=PANEL_STYLE,
|
||||
)
|
||||
self._sub_panels["declutter"] = AetherSettingsView(
|
||||
self,
|
||||
[SettingSection(title="", rows=self._declutter_rows)],
|
||||
header_title=tr_noop("Screen Declutter & Visibility"),
|
||||
header_subtitle=tr_noop("Toggle speed limits, alert banners, and driver monitoring icon."),
|
||||
panel_style=PANEL_STYLE,
|
||||
)
|
||||
self._sub_panels["nav"] = AetherSettingsView(
|
||||
self,
|
||||
[SettingSection(title="", rows=self._nav_rows)],
|
||||
header_title=tr_noop("Navigation & Mapping"),
|
||||
header_subtitle=tr_noop("Configure road names, Vienna signs, and offroad routes."),
|
||||
panel_style=PANEL_STYLE,
|
||||
)
|
||||
self._sub_panels["system"] = AetherSettingsView(
|
||||
self,
|
||||
[SettingSection(title="", rows=self._system_rows)],
|
||||
header_title=tr_noop("Camera & System Startup"),
|
||||
header_subtitle=tr_noop("Manage driver monitoring cameras, boot logos, and startup sounds."),
|
||||
panel_style=PANEL_STYLE,
|
||||
)
|
||||
self._sub_panels["dev"] = AetherSettingsView(
|
||||
self,
|
||||
[SettingSection(title="", rows=self._dev_rows)],
|
||||
header_title=tr_noop("Advanced Metrics"),
|
||||
header_subtitle=tr_noop("Adjust radar plots, lead vehicle info, and stop sign metrics."),
|
||||
panel_style=PANEL_STYLE,
|
||||
)
|
||||
self._wire_sub_panels()
|
||||
|
||||
# ── Theme helpers ──
|
||||
|
||||
def _build_theme_options(self, key: str) -> tuple[list[str], dict[str, str], str]:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,6 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import (
|
||||
SettingRow,
|
||||
SettingSection,
|
||||
AetherSettingsView,
|
||||
AetherCategoryTileView,
|
||||
TileGrid,
|
||||
HubTile,
|
||||
hex_to_color,
|
||||
@@ -118,48 +117,21 @@ class LongitudinalManagerView(AetherSettingsView):
|
||||
"desc": tr("Configure acceleration profiles, smooth following, lane changes, and route speed control."),
|
||||
"icon": "steering",
|
||||
"color": "#8B5CF6",
|
||||
"on_click": lambda: gui_app.push_widget(
|
||||
AetherCategoryTileView(
|
||||
self._controller,
|
||||
tr("Longitudinal Tuning"),
|
||||
self._controller._tune_rows,
|
||||
color="#8B5CF6",
|
||||
subtitle=tr("Configure acceleration profiles, smooth following, lane changes, and route speed control."),
|
||||
panel_style=self._panel_style,
|
||||
)
|
||||
)
|
||||
"on_click": lambda: self._controller._navigate_to("tune")
|
||||
},
|
||||
{
|
||||
"title": tr("Advanced Actuators"),
|
||||
"desc": tr("Adjust actuator delay, EV/Truck tuning, and launch/stop speeds/rates."),
|
||||
"icon": "vehicle",
|
||||
"color": "#8B5CF6",
|
||||
"on_click": lambda: gui_app.push_widget(
|
||||
AetherCategoryTileView(
|
||||
self._controller,
|
||||
tr("Advanced Actuators"),
|
||||
self._controller._advanced_rows,
|
||||
color="#8B5CF6",
|
||||
subtitle=tr("Adjust actuator delay, EV/Truck tuning, and launch/stop speeds/rates."),
|
||||
panel_style=self._panel_style,
|
||||
)
|
||||
)
|
||||
"on_click": lambda: self._controller._navigate_to("advanced")
|
||||
},
|
||||
{
|
||||
"title": tr("Speed Limit Controller"),
|
||||
"desc": tr("Manage auto speed matching, confirmation, offsets, and source priority."),
|
||||
"icon": "navigate",
|
||||
"color": "#8B5CF6",
|
||||
"on_click": lambda: gui_app.push_widget(
|
||||
AetherCategoryTileView(
|
||||
self._controller,
|
||||
tr("Speed Limit Controller"),
|
||||
self._controller._slc_rows,
|
||||
color="#8B5CF6",
|
||||
subtitle=tr("Manage auto speed matching, confirmation, offsets, and source priority."),
|
||||
panel_style=self._panel_style,
|
||||
)
|
||||
)
|
||||
"on_click": lambda: self._controller._navigate_to("slc")
|
||||
},
|
||||
]
|
||||
|
||||
@@ -176,32 +148,14 @@ class LongitudinalManagerView(AetherSettingsView):
|
||||
"desc": tr("Customize follow distance and jerk/response metrics for each personality profile."),
|
||||
"icon": "system",
|
||||
"color": "#8B5CF6",
|
||||
"on_click": lambda: gui_app.push_widget(
|
||||
AetherCategoryTileView(
|
||||
self._controller,
|
||||
tr("Driving Personalities"),
|
||||
self._controller._personality_rows,
|
||||
color="#8B5CF6",
|
||||
subtitle=tr("Customize follow distance and jerk/response metrics for each personality profile."),
|
||||
panel_style=self._panel_style,
|
||||
)
|
||||
)
|
||||
"on_click": lambda: self._controller._navigate_to("personality")
|
||||
},
|
||||
{
|
||||
"title": tr("Daily QOL & Weather"),
|
||||
"desc": tr("Configure cruise intervals, standstill behaviors, gear mapping, and weather presets."),
|
||||
"icon": "sound",
|
||||
"color": "#8B5CF6",
|
||||
"on_click": lambda: gui_app.push_widget(
|
||||
AetherCategoryTileView(
|
||||
self._controller,
|
||||
tr("Daily QOL & Weather"),
|
||||
self._controller._daily_rows,
|
||||
color="#8B5CF6",
|
||||
subtitle=tr("Configure cruise intervals, standstill behaviors, gear mapping, and weather presets."),
|
||||
panel_style=self._panel_style,
|
||||
)
|
||||
)
|
||||
"on_click": lambda: self._controller._navigate_to("daily")
|
||||
},
|
||||
]
|
||||
|
||||
@@ -731,6 +685,43 @@ class StarPilotLongitudinalLayout(_SettingsPage):
|
||||
)
|
||||
|
||||
self._sub_panels["adaptive_speed"] = AdaptiveSpeedView(self)
|
||||
|
||||
# Register subpanels for Level 2 slide transitions
|
||||
self._sub_panels["tune"] = AetherSettingsView(
|
||||
self,
|
||||
[SettingSection(title="", rows=self._tune_rows)],
|
||||
header_title=tr_noop("Longitudinal Tuning"),
|
||||
header_subtitle=tr_noop("Configure acceleration profiles, smooth following, lane changes, and route speed control."),
|
||||
panel_style=PANEL_STYLE,
|
||||
)
|
||||
self._sub_panels["advanced"] = AetherSettingsView(
|
||||
self,
|
||||
[SettingSection(title="", rows=self._advanced_rows)],
|
||||
header_title=tr_noop("Advanced Actuators"),
|
||||
header_subtitle=tr_noop("Adjust actuator delay, EV/Truck tuning, and launch/stop speeds/rates."),
|
||||
panel_style=PANEL_STYLE,
|
||||
)
|
||||
self._sub_panels["slc"] = AetherSettingsView(
|
||||
self,
|
||||
[SettingSection(title="", rows=self._slc_rows)],
|
||||
header_title=tr_noop("Speed Limit Controller"),
|
||||
header_subtitle=tr_noop("Manage auto speed matching, confirmation, offsets, and source priority."),
|
||||
panel_style=PANEL_STYLE,
|
||||
)
|
||||
self._sub_panels["personality"] = AetherSettingsView(
|
||||
self,
|
||||
[SettingSection(title="", rows=self._personality_rows)],
|
||||
header_title=tr_noop("Driving Personalities"),
|
||||
header_subtitle=tr_noop("Customize follow distance and jerk/response metrics for each personality profile."),
|
||||
panel_style=PANEL_STYLE,
|
||||
)
|
||||
self._sub_panels["daily"] = AetherSettingsView(
|
||||
self,
|
||||
[SettingSection(title="", rows=self._daily_rows)],
|
||||
header_title=tr_noop("Daily QOL & Weather"),
|
||||
header_subtitle=tr_noop("Configure cruise intervals, standstill behaviors, gear mapping, and weather presets."),
|
||||
panel_style=PANEL_STYLE,
|
||||
)
|
||||
self._wire_sub_panels()
|
||||
|
||||
def _get_priority_value(self) -> str:
|
||||
@@ -831,42 +822,41 @@ class StarPilotLongitudinalLayout(_SettingsPage):
|
||||
return (-150, 150) if self._is_metric() else (-99, 99)
|
||||
|
||||
def _show_slc_offsets_category(self):
|
||||
gui_app.push_widget(
|
||||
AetherCategoryTileView(
|
||||
self,
|
||||
tr("SLC Offsets"),
|
||||
self._slc_offset_rows,
|
||||
color="#8B5CF6",
|
||||
subtitle=tr("Per-limit speed adjustments for the Speed Limit Controller."),
|
||||
panel_style=PANEL_STYLE,
|
||||
)
|
||||
self._sub_panels["slc_offsets"] = AetherSettingsView(
|
||||
self,
|
||||
[SettingSection(title="", rows=self._slc_offset_rows)],
|
||||
header_title=tr_noop("SLC Offsets"),
|
||||
header_subtitle=tr_noop("Per-limit speed adjustments for the Speed Limit Controller."),
|
||||
panel_style=PANEL_STYLE,
|
||||
)
|
||||
self._wire_sub_panels()
|
||||
self._navigate_to("slc_offsets")
|
||||
|
||||
def _show_personality_profile_category(self, profile: str):
|
||||
rows = self._build_personality_profile_rows(profile)
|
||||
gui_app.push_widget(
|
||||
AetherCategoryTileView(
|
||||
self,
|
||||
tr(f"{profile} Profile"),
|
||||
rows,
|
||||
color="#8B5CF6",
|
||||
subtitle=tr("Customize follow distance and smoothness for this driving personality."),
|
||||
panel_style=PANEL_STYLE,
|
||||
)
|
||||
panel_name = f"profile_{profile.lower()}"
|
||||
self._sub_panels[panel_name] = AetherSettingsView(
|
||||
self,
|
||||
[SettingSection(title="", rows=rows)],
|
||||
header_title=tr_noop(f"{profile} Profile"),
|
||||
header_subtitle=tr_noop("Customize follow distance and smoothness for this driving personality."),
|
||||
panel_style=PANEL_STYLE,
|
||||
)
|
||||
self._wire_sub_panels()
|
||||
self._navigate_to(panel_name)
|
||||
|
||||
def _show_weather_offsets_category(self, suffix: str, title: str):
|
||||
rows = self._build_weather_offsets_rows(suffix)
|
||||
gui_app.push_widget(
|
||||
AetherCategoryTileView(
|
||||
self,
|
||||
tr(title),
|
||||
rows,
|
||||
color="#8B5CF6",
|
||||
subtitle=tr("Adjust driving parameters for this weather condition."),
|
||||
panel_style=PANEL_STYLE,
|
||||
)
|
||||
panel_name = f"weather_{suffix.lower()}"
|
||||
self._sub_panels[panel_name] = AetherSettingsView(
|
||||
self,
|
||||
[SettingSection(title="", rows=rows)],
|
||||
header_title=tr_noop(title),
|
||||
header_subtitle=tr_noop("Adjust driving parameters for this weather condition."),
|
||||
panel_style=PANEL_STYLE,
|
||||
)
|
||||
self._wire_sub_panels()
|
||||
self._navigate_to(panel_name)
|
||||
|
||||
def _build_personality_profile_rows(self, profile: str) -> list[SettingRow]:
|
||||
follow_min = 1.0 if profile == "Traffic" else 0.5
|
||||
|
||||
@@ -7,7 +7,7 @@ from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.lib.application import MousePos, gui_app, FontWeight
|
||||
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanelType, StarPilotPanelInfo
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanelType, StarPilotPanelInfo, FrameCachedParams
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.sounds import StarPilotSoundsLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.driving_model import StarPilotDrivingModelLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.longitudinal import StarPilotLongitudinalLayout
|
||||
@@ -17,7 +17,7 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.system_settings import St
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.appearance import StarPilotAppearanceLayout
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.vehicle import StarPilotVehicleSettingsLayout
|
||||
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, HubTile, SPACING, BreadcrumbController, AETHER_LIST_METRICS, draw_rounded_fill, draw_rounded_stroke
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, HubTile, SPACING, BreadcrumbController, AETHER_LIST_METRICS, draw_rounded_fill, draw_rounded_stroke, AetherTransitionManager
|
||||
|
||||
class StarPilotLayout(Widget):
|
||||
CATEGORIES = [
|
||||
@@ -55,7 +55,7 @@ class StarPilotLayout(Widget):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._params = FrameCachedParams()
|
||||
|
||||
self._current_panel = StarPilotPanelType.MAIN
|
||||
self._current_category_idx: int | None = None
|
||||
@@ -92,6 +92,23 @@ class StarPilotLayout(Widget):
|
||||
self._breadcrumbs = BreadcrumbController()
|
||||
self._main_grid = TileGrid(columns=None, padding=SPACING.tile_gap)
|
||||
self._rebuild_grid()
|
||||
self._transition_manager = AetherTransitionManager()
|
||||
|
||||
def _make_render_fn(self, panel_type: StarPilotPanelType) -> Callable[[rl.Rectangle], None]:
|
||||
if panel_type == StarPilotPanelType.MAIN:
|
||||
def render_main(rect: rl.Rectangle):
|
||||
metrics = AETHER_LIST_METRICS
|
||||
shell_w = min(rect.width - metrics.outer_margin_x * 2, metrics.max_content_width)
|
||||
shell_x = rect.x + (rect.width - shell_w) / 2
|
||||
grid_rect = rl.Rectangle(
|
||||
shell_x, rect.y + metrics.outer_margin_y,
|
||||
shell_w, rect.height - metrics.outer_margin_y * 2
|
||||
)
|
||||
self._main_grid.render(grid_rect)
|
||||
return render_main
|
||||
else:
|
||||
panel = self._panels[panel_type]
|
||||
return lambda r: panel.instance.render(r) if panel.instance else None
|
||||
|
||||
def set_depth_callback(self, callback: Callable):
|
||||
self._depth_callback = callback
|
||||
@@ -164,6 +181,10 @@ class StarPilotLayout(Widget):
|
||||
panel.set_navigate_callback(self._push_sub_panel)
|
||||
|
||||
def _rebuild_grid(self):
|
||||
state = (self._current_category_idx,)
|
||||
if getattr(self, "_last_grid_state", None) == state:
|
||||
return
|
||||
self._last_grid_state = state
|
||||
self._main_grid.clear()
|
||||
|
||||
panel_type_map = {
|
||||
@@ -226,6 +247,14 @@ class StarPilotLayout(Widget):
|
||||
|
||||
def _set_current_panel(self, panel_type: StarPilotPanelType):
|
||||
if panel_type != self._current_panel:
|
||||
old_panel = self._current_panel
|
||||
direction = -1 if panel_type == StarPilotPanelType.MAIN else 1
|
||||
self._transition_manager.start(
|
||||
self._make_render_fn(old_panel),
|
||||
self._make_render_fn(panel_type),
|
||||
direction
|
||||
)
|
||||
|
||||
if self._current_panel != StarPilotPanelType.MAIN:
|
||||
old = self._panels[self._current_panel].instance
|
||||
old.hide_event()
|
||||
@@ -271,19 +300,29 @@ class StarPilotLayout(Widget):
|
||||
crumb_rect = rl.Rectangle(glass_rect.x, glass_rect.y, glass_rect.width, glass_rect.height)
|
||||
self._breadcrumbs.draw(crumb_rect)
|
||||
|
||||
# Update transitions
|
||||
self._transition_manager.update(rl.get_frame_time())
|
||||
|
||||
# 4. Render active content panel
|
||||
if self._current_panel == StarPilotPanelType.MAIN:
|
||||
grid_rect = rl.Rectangle(shell_x, content_rect.y + AETHER_LIST_METRICS.outer_margin_y, shell_w, content_rect.height - AETHER_LIST_METRICS.outer_margin_y * 2)
|
||||
self._main_grid.render(grid_rect)
|
||||
if self._transition_manager.is_animating():
|
||||
self._transition_manager.render(content_rect)
|
||||
else:
|
||||
panel = self._panels[self._current_panel]
|
||||
if panel.instance:
|
||||
panel.instance.render(content_rect)
|
||||
if self._current_panel == StarPilotPanelType.MAIN:
|
||||
grid_rect = rl.Rectangle(shell_x, content_rect.y + AETHER_LIST_METRICS.outer_margin_y, shell_w, content_rect.height - AETHER_LIST_METRICS.outer_margin_y * 2)
|
||||
self._main_grid.render(grid_rect)
|
||||
else:
|
||||
panel = self._panels[self._current_panel]
|
||||
if panel.instance:
|
||||
panel.instance.render(content_rect)
|
||||
|
||||
def _handle_mouse_press(self, mouse_pos: MousePos):
|
||||
if self._transition_manager.is_animating():
|
||||
return
|
||||
self._breadcrumbs.init_interaction(mouse_pos)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
if self._transition_manager.is_animating():
|
||||
return
|
||||
action = self._breadcrumbs.finish_interaction(mouse_pos)
|
||||
if action:
|
||||
self._breadcrumbs.handle_click(action)
|
||||
|
||||
@@ -10,8 +10,73 @@ from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import DialogResult, Widget
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, HubTile, ToggleTile, ValueTile, SliderTile, SPACING, AetherSliderDialog
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, HubTile, ToggleTile, ValueTile, SliderTile, SPACING, AetherSliderDialog, AetherTransitionManager
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.sectioned_panel import SectionedTileLayout, TileSection
|
||||
import time
|
||||
|
||||
|
||||
class FrameCachedParams:
|
||||
def __init__(self):
|
||||
self._params = Params()
|
||||
self._cache = {}
|
||||
self._last_frame_time = 0.0
|
||||
|
||||
def _check_clear_cache(self):
|
||||
now = time.monotonic()
|
||||
if now != self._last_frame_time:
|
||||
self._cache.clear()
|
||||
self._last_frame_time = now
|
||||
|
||||
def get(self, key, **kwargs):
|
||||
self._check_clear_cache()
|
||||
cache_key = (key, "get", tuple(kwargs.items()))
|
||||
if cache_key not in self._cache:
|
||||
self._cache[cache_key] = self._params.get(key, **kwargs)
|
||||
return self._cache[cache_key]
|
||||
|
||||
def get_bool(self, key, **kwargs):
|
||||
self._check_clear_cache()
|
||||
cache_key = (key, "get_bool", tuple(kwargs.items()))
|
||||
if cache_key not in self._cache:
|
||||
self._cache[cache_key] = self._params.get_bool(key, **kwargs)
|
||||
return self._cache[cache_key]
|
||||
|
||||
def get_int(self, key, **kwargs):
|
||||
self._check_clear_cache()
|
||||
cache_key = (key, "get_int", tuple(kwargs.items()))
|
||||
if cache_key not in self._cache:
|
||||
self._cache[cache_key] = self._params.get_int(key, **kwargs)
|
||||
return self._cache[cache_key]
|
||||
|
||||
def get_float(self, key, **kwargs):
|
||||
self._check_clear_cache()
|
||||
cache_key = (key, "get_float", tuple(kwargs.items()))
|
||||
if cache_key not in self._cache:
|
||||
self._cache[cache_key] = self._params.get_float(key, **kwargs)
|
||||
return self._cache[cache_key]
|
||||
|
||||
def put(self, key, val, **kwargs):
|
||||
self._params.put(key, val, **kwargs)
|
||||
self._cache.clear()
|
||||
|
||||
def put_bool(self, key, val, **kwargs):
|
||||
self._params.put_bool(key, val, **kwargs)
|
||||
self._cache.clear()
|
||||
|
||||
def put_int(self, key, val, **kwargs):
|
||||
self._params.put_int(key, val, **kwargs)
|
||||
self._cache.clear()
|
||||
|
||||
def put_float(self, key, val, **kwargs):
|
||||
self._params.put_float(key, val, **kwargs)
|
||||
self._cache.clear()
|
||||
|
||||
def remove(self, key):
|
||||
self._params.remove(key)
|
||||
self._cache.clear()
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._params, name)
|
||||
|
||||
|
||||
class StarPilotPanelType(IntEnum):
|
||||
@@ -41,7 +106,7 @@ class StarPilotPanel(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params_memory = Params(memory=True)
|
||||
self._params = Params()
|
||||
self._params = FrameCachedParams()
|
||||
self._navigate_callback: Callable | None = None
|
||||
self._back_callback: Callable | None = None
|
||||
self._current_sub_panel = ""
|
||||
@@ -51,6 +116,7 @@ class StarPilotPanel(Widget):
|
||||
self._sectioned_grid = None
|
||||
self.CATEGORIES = []
|
||||
self.SECTIONS = []
|
||||
self._transition_manager = AetherTransitionManager()
|
||||
|
||||
def set_navigate_callback(self, callback: Callable):
|
||||
self._navigate_callback = callback
|
||||
@@ -159,17 +225,50 @@ class StarPilotPanel(Widget):
|
||||
if tile is not None:
|
||||
self._tile_grid.add_tile(tile)
|
||||
|
||||
def _make_render_fn(self, sub_panel: str) -> Callable[[rl.Rectangle], None]:
|
||||
if sub_panel and sub_panel in self._sub_panels:
|
||||
panel = self._sub_panels[sub_panel]
|
||||
return lambda r: panel.render(r)
|
||||
else:
|
||||
def render_base(rect: rl.Rectangle):
|
||||
if self.SECTIONS and self._sectioned_grid:
|
||||
self._sectioned_grid.render(rect)
|
||||
elif self.CATEGORIES and self._tile_grid:
|
||||
self._tile_grid.render(rect)
|
||||
elif self._scroller:
|
||||
self._scroller.render(rect)
|
||||
return render_base
|
||||
|
||||
def _navigate_to(self, sub_panel: str):
|
||||
self._current_sub_panel = sub_panel
|
||||
if self._navigate_callback:
|
||||
self._navigate_callback(sub_panel)
|
||||
if sub_panel != self._current_sub_panel:
|
||||
old_sub = self._current_sub_panel
|
||||
self._transition_manager.start(
|
||||
self._make_render_fn(old_sub),
|
||||
self._make_render_fn(sub_panel),
|
||||
1
|
||||
)
|
||||
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()
|
||||
if self._current_sub_panel:
|
||||
old_sub = self._current_sub_panel
|
||||
self._transition_manager.start(
|
||||
self._make_render_fn(old_sub),
|
||||
self._make_render_fn(""),
|
||||
-1
|
||||
)
|
||||
self._current_sub_panel = ""
|
||||
if self._back_callback:
|
||||
self._back_callback()
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self._transition_manager.update(rl.get_frame_time())
|
||||
if self._transition_manager.is_animating():
|
||||
self._transition_manager.render(rect)
|
||||
return
|
||||
|
||||
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.SECTIONS and self._sectioned_grid:
|
||||
@@ -179,9 +278,27 @@ class StarPilotPanel(Widget):
|
||||
elif self._scroller:
|
||||
self._scroller.render(rect)
|
||||
|
||||
def _handle_mouse_press(self, mouse_pos):
|
||||
if self._transition_manager.is_animating():
|
||||
return
|
||||
super()._handle_mouse_press(mouse_pos)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos):
|
||||
if self._transition_manager.is_animating():
|
||||
return
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
def _handle_mouse_event(self, mouse_event):
|
||||
if self._transition_manager.is_animating():
|
||||
return
|
||||
super()._handle_mouse_event(mouse_event)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._rebuild_grid()
|
||||
if self.SECTIONS and self._sectioned_grid is None:
|
||||
self._rebuild_grid()
|
||||
elif self.CATEGORIES and self._tile_grid is None:
|
||||
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.SECTIONS and self._sectioned_grid:
|
||||
|
||||
@@ -446,7 +446,7 @@ class TestAethergridContracts(unittest.TestCase):
|
||||
|
||||
grid.render(mod.rl.Rectangle(0, 50, 500, 300))
|
||||
self.assertTrue(spy.rects)
|
||||
self.assertEqual(spy.rects[0].y, 50)
|
||||
self.assertEqual(spy.rects[0].y, 130)
|
||||
self.assertEqual(spy.rects[0].x, 0)
|
||||
|
||||
def test_disabled_tiles_hud_mode_rendering(self):
|
||||
@@ -559,28 +559,28 @@ class TestAethergridContracts(unittest.TestCase):
|
||||
|
||||
view = mod.AetherCategoryTileView(controller_mock, "Category Title", rows, color="#FF0000", subtitle="Category Description")
|
||||
|
||||
self.assertEqual(len(view._row_to_tile_map), 3)
|
||||
self.assertIsInstance(view._row_to_tile_map["toggle_row"], mod.RowToggleTile)
|
||||
self.assertIsInstance(view._row_to_tile_map["value_row"], mod.RowPanelTile)
|
||||
self.assertIsInstance(view._row_to_tile_map["action_row"], mod.RowPanelTile)
|
||||
self.assertEqual(len(view._sections), 1)
|
||||
self.assertEqual(len(view._sections[0].rows), 3)
|
||||
|
||||
view._update_visible_tiles()
|
||||
self.assertEqual(len(view._tile_grid.tiles), 3)
|
||||
visible = view._visible_rows(view._sections[0])
|
||||
self.assertEqual(len(visible), 3)
|
||||
|
||||
toggle_visible = False
|
||||
view._update_visible_tiles()
|
||||
self.assertEqual(len(view._tile_grid.tiles), 2)
|
||||
self.assertNotIn(view._row_to_tile_map["toggle_row"], view._tile_grid.tiles)
|
||||
visible = view._visible_rows(view._sections[0])
|
||||
self.assertEqual(len(visible), 2)
|
||||
self.assertNotIn(rows[0], visible)
|
||||
|
||||
view._back_btn_rect = mod.rl.Rectangle(196, 56, 68, 68)
|
||||
view._slide_progress = 1.0 # fully slide-in
|
||||
view.set_rect(mod.rl.Rectangle(0, 0, 1920, 1080))
|
||||
|
||||
self.assertEqual(view._target_at(mod.rl.Vector2(200, 60)), "static:back")
|
||||
self.assertNotEqual(view._target_at(mod.rl.Vector2(0, 0)), "static:back")
|
||||
self.assertEqual(view._target_at(mod.rl.Vector2(200, 60)), mod.BACK_BTN)
|
||||
self.assertEqual(view._target_at(mod.rl.Vector2(1000, 60)), "__dismiss__")
|
||||
|
||||
app_mod = sys.modules["openpilot.system.ui.lib.application"]
|
||||
app_mod.gui_app.pop_widget = MagicMock()
|
||||
|
||||
view._activate_target("static:back")
|
||||
view._activate_target(mod.BACK_BTN)
|
||||
app_mod.gui_app.pop_widget.assert_called_once()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user