diff --git a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py index 1eaf41550..4297b98c8 100644 --- a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py +++ b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py @@ -227,7 +227,7 @@ class AetherListMetrics: panel_padding_x: int = 16 panel_padding_top: int = 28 panel_padding_bottom: int = 22 - header_height: int = 244 + header_height: int = 0 section_gap: int = 28 section_header_height: int = 34 section_header_gap: int = 12 @@ -266,7 +266,7 @@ class AetherListFrame: AETHER_LIST_METRICS = AetherListMetrics() AETHER_COMPACT_ROW_HEIGHT = AETHER_LIST_METRICS.utility_row_height -COMPACT_PANEL_METRICS = replace(AETHER_LIST_METRICS, header_height=125) +COMPACT_PANEL_METRICS = replace(AETHER_LIST_METRICS, header_height=0) @dataclass(frozen=True, slots=True) @@ -338,6 +338,26 @@ def _point_hits(mouse_pos: MousePos, rect: rl.Rectangle, parent_rect: rl.Rectang return hit.width > 0 and hit.height > 0 and rl.check_collision_point_rec(mouse_pos, hit) +def wrap_text(font: rl.Font, text: str, max_width: float, font_size: float, max_lines: int = 2) -> list[str]: + spacing = font_size * 0.15 + words = text.split() + lines: list[str] = [] + current = "" + for word in words: + candidate = f"{current} {word}".strip() if current else word + if measure_text_cached(font, candidate, int(font_size), spacing=spacing).x <= max_width: + current = candidate + else: + if current: + lines.append(current) + current = word + if len(lines) >= max_lines: + break + if current and len(lines) < max_lines: + lines.append(current) + return lines if lines else [text] + + def build_list_panel_frame(rect: rl.Rectangle, metrics: AetherListMetrics = AETHER_LIST_METRICS) -> AetherListFrame: shell_w = min(rect.width - metrics.outer_margin_x * 2, metrics.max_content_width) shell_x = rect.x + (rect.width - shell_w) / 2 @@ -864,40 +884,34 @@ PRESSED_BREADCRUMB: str | None = None def get_breadcrumbs_path() -> list[tuple[str, str]]: import openpilot.selfdrive.ui.layouts.settings.starpilot.main_panel as main_panel + from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanelType layout = getattr(main_panel.StarPilotLayout, "active_instance", None) path = [("HOME", "action:home")] if not layout: return path - from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanelType - pushed_widgets = gui_app._nav_stack[1:] + # 1. Add Category if selected cat_title = "" is_folder = False if layout._current_category_idx is not None: cat = layout.CATEGORIES[layout._current_category_idx] cat_title = cat["title"].upper() is_folder = "buttons" in cat - - if is_folder: - if layout._current_panel != StarPilotPanelType.MAIN: - path.append((cat_title, "action:category")) - elif pushed_widgets: - path.append((cat_title, "action:category")) - else: - if pushed_widgets: - path.append((cat_title, "action:category")) - - if layout._current_panel != StarPilotPanelType.MAIN and pushed_widgets: + path.append((cat_title, "action:category")) + + # 2. Add Sub-panel if active + if layout._current_panel != StarPilotPanelType.MAIN: panel_info = layout._panels[layout._current_panel] if panel_info.name: - panel_title = panel_info.name.upper() if is_folder or layout._current_category_idx is None: + panel_title = panel_info.name.upper() path.append((panel_title, "action:panel")) - - for i, widget in enumerate(pushed_widgets[:-1]): + + # 3. Add any pushed widgets from nav stack + for i, widget in enumerate(pushed_widgets): if hasattr(widget, '_header_title') and widget._header_title: path.append((widget._header_title.upper(), f"action:nav_stack:{i+1}")) @@ -935,45 +949,152 @@ def handle_breadcrumb_click(target: str): target_idx = int(target.split(":")[-1]) while len(gui_app._nav_stack) > target_idx + 1: gui_app.pop_widget() + elif target == "action:breadcrumb_history": + full_path = get_breadcrumbs_path() + middle_steps = full_path[1:-1] + options = [text for text, action in middle_steps] + action_map = {text: action for text, action in middle_steps} + + def on_select(res): + if res == DialogResult.CONFIRM and dialog.selection: + chosen_action = action_map.get(dialog.selection) + if chosen_action: + handle_breadcrumb_click(chosen_action) + + from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog + dialog = MultiOptionDialog(tr("Navigation History"), options, "", callback=on_select) + gui_app.push_widget(dialog) -def draw_breadcrumbs(start_pos: rl.Vector2, max_width: float) -> None: +def draw_breadcrumbs(rect: rl.Rectangle) -> None: + """Draw the breadcrumb trail centered inside `rect`. + + All glyphs — past-step labels, chevrons, and the active-step label — share + a single vertical midline so nothing looks dropped or misaligned. + """ BREADCRUMB_RECTS.clear() path = get_breadcrumbs_path() if not path: - return - - font = gui_app.font(FontWeight.SEMI_BOLD) - font_size = 18 - color_normal = rl.Color(160, 170, 185, 255) - color_hover = rl.Color(236, 242, 250, 255) - color_pressed = rl.Color(255, 255, 255, 180) - color_sep = rl.Color(92, 116, 151, 150) - - current_x = start_pos.x - - for i, (text, action) in enumerate(path): + return + + display_path = list(path) + if len(path) > 3: + display_path = [path[0], ("...", "action:breadcrumb_history"), path[-1]] + + # Sizes + ACTIVE_SIZE = 26 # font size for current/last step + PAST_SIZE = 19 # font size for ancestor steps + CHEVRON_SIZE = 16 # visual half-height of chevron arms + CHEVRON_W = 14 # horizontal extent of chevron + GAP = 14 # spacing between every element + + center_y = rect.y + rect.height / 2 # single shared vertical center + + # ── measure total row width so we can clip/left-align ───────────────────── + # (We just render left-to-right; no centering needed for breadcrumbs) + + color_sep = rl.Color(80, 90, 115, 160) + mouse_pos = gui_app.last_mouse_event.pos + + current_x = rect.x + 20 # left inset inside the pill + + for i, (text, action) in enumerate(display_path): + is_last = (i == len(display_path) - 1) + is_overflow = (action == "action:breadcrumb_history") + pressed = PRESSED_BREADCRUMB == action + + if is_overflow: + # ── glowing "..." capsule ──────────────────────────────────────────── + capsule_w, capsule_h = 50, 26 + cap_rect = rl.Rectangle( + current_x, + center_y - capsule_h / 2, + capsule_w, + capsule_h, + ) + hovered = _point_hits(mouse_pos, cap_rect, None, pad_x=4, pad_y=6) + BREADCRUMB_RECTS[action] = cap_rect + + if pressed: + fill, outline, glow, dots_c = ( + rl.Color(45, 30, 75, 230), + rl.Color(167, 139, 250, 255), + rl.Color(167, 139, 250, 90), + rl.Color(255, 255, 255, 255), + ) + elif hovered: + fill, outline, glow, dots_c = ( + rl.Color(30, 20, 50, 200), + rl.Color(139, 92, 246, 200), + rl.Color(139, 92, 246, 60), + rl.Color(255, 255, 255, 255), + ) + else: + fill, outline, glow, dots_c = ( + rl.Color(20, 15, 30, 150), + rl.Color(120, 110, 220, 80), + rl.Color(120, 110, 220, 20), + rl.Color(190, 180, 220, 180), + ) + + # outer glow halo + rl.draw_rectangle_rounded_lines_ex( + rl.Rectangle(cap_rect.x - 2, cap_rect.y - 2, cap_rect.width + 4, cap_rect.height + 4), + 1.0, 16, 1.5, glow, + ) + rl.draw_rectangle_rounded(cap_rect, 1.0, 16, fill) + rl.draw_rectangle_rounded_lines_ex(cap_rect, 1.0, 16, 1.0, outline) + + font_dots = gui_app.font(FontWeight.BOLD) + dots_w = measure_text_cached(font_dots, "...", 18).x + rl.draw_text_ex( + font_dots, "...", + rl.Vector2(cap_rect.x + (cap_rect.width - dots_w) / 2, center_y - 10), + 18, 0, dots_c, + ) + current_x += capsule_w + GAP + + else: + # ── normal breadcrumb label ────────────────────────────────────────── + if is_last: + font = gui_app.font(FontWeight.BOLD) + font_size = ACTIVE_SIZE + c_normal = rl.Color(255, 255, 255, 255) + c_hover = rl.Color(255, 255, 255, 255) + c_pressed = rl.Color(200, 200, 200, 255) + else: + font = gui_app.font(FontWeight.MEDIUM) + font_size = PAST_SIZE + c_normal = rl.Color(110, 105, 130, 255) + c_hover = rl.Color(160, 155, 185, 255) + c_pressed = rl.Color(190, 185, 215, 255) + text_w = measure_text_cached(font, text, font_size).x - rect = rl.Rectangle(current_x - 8, start_pos.y - 4, text_w + 16, font_size + 8) - - mouse_pos = gui_app.last_mouse_event.pos - hovered = _point_hits(mouse_pos, rect, None, pad_x=0, pad_y=0) - pressed = PRESSED_BREADCRUMB == action - - color = color_pressed if pressed else (color_hover if hovered else color_normal) - - BREADCRUMB_RECTS[action] = rect - - if hovered: - rl.draw_rectangle_rounded(rect, 0.3, 8, rl.Color(255, 255, 255, 15)) - - rl.draw_text_ex(font, text, rl.Vector2(current_x, start_pos.y + 2), font_size, 0, color) - current_x += text_w + 16 - - if i < len(path) - 1: - sep_w = measure_text_cached(font, "/", font_size).x - rl.draw_text_ex(font, "/", rl.Vector2(current_x, start_pos.y + 2), font_size, 0, color_sep) - current_x += sep_w + 16 + # hit rect: generous padding for touch + hit_rect = rl.Rectangle(current_x - 6, center_y - 20, text_w + 12, 40) + hovered = _point_hits(mouse_pos, hit_rect, None, pad_x=0, pad_y=0) + BREADCRUMB_RECTS[action] = hit_rect + + color = c_pressed if pressed else (c_hover if hovered else c_normal) + + if hovered and not is_last: + rl.draw_rectangle_rounded(hit_rect, 0.4, 8, rl.Color(255, 255, 255, 12)) + + # draw text centered on the shared midline + text_y = center_y - font_size / 2 + rl.draw_text_ex(font, text, rl.Vector2(current_x, text_y), font_size, 0, color) + current_x += text_w + GAP + + # ── chevron separator ────────────────────────────────────────────────── + if i < len(display_path) - 1: + chev_rect = rl.Rectangle( + current_x, + center_y - CHEVRON_SIZE / 2, + CHEVRON_W, + CHEVRON_SIZE, + ) + draw_chevron_icon(chev_rect, color_sep, thickness=2.0, direction="right") + current_x += CHEVRON_W + GAP PANEL_HEADER_TITLE_Y: int = 34 @@ -994,13 +1115,7 @@ def draw_settings_panel_header(header_rect: rl.Rectangle, title: str, subtitle: subtitle_color: rl.Color = AetherListColors.SUBTEXT, title_weight: FontWeight = PANEL_HEADER_TITLE_FONT, subtitle_weight: FontWeight = PANEL_HEADER_SUBTITLE_FONT): - draw_breadcrumbs(rl.Vector2(header_rect.x, header_rect.y + 2), header_rect.width) - - title_rect = rl.Rectangle(header_rect.x, header_rect.y + PANEL_HEADER_TITLE_Y, header_rect.width * max_title_width, title_size + 2) - gui_label(title_rect, title, title_size, title_color, title_weight) - if subtitle: - subtitle_rect = rl.Rectangle(header_rect.x, header_rect.y + PANEL_HEADER_SUBTITLE_Y, header_rect.width * max_subtitle_width, subtitle_size + 4) - gui_label(subtitle_rect, subtitle, subtitle_size, subtitle_color, subtitle_weight) + pass @@ -2625,6 +2740,27 @@ class AetherSettingsView(PanelManagerView): def _draw_scroll_content(self, rect: rl.Rectangle, width: float): y = rect.y + self._scroll_offset + + if self._has_header: + title = tr(self._header_title) if self._header_title else "" + subtitle = tr(self._header_subtitle) if self._header_subtitle else "" + + col_w = (width - self.COLUMN_GAP) / 2 if self._uses_two_columns(width) else width + + title_font = gui_app.font(FontWeight.SEMI_BOLD) + title_size = 32 + rl.draw_text_ex(title_font, title, rl.Vector2(rect.x + 8, y), title_size, 0, AetherListColors.HEADER) + y += title_size + 8 + + if subtitle: + desc_font = gui_app.font(FontWeight.NORMAL) + desc_size = 18 + desc_lines = wrap_text(desc_font, subtitle, col_w - 16, desc_size, max_lines=4) + for line in desc_lines: + rl.draw_text_ex(desc_font, line, rl.Vector2(rect.x + 8, y), desc_size, 0, AetherListColors.SUBTEXT) + y += desc_size + 4 + y += 12 + if self._tab_defs: y = self._draw_tabs(y, rect.x, width) active = self._active_sections() @@ -2668,17 +2804,11 @@ class AetherSettingsView(PanelManagerView): draw_list_group_shell(right_group, style=self._panel_style) for j, row in enumerate(visible_rows): - self._draw_row(rl.Rectangle(rect.x, y + j * section.row_height, col_w, section.row_height), row, is_last=(j == len(visible_rows) - 1)) + row_rect = rl.Rectangle(rect.x, y + j * section.row_height, col_w, section.row_height) + row.set_is_last(j == len(visible_rows) - 1) + row.set_parent_rect(self._scroll_rect) + row.render(row_rect) for j, row in enumerate(right_rows): - self._draw_row(rl.Rectangle(rect.x + col_w + self.COLUMN_GAP, y + j * right_section.row_height, col_w, right_section.row_height), row, is_last=(j == len(right_rows) - 1)) - - y += group_h - if self._has_subsequent_visible(i + 2, active): - y += SECTION_GAP - i += 2 - else: - y = self._draw_section(y, rect.x, width, section, visible_rows) - if self._has_subsequent_visible(i + 1, active): y += SECTION_GAP i += 1 @@ -2909,46 +3039,31 @@ class AetherCategoryTileView(AetherSettingsView): ) def _target_at(self, mouse_pos: MousePos) -> str | None: - if self._back_btn_rect and rl.check_collision_point_rec(mouse_pos, self._back_btn_rect): - return "static:back" + # Check breadcrumb hits first (they're drawn in the header) + crumb_target = resolve_interactive_target(mouse_pos, BREADCRUMB_RECTS, None, pad_x=8, pad_y=8) + if crumb_target: + return f"breadcrumb:{crumb_target}" return super()._target_at(mouse_pos) def _activate_target(self, target_id: str | None): - if target_id == "static:back": - gui_app.pop_widget() + if target_id and target_id.startswith("breadcrumb:"): + action = target_id[len("breadcrumb:"):] + if action == "action:panel": # topmost panel step = pop this dialog + gui_app.pop_widget() + else: + handle_breadcrumb_click(action) else: super()._activate_target(target_id) def _draw_header(self, rect: rl.Rectangle): - btn_w = 68.0 - btn_h = 68.0 - self._back_btn_rect = rl.Rectangle(rect.x, rect.y + 4.0, btn_w, btn_h) - - self._interactive_rects["static:back"] = self._back_btn_rect - - hovered = rl.check_collision_point_rec(gui_app.last_mouse_event.pos, self._back_btn_rect) - pressed = self._pressed_target == "static:back" and hovered - - if pressed: - fill = rl.Color(255, 255, 255, 30) - border = self._panel_style.accent - elif hovered: - fill = rl.Color(255, 255, 255, 18) - border = self._panel_style.accent - else: - fill = rl.Color(255, 255, 255, 8) - border = rl.Color(255, 255, 255, 20) - - draw_soft_card(self._back_btn_rect, fill, border, radius=0.5) - draw_chevron_icon(self._back_btn_rect, self._panel_style.accent if (hovered or pressed) else AetherListColors.HEADER, direction="left") - - title_x = rect.x + btn_w + 24 - title_rect = rl.Rectangle(title_x, rect.y, rect.width - btn_w - 24, rect.height) - - title = tr(self._header_title) if self._header_title else "" - subtitle = tr(self._header_subtitle) if self._header_subtitle else "" - - draw_settings_panel_header(title_rect, title, subtitle) + # Draw the same glass breadcrumb pill that appears in the main nav bar + pill_h = 52 + pill_rect = rl.Rectangle(rect.x, rect.y + (rect.height - pill_h) / 2, rect.width, pill_h) + _draw_rounded_fill(pill_rect, rl.Color(18, 16, 24, 200), radius_px=14) + _draw_rounded_stroke(pill_rect, rl.Color(255, 255, 255, 22), radius_px=14) + # Breadcrumbs fill left portion; leave ~20px right inset + crumb_rect = rl.Rectangle(pill_rect.x, pill_rect.y, pill_rect.width - 20, pill_rect.height) + draw_breadcrumbs(crumb_rect) # ── AetherSubMenuTileView — category panel containing navigation HubTiles ── @@ -3063,46 +3178,31 @@ class AetherSubMenuTileView(AetherSettingsView): ) def _target_at(self, mouse_pos: MousePos) -> str | None: - if self._back_btn_rect and rl.check_collision_point_rec(mouse_pos, self._back_btn_rect): - return "static:back" + # Check breadcrumb hits first (they're drawn in the header) + crumb_target = resolve_interactive_target(mouse_pos, BREADCRUMB_RECTS, None, pad_x=8, pad_y=8) + if crumb_target: + return f"breadcrumb:{crumb_target}" return super()._target_at(mouse_pos) def _activate_target(self, target_id: str | None): - if target_id == "static:back": - gui_app.pop_widget() + if target_id and target_id.startswith("breadcrumb:"): + action = target_id[len("breadcrumb:"):] + if action == "action:panel": # topmost panel step = pop this dialog + gui_app.pop_widget() + else: + handle_breadcrumb_click(action) else: super()._activate_target(target_id) def _draw_header(self, rect: rl.Rectangle): - btn_w = 68.0 - btn_h = 68.0 - self._back_btn_rect = rl.Rectangle(rect.x, rect.y + 4.0, btn_w, btn_h) - - self._interactive_rects["static:back"] = self._back_btn_rect - - hovered = rl.check_collision_point_rec(gui_app.last_mouse_event.pos, self._back_btn_rect) - pressed = self._pressed_target == "static:back" and hovered - - if pressed: - fill = rl.Color(255, 255, 255, 30) - border = self._panel_style.accent - elif hovered: - fill = rl.Color(255, 255, 255, 18) - border = self._panel_style.accent - else: - fill = rl.Color(255, 255, 255, 8) - border = rl.Color(255, 255, 255, 20) - - draw_soft_card(self._back_btn_rect, fill, border, radius=0.5) - draw_chevron_icon(self._back_btn_rect, self._panel_style.accent if (hovered or pressed) else AetherListColors.HEADER, direction="left") - - title_x = rect.x + btn_w + 24 - title_rect = rl.Rectangle(title_x, rect.y, rect.width - btn_w - 24, rect.height) - - title = tr(self._header_title) if self._header_title else "" - subtitle = tr(self._header_subtitle) if self._header_subtitle else "" - - draw_settings_panel_header(title_rect, title, subtitle) + # Draw the same glass breadcrumb pill that appears in the main nav bar + pill_h = 52 + pill_rect = rl.Rectangle(rect.x, rect.y + (rect.height - pill_h) / 2, rect.width, pill_h) + _draw_rounded_fill(pill_rect, rl.Color(18, 16, 24, 200), radius_px=14) + _draw_rounded_stroke(pill_rect, rl.Color(255, 255, 255, 22), radius_px=14) + # Breadcrumbs fill left portion; leave ~20px right inset + crumb_rect = rl.Rectangle(pill_rect.x, pill_rect.y, pill_rect.width - 20, pill_rect.height) + draw_breadcrumbs(crumb_rect) class AetherTile(Widget): @@ -3285,23 +3385,7 @@ class AetherTile(Widget): return face, ty def _wrap_text(self, font: rl.Font, text: str, max_width: float, font_size: float, max_lines: int = 2) -> list[str]: - spacing = font_size * 0.15 - words = text.split() - lines: list[str] = [] - current = "" - for word in words: - candidate = f"{current} {word}".strip() if current else word - if measure_text_cached(font, candidate, int(font_size), spacing=spacing).x <= max_width: - current = candidate - else: - if current: - lines.append(current) - current = word - if len(lines) >= max_lines: - break - if current and len(lines) < max_lines: - lines.append(current) - return lines if lines else [text] + return wrap_text(font, text, max_width, font_size, max_lines) def _draw_signal_edge(self, face: rl.Rectangle, color: rl.Color, width: int = 2, alpha: int = 58): snapped_face = _snap_rect(face) @@ -5243,6 +5327,10 @@ class TileGrid(Widget): cols = self.get_effective_column_count(width, count) col_w = (width - (self._gap * (cols - 1))) / cols h = col_w + if self.min_tile_height is not None: + h = max(self.min_tile_height, h) + if self.max_tile_height is not None: + h = min(self.max_tile_height, h) else: if self._tile_height is not None: h = self._tile_height @@ -5295,6 +5383,11 @@ class TileGrid(Widget): if self.force_square: uniform_tile_w = (rect.width - (self._gap * (cols - 1))) / cols tile_h = uniform_tile_w + if self.min_tile_height is not None: + tile_h = max(self.min_tile_height, tile_h) + if self.max_tile_height is not None: + tile_h = min(self.max_tile_height, tile_h) + uniform_tile_w = tile_h else: if self._tile_height is not None: tile_h = self._tile_height @@ -5313,7 +5406,8 @@ class TileGrid(Widget): items_in_row = min(cols, remaining) if self.force_square or self._uniform_width: row_tile_w = uniform_tile_w - row_x = rect.x + row_width = row_tile_w * items_in_row + self._gap * (items_in_row - 1) + row_x = rect.x + (rect.width - row_width) / 2 else: row_tile_w = (rect.width - (self._gap * (items_in_row - 1))) / items_in_row row_x = rect.x diff --git a/selfdrive/ui/layouts/settings/starpilot/appearance.py b/selfdrive/ui/layouts/settings/starpilot/appearance.py index ad4e523a0..0e04524dd 100644 --- a/selfdrive/ui/layouts/settings/starpilot/appearance.py +++ b/selfdrive/ui/layouts/settings/starpilot/appearance.py @@ -208,24 +208,9 @@ class AppearanceManagerView(AetherSettingsView): margin_x = 12.0 margin_top = 16.0 margin_bottom = 16.0 - header_h = 125.0 - gap_after_header = 16.0 - # Draw the header at the top of rect: - header_rect = rl.Rectangle(rect.x + margin_x, rect.y + margin_top, rect.width - margin_x * 2, header_h) - self._draw_header(header_rect) - - # Draw a nice separator line under the header: - divider_y = rect.y + margin_top + header_h + 8.0 - rl.draw_line_ex( - rl.Vector2(rect.x + margin_x, divider_y), - rl.Vector2(rect.x + rect.width - margin_x, divider_y), - 2.0, - rl.Color(255, 255, 255, 16) - ) - - # Compute remaining scroll rect: - grid_top = divider_y + gap_after_header + # Compute remaining scroll rect starting directly from the top + grid_top = rect.y + margin_top grid_h = rect.y + rect.height - grid_top - margin_bottom self._scroll_rect = rl.Rectangle(rect.x + margin_x, grid_top, rect.width - margin_x * 2, grid_h) diff --git a/selfdrive/ui/layouts/settings/starpilot/driving_model.py b/selfdrive/ui/layouts/settings/starpilot/driving_model.py index caa92cc1c..4b21f96ca 100644 --- a/selfdrive/ui/layouts/settings/starpilot/driving_model.py +++ b/selfdrive/ui/layouts/settings/starpilot/driving_model.py @@ -58,6 +58,7 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( init_list_panel, draw_interactive_rect, resolve_interactive_target, + wrap_text, ) @@ -70,7 +71,7 @@ ROW_RADIUS = AETHER_LIST_METRICS.row_radius ACTION_WIDTH = AETHER_LIST_METRICS.action_width BUTTON_HEIGHT = AETHER_LIST_METRICS.header_button_height FADE_HEIGHT = AETHER_LIST_METRICS.fade_height -DRIVING_MODEL_METRICS = replace(AETHER_LIST_METRICS, header_height=244) +DRIVING_MODEL_METRICS = replace(AETHER_LIST_METRICS, header_height=0) CONFIRM_TIMEOUT_SECONDS = 3.0 TRANSITION_SECONDS = 0.24 PANEL_STYLE = DEFAULT_PANEL_STYLE @@ -265,6 +266,10 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget): self._shell_rect = frame.shell self._scroll_rect = scroll_rect + self._primary_header_button.set_parent_rect(scroll_rect) + self._secondary_header_button.set_parent_rect(scroll_rect) + self._random_model_button.set_parent_rect(scroll_rect) + self._draw_header(frame.header) self._content_height = self._measure_content_height(content_width) self._scroll_panel.set_enabled(lambda: not self._controller._is_download_active()) @@ -280,28 +285,22 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget): draw_list_scroll_fades(scroll_rect, self._content_height, self._scroll_offset, AetherListColors.PANEL_BG, fade_height=FADE_HEIGHT) def _draw_header(self, rect: rl.Rectangle): - draw_settings_panel_header(rect, tr("Driving Models"), self._controller.header_description_text(), subtitle_size=24) + pass - current_label_rect = rl.Rectangle(rect.x, rect.y + 130, 150, 22) - gui_label(current_label_rect, tr("Current Model"), 20, AetherListColors.MUTED, FontWeight.MEDIUM) - - current_value_rect = rl.Rectangle(rect.x + 150, rect.y + 128, rect.width * 0.44, 24) - gui_label(current_value_rect, self._controller._current_model_name, 22, AetherListColors.HEADER, FontWeight.MEDIUM) - - right_panel_w = min(390, rect.width * 0.35) - btn_gap = 10 - stack_y = rect.y + 8 - right_x = rect.x + rect.width - right_panel_w - - primary_rect = rl.Rectangle(right_x, stack_y, right_panel_w, BUTTON_HEIGHT) - secondary_rect = rl.Rectangle(right_x, stack_y + BUTTON_HEIGHT + btn_gap, right_panel_w, BUTTON_HEIGHT) - random_rect = rl.Rectangle(right_x, stack_y + (BUTTON_HEIGHT + btn_gap) * 2, right_panel_w, BUTTON_HEIGHT) + def _draw_relocated_header(self, x: float, y: float, width: float): + # Buttons placed horizontally + btn_gap = 12.0 + btn_w = (width - 16.0 - btn_gap * 2) / 3.0 + + primary_rect = rl.Rectangle(x + 8, y, btn_w, BUTTON_HEIGHT) + secondary_rect = rl.Rectangle(x + 8 + btn_w + btn_gap, y, btn_w, BUTTON_HEIGHT) + random_rect = rl.Rectangle(x + 8 + (btn_w + btn_gap) * 2, y, btn_w, BUTTON_HEIGHT) self._primary_header_button.render(primary_rect) self._secondary_header_button.render(secondary_rect) self._random_model_button.render(random_rect) - # LED indicator drawn on top of the randomizer button + # LED indicator led_x = int(random_rect.x + random_rect.width - 26) led_y = int(random_rect.y + random_rect.height / 2) randomizer_on = self._controller._params.get_bool("ModelRandomizer") @@ -309,9 +308,10 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget): def _measure_content_height(self, width: float) -> float: sections = self._build_sections(width) + RELOCATED_HEADER_HEIGHT = 74.0 if not sections: - return 260 - return max(sum(height for _key, height in sections) - SECTION_GAP, 0.0) + return 260 + RELOCATED_HEADER_HEIGHT + return max(sum(height for _key, height in sections) - SECTION_GAP, 0.0) + RELOCATED_HEADER_HEIGHT def _build_sections(self, width: float) -> list[tuple[str, float]]: sections: list[tuple[str, float]] = [] @@ -339,6 +339,10 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget): utility_rows = self._controller.utility_rows() y = rect.y + self._scroll_offset + RELOCATED_HEADER_HEIGHT = 74.0 + self._draw_relocated_header(rect.x, y, width) + y += RELOCATED_HEADER_HEIGHT + if not installed and not available and not utility_rows: self._draw_empty_state(rl.Rectangle(rect.x, y + 36, width, 200)) return @@ -366,7 +370,10 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget): ) def _draw_model_section(self, x: float, y: float, width: float, title: str, entries: list[ModelCatalogEntry]) -> float: - draw_section_header(rl.Rectangle(x, y, width, SECTION_HEADER_HEIGHT), title, style=PANEL_STYLE) + trailing = "" + if title == tr("On Device"): + trailing = tr("Current: {}").format(self._controller._current_model_name) + draw_section_header(rl.Rectangle(x, y, width, SECTION_HEADER_HEIGHT), title, trailing_text=trailing, style=PANEL_STYLE) y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP group_rect = rl.Rectangle(x, y, width, len(entries) * ROW_HEIGHT) diff --git a/selfdrive/ui/layouts/settings/starpilot/lateral.py b/selfdrive/ui/layouts/settings/starpilot/lateral.py index d91df8f2f..407c3803e 100644 --- a/selfdrive/ui/layouts/settings/starpilot/lateral.py +++ b/selfdrive/ui/layouts/settings/starpilot/lateral.py @@ -43,7 +43,7 @@ CUSTOM_METRICS = AetherListMetrics( panel_padding_x=16, panel_padding_top=16, panel_padding_bottom=12, - header_height=198, + header_height=0, section_gap=12, section_header_height=28, section_header_gap=8, @@ -83,7 +83,7 @@ class SteeringManagerView(PanelManagerView): self._controller = controller self._shell_rect = rl.Rectangle(0, 0, 0, 0) - self._toggle_grid = TileGrid(columns=2, padding=12, min_tile_width=100, min_tile_height=130.0, max_tile_height=180.0) + self._toggle_grid = TileGrid(columns=2, padding=12, force_square=True, min_tile_width=100, min_tile_height=130.0, max_tile_height=180.0) self._toggle_grid.set_touch_valid_callback(lambda: self._scroll_panel.is_touch_valid()) self._child(self._toggle_grid) @@ -103,9 +103,7 @@ class SteeringManagerView(PanelManagerView): self._controller._on_select(value) def _draw_header(self, rect: rl.Rectangle): - draw_settings_panel_header(rect, tr("Steering"), - tr("Fine-tune lateral control, lane changes, and steering behavior."), - subtitle_size=22) + pass def _build_toggle_defs(self) -> list[dict]: p = self._controller._params diff --git a/selfdrive/ui/layouts/settings/starpilot/longitudinal.py b/selfdrive/ui/layouts/settings/starpilot/longitudinal.py index 4d2f7bcce..47b2ffea4 100644 --- a/selfdrive/ui/layouts/settings/starpilot/longitudinal.py +++ b/selfdrive/ui/layouts/settings/starpilot/longitudinal.py @@ -240,24 +240,9 @@ class LongitudinalManagerView(AetherSettingsView): margin_x = 12.0 margin_top = 16.0 margin_bottom = 16.0 - header_h = 90.0 - gap_after_header = 16.0 - # Draw the header at the top of rect: - header_rect = rl.Rectangle(rect.x + margin_x, rect.y + margin_top, rect.width - margin_x * 2, header_h) - self._draw_header(header_rect) - - # Draw a nice separator line under the header: - divider_y = rect.y + margin_top + header_h + 8.0 - rl.draw_line_ex( - rl.Vector2(rect.x + margin_x, divider_y), - rl.Vector2(rect.x + rect.width - margin_x, divider_y), - 2.0, - rl.Color(255, 255, 255, 16) - ) - - # Compute remaining scroll rect: - grid_top = divider_y + gap_after_header + # Compute remaining scroll rect starting directly from the top + grid_top = rect.y + margin_top grid_h = rect.y + rect.height - grid_top - margin_bottom self._scroll_rect = rl.Rectangle(rect.x + margin_x, grid_top, rect.width - margin_x * 2, grid_h) diff --git a/selfdrive/ui/layouts/settings/starpilot/main_panel.py b/selfdrive/ui/layouts/settings/starpilot/main_panel.py index 195ea2b90..163324dd9 100644 --- a/selfdrive/ui/layouts/settings/starpilot/main_panel.py +++ b/selfdrive/ui/layouts/settings/starpilot/main_panel.py @@ -5,7 +5,7 @@ import pyray as rl from openpilot.common.params import Params 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 +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.sounds import StarPilotSoundsLayout @@ -237,22 +237,64 @@ class StarPilotLayout(Widget): def _render(self, rect: rl.Rectangle): import openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid as aethergrid + metrics = aethergrid.AETHER_LIST_METRICS + + TOP_BAR_HEIGHT = 80 + content_rect = rl.Rectangle(rect.x, rect.y + TOP_BAR_HEIGHT, rect.width, rect.height - TOP_BAR_HEIGHT) + + # Standardize width to perfectly match subpanel shells + shell_w = min(rect.width - metrics.outer_margin_x * 2, metrics.max_content_width) + shell_x = rect.x + (rect.width - shell_w) / 2 + + # 0. Draw Tinted Cockpit Glass Background as a floating, rounded panel + glass_rect = rl.Rectangle(shell_x, rect.y + 18, shell_w, TOP_BAR_HEIGHT - 24) + aethergrid._draw_rounded_fill(glass_rect, rl.Color(18, 16, 24, 180), radius_px=16) + aethergrid._draw_rounded_stroke(glass_rect, rl.Color(255, 255, 255, 20), radius_px=16) + + # 1. Draw breadcrumbs in top bar — pass the full pill rect for proper vertical centering + # Reserve space on the right for the badge + time (estimated ~260px) + crumb_rect = rl.Rectangle(glass_rect.x, glass_rect.y, glass_rect.width - 260, glass_rect.height) + aethergrid.draw_breadcrumbs(crumb_rect) + + # 2. Draw Time/Clock on right + import time + from openpilot.system.ui.lib.text_measure import measure_text_cached + current_time = time.strftime("%I:%M %p").lstrip("0") + font = gui_app.font(FontWeight.SEMI_BOLD) + font_size = 28 + time_w = measure_text_cached(font, current_time, font_size).x + time_x = glass_rect.x + glass_rect.width - time_w - 20 + time_y = glass_rect.y + (glass_rect.height - 28) / 2 + rl.draw_text_ex(font, current_time, rl.Vector2(time_x, time_y), font_size, 0, rl.Color(160, 170, 185, 255)) + + # 3. Draw Network/Wifi Badge + from openpilot.selfdrive.ui.ui_state import ui_state + network_type = ui_state.sm["deviceState"].networkType if ui_state.sm.valid.get("deviceState", False) else 0 + if network_type == 1: + network_str = "WIFI" + network_color = rl.Color(34, 197, 94, 255) + elif network_type in (2, 3, 4, 5): + network_str = "CELL" + network_color = rl.Color(59, 130, 246, 255) + else: + network_str = "OFFLINE" + network_color = rl.Color(239, 68, 68, 255) + + badge_w = measure_text_cached(font, network_str, 20).x + 24 + badge_rect = rl.Rectangle(time_x - badge_w - 20, glass_rect.y + (glass_rect.height - 32) / 2, badge_w, 32) + rl.draw_rectangle_rounded(badge_rect, 0.35, 8, rl.Color(network_color.r, network_color.g, network_color.b, 24)) + rl.draw_rectangle_rounded_lines_ex(badge_rect, 0.35, 8, 1.5, rl.Color(network_color.r, network_color.g, network_color.b, 70)) + badge_text_pos = rl.Vector2(badge_rect.x + 12, badge_rect.y + 6) + rl.draw_text_ex(font, network_str, badge_text_pos, 20, 0, network_color) + + # 5. Render active content panel if self._current_panel == StarPilotPanelType.MAIN: - if self._current_category_idx is not None: - # We are in a category folder, draw the breadcrumbs header - header_rect = rl.Rectangle(rect.x + 16, rect.y + 28, rect.width - 32, 96) - cat = self.CATEGORIES[self._current_category_idx] - aethergrid.draw_settings_panel_header(header_rect, tr(cat["title"]), subtitle=None) - - # Adjust grid to fit under header - grid_rect = rl.Rectangle(rect.x, header_rect.y + header_rect.height, rect.width, rect.height - header_rect.height - 28) - self._main_grid.render(grid_rect) - else: - self._main_grid.render(rect) + grid_rect = rl.Rectangle(shell_x, content_rect.y + metrics.outer_margin_y, shell_w, content_rect.height - metrics.outer_margin_y * 2) + self._main_grid.render(grid_rect) else: panel = self._panels[self._current_panel] if panel.instance: - panel.instance.render(rect) + panel.instance.render(content_rect) def _handle_mouse_press(self, mouse_pos: MousePos): import openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid as aethergrid diff --git a/selfdrive/ui/layouts/settings/starpilot/maps.py b/selfdrive/ui/layouts/settings/starpilot/maps.py index 9f0578e88..9f1876e81 100644 --- a/selfdrive/ui/layouts/settings/starpilot/maps.py +++ b/selfdrive/ui/layouts/settings/starpilot/maps.py @@ -40,6 +40,7 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( draw_soft_card, init_list_panel, _point_hits, + wrap_text, ) from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel from openpilot.starpilot.common.maps_catalog import ( @@ -85,7 +86,7 @@ STATUS_REMOVE_HEIGHT = 40 STATUS_METRIC_GAP = 18 STATUS_SELECTION_CHIP_HEIGHT = 30 PANEL_STYLE = DEFAULT_PANEL_STYLE -MAPS_METRICS = replace(AETHER_LIST_METRICS, header_height=274) +MAPS_METRICS = replace(AETHER_LIST_METRICS, header_height=0) COUNTRIES_SECTION = next(section for section in MAPS_CATALOG if section["key"] == "countries") STATES_SECTION = next(section for section in MAPS_CATALOG if section["key"] == "states") @@ -1115,13 +1116,22 @@ class StarPilotMapsLayout(StarPilotPanel): return tr("Ready to download {}.").format(self._selection_preview_text()) def _measure_content_height(self, width: float) -> float: - return self._browser_card._measure_height(width) + RELOCATED_HEADER_HEIGHT = 156.0 + return self._browser_card._measure_height(width) + RELOCATED_HEADER_HEIGHT def _draw_scroll_content(self, rect: rl.Rectangle, width: float): self._browser_card.set_parent_rect(rect) + self._status_card.set_parent_rect(rect) y = rect.y + self._scroll_offset + # 1. Draw Status Card + status_rect = rl.Rectangle(rect.x, y, width, 144.0) + self._status_card.render(status_rect) + + RELOCATED_HEADER_HEIGHT = 156.0 + y += RELOCATED_HEADER_HEIGHT + browser_height = self._browser_card._measure_height(width) self._browser_card.render(rl.Rectangle(rect.x, y, width, browser_height)) @@ -1129,13 +1139,7 @@ class StarPilotMapsLayout(StarPilotPanel): self.set_rect(rect) frame, scroll_rect, content_width = init_list_panel(rect, PANEL_STYLE, MAPS_METRICS) - hdr = frame.header - draw_settings_panel_header(hdr, tr("Map Data"), tr("Use offline maps for speed-limit control and keep only the regions you need."), - max_title_width=1.0, max_subtitle_width=0.60) - - header_status_y = hdr.y + 82 + HEADER_SUBTITLE_HEIGHT + 12 - header_status_rect = rl.Rectangle(hdr.x, header_status_y, hdr.width, hdr.y + hdr.height - header_status_y - HEADER_BOTTOM_GAP) - self._status_card.render(header_status_rect) + self._status_card.set_parent_rect(scroll_rect) scroll_content_rect = rl.Rectangle(scroll_rect.x, scroll_rect.y, scroll_rect.width, scroll_rect.height) self._content_height = self._measure_content_height(content_width) diff --git a/selfdrive/ui/layouts/settings/starpilot/sounds.py b/selfdrive/ui/layouts/settings/starpilot/sounds.py index 680215997..15990c315 100644 --- a/selfdrive/ui/layouts/settings/starpilot/sounds.py +++ b/selfdrive/ui/layouts/settings/starpilot/sounds.py @@ -45,7 +45,7 @@ SOUNDS_PANEL_METRICS = replace( outer_margin_y=14, panel_padding_top=16, panel_padding_bottom=14, - header_height=125, + header_height=0, ) @@ -76,7 +76,7 @@ class SoundsManagerView(PanelManagerView): ) def _init_toggles(self): - self._toggle_grid = TileGrid(columns=2, padding=12, min_tile_height=130.0, max_tile_height=180.0) + self._toggle_grid = TileGrid(columns=2, padding=12, force_square=True, min_tile_height=130.0, max_tile_height=180.0) self._child(self._toggle_grid) self._page_grid = self._toggle_grid @@ -251,7 +251,7 @@ class SoundsManagerView(PanelManagerView): # 9 adjustors (8 volume keys + 1 cooldown key) and 2 group headers ("SYSTEM STATE" and "INFORMATIONAL") left_h = ((len(self._controller.VOLUME_KEYS) + 1) * default_adjustor_h) left_h += (2 * (GROUP_HEADER_HEIGHT + GROUP_HEADER_GAP)) - left_natural_container_h = left_h + 16 + left_natural_container_h = left_h + 16.0 tiles_needed_h = self.measure_page_grid_height(self._toggle_grid, col_width - 24) + 24 right_natural_container_h = tiles_needed_h @@ -281,22 +281,7 @@ class SoundsManagerView(PanelManagerView): return self._compute_two_column_height(section_overhead + max_container_h) def _draw_header(self, rect: rl.Rectangle): - draw_settings_panel_header(rect, tr("Sounds & Alerts"), tr("Manage system volumes and custom alert toggles."), subtitle_size=24) - - btn_w = 120.0 - btn_h = 38.0 - btn_x = rect.x + rect.width - btn_w - btn_y = rect.y + (rect.height - btn_h) / 2 - self._reset_rect = rl.Rectangle(btn_x, btn_y, btn_w, btn_h) - - hovered = _point_hits(gui_app.last_mouse_event.pos, self._reset_rect, None, pad_x=6, pad_y=0) - pressed = self._pressed_target == "action:restore_defaults" - draw_action_pill( - self._reset_rect, tr("Reset All"), - rl.Color(255, 255, 255, 8 if not pressed else 14), - rl.Color(255, 255, 255, 18 if not pressed else 28), - AetherListColors.SUBTEXT if not hovered else AetherListColors.HEADER, - ) + pass def _draw_scroll_content(self, rect: rl.Rectangle, content_width: float): y = rect.y + self._scroll_offset @@ -310,6 +295,25 @@ class SoundsManagerView(PanelManagerView): rl.Rectangle(rect.x + col_width + SECTION_GAP, y, col_width, SECTION_HEADER_HEIGHT), tr("Alerts"), style=PANEL_STYLE ) + + # Draw Reset All button aligned with the Volume header + btn_w = 120.0 + btn_h = 32.0 + btn_x = rect.x + col_width - btn_w - 8 + btn_y = y + (SECTION_HEADER_HEIGHT - btn_h) / 2 + self._reset_rect = rl.Rectangle(btn_x, btn_y, btn_w, btn_h) + + self._interactive_rects["action:restore_defaults"] = self._reset_rect + + hovered = _point_hits(gui_app.last_mouse_event.pos, self._reset_rect, self._scroll_rect, pad_x=6, pad_y=0) + pressed = self._pressed_target == "action:restore_defaults" + draw_action_pill( + self._reset_rect, tr("Reset All"), + rl.Color(255, 255, 255, 8 if not pressed else 14), + rl.Color(255, 255, 255, 18 if not pressed else 28), + AetherListColors.SUBTEXT if not hovered else AetherListColors.HEADER, + ) + y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP self._draw_volume_column(y, rect.x, col_width) diff --git a/selfdrive/ui/layouts/settings/starpilot/system_settings.py b/selfdrive/ui/layouts/settings/starpilot/system_settings.py index 430d1a504..70b7f4b3c 100644 --- a/selfdrive/ui/layouts/settings/starpilot/system_settings.py +++ b/selfdrive/ui/layouts/settings/starpilot/system_settings.py @@ -51,6 +51,7 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( _draw_rounded_stroke, _point_hits, _draw_text_fit_common, + wrap_text, ) from openpilot.starpilot.common.connect_server import prepare_konik_server_switch @@ -305,7 +306,7 @@ class SystemSettingsManagerView(PanelManagerView): self._basics_tile_grid_h = 0.0 - self._connectivity_tile_grid = TileGrid(columns=2, padding=12, min_tile_height=130.0, max_tile_height=180.0) + self._connectivity_tile_grid = TileGrid(columns=2, padding=12, force_square=True, min_tile_height=130.0, max_tile_height=180.0) for toggle_def in self._toggle_defs: tile = self._make_toggle_tile(toggle_def) self._connectivity_tile_grid.add_tile(tile) @@ -433,17 +434,80 @@ class SystemSettingsManagerView(PanelManagerView): gui_app.push_widget(AetherBackupsCareDialog(self._controller)) def _on_frame_created(self, frame) -> None: - self._drive_mode_control.set_parent_rect(frame.header) + self._drive_mode_control.set_parent_rect(self._scroll_rect) def _draw_header(self, rect: rl.Rectangle): - draw_settings_panel_header(rect, tr("System Settings"), - tr("Manage display, backups, connectivity, and device maintenance from one touch-first panel."), - max_title_width=0.60, max_subtitle_width=0.62) + pass - # First Aid Button in top right + 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)) + + if self._uses_two_columns(width): + column_w = self._column_width(width) + + # Reset custom heights to calculate natural measurements first + for key in self._display_slider_keys + self._power_slider_keys: + self._adjustor_rows[key].custom_row_height = None + self._connectivity_tile_grid._tile_height = None + + display_container_h = self._slider_section_height(self._display_slider_keys, column_w) + 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 + + tiles_content_h = self.measure_page_grid_height(self._connectivity_tile_grid, column_w - 24) + right_natural_container_h = tiles_content_h + 24 + + max_natural_h = max(left_natural_content_h, right_natural_container_h) + + if self._scroll_rect: + available_container_h = self._scroll_rect.height - 12.0 + else: + available_container_h = max_natural_h + + # Always stretch container to fill available height, preventing empty space at bottom + max_container_h = available_container_h + + # 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) + 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 + + self._system_max_container_h = max_container_h + + return self._compute_two_column_height(max_container_h) + else: + # Ensure defaults are restored in single column mode + for key in self._display_slider_keys + self._power_slider_keys: + 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 + + def _slider_section_height(self, keys: list[str], width: float) -> float: + total = 0.0 + for key in keys: + adjustor = self._adjustor_rows[key] + total += adjustor.measure_height(width) + return total + + def _draw_scroll_content(self, rect: rl.Rectangle, width: float): + y = rect.y + self._scroll_offset + 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 = rect.x + rect.width - btn_w - btn_y = rect.y + 4.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) @@ -466,81 +530,19 @@ class SystemSettingsManagerView(PanelManagerView): icon_color = PANEL_STYLE.accent if (hovered or pressed) else AetherListColors.HEADER draw_custom_icon("first_aid", icon_x, icon_y, s, icon_color) - content_width = rect.width - AETHER_LIST_METRICS.content_right_gutter - summary_y = rect.y + 126 - - control_rect = rl.Rectangle(rect.x, summary_y, content_width, 108) + # 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) - def _measure_content_height(self, width: float) -> float: - 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)) + RELOCATED_HEADER_HEIGHT = 80.0 + y += RELOCATED_HEADER_HEIGHT if self._uses_two_columns(width): column_w = self._column_width(width) - - # Reset custom heights to calculate natural measurements first - for key in self._display_slider_keys + self._power_slider_keys: - self._adjustor_rows[key].custom_row_height = None - self._connectivity_tile_grid._tile_height = None + adj_container_h = self._system_max_container_h - RELOCATED_HEADER_HEIGHT - display_container_h = self._slider_section_height(self._display_slider_keys, column_w) - 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 - - tiles_content_h = self.measure_page_grid_height(self._connectivity_tile_grid, column_w - 24) - right_natural_container_h = tiles_content_h + 24 - - max_natural_h = max(left_natural_content_h, right_natural_container_h) - - if self._scroll_rect: - available_container_h = self._scroll_rect.height - 12.0 - else: - available_container_h = max_natural_h - - # Always stretch container to fill available height, preventing empty space at bottom - max_container_h = available_container_h - - # Scale adjustors if needed - if max_container_h < max_natural_h: - scale_f = (max_container_h - left_overhead) / (left_natural_content_h - left_overhead) - 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 - - self._system_max_container_h = max_container_h - - pass - - return self._compute_two_column_height(max_container_h) - else: - # Ensure defaults are restored in single column mode - for key in self._display_slider_keys + self._power_slider_keys: - 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]) - - def _slider_section_height(self, keys: list[str], width: float) -> float: - total = 0.0 - for key in keys: - adjustor = self._adjustor_rows[key] - total += adjustor.measure_height(width) - return total - - def _draw_scroll_content(self, rect: rl.Rectangle, width: float): - y = rect.y + self._scroll_offset - self._draw_basics_tab(y, rect.x, width) - - def _draw_basics_tab(self, y: float, x: float, width: float): - if self._uses_two_columns(width): - column_w = self._column_width(width) - display_container_h = self._slider_section_height(self._display_slider_keys, column_w) - power_container_h = self._slider_section_height(self._power_slider_keys, column_w) - # Single unified shell for the left side - draw_list_group_shell(rl.Rectangle(x, y, column_w, self._system_max_container_h), style=PANEL_STYLE) + # 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 current_y = draw_group_header(x + 24, current_y, column_w - 48, tr("DISPLAY")) @@ -553,8 +555,9 @@ 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, column_w, self._system_max_container_h) + 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) return + y = self._draw_slider_section(y, x, width, tr("Display"), self._display_slider_keys) y += SECTION_GAP y = self._draw_slider_section(y, x, width, tr("Power"), self._power_slider_keys) diff --git a/selfdrive/ui/layouts/settings/starpilot/vehicle.py b/selfdrive/ui/layouts/settings/starpilot/vehicle.py index 3590caec7..57c340694 100644 --- a/selfdrive/ui/layouts/settings/starpilot/vehicle.py +++ b/selfdrive/ui/layouts/settings/starpilot/vehicle.py @@ -32,6 +32,7 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( _draw_rounded_fill, _draw_rounded_stroke, draw_status_badges, + wrap_text, ) from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state from openpilot.selfdrive.ui.lib.fingerprint_catalog import ( @@ -78,7 +79,7 @@ CUSTOM_METRICS = AetherListMetrics( panel_padding_x=16, panel_padding_top=16, panel_padding_bottom=12, - header_height=198, + header_height=0, section_gap=12, section_header_height=28, section_header_gap=8, @@ -105,7 +106,7 @@ class VehicleSettingsManagerView(PanelManagerView): self._controller = controller self._shell_rect = rl.Rectangle(0, 0, 0, 0) - self._toggle_grid = TileGrid(columns=2, padding=12, min_tile_width=100, min_tile_height=130.0, max_tile_height=180.0) + self._toggle_grid = TileGrid(columns=2, padding=12, force_square=True, min_tile_width=100, min_tile_height=130.0, max_tile_height=180.0) self.register_page_grid(self._toggle_grid) self._last_make = "" @@ -244,13 +245,7 @@ class VehicleSettingsManagerView(PanelManagerView): self._controller._on_select(value) def _draw_header(self, rect: rl.Rectangle): - draw_settings_panel_header(rect, tr("Vehicle Settings"), - tr("Configure vehicle fingerprint, driving features, and steering controls."), - subtitle_size=22) - - summary_y = rect.y + 78 + self.HEADER_SUBTITLE_HEIGHT + self.HEADER_SUMMARY_GAP - summary_rect = rl.Rectangle(rect.x, summary_y, rect.width, min(self.HEADER_CARD_HEIGHT, rect.y + rect.height - summary_y)) - self._draw_summary_card(summary_rect) + pass def _draw_summary_card(self, rect: rl.Rectangle): draw_soft_card(rect, PANEL_STYLE.surface_fill, PANEL_STYLE.surface_border) @@ -319,6 +314,7 @@ class VehicleSettingsManagerView(PanelManagerView): def _measure_content_height(self, width: float) -> float: self._check_rebuild_grid() cs = starpilot_state.car_state + RELOCATED_HEADER_HEIGHT = 112.0 # Left Column heights identity_rows = 2 @@ -344,14 +340,14 @@ class VehicleSettingsManagerView(PanelManagerView): column_w = self._column_width(width) tiles_content_h = self.measure_page_grid_height(self._toggle_grid, column_w - 24) right_natural_container_h = tiles_content_h + 24 - left_natural_content_h = identity_h + SECTION_GAP + SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP + steering_h + left_natural_content_h = identity_h + SECTION_GAP + SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP + steering_h + RELOCATED_HEADER_HEIGHT max_container_h = max(left_natural_content_h, right_natural_container_h) self._vehicle_max_container_h = max_container_h - self._vehicle_section_gap = max(SECTION_GAP, max_container_h - (identity_h + steering_h + SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP)) + self._vehicle_section_gap = max(SECTION_GAP, (max_container_h - RELOCATED_HEADER_HEIGHT) - (identity_h + steering_h + SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP)) return self._compute_two_column_height(max_container_h + SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP) - return left_h + tiles_height + return left_h + tiles_height + RELOCATED_HEADER_HEIGHT def _draw_scroll_content(self, rect: rl.Rectangle, width: float): self._interactive_rects.clear() @@ -362,6 +358,16 @@ class VehicleSettingsManagerView(PanelManagerView): self._check_rebuild_grid() cs = starpilot_state.car_state + # Relocated Header elements drawn at the top of the left column + col_w = self._column_width(width) if self._uses_two_columns(width) else width + + # 1. Draw Summary Card + summary_rect = rl.Rectangle(x, y, col_w, 100.0) + self._draw_summary_card(summary_rect) + + RELOCATED_HEADER_HEIGHT = 112.0 + y += RELOCATED_HEADER_HEIGHT + identity_rows = [ {"target_id": "select:CarMake", "type": "select", "title": tr("Car Make"), "get_value": self._controller._get_display_make, "pill_width": 160, @@ -408,7 +414,7 @@ class VehicleSettingsManagerView(PanelManagerView): # Right Column: Features if self._toggle_grid.tiles: rx = x + column_w + self.COLUMN_GAP - self._draw_two_column_tile_grid(self._toggle_grid, rx, y, column_w, self._vehicle_max_container_h, title=tr("Features"), style=PANEL_STYLE) + self._draw_two_column_tile_grid(self._toggle_grid, rx, y - RELOCATED_HEADER_HEIGHT, column_w, self._vehicle_max_container_h, title=tr("Features"), style=PANEL_STYLE) else: # Single Column Stacked Layout draw_section_header(rl.Rectangle(x, y, width, SECTION_HEADER_HEIGHT), tr("Vehicle Identity"), style=PANEL_STYLE)