From 50630f121c7eeff47ffec996c63d6d715f42d1e8 Mon Sep 17 00:00:00 2001 From: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Date: Mon, 29 Jun 2026 03:53:12 -0400 Subject: [PATCH] BigUI WIP: System Panel Cleanup --- scratch/render_icon.py | 31 ----- .../layouts/settings/starpilot/aethergrid.py | 129 +++++++++++++++--- .../layouts/settings/starpilot/main_panel.py | 2 +- .../settings/starpilot/system_settings.py | 101 ++++++++------ 4 files changed, 164 insertions(+), 99 deletions(-) delete mode 100644 scratch/render_icon.py diff --git a/scratch/render_icon.py b/scratch/render_icon.py deleted file mode 100644 index a4d4d6fc6..000000000 --- a/scratch/render_icon.py +++ /dev/null @@ -1,31 +0,0 @@ -import sys -import os -import pyray as rl - -# Add selfdrive's parent to path to support openpilot imports -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -from selfdrive.ui.layouts.settings.starpilot.scribble import draw_custom_icon - -def main(): - rl.init_window(200, 200, "Icon Render") - rl.set_target_fps(60) - - # Run for a few frames to make sure rendering is ready - for _ in range(10): - rl.begin_drawing() - rl.clear_background(rl.Color(15, 12, 30, 255)) # Dark background matching Driving Model tile - - # Draw aicar icon centered - # x_c and y_c will be at x + 30*s, y + 30*s. - # Let's draw it at x=70, y=70, scale s=1.0. Centered at 100, 100. - draw_custom_icon("aicar", 70.0, 70.0, 1.0, rl.WHITE) - - rl.end_drawing() - - rl.take_screenshot("scratch/aicar_preview.png") - rl.close_window() - print("Screenshot saved to scratch/aicar_preview.png") - -if __name__ == "__main__": - main() diff --git a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py index 7f45aa6d7..5d28f0b2c 100644 --- a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py +++ b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py @@ -410,6 +410,87 @@ def init_list_panel(rect: rl.Rectangle, style: PanelStyle | None = None, metrics return frame, scroll_rect, content_width +def draw_hud_background(rect: rl.Rectangle, accent: rl.Color, glow: float = 1.0) -> tuple[rl.Rectangle, rl.Color]: + snapped = _snap_rect(rect) + rx, ry, rw, rh = int(snapped.x), int(snapped.y), int(snapped.width), int(snapped.height) + face = rl.Rectangle(rx, ry, rw, rh) + + off_border = _HUD_BORDER_OFF + + for i in range(4, 0, -1): + off = i * 2.5 * glow + gr = rl.Rectangle(rx - off, ry - off, rw + off * 2, rh + off * 2) + a = int(25 * (1.0 - i / 5) * glow) + _draw_rounded_fill(gr, rl.Color(accent.r, accent.g, accent.b, max(0, min(255, a))), radius_px=100) + + _draw_rounded_fill(face, _HUD_BG_ON, radius_px=100) + + bc = rl.Color( + max(0, min(255, int(off_border.r + (accent.r - off_border.r) * glow))), + max(0, min(255, int(off_border.g + (accent.g - off_border.g) * glow))), + max(0, min(255, int(off_border.b + (accent.b - off_border.b) * glow))), + 255) + _draw_rounded_stroke(face, bc, radius_px=100) + + return face, accent + + +def draw_constellation(face: rl.Rectangle, accent: rl.Color, glow: float, seed: str): + rng = random.Random(seed) + num = _NODE_NUM_MIN + rng.randint(0, _NODE_NUM_MAX - _NODE_NUM_MIN) + regions = [ + (0.18, 0.30, 0.18, 0.30), (0.70, 0.82, 0.18, 0.30), + (0.18, 0.30, 0.70, 0.82), (0.70, 0.82, 0.70, 0.82), + (0.38, 0.62, 0.18, 0.28), (0.38, 0.62, 0.72, 0.82), + (0.18, 0.28, 0.38, 0.62), (0.72, 0.82, 0.38, 0.62), + ] + ax_min, ax_max, ay_min, ay_max = regions[rng.randint(0, 7)] + ax = ax_min + rng.random() * (ax_max - ax_min) + ay = ay_min + rng.random() * (ay_max - ay_min) + nodes = [] + for _ in range(num): + for _ in range(20): + a = rng.random() * 2.0 * math.pi + r = 0.05 + rng.random() * 0.11 + x = max(0.04, min(0.96, ax + r * math.cos(a))) + y = max(0.04, min(0.96, ay + r * math.sin(a))) + if all(math.sqrt((x - n['x'])**2 + (y - n['y'])**2) >= 0.07 for n in nodes): + nodes.append({'x': x, 'y': y}) + break + else: + nodes.append({'x': x, 'y': y}) + nodes.sort(key=lambda n: -(abs(n['x'] - 0.5) + abs(n['y'] - 0.5))) + for i, n in enumerate(nodes): + n['w'] = 0 if i == 0 else 1 if i == 1 else 2 + vecs = [(0, j) for j in range(1, num)] + + rx, ry, rw, rh = int(face.x), int(face.y), int(face.width), int(face.height) + va = int(10 + glow * 25) + if va > 2: + vc = rl.Color(accent.r, accent.g, accent.b, min(255, va)) + for i, j in vecs: + x1 = int(rx + nodes[i]['x'] * rw) + y1 = int(ry + nodes[i]['y'] * rh) + x2 = int(rx + nodes[j]['x'] * rw) + y2 = int(ry + nodes[j]['y'] * rh) + rl.draw_line_ex(rl.Vector2(x1, y1), rl.Vector2(x2, y2), 1.0, vc) + + for nd in nodes: + nx = int(rx + nd['x'] * rw) + ny = int(ry + nd['y'] * rh) + if nd['w'] == 0: + core_r, diff_r, col = 3.0, 12.0, _CONST_PRIMARY + elif nd['w'] == 1: + core_r, diff_r, col = 2.0, 8.0, _CONST_SECONDARY + else: + core_r, diff_r, col = 1.2, 0.0, _CONST_TERTIARY + da = int(5 + glow * 20) + if diff_r > 0 and da > 2: + rl.draw_circle(nx, ny, int(diff_r), rl.Color(col.r, col.g, col.b, min(255, da))) + ca = int(130 + glow * 125) + rl.draw_circle(nx, ny, int(core_r), rl.Color(col.r, col.g, col.b, min(255, ca))) + + def draw_interactive_rect(target_id: str, rect: rl.Rectangle, interactive_rects: dict[str, rl.Rectangle], pressed_target: str | None, scroll_rect: rl.Rectangle | None = None, pad_x: float = 6, pad_y: float = 0) -> tuple[bool, bool]: @@ -556,6 +637,16 @@ class PanelManagerView(AetherInteractiveMixin, Widget): def _draw_scroll_content(self, scroll_rect: rl.Rectangle, content_width: float) -> None: """Override to render visible content rows inside the scissor region.""" + def _draw_static_elements(self, scroll_rect: rl.Rectangle, content_width: float) -> None: + """Override to render fixed-position elements above the scroll area (outside scissor).""" + + def _draw_bottombar(self, bottombar_rect: rl.Rectangle) -> None: + """Override to render a fixed bottom bar below the scroll area.""" + + @property + def bottombar_height(self) -> float: + return 0.0 + @property def vertical_scrolling_disabled(self) -> bool: return False @@ -572,6 +663,10 @@ class PanelManagerView(AetherInteractiveMixin, Widget): self._draw_header(frame.header) + btm_h = self.bottombar_height + if btm_h > 0: + scroll_rect.height = max(0.0, scroll_rect.height - btm_h) + self._content_height = self._measure_content_height(content_width) self._scroll_panel.set_enabled(self.is_visible) @@ -590,6 +685,8 @@ class PanelManagerView(AetherInteractiveMixin, Widget): self._draw_scroll_content(scroll_rect, content_width) rl.end_scissor_mode() + self._draw_static_elements(scroll_rect, content_width) + if self._content_height > scroll_rect.height and not scroll_disabled: self._scrollbar.render(scroll_rect, self._content_height, self._scroll_offset) @@ -597,6 +694,11 @@ class PanelManagerView(AetherInteractiveMixin, Widget): draw_list_scroll_fades(scroll_rect, self._content_height, self._scroll_offset, AetherListColors.PANEL_BG) + if btm_h > 0: + btm_rect = rl.Rectangle(scroll_rect.x, scroll_rect.y + scroll_rect.height, + content_width, btm_h) + self._draw_bottombar(btm_rect) + # ── shared layout helpers ───────────────────────────────── def _uses_two_columns(self, width: float) -> bool: @@ -3753,28 +3855,8 @@ class AetherTile(Widget): sh = snapped.height * sq ox = snapped.x + (snapped.width - sw) / 2 oy = snapped.y + (snapped.height - sh) / 2 - rx, ry, rw, rh = int(ox), int(oy), int(sw), int(sh) - face = rl.Rectangle(rx, ry, rw, rh) - - off_border = _HUD_BORDER_OFF - - for i in range(4, 0, -1): - off = i * 2.5 * glow - gr = rl.Rectangle(rx - off, ry - off, rw + off * 2, rh + off * 2) - a = int(25 * (1.0 - i / 5) * glow) - _draw_rounded_fill(gr, rl.Color(accent.r, accent.g, accent.b, max(0, min(255, a))), radius_px=100) - - _draw_rounded_fill(face, _HUD_BG_ON, radius_px=100) - - bc = rl.Color( - max(0, min(255, int(off_border.r + (accent.r - off_border.r) * glow))), - max(0, min(255, int(off_border.g + (accent.g - off_border.g) * glow))), - max(0, min(255, int(off_border.b + (accent.b - off_border.b) * glow))), - 255) - _draw_rounded_stroke(face, bc, radius_px=100) - + face, accent = draw_hud_background(rl.Rectangle(ox, oy, sw, sh), accent, glow) self._draw_constellation(face, accent, glow) - return face, accent def _render(self, rect: rl.Rectangle): @@ -5282,6 +5364,7 @@ class AetherSegmentedControl(Widget): statuses: list[str | Callable[[], str] | None] | None = None, compact: bool = False, style: PanelStyle | None = None, + suppress_background: bool = False, ): super().__init__() self._options = options @@ -5292,6 +5375,7 @@ class AetherSegmentedControl(Widget): self._statuses += [""] * (len(self._options) - len(self._statuses)) self._compact = compact self._style = style + self._suppress_background = suppress_background self._font = gui_app.font(FontWeight.BOLD) self._font_status = gui_app.font(FontWeight.NORMAL) self._pressed_index = -1 @@ -5364,7 +5448,8 @@ class AetherSegmentedControl(Widget): 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)) - draw_soft_card(rect, rl.Color(255, 255, 255, 4), rl.Color(255, 255, 255, 14)) + if not self._suppress_background: + draw_soft_card(rect, rl.Color(255, 255, 255, 4), rl.Color(255, 255, 255, 14)) inner_pad = 4 if self._compact else 5 gap = 4 if self._compact else 6 diff --git a/selfdrive/ui/layouts/settings/starpilot/main_panel.py b/selfdrive/ui/layouts/settings/starpilot/main_panel.py index 7fa8ef4a7..c67de0e0c 100644 --- a/selfdrive/ui/layouts/settings/starpilot/main_panel.py +++ b/selfdrive/ui/layouts/settings/starpilot/main_panel.py @@ -258,7 +258,7 @@ class StarPilotLayout(Widget): aethergrid._draw_rounded_fill(gr, rl.Color(139, 92, 246, max(0, min(255, a))), radius_px=100) # 0b. Dark fill — strict parity with HubTile _HUD_BG_ON - aethergrid._draw_rounded_fill(glass_rect, rl.Color(12, 10, 18, 230), radius_px=100) + aethergrid._draw_rounded_fill(glass_rect, rl.Color(12, 10, 18, 255), radius_px=100) # 0c. Full bright purple border — strict parity aethergrid._draw_rounded_stroke(glass_rect, rl.Color(139, 92, 246, 255), radius_px=100) diff --git a/selfdrive/ui/layouts/settings/starpilot/system_settings.py b/selfdrive/ui/layouts/settings/starpilot/system_settings.py index 88321e6e4..2ce495a4e 100644 --- a/selfdrive/ui/layouts/settings/starpilot/system_settings.py +++ b/selfdrive/ui/layouts/settings/starpilot/system_settings.py @@ -52,6 +52,7 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( _point_hits, _draw_text_fit_common, wrap_text, + ) from openpilot.starpilot.common.connect_server import prepare_konik_server_switch @@ -318,8 +319,8 @@ class SystemSettingsManagerView(PanelManagerView): [tr("Auto"), tr("Onroad"), tr("Offroad")], self._get_drive_mode_index, self._on_drive_mode_change, - statuses=[tr("Default"), tr("Force on"), tr("Force off")], style=PANEL_STYLE, + suppress_background=True, ) ) @@ -434,13 +435,60 @@ class SystemSettingsManagerView(PanelManagerView): gui_app.push_widget(AetherBackupsCareDialog(self._controller)) def _on_frame_created(self, frame) -> None: - self._drive_mode_control.set_parent_rect(self._scroll_rect) + pass + + @property + def bottombar_height(self) -> float: + return 160.0 + + def _draw_bottombar(self, bottombar_rect: rl.Rectangle) -> None: + draw_list_group_shell(bottombar_rect, style=self.PANEL_STYLE) + self._drive_mode_control.render(bottombar_rect) + + def _draw_static_elements(self, scroll_rect: rl.Rectangle, content_width: float) -> None: + if not self._uses_two_columns(content_width): + return + + column_w = self._column_width(content_width) + right_x = scroll_rect.x + column_w + self.COLUMN_GAP + + btn_size = 32.0 + btn_rect = rl.Rectangle( + right_x + column_w - 12 - 44 - btn_size, + scroll_rect.y + self._system_max_container_h - 12 - 10 - btn_size, + btn_size, btn_size, + ) + + hovered, pressed = self._interactive_state("static:first_aid", btn_rect, pad_y=6) + + if pressed: + fill = rl.Color(139, 92, 246, 8) + border = rl.Color(139, 92, 246, 28) + elif hovered: + fill = rl.Color(255, 255, 255, 4) + border = rl.Color(255, 255, 255, 10) + else: + fill = rl.Color(255, 255, 255, 0) + border = rl.Color(255, 255, 255, 0) + + draw_soft_card(btn_rect, fill, border) + + s = btn_size / 60.0 + icon_x = btn_rect.x + (btn_rect.width - 60.0 * s) / 2.0 + icon_y = btn_rect.y + (btn_rect.height - 60.0 * s) / 2.0 + + if pressed: + icon_color = rl.Color(139, 92, 246, 190) + elif hovered: + icon_color = rl.Color(160, 170, 185, 170) + else: + icon_color = rl.Color(160, 170, 185, 80) + draw_custom_icon("first_aid", icon_x, icon_y, s, icon_color) def _draw_header(self, rect: rl.Rectangle): pass def _measure_content_height(self, width: float) -> float: - RELOCATED_HEADER_HEIGHT = 80.0 display_h = self._section_block_height(self._slider_section_height(self._display_slider_keys, width)) power_h = self._section_block_height(self._slider_section_height(self._power_slider_keys, width)) @@ -456,7 +504,7 @@ class SystemSettingsManagerView(PanelManagerView): power_container_h = self._slider_section_height(self._power_slider_keys, column_w) left_overhead = 8.0 + 2 * (GROUP_HEADER_HEIGHT + GROUP_HEADER_GAP) + SECTION_GAP - left_natural_content_h = left_overhead + display_container_h + power_container_h + RELOCATED_HEADER_HEIGHT + left_natural_content_h = left_overhead + display_container_h + power_container_h tiles_content_h = self.measure_page_grid_height(self._connectivity_tile_grid, column_w - 24) right_natural_container_h = tiles_content_h + 24 @@ -473,7 +521,7 @@ class SystemSettingsManagerView(PanelManagerView): # Scale adjustors if needed if max_container_h < max_natural_h: - scale_f = (max_container_h - left_overhead - RELOCATED_HEADER_HEIGHT) / (left_natural_content_h - left_overhead - RELOCATED_HEADER_HEIGHT) + scale_f = max_container_h / left_natural_content_h row_h = max(60.0, 94.0 * scale_f) for key in self._display_slider_keys + self._power_slider_keys: self._adjustor_rows[key].custom_row_height = row_h @@ -487,7 +535,7 @@ class SystemSettingsManagerView(PanelManagerView): self._adjustor_rows[key].custom_row_height = None self._connectivity_tile_grid._tile_height = None tiles_content_h = self.measure_page_grid_height(self._connectivity_tile_grid, width - 24) - return self._stacked_section_height([display_h, power_h, tiles_content_h + 24]) + RELOCATED_HEADER_HEIGHT + return self._stacked_section_height([display_h, power_h, tiles_content_h + 24]) def _slider_section_height(self, keys: list[str], width: float) -> float: total = 0.0 @@ -501,47 +549,10 @@ class SystemSettingsManagerView(PanelManagerView): self._draw_basics_tab(y, rect.x, width) def _draw_basics_tab(self, y: float, x: float, width: float): - # Relocated Header elements drawn at the top - col_w = self._column_width(width) if self._uses_two_columns(width) else width - - # 1. Draw First Aid Button - btn_w, btn_h = 68.0, 68.0 - btn_x = x + col_w - btn_w - 8 - btn_y = y - btn_rect = rl.Rectangle(btn_x, btn_y, btn_w, btn_h) - - hovered, pressed = self._interactive_state("static:first_aid", btn_rect) - - if pressed: - fill = rl.Color(255, 255, 255, 30) - border = PANEL_STYLE.accent - elif hovered: - fill = rl.Color(255, 255, 255, 18) - border = PANEL_STYLE.accent - else: - fill = rl.Color(255, 255, 255, 8) - border = rl.Color(255, 255, 255, 20) - - draw_soft_card(btn_rect, fill, border) - - s = 48.0 / 60.0 - icon_x = btn_x + (btn_w - 60.0 * s) / 2.0 - icon_y = btn_y + (btn_h - 60.0 * s) / 2.0 - icon_color = PANEL_STYLE.accent if (hovered or pressed) else AetherListColors.HEADER - draw_custom_icon("first_aid", icon_x, icon_y, s, icon_color) - - # 2. Draw Drive Mode Control (AetherSegmentedControl) - control_rect = rl.Rectangle(x, y, col_w - btn_w - 12.0, btn_h) - self._drive_mode_control.render(control_rect) - - RELOCATED_HEADER_HEIGHT = 80.0 - y += RELOCATED_HEADER_HEIGHT - if self._uses_two_columns(width): column_w = self._column_width(width) - adj_container_h = self._system_max_container_h - RELOCATED_HEADER_HEIGHT + adj_container_h = self._system_max_container_h - # Draw unified shell for the adjustors draw_list_group_shell(rl.Rectangle(x, y, column_w, adj_container_h), style=PANEL_STYLE) current_y = y + 8 @@ -555,7 +566,7 @@ class SystemSettingsManagerView(PanelManagerView): for index, key in enumerate(self._power_slider_keys): current_y = self._draw_slider_row(rl.Rectangle(x, current_y, column_w, 0), key, is_last=index == len(self._power_slider_keys) - 1) - self._draw_two_column_tile_grid(self._connectivity_tile_grid, x + column_w + self.COLUMN_GAP, y - RELOCATED_HEADER_HEIGHT, column_w, self._system_max_container_h) + self._draw_two_column_tile_grid(self._connectivity_tile_grid, x + column_w + self.COLUMN_GAP, y, column_w, self._system_max_container_h) return y = self._draw_slider_section(y, x, width, tr("Display"), self._display_slider_keys)