From bd7394ea3341509a111f07dd922d77687c76e530 Mon Sep 17 00:00:00 2001 From: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Date: Sat, 13 Jun 2026 11:56:21 -0400 Subject: [PATCH] BigUI WIP: Tile updates --- .../layouts/settings/starpilot/aethergrid.py | 155 +++++++++++------- .../ui/layouts/settings/starpilot/lateral.py | 63 ++----- .../ui/layouts/settings/starpilot/sounds.py | 56 +++---- .../settings/starpilot/system_settings.py | 27 +-- .../ui/layouts/settings/starpilot/vehicle.py | 65 ++------ selfdrive/ui/tests/test_aethergrid.py | 123 ++++++++++++++ 6 files changed, 290 insertions(+), 199 deletions(-) diff --git a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py index af5e2f4ee..7e8532da9 100644 --- a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py +++ b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py @@ -567,7 +567,7 @@ class PanelManagerView(AetherInteractiveMixin, Widget): set_state=d.get("set_state", d.get("set")), bg_color=self.PANEL_STYLE.accent, desc=d.get("subtitle", d.get("desc", "")), - is_enabled=d.get("is_enabled"), + is_enabled=d.get("is_enabled", d.get("enabled")), disabled_label=d.get("disabled_label", ""), ) @@ -596,10 +596,11 @@ class PanelManagerView(AetherInteractiveMixin, Widget): return [] def _on_page_changed(self) -> None: - if not self._has_pagination or self._page_grid is None: + if not getattr(self, '_toggle_pages', None) or self._page_grid is None: return self._page_grid.clear() - for d in self._get_page_defs(): + page_idx = self._current_page if self._current_page < len(self._toggle_pages) else 0 + for d in self._toggle_pages[page_idx]: self._page_grid.add_tile(self._make_toggle_tile(d)) # ── scissor helpers ──────────────────────────────────────── @@ -674,7 +675,7 @@ class PanelManagerView(AetherInteractiveMixin, Widget): self._page_scissor_push(clip_rect) if self._page_anim_prev_tiles: - old_grid = TileGrid(columns=grid.get_column_count(), padding=grid.gap) + old_grid = TileGrid(columns=grid.get_column_count(), padding=grid.gap, tile_height=grid._tile_height) old_grid.tiles.extend(self._page_anim_prev_tiles) old_grid.set_parent_rect(self._scroll_rect) old_grid.render(rl.Rectangle(rect.x + prev_offset, rect.y, rect.width, rect.height)) @@ -2819,14 +2820,14 @@ class AetherTile(Widget): 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, a), radius_px=100) + _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( - int(off_border.r + (accent.r - off_border.r) * glow), - int(off_border.g + (accent.g - off_border.g) * glow), - int(off_border.b + (accent.b - off_border.b) * glow), + 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) @@ -2835,9 +2836,9 @@ class AetherTile(Widget): led_y = ry + 12 led_base = _HUD_LED_BASE led_col = rl.Color( - int(led_base.r + (accent.r - led_base.r) * glow), - int(led_base.g + (accent.g - led_base.g) * glow), - int(led_base.b + (accent.b - led_base.b) * glow), + max(0, min(255, int(led_base.r + (accent.r - led_base.r) * glow))), + max(0, min(255, int(led_base.g + (accent.g - led_base.g) * glow))), + max(0, min(255, int(led_base.b + (accent.b - led_base.b) * glow))), 255) rl.draw_rectangle(led_x, led_y, led_w, led_h, led_col) @@ -3053,49 +3054,55 @@ class ToggleTile(AetherTile): self._animate_plate(rl.get_frame_time()) if not enabled: - self.surface_color = self._disabled_color self._plate_offset = 0.0 self._plate_target = 0.0 - face = self._render_layers(rect) - state_text = tr(self._disabled_label) if self._disabled_label else tr("LOCKED") - self._draw_signal_edge(face, self.surface_color, width=TILE_SIGNAL_WIDTH, alpha=28) - self._render_tile_stack(face, title=self.title, primary=state_text, desc=self.desc, - title_font=self._font, primary_font=self._font, desc_font=self._font_desc, - title_size=28, primary_size=30) - return if not self._show_led: - self.surface_color = self._active_color if active else self._inactive_color + self.surface_color = self._disabled_color if not enabled else (self._active_color if active else self._inactive_color) + state_text = tr(self._disabled_label) if not enabled and self._disabled_label else (tr("LOCKED") if not enabled else (tr("ON") if active else tr("OFF"))) + signal_color = self._active_color if (enabled and active) else self.surface_color + alpha = 62 if (enabled and active) else 28 + face = self._render_layers(rect) - state_text = tr("ON") if active else tr("OFF") - self._draw_signal_edge(face, self._active_color if active else self.surface_color, - width=TILE_SIGNAL_WIDTH, alpha=62 if active else 28) + self._draw_signal_edge(face, signal_color, width=TILE_SIGNAL_WIDTH, alpha=alpha) self._render_tile_stack(face, title=self.title, primary=state_text, desc=self.desc, title_font=self._font, primary_font=self._font, desc_font=self._font_desc, title_size=28, primary_size=30) return - # --- HUD toggle path (show_led + enabled) --- - face, accent = self._render_hud_background(rect, self._active_color, self._glow) + # --- HUD toggle path (show_led) --- + color = self._active_color if enabled else self._disabled_color + glow = self._glow if enabled else 0.0 + face, accent = self._render_hud_background(rect, color, glow) rx, ry, rw, rh = face.x, face.y, face.width, face.height content_pad = SPACING.tile_content max_w = rw - content_pad * 2 text_scale = min(rw / 360.0, rh / 205.0) title_size = max(18, int(round(22 * text_scale))) - title_color = rl.WHITE if self.get_state() else _HUD_TEXT_DIM - self._draw_text_fit(self._font, self.title, - rl.Vector2(rx + content_pad, ry + int(rh * 0.50)), - max_w, title_size, align_center=True, color=title_color) - led_cx = rx + rw // 2 - led_cy = ry + int(rh * 0.75) - if self.get_state(): - rl.draw_circle(int(led_cx), int(led_cy), 11, rl.Color(accent.r, accent.g, accent.b, 24)) - rl.draw_circle(int(led_cx), int(led_cy), 6, accent) + if not enabled: + self._draw_text_fit(self._font, self.title, + rl.Vector2(rx + content_pad, ry + int(rh * 0.40)), + max_w, title_size, align_center=True, color=_HUD_TEXT_DIM) + disabled_text = tr(self._disabled_label) if self._disabled_label else tr("LOCKED") + desc_size = max(14, int(round(16 * text_scale))) + self._draw_text_fit(self._font_desc, disabled_text, + rl.Vector2(rx + content_pad, ry + int(rh * 0.68)), + max_w, desc_size, align_center=True, color=_HUD_TEXT_DIM) else: - rl.draw_circle(int(led_cx), int(led_cy), 7, rl.Color(14, 16, 22, 255)) - rl.draw_ring(rl.Vector2(led_cx, led_cy), 5, 6, 0, 360, 24, rl.Color(70, 78, 95, 140)) + title_color = rl.WHITE if active else _HUD_TEXT_DIM + self._draw_text_fit(self._font, self.title, + rl.Vector2(rx + content_pad, ry + int(rh * 0.50)), + max_w, title_size, align_center=True, color=title_color) + led_cx = rx + rw // 2 + led_cy = ry + int(rh * 0.75) + if active: + rl.draw_circle(int(led_cx), int(led_cy), 11, rl.Color(accent.r, accent.g, accent.b, 24)) + rl.draw_circle(int(led_cx), int(led_cy), 6, accent) + else: + rl.draw_circle(int(led_cx), int(led_cy), 7, rl.Color(14, 16, 22, 255)) + rl.draw_ring(rl.Vector2(led_cx, led_cy), 5, 6, 0, 360, 24, rl.Color(70, 78, 95, 140)) class ValueTile(AetherTile): @@ -3137,32 +3144,31 @@ class ValueTile(AetherTile): self._animate_plate(rl.get_frame_time()) if not enabled: - self.surface_color = self._disabled_color self._plate_offset = 0.0 self._plate_target = 0.0 - face = self._render_layers(rect) - self._draw_signal_edge(face, self.surface_color, width=TILE_SIGNAL_WIDTH, alpha=28) - self._render_tile_stack(face, title=self.title, primary=self.get_value(), desc=self.desc, - title_font=self._font, primary_font=self._font, desc_font=self._font_desc, - title_size=28, primary_size=28) - return - face, accent = self._render_hud_background(rect, self._active_color) + color = self._active_color if enabled else self._disabled_color + glow = 1.0 if enabled else 0.0 + face, accent = self._render_hud_background(rect, color, glow) rx, ry, rw, rh = face.x, face.y, face.width, face.height content_pad = SPACING.tile_content max_w = rw - content_pad * 2 text_scale = min(rw / 360.0, rh / 205.0) + + # Title title_size = max(18, int(round(22 * text_scale))) self._draw_text_fit(self._font, self.title, rl.Vector2(rx + content_pad, ry + int(rh * 0.35)), max_w, title_size, align_center=True, color=_HUD_TEXT_DIM) + # Value val_text = self.get_value() val_size = max(18, int(round(24 * text_scale))) + val_color = accent if enabled else _HUD_TEXT_DIM self._draw_text_fit(self._font, val_text, rl.Vector2(rx + content_pad, ry + int(rh * 0.58)), - max_w, val_size, align_center=True, color=accent) + max_w, val_size, align_center=True, color=val_color) class SliderTile(AetherTile): @@ -3282,34 +3288,33 @@ class SliderTile(AetherTile): self._animate_plate(dt) if not enabled: - self.surface_color = self._disabled_color self._plate_offset = 0.0 self._plate_target = 0.0 - face = self._render_layers(rect) - self._draw_signal_edge(face, self.surface_color, width=TILE_SIGNAL_WIDTH, alpha=28) - val_str = self.labels.get(current_val, f"{int(current_val)}{self.unit}") - self._render_tile_stack(face, title=self.title, primary=val_str, desc=self.desc, - title_font=self._font, primary_font=self._font, desc_font=self._font_desc, - title_size=28, primary_size=28) - return - face, accent = self._render_hud_background(rect, self._active_color) + color = self._active_color if enabled else self._disabled_color + glow = 1.0 if enabled else 0.0 + face, accent = self._render_hud_background(rect, color, glow) rx, ry, rw, rh = face.x, face.y, face.width, face.height content_pad = SPACING.tile_content max_w = rw - content_pad * 2 text_scale = min(rw / 360.0, rh / 205.0) + + # Title title_size = max(18, int(round(22 * text_scale))) self._draw_text_fit(self._font, self.title, rl.Vector2(rx + content_pad, ry + int(rh * 0.30)), max_w, title_size, align_center=True, color=_HUD_TEXT_DIM) + # Value text val_str = self.labels.get(current_val, f"{int(current_val)}{self.unit}") val_size = max(18, int(round(24 * text_scale))) + val_color = accent if enabled else _HUD_TEXT_DIM self._draw_text_fit(self._font, val_str, rl.Vector2(rx + content_pad, ry + int(rh * 0.52)), - max_w, val_size, align_center=True, color=accent) + max_w, val_size, align_center=True, color=val_color) + # Slider meter value_range = self.max_val - self.min_val frac = 0.0 if value_range == 0 else max(0.0, min(1.0, (self._smooth_value - self.min_val) / value_range)) meter_h = 6 @@ -3318,7 +3323,8 @@ class SliderTile(AetherTile): rl.draw_rectangle_rec(_snap_rect(meter_rect), rl.Color(255, 255, 255, 14)) if fill_w > 1: fill_rect = rl.Rectangle(meter_rect.x, meter_rect.y, fill_w, meter_rect.height) - rl.draw_rectangle_rec(_snap_rect(fill_rect), _with_alpha(accent, 176)) + fill_color = _with_alpha(accent, 176) if enabled else rl.Color(120, 120, 120, 100) + rl.draw_rectangle_rec(_snap_rect(fill_rect), fill_color) class AetherSlider(Widget): @@ -4113,13 +4119,16 @@ class AetherSegmentedControl(Widget): class TileGrid(Widget): - def __init__(self, columns: int | None = None, padding: int | None = None, uniform_width: bool = False, min_tile_width: int | None = None): + def __init__(self, columns: int | None = None, padding: int | None = None, uniform_width: bool = False, min_tile_width: int | None = None, tile_height: float | None = None, force_square: bool = False): super().__init__() self._columns = columns self._gap = padding if padding is not None else SPACING.tile_gap self.tiles = [] self._uniform_width = uniform_width self._min_tile_width = min_tile_width if min_tile_width is not None else MIN_TILE_WIDTH + self._tile_height = tile_height + self.force_square = force_square + @property def gap(self) -> int: @@ -4165,7 +4174,8 @@ class TileGrid(Widget): return preferred min_tile_width = max(1, self._min_tile_width) max_cols_by_width = max(1, int((available_width + self._gap) / (min_tile_width + self._gap))) - return max(1, min(preferred, count, max_cols_by_width)) + limit_by_count = count if self._columns is None else preferred + return max(1, min(preferred, limit_by_count, max_cols_by_width)) def get_row_count(self, tile_count: int | None = None, available_width: float | None = None) -> int: count = len(self.tiles) if tile_count is None else tile_count @@ -4178,6 +4188,19 @@ class TileGrid(Widget): rows = self.get_row_count(tile_count, available_width=available_width) return self._gap * max(0, rows - 1) + def measure_height(self, width: float) -> float: + if not self.tiles: + return 0.0 + count = len(self.tiles) + rows = self.get_row_count(count, available_width=width) + if self.force_square: + cols = self.get_effective_column_count(width, count) + col_w = (width - (self._gap * (cols - 1))) / cols + h = col_w + else: + h = self._tile_height if self._tile_height is not None else 130.0 + return rows * h + self.get_internal_gap_height(count, available_width=width) + def _render(self, rect: rl.Rectangle): rect = _snap_rect(rect) self.set_rect(rect) @@ -4187,18 +4210,24 @@ class TileGrid(Widget): count = len(tiles_to_render) cols = self.get_effective_column_count(rect.width, count) rows = self.get_row_count(count, available_width=rect.width) - 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 + if self.force_square: + uniform_tile_w = (rect.width - (self._gap * (cols - 1))) / cols + tile_h = uniform_tile_w + else: + if self._tile_height is not None: + tile_h = self._tile_height + else: + 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 if remaining <= 0: break items_in_row = min(cols, remaining) - if self._uniform_width: + if self.force_square or self._uniform_width: row_tile_w = uniform_tile_w - row_width = (row_tile_w * items_in_row) + (self._gap * (items_in_row - 1)) - row_x = rect.x + (rect.width - row_width) / 2 + row_x = rect.x else: row_tile_w = (rect.width - (self._gap * (items_in_row - 1))) / items_in_row row_x = rect.x diff --git a/selfdrive/ui/layouts/settings/starpilot/lateral.py b/selfdrive/ui/layouts/settings/starpilot/lateral.py index 897a37d3b..1b03c609d 100644 --- a/selfdrive/ui/layouts/settings/starpilot/lateral.py +++ b/selfdrive/ui/layouts/settings/starpilot/lateral.py @@ -239,18 +239,9 @@ class SteeringManagerView(PanelManagerView): return toggles def _rebuild_toggle_grid(self): - self._toggle_grid.clear() - for td in self._build_toggle_defs(): - self._toggle_grid.add_tile(ToggleTile( - title=td["title"], - get_state=td["get"], - set_state=td["set"], - bg_color=PANEL_STYLE.accent, - desc=td.get("subtitle", ""), - is_enabled=td.get("enabled", True), - disabled_label=td.get("disabled_label", ""), - show_led=True, - )) + self._page_grid = self._toggle_grid + defs = self._build_toggle_defs() + self._set_toggle_pages([defs[i:i+6] for i in range(0, len(defs), 6)]) def _measure_content_height(self, width: float) -> float: sections = self._build_left_sections() @@ -258,19 +249,15 @@ class SteeringManagerView(PanelManagerView): tiles_height = 0.0 if self._toggle_grid.tiles: - N = len(self._toggle_grid.tiles) - gap = self._toggle_grid.gap if self._uses_two_columns(width): - cols = 2 - tile_rows = (N + cols - 1) // cols - tile_gaps = gap * (tile_rows - 1) if tile_rows > 0 else 0 - tiles_content_h = tile_rows * 130 + tile_gaps - tiles_height = self._section_block_height(tiles_content_h + 24) + self._toggle_grid._columns = 2 + column_w = self._column_width(width) + col_w = (column_w - 24 - 12) / 2 + grid_h = 2 * col_w + 12 + tiles_height = self._section_block_height(grid_h + 24) else: - cols = 3 - tile_rows = (N + cols - 1) // cols - tile_gaps = gap * (tile_rows - 1) if tile_rows > 0 else 0 - tiles_content_h = tile_rows * 130 + tile_gaps + self._toggle_grid._columns = 3 + tiles_content_h = self._toggle_grid.measure_height(width - 24) tiles_height = SECTION_GAP + self._section_block_height(tiles_content_h + 24) if self._uses_two_columns(width): @@ -315,21 +302,13 @@ class SteeringManagerView(PanelManagerView): tr("Toggles"), style=PANEL_STYLE) right_container_y = y + SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP - N = len(self._toggle_grid.tiles) - cols = 2 - self._toggle_grid._columns = cols - gap = self._toggle_grid.gap - tile_rows = (N + cols - 1) // cols - tile_gaps = gap * (tile_rows - 1) if tile_rows > 0 else 0 - tiles_content_h = tile_rows * 130 + tile_gaps - - needed_height = tiles_content_h + 24 - viewport_remaining = (self._scroll_rect.y + self._scroll_rect.height) - right_container_y - container_h = max(needed_height, viewport_remaining) + self._toggle_grid._columns = 2 + col_w = (column_w - 24 - 12) / 2 + grid_h = 2 * col_w + 12 + container_h = grid_h + 24 draw_list_group_shell(rl.Rectangle(rx, right_container_y, column_w, container_h), style=PANEL_STYLE) - self._toggle_grid.set_parent_rect(self._scroll_rect) - self._toggle_grid.render(rl.Rectangle(rx + 12, right_container_y + 12, column_w - 24, container_h - 24)) + self._render_page_grid(self._toggle_grid, rl.Rectangle(rx + 12, right_container_y + 12, column_w - 24, grid_h)) else: curr_y = y for section in sections: @@ -355,18 +334,12 @@ class SteeringManagerView(PanelManagerView): tr("Toggles"), style=PANEL_STYLE) curr_y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP - N = len(self._toggle_grid.tiles) - cols = 3 - self._toggle_grid._columns = cols - gap = self._toggle_grid.gap + self._toggle_grid._columns = 3 avail_w = width - 24 - tile_rows = (N + cols - 1) // cols - tile_gaps = gap * (tile_rows - 1) if tile_rows > 0 else 0 - tiles_content_h = tile_rows * 130 + tile_gaps + tiles_content_h = self._toggle_grid.measure_height(avail_w) draw_list_group_shell(rl.Rectangle(x, curr_y, width, tiles_content_h + 24), style=PANEL_STYLE) - self._toggle_grid.set_parent_rect(self._scroll_rect) - self._toggle_grid.render(rl.Rectangle(x + 12, curr_y + 12, avail_w, tiles_content_h)) + self._render_page_grid(self._toggle_grid, rl.Rectangle(x + 12, curr_y + 12, avail_w, tiles_content_h)) def _draw_row(self, rect: rl.Rectangle, row: dict, is_last: bool): target_id = row["target_id"] diff --git a/selfdrive/ui/layouts/settings/starpilot/sounds.py b/selfdrive/ui/layouts/settings/starpilot/sounds.py index b29f9c783..7db5647d2 100644 --- a/selfdrive/ui/layouts/settings/starpilot/sounds.py +++ b/selfdrive/ui/layouts/settings/starpilot/sounds.py @@ -74,19 +74,22 @@ class SoundsManagerView(PanelManagerView): def _init_toggles(self): self._toggle_grid = TileGrid(columns=2, padding=12) + self._child(self._toggle_grid) + self._page_grid = self._toggle_grid + + toggle_defs = [] for key in self._controller.CUSTOM_ALERTS_KEYS: info = self._controller.ALERT_INFO[key] - tile = ToggleTile( - title=tr(info["title"]), - get_state=lambda k=key: self._controller._params.get_bool(k), - set_state=lambda state, k=key: self._controller._params.put_bool(k, state), - bg_color=PANEL_STYLE.accent, - desc=tr(info.get("subtitle", "")), - is_enabled=info.get("is_enabled"), - disabled_label=tr(info.get("disabled_label", "")) if info.get("disabled_label") else "", - ) - self._toggle_grid.add_tile(tile) - self._child(self._toggle_grid) + toggle_defs.append({ + "title": tr(info["title"]), + "subtitle": tr(info.get("subtitle", "")), + "get": lambda k=key: self._controller._params.get_bool(k), + "set": lambda state, k=key: self._controller._params.put_bool(k, state), + "is_enabled": info.get("is_enabled"), + "disabled_label": tr(info.get("disabled_label", "")) if info.get("disabled_label") else "", + }) + + self._set_toggle_pages([toggle_defs[i:i+6] for i in range(0, len(toggle_defs), 6)]) def _set_active_adjustor(self, key: str, active: bool): if active: @@ -195,8 +198,7 @@ class SoundsManagerView(PanelManagerView): ) def _handle_mouse_press(self, mouse_pos: MousePos): - self._pressed_target = self._target_at(mouse_pos) - self._can_click = True + super()._handle_mouse_press(mouse_pos) for adjustor in self._adjustor_rows.values(): adjustor._handle_mouse_press(mouse_pos) self._toggle_grid._handle_mouse_press(mouse_pos) @@ -205,17 +207,10 @@ class SoundsManagerView(PanelManagerView): for adjustor in self._adjustor_rows.values(): adjustor._handle_mouse_release(mouse_pos) self._toggle_grid._handle_mouse_release(mouse_pos) - - target = self._target_at(mouse_pos) if self._scroll_panel.is_touch_valid() else None - if self._pressed_target is not None and self._pressed_target == target and self._can_click: - self._activate_target(target) - self._pressed_target = None - self._can_click = True + super()._handle_mouse_release(mouse_pos) def _handle_mouse_event(self, mouse_event: MouseEvent): - if not self._scroll_panel.is_touch_valid(): - self._can_click = False - return + super()._handle_mouse_event(mouse_event) for adjustor in self._adjustor_rows.values(): adjustor._handle_mouse_event(mouse_event) self._toggle_grid._handle_mouse_event(mouse_event) @@ -251,13 +246,13 @@ class SoundsManagerView(PanelManagerView): cd_h = self._adjustor_rows[self._controller.COOLDOWN_KEY].measure_height(col_width) - tile_rows = self._toggle_grid.get_row_count(len(self._toggle_grid.tiles), available_width=col_width) - tile_gaps = self._toggle_grid.get_internal_gap_height(len(self._toggle_grid.tiles), available_width=col_width) - tiles_content_h = tile_rows * 130 + tile_gaps + tiles_content_h = self._toggle_grid.measure_height(col_width - 24) + # col_w is the width of each tile in the 2-column grid + col_w = (col_width - 24 - 12) / 2 + # Standardize right-side grid height: 2x3 rectangular layout (same container as before) + self._tile_grid_h = 2 * col_w + 12 - self._tile_grid_h = max(tiles_content_h, left_column_total_h - cd_h - SECTION_GAP - (GROUP_HEADER_HEIGHT + GROUP_HEADER_GAP) - 16.0) - - right_column_total_h = cd_h + SECTION_GAP + (GROUP_HEADER_HEIGHT + GROUP_HEADER_GAP) + (self._tile_grid_h + 16.0) + right_column_total_h = cd_h + SECTION_GAP + (GROUP_HEADER_HEIGHT + GROUP_HEADER_GAP) + (self._tile_grid_h + 24.0) section_overhead = SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP min_h = self._scroll_rect.height if self._scroll_rect else 0.0 @@ -349,10 +344,9 @@ class SoundsManagerView(PanelManagerView): current_y = self._draw_group_header(x + 24, current_y, width - 48, tr("CUSTOM ALERTS")) - draw_list_group_shell(rl.Rectangle(x, current_y, width, self._tile_grid_h + 16), style=PANEL_STYLE) + draw_list_group_shell(rl.Rectangle(x, current_y, width, self._tile_grid_h + 24), style=PANEL_STYLE) - self._toggle_grid.set_parent_rect(self._scroll_rect) - self._toggle_grid.render(rl.Rectangle(x + 12, current_y + 8, width - 24, self._tile_grid_h)) + self._render_page_grid(self._toggle_grid, rl.Rectangle(x + 12, current_y + 12, width - 24, self._tile_grid_h)) class StarPilotSoundsLayout(_SettingsPage): diff --git a/selfdrive/ui/layouts/settings/starpilot/system_settings.py b/selfdrive/ui/layouts/settings/starpilot/system_settings.py index 1bd39a888..d82705a9b 100644 --- a/selfdrive/ui/layouts/settings/starpilot/system_settings.py +++ b/selfdrive/ui/layouts/settings/starpilot/system_settings.py @@ -311,7 +311,7 @@ class SystemSettingsManagerView(PanelManagerView): self._connectivity_tile_grid.set_touch_valid_callback(lambda: self._scroll_panel.is_touch_valid()) self._child(self._connectivity_tile_grid) self._page_grid = self._connectivity_tile_grid - self._set_toggle_pages([self._toggle_defs[i:i+4] for i in range(0, len(self._toggle_defs), 4)]) + self._set_toggle_pages([self._toggle_defs[i:i+6] for i in range(0, len(self._toggle_defs), 6)]) self._drive_mode_control = self._child( AetherSegmentedControl( @@ -477,16 +477,17 @@ class SystemSettingsManagerView(PanelManagerView): 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)) - tile_rows = self._connectivity_tile_grid.get_row_count( - len(self._connectivity_tile_grid.tiles), available_width=width) - tile_gaps = self._connectivity_tile_grid.get_internal_gap_height( - len(self._connectivity_tile_grid.tiles), available_width=width) - tiles_content_h = tile_rows * 130 + tile_gaps - if self._uses_two_columns(width): + column_w = self._column_width(width) + col_w = (column_w - 24 - 12) / 2 + grid_h = 2 * col_w + 12 + container_h = grid_h + 24 + right_h = self._section_block_height(container_h) + left_h = display_h + SECTION_GAP + power_h min_h = self._scroll_rect.height if self._scroll_rect else 0.0 - return max(min_h, display_h + SECTION_GAP + power_h) + return max(min_h, left_h, right_h) else: + tiles_content_h = self._connectivity_tile_grid.measure_height(width - 24) return self._stacked_section_height([display_h, power_h, tiles_content_h + 24]) def _slider_section_height(self, keys: list[str], width: float) -> float: @@ -513,10 +514,12 @@ class SystemSettingsManagerView(PanelManagerView): display_bottom = self._draw_slider_section(y, x, column_w, tr("Display"), self._display_slider_keys) power_y = display_bottom + section_gap - power_bottom = self._draw_slider_section(power_y, x, column_w, tr("Power"), self._power_slider_keys) + self._draw_slider_section(power_y, x, column_w, tr("Power"), self._power_slider_keys) container_top = y + SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP - container_height = power_bottom - container_top + col_w = (column_w - 24 - 12) / 2 + grid_h = 2 * col_w + 12 + container_height = grid_h + 24 self._draw_connectivity_tiles_column(container_top, x + column_w + self.COLUMN_GAP, column_w, container_height) return y = self._draw_slider_section(y, x, width, tr("Display"), self._display_slider_keys) @@ -530,9 +533,7 @@ class SystemSettingsManagerView(PanelManagerView): self._render_page_grid(self._connectivity_tile_grid, rl.Rectangle(x + 12, y + 12, width - 24, height - 24)) def _draw_connectivity_tiles_section(self, y: float, x: float, width: float): - tile_rows = self._connectivity_tile_grid.get_row_count(len(self._connectivity_tile_grid.tiles), available_width=width) - tile_gaps = self._connectivity_tile_grid.get_internal_gap_height(len(self._connectivity_tile_grid.tiles), available_width=width) - tiles_content_h = tile_rows * 130 + tile_gaps + tiles_content_h = self._connectivity_tile_grid.measure_height(width - 24) draw_list_group_shell(rl.Rectangle(x, y, width, tiles_content_h + 24), style=PANEL_STYLE) self._render_page_grid(self._connectivity_tile_grid, rl.Rectangle(x + 12, y + 12, width - 24, tiles_content_h)) diff --git a/selfdrive/ui/layouts/settings/starpilot/vehicle.py b/selfdrive/ui/layouts/settings/starpilot/vehicle.py index 2c1ce1582..58c9b2144 100644 --- a/selfdrive/ui/layouts/settings/starpilot/vehicle.py +++ b/selfdrive/ui/layouts/settings/starpilot/vehicle.py @@ -209,20 +209,9 @@ class VehicleSettingsManagerView(PanelManagerView): return toggles def _rebuild_toggle_grid(self): - self._toggle_grid.clear() - toggles = self._build_driving_toggles() - self._toggle_grid._columns = len(toggles) - for toggle_def in toggles: - tile = ToggleTile( - title=toggle_def["title"], - get_state=toggle_def["get_state"], - set_state=toggle_def["set_state"], - bg_color=PANEL_STYLE.accent, - desc=toggle_def.get("subtitle", ""), - is_enabled=toggle_def.get("is_enabled"), - disabled_label=toggle_def.get("disabled_label", ""), - ) - self._toggle_grid.add_tile(tile) + self._page_grid = self._toggle_grid + defs = self._build_driving_toggles() + self._set_toggle_pages([defs[i:i+6] for i in range(0, len(defs), 6)]) def _check_rebuild_grid(self): current_make = self._controller._get_display_make() @@ -335,20 +324,16 @@ class VehicleSettingsManagerView(PanelManagerView): # Right Column/Features height tiles_height = 0.0 if self._toggle_grid.tiles: - N = len(self._toggle_grid.tiles) - gap = self._toggle_grid.gap if self._uses_two_columns(width): - cols = 2 - tile_rows = (N + cols - 1) // cols - tile_gaps = gap * (tile_rows - 1) if tile_rows > 0 else 0 - tiles_content_h = tile_rows * 130 + tile_gaps - tiles_height = self._section_block_height(tiles_content_h + 24) + self._toggle_grid._columns = 2 + column_w = self._column_width(width) + col_w = (column_w - 24 - 12) / 2 + grid_h = 2 * col_w + 12 + tiles_height = self._section_block_height(grid_h + 24) else: + self._toggle_grid._columns = 3 avail_w = width - 24 - cols = 3 - tile_rows = (N + cols - 1) // cols - tile_gaps = gap * (tile_rows - 1) if tile_rows > 0 else 0 - tiles_content_h = tile_rows * 130 + tile_gaps + tiles_content_h = self._toggle_grid.measure_height(avail_w) tiles_height = SECTION_GAP + self._section_block_height(tiles_content_h + 24) if self._uses_two_columns(width): @@ -414,21 +399,13 @@ class VehicleSettingsManagerView(PanelManagerView): draw_section_header(rl.Rectangle(rx, y, column_w, SECTION_HEADER_HEIGHT), tr("Features"), style=PANEL_STYLE) right_container_y = y + SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP - N = len(self._toggle_grid.tiles) - cols = 2 - self._toggle_grid._columns = cols - gap = self._toggle_grid.gap - tile_rows = (N + cols - 1) // cols - tile_gaps = gap * (tile_rows - 1) if tile_rows > 0 else 0 - tiles_content_h = tile_rows * 130 + tile_gaps - - needed_height = tiles_content_h + 24 - viewport_remaining = (self._scroll_rect.y + self._scroll_rect.height) - right_container_y - container_h = max(needed_height, viewport_remaining) + self._toggle_grid._columns = 2 + col_w = (column_w - 24 - 12) / 2 + grid_h = 2 * col_w + 12 + container_h = grid_h + 24 draw_list_group_shell(rl.Rectangle(rx, right_container_y, column_w, container_h), style=PANEL_STYLE) - self._toggle_grid.set_parent_rect(self._scroll_rect) - self._toggle_grid.render(rl.Rectangle(rx + 12, right_container_y + 12, column_w - 24, container_h - 24)) + self._render_page_grid(self._toggle_grid, rl.Rectangle(rx + 12, right_container_y + 12, column_w - 24, grid_h)) else: # Single Column Stacked Layout draw_section_header(rl.Rectangle(x, y, width, SECTION_HEADER_HEIGHT), tr("Vehicle Identity"), style=PANEL_STYLE) @@ -455,18 +432,12 @@ class VehicleSettingsManagerView(PanelManagerView): draw_section_header(rl.Rectangle(x, y, width, SECTION_HEADER_HEIGHT), tr("Features"), style=PANEL_STYLE) y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP - N = len(self._toggle_grid.tiles) - cols = 3 - self._toggle_grid._columns = cols - gap = self._toggle_grid.gap + self._toggle_grid._columns = 3 avail_w = width - 24 - tile_rows = (N + cols - 1) // cols - tile_gaps = gap * (tile_rows - 1) if tile_rows > 0 else 0 - tiles_content_h = tile_rows * 130 + tile_gaps + tiles_content_h = self._toggle_grid.measure_height(avail_w) draw_list_group_shell(rl.Rectangle(x, y, width, tiles_content_h + 24), style=PANEL_STYLE) - self._toggle_grid.set_parent_rect(self._scroll_rect) - self._toggle_grid.render(rl.Rectangle(x + 12, y + 12, avail_w, tiles_content_h)) + self._render_page_grid(self._toggle_grid, rl.Rectangle(x + 12, y + 12, avail_w, tiles_content_h)) def _draw_row(self, rect: rl.Rectangle, row: dict, is_last: bool): target_id = row["target_id"] diff --git a/selfdrive/ui/tests/test_aethergrid.py b/selfdrive/ui/tests/test_aethergrid.py index 3511560fb..cafffb524 100644 --- a/selfdrive/ui/tests/test_aethergrid.py +++ b/selfdrive/ui/tests/test_aethergrid.py @@ -396,6 +396,129 @@ class TestAethergridContracts(unittest.TestCase): self.assertEqual(dialog._current_val, 6) self.assertEqual(captured_changes, [6]) + def test_tile_grid_measure_height_with_explicit_tile_height(self): + mod = _import_aethergrid() + grid = mod.TileGrid(columns=2, padding=10, tile_height=140) + for _ in range(5): + grid.add_tile(RenderSpy()) + + h = grid.measure_height(500) + self.assertEqual(h, 740) + + def test_tile_grid_measure_height_default_fallback(self): + mod = _import_aethergrid() + grid = mod.TileGrid(columns=2, padding=10, tile_height=None) + for _ in range(5): + grid.add_tile(RenderSpy()) + + h = grid.measure_height(500) + self.assertEqual(h, 690) + + def test_tile_grid_render_top_left_aligned_with_tile_height(self): + mod = _import_aethergrid() + grid = mod.TileGrid(columns=2, padding=10, tile_height=140) + spy = RenderSpy() + grid.add_tile(spy) + + 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].x, 0) + + def test_disabled_tiles_hud_mode_rendering(self): + mod = _import_aethergrid() + + # ToggleTile disabled, show_led=True + toggle = mod.ToggleTile( + title="Test Loud", + get_state=lambda: True, + set_state=lambda s: None, + is_enabled=lambda: False, + show_led=True + ) + # Spy on _render_hud_background + orig_hud_bg = toggle._render_hud_background + spy_called = [] + def spy_hud_bg(*a, **k): + spy_called.append("toggle") + return orig_hud_bg(*a, **k) + toggle._render_hud_background = spy_hud_bg + toggle.render(mod.rl.Rectangle(0, 0, 150, 130)) + self.assertIn("toggle", spy_called) + + # ValueTile disabled + value_tile = mod.ValueTile( + title="Test Value", + get_value=lambda: "Off", + on_click=lambda: None, + is_enabled=lambda: False + ) + orig_value_hud_bg = value_tile._render_hud_background + def spy_value_hud_bg(*a, **k): + spy_called.append("value") + return orig_value_hud_bg(*a, **k) + value_tile._render_hud_background = spy_value_hud_bg + value_tile.render(mod.rl.Rectangle(0, 0, 150, 130)) + self.assertIn("value", spy_called) + + # SliderTile disabled + slider_tile = mod.SliderTile( + title="Test Slider", + get_value=lambda: 50.0, + set_value=lambda v: None, + min_val=0.0, + max_val=100.0, + step=1.0, + is_enabled=lambda: False + ) + orig_slider_hud_bg = slider_tile._render_hud_background + def spy_slider_hud_bg(*a, **k): + spy_called.append("slider") + return orig_slider_hud_bg(*a, **k) + slider_tile._render_hud_background = spy_slider_hud_bg + slider_tile.render(mod.rl.Rectangle(0, 0, 150, 130)) + self.assertIn("slider", spy_called) + + def test_tile_grid_force_square(self): + mod = _import_aethergrid() + grid = mod.TileGrid(columns=2, padding=10, min_tile_width=100, force_square=True) + spies = [RenderSpy() for _ in range(5)] + for spy in spies: + grid.add_tile(spy) + + # col_w = (500 - 10) / 2 = 245 + # rows = 3, gap_h = 2 * 10 = 20 + # expected height = 3 * 245 + 20 = 755 + self.assertEqual(grid.measure_height(500), 755) + + grid.render(mod.rl.Rectangle(0, 0, 500, 300)) + self.assertTrue(spies[0].rects) + self.assertEqual(spies[0].rects[0].width, 245) + self.assertEqual(spies[0].rects[0].height, 245) + + def test_tile_grid_column_preservation_with_single_tile(self): + mod = _import_aethergrid() + grid = mod.TileGrid(columns=2, padding=10, min_tile_width=100, force_square=True) + spy = RenderSpy() + grid.add_tile(spy) + + # With only 1 tile in a 2-column layout, it should still calculate the tile size based on 2 columns + # col_w = (500 - 10) / 2 = 245 + grid.render(mod.rl.Rectangle(0, 0, 500, 300)) + self.assertTrue(spy.rects) + self.assertEqual(spy.rects[0].width, 245) + self.assertEqual(spy.rects[0].height, 245) + + def test_hud_background_glow_overflow_protection(self): + mod = _import_aethergrid() + tile = mod.ToggleTile("Test", lambda: True, lambda v: None) + # Test with extreme glow values (negative and large positive) to verify no OverflowError occurs + try: + tile._render_hud_background(mod.rl.Rectangle(0, 0, 150, 130), mod.rl.Color(255, 0, 0, 255), glow=5.0) + tile._render_hud_background(mod.rl.Rectangle(0, 0, 150, 130), mod.rl.Color(255, 0, 0, 255), glow=-2.0) + except OverflowError: + self.fail("OverflowError raised with extreme glow values") + if __name__ == "__main__": unittest.main()