diff --git a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py index c292841aa..e1827bb8e 100644 --- a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py +++ b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py @@ -232,6 +232,7 @@ class AetherListFrame: AETHER_LIST_METRICS = AetherListMetrics() +AETHER_COMPACT_ROW_HEIGHT = AETHER_LIST_METRICS.utility_row_height @dataclass(frozen=True, slots=True) @@ -582,6 +583,155 @@ def draw_metric_strip( ) +def draw_section_header( + rect: rl.Rectangle, + title: str = "", + *, + trailing_text: str = "", + title_size: int = 26, + trailing_size: int = 20, + title_color: rl.Color | None = None, + trailing_color: rl.Color | None = None, + style: PanelStyle = DEFAULT_PANEL_STYLE, +): + if title: + trailing_reserved = min(320.0, rect.width * 0.38) if trailing_text else 0.0 + title_rect = rl.Rectangle(rect.x, rect.y + (rect.height - title_size) / 2, max(1.0, rect.width - trailing_reserved), title_size + 4) + gui_label(title_rect, title, title_size, title_color or style.subtitle_color, FontWeight.MEDIUM) + + if trailing_text: + trailing_rect = rl.Rectangle(rect.x, rect.y + (rect.height - trailing_size) / 2, rect.width, trailing_size + 4) + gui_label( + trailing_rect, + trailing_text, + trailing_size, + trailing_color or style.subtitle_color, + FontWeight.NORMAL, + alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, + ) + + +def draw_empty_state_card( + rect: rl.Rectangle, + title: str, + body: str, + *, + title_size: int = 30, + body_size: int = 22, + body_inset_x: int = 48, + title_gap: int = 14, + title_top_padding: float | None = None, + body_height: float | None = None, + fill: rl.Color | None = None, + border: rl.Color | None = None, + radius: float = 0.08, + segments: int = 18, + style: PanelStyle = DEFAULT_PANEL_STYLE, +): + card_rect = _snap_rect(rect) + resolved_fill = fill if fill is not None else style.surface_fill + resolved_border = border if border is not None else style.surface_border + draw_soft_card(card_rect, resolved_fill, resolved_border, radius=radius, segments=segments) + + title_h = max(34.0, title_size + 8) + title_y = card_rect.y + (title_top_padding if title_top_padding is not None else max(24.0, min(42.0, card_rect.height * 0.22))) + inset_x = min(float(body_inset_x), max(18.0, card_rect.width * 0.22)) + body_y = title_y + title_h + title_gap + resolved_body_h = body_height if body_height is not None else max(40.0, card_rect.height - (body_y - card_rect.y) - 24.0) + + gui_label( + rl.Rectangle(card_rect.x, title_y, card_rect.width, title_h), + title, + title_size, + style.title_color, + FontWeight.MEDIUM, + alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, + ) + gui_label( + rl.Rectangle(card_rect.x + inset_x, body_y, max(1.0, card_rect.width - inset_x * 2), resolved_body_h), + body, + body_size, + style.subtitle_color, + FontWeight.NORMAL, + alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, + ) + + +def draw_list_group_shell( + rect: rl.Rectangle, + *, + fill: rl.Color | None = None, + border: rl.Color | None = None, + radius: float = 0.055, + segments: int = 18, + style: PanelStyle = DEFAULT_PANEL_STYLE, +): + draw_soft_card(rect, fill if fill is not None else style.surface_fill, border if border is not None else style.surface_border, radius=radius, segments=segments) + + +def draw_settings_list_row( + rect: rl.Rectangle, + *, + title: str, + subtitle: str = "", + value: str = "", + toggle_value: bool | None = None, + hovered: bool = False, + pressed: bool = False, + is_last: bool = False, + show_chevron: bool = True, + title_size: int = 28, + subtitle_size: int = 20, + value_size: int = 24, + separator_inset: int = 22, + value_color: rl.Color | None = None, + style: PanelStyle = DEFAULT_PANEL_STYLE, +): + draw_rect = _snap_rect(rect) + draw_list_row_shell( + draw_rect, + hovered=hovered, + pressed=pressed, + is_last=is_last, + row_bg=rl.Color(255, 255, 255, 0), + row_border=rl.Color(255, 255, 255, 0), + row_separator=style.divider_color, + current_bg=rl.Color(255, 255, 255, 0), + current_border=rl.Color(255, 255, 255, 0), + separator_inset=separator_inset, + ) + + label_rect = rl.Rectangle(draw_rect.x + 24, draw_rect.y + 14, draw_rect.width * 0.55, 28) + subtitle_rect = rl.Rectangle(draw_rect.x + 24, draw_rect.y + 46, draw_rect.width * 0.60, 24) + gui_label(label_rect, title, title_size, style.title_color, FontWeight.MEDIUM) + if subtitle: + gui_label(subtitle_rect, subtitle, subtitle_size, style.subtitle_color, FontWeight.NORMAL) + + if toggle_value is not None: + draw_toggle_switch(draw_rect, bool(toggle_value), track_color=style.accent) + return + + if value: + value_rect = rl.Rectangle(draw_rect.x + draw_rect.width - AETHER_LIST_METRICS.utility_value_right, draw_rect.y + 20, AETHER_LIST_METRICS.utility_value_width, 28) + gui_label( + value_rect, + value, + value_size, + value_color or style.title_color, + FontWeight.MEDIUM, + alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, + ) + if show_chevron: + gui_label( + rl.Rectangle(draw_rect.x + draw_rect.width - AETHER_LIST_METRICS.utility_chevron_right, draw_rect.y + 18, 26, 26), + "›", + 32, + style.muted_color, + FontWeight.MEDIUM, + alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, + ) + + def draw_selection_list_row( rect: rl.Rectangle, *, @@ -606,9 +756,28 @@ def draw_selection_list_row( action_fill: rl.Color = AetherListColors.CURRENT_BG, action_border: rl.Color = AetherListColors.CURRENT_BORDER, action_text_color: rl.Color = AetherListColors.HEADER, + row_bg: rl.Color = AetherListColors.ROW_BG, + row_border: rl.Color = AetherListColors.ROW_BORDER, + row_separator: rl.Color = AetherListColors.ROW_SEPARATOR, + row_hover: rl.Color = AetherListColors.ROW_HOVER, + current_bg: rl.Color = AetherListColors.CURRENT_BG, + current_border: rl.Color = AetherListColors.CURRENT_BORDER, ): draw_rect = _snap_rect(rect) - draw_list_row_shell(draw_rect, current=current, hovered=hovered, pressed=pressed, is_last=is_last, alpha=alpha) + draw_list_row_shell( + draw_rect, + current=current, + hovered=hovered, + pressed=pressed, + is_last=is_last, + alpha=alpha, + row_bg=row_bg, + row_border=row_border, + row_separator=row_separator, + row_hover=row_hover, + current_bg=current_bg, + current_border=current_border, + ) action_rect = rl.Rectangle(draw_rect.x + draw_rect.width - action_width, draw_rect.y, action_width, draw_rect.height) if not action_pill: action_rect = draw_action_rail(draw_rect, action_width, current=current, alpha=alpha) diff --git a/selfdrive/ui/layouts/settings/starpilot/driving_model.py b/selfdrive/ui/layouts/settings/starpilot/driving_model.py index 1c58cf9e3..d44f996d9 100644 --- a/selfdrive/ui/layouts/settings/starpilot/driving_model.py +++ b/selfdrive/ui/layouts/settings/starpilot/driving_model.py @@ -38,18 +38,21 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( AetherListColors, AetherScrollbar, AetherSliderDialog, + _point_hits, draw_action_rail, draw_action_pill, draw_busy_ring, draw_download_icon, + draw_empty_state_card, draw_heart_icon, draw_list_panel_shell, + draw_list_group_shell, build_list_panel_frame, draw_list_row_shell, draw_list_scroll_fades, - draw_soft_card, + draw_section_header, + draw_settings_list_row, draw_status_led, - draw_toggle_switch, draw_overflow_dots, ) @@ -224,8 +227,7 @@ class DrivingModelManagerView(Widget): def _target_at(self, mouse_pos: MousePos) -> str | None: for sub_key, rect in self._menu_sub_rects.items(): - visible_rect = rl.get_collision_rec(rect, self._scroll_rect) - if visible_rect.width > 0 and visible_rect.height > 0 and rl.check_collision_point_rec(mouse_pos, visible_rect): + if _point_hits(mouse_pos, rect, self._scroll_rect, pad_x=6, pad_y=6): return f"menu:{sub_key}" for key, rect in self._action_rects.items(): @@ -411,32 +413,25 @@ class DrivingModelManagerView(Widget): self._draw_utility_section(rect.x, y, width, utility_rows) def _draw_empty_state(self, rect: rl.Rectangle): - state_rect = rl.Rectangle(rect.x, rect.y, rect.width - 18, rect.height) - draw_soft_card(state_rect, rl.Color(255, 255, 255, 5), rl.Color(255, 255, 255, 14), radius=0.08, segments=18) - gui_label( - rl.Rectangle(state_rect.x, state_rect.y + 42, state_rect.width, 40), + draw_empty_state_card( + rl.Rectangle(rect.x, rect.y, rect.width - AETHER_LIST_METRICS.content_right_gutter, rect.height), self._controller.empty_state_title(), - 32, - MODEL_HEADER_TEXT, - FontWeight.MEDIUM, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - ) - gui_label( - rl.Rectangle(state_rect.x + 48, state_rect.y + 88, state_rect.width - 96, 72), self._controller.empty_state_body(), - 24, - MODEL_SUBTEXT, - FontWeight.NORMAL, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, + title_size=32, + body_size=24, + body_inset_x=48, + title_top_padding=42, + body_height=72, + fill=rl.Color(255, 255, 255, 5), + border=rl.Color(255, 255, 255, 14), ) def _draw_model_section(self, x: float, y: float, width: float, title: str, entries: list[ModelCatalogEntry]) -> float: - title_rect = rl.Rectangle(x, y, width - 18, SECTION_HEADER_HEIGHT) - gui_label(title_rect, title, 26, MODEL_SUBTEXT, FontWeight.MEDIUM) + draw_section_header(rl.Rectangle(x, y, width - AETHER_LIST_METRICS.content_right_gutter, SECTION_HEADER_HEIGHT), title) y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP for index, entry in enumerate(entries): - row_rect = rl.Rectangle(x, y + index * ROW_HEIGHT, width - 18, ROW_HEIGHT) + row_rect = rl.Rectangle(x, y + index * ROW_HEIGHT, width - AETHER_LIST_METRICS.content_right_gutter, ROW_HEIGHT) self._draw_model_row(row_rect, entry, is_last=index == len(entries) - 1) return y + len(entries) * ROW_HEIGHT @@ -571,7 +566,7 @@ class DrivingModelManagerView(Widget): ) else: # Expanded sub-button menu - btn_h = 40 + btn_h = 44 gap = 8 total_h = btn_h * 2 + gap start_y = rect.y + (rect.height - total_h) / 2 @@ -602,46 +597,32 @@ class DrivingModelManagerView(Widget): AetherChip(tr("Protected"), rl.Color(255, 255, 255, 10), MODEL_MUTED, MODEL_SUBTEXT, font_size=18).render(chip_rect) def _draw_utility_section(self, x: float, y: float, width: float, rows: list[dict]): - title_rect = rl.Rectangle(x, y, width - 18, SECTION_HEADER_HEIGHT) - gui_label(title_rect, tr("Automation and Tuning"), 26, MODEL_SUBTEXT, FontWeight.MEDIUM) + content_w = width - AETHER_LIST_METRICS.content_right_gutter + draw_section_header(rl.Rectangle(x, y, content_w, SECTION_HEADER_HEIGHT), tr("Automation and Tuning")) y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP - container_rect = rl.Rectangle(x, y, width - 18, len(rows) * UTILITY_ROW_HEIGHT) - draw_soft_card(container_rect, rl.Color(255, 255, 255, 4), rl.Color(255, 255, 255, 15), radius=0.055, segments=18) + container_rect = rl.Rectangle(x, y, content_w, len(rows) * UTILITY_ROW_HEIGHT) + draw_list_group_shell(container_rect, fill=rl.Color(255, 255, 255, 4), border=rl.Color(255, 255, 255, 15)) for index, row in enumerate(rows): - row_rect = rl.Rectangle(x, y + index * UTILITY_ROW_HEIGHT, width - 18, UTILITY_ROW_HEIGHT) + row_rect = rl.Rectangle(x, y + index * UTILITY_ROW_HEIGHT, content_w, UTILITY_ROW_HEIGHT) self._draw_utility_row(row_rect, row, is_last=index == len(rows) - 1) def _draw_utility_row(self, rect: rl.Rectangle, row: dict, is_last: bool): mouse_pos = gui_app.last_mouse_event.pos hovered = rl.check_collision_point_rec(mouse_pos, rect) pressed = self._pressed_target == f"utility:{row['id']}" - bg = rl.Color(255, 255, 255, 8 if hovered else 0) - if pressed: - bg = rl.Color(255, 255, 255, 14) - if bg.a: - rl.draw_rectangle_rounded(rect, ROW_RADIUS, 18, bg) - - if not is_last: - line_y = int(rect.y + rect.height - 1) - rl.draw_line(int(rect.x + 22), line_y, int(rect.x + rect.width - 22), line_y, MODEL_ROW_SEPARATOR) - self._utility_rects[row["id"]] = rect - label_rect = rl.Rectangle(rect.x + 24, rect.y + 14, rect.width * 0.55, 28) - subtitle_rect = rl.Rectangle(rect.x + 24, rect.y + 46, rect.width * 0.60, 24) - gui_label(label_rect, row["title"], 28, MODEL_HEADER_TEXT, FontWeight.MEDIUM) - if row.get("subtitle"): - gui_label(subtitle_rect, row["subtitle"], 20, MODEL_SUBTEXT, FontWeight.NORMAL) - - if row["type"] == "toggle": - draw_toggle_switch(rect, bool(row["value"])) - else: - value_rect = rl.Rectangle(rect.x + rect.width - AETHER_LIST_METRICS.utility_value_right, rect.y + 20, AETHER_LIST_METRICS.utility_value_width, 28) - gui_label(value_rect, row["value"], 24, MODEL_HEADER_TEXT, FontWeight.MEDIUM, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT) - gui_label( - rl.Rectangle(rect.x + rect.width - AETHER_LIST_METRICS.utility_chevron_right, rect.y + 18, 26, 26), "›", 32, MODEL_MUTED, FontWeight.MEDIUM, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER - ) + draw_settings_list_row( + rect, + title=row["title"], + subtitle=row.get("subtitle", ""), + value="" if row["type"] == "toggle" else row["value"], + toggle_value=bool(row["value"]) if row["type"] == "toggle" else None, + hovered=hovered, + pressed=pressed, + is_last=is_last, + ) def _draw_scrollbar(self, rect: rl.Rectangle): self._scrollbar.render(rect, self._content_height, self._scroll_offset) diff --git a/selfdrive/ui/layouts/settings/starpilot/maps.py b/selfdrive/ui/layouts/settings/starpilot/maps.py index 9f8797d67..a06f62387 100644 --- a/selfdrive/ui/layouts/settings/starpilot/maps.py +++ b/selfdrive/ui/layouts/settings/starpilot/maps.py @@ -20,16 +20,19 @@ from openpilot.system.ui.widgets.label import gui_label, gui_text_box from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( + AETHER_COMPACT_ROW_HEIGHT, + AETHER_LIST_METRICS, AetherButton, - AetherChip, AetherListColors, AetherScrollbar, DEFAULT_PANEL_STYLE, build_list_panel_frame, draw_action_pill, draw_busy_ring, + draw_empty_state_card, draw_list_panel_shell, draw_metric_strip, + draw_section_header, draw_tab_card, draw_selection_list_row, draw_list_scroll_fades, @@ -62,17 +65,16 @@ NETWORK_TYPE_LABELS = { OFFLINE_MAPS_PATH = Path("/data/media/0/osm/offline") CANCEL_REQUEST_TIMEOUT = 3.0 -HEADER_TOP_OFFSET = 10 +HEADER_TOP_OFFSET = 4 HEADER_TITLE_HEIGHT = 40 -HEADER_SUBTITLE_HEIGHT = 28 +HEADER_SUBTITLE_HEIGHT = 24 HEADER_BOTTOM_GAP = 12 -BROWSER_TOOLBAR_HEIGHT = 64 -BROWSER_SECTION_HEADER_HEIGHT = 34 -BROWSER_INSET = 18 -BROWSER_TAB_GAP = 12 -BROWSER_CONTEXT_TAB_GAP = 10 +BROWSER_SECTION_HEADER_HEIGHT = AETHER_LIST_METRICS.section_header_height +BROWSER_SECTION_HEADER_GAP = AETHER_LIST_METRICS.section_header_gap +BROWSER_INSET = AETHER_LIST_METRICS.content_right_gutter +BROWSER_TAB_GAP = AETHER_LIST_METRICS.section_header_gap BROWSER_CONTEXT_TAB_HEIGHT = 52 -BROWSER_REGION_ROW_HEIGHT = 88 +BROWSER_REGION_ROW_HEIGHT = AETHER_COMPACT_ROW_HEIGHT BROWSER_EMPTY_STATE_HEIGHT = 128 STATUS_CARD_INSET = BROWSER_INSET STATUS_BUTTON_HEIGHT = 52 @@ -81,7 +83,13 @@ STATUS_REMOVE_HEIGHT = 40 STATUS_METRIC_GAP = 18 STATUS_SELECTION_CHIP_HEIGHT = 30 MAPS_TILE_GREEN = rl.Color(16, 185, 129, 255) -MAPS_PANEL_STYLE = replace(DEFAULT_PANEL_STYLE, accent=MAPS_TILE_GREEN) +MAPS_PANEL_STYLE = replace( + DEFAULT_PANEL_STYLE, + accent=MAPS_TILE_GREEN, + current_fill=rl.Color(16, 185, 129, 16), + current_border=rl.Color(16, 185, 129, 42), + underline_color=rl.Color(16, 185, 129, 150), +) 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") @@ -389,23 +397,17 @@ class MapBrowserCard(Widget): ) def _render_empty_state(self, rect: rl.Rectangle, title: str, body: str): - state_rect = rl.Rectangle(rect.x, rect.y, rect.width, rect.height) - draw_soft_card(state_rect, MAPS_PANEL_STYLE.surface_fill, rl.Color(255, 255, 255, 10)) - gui_label( - rl.Rectangle(state_rect.x, state_rect.y + 24, state_rect.width, 32), + draw_empty_state_card( + rect, title, - 26, - AetherListColors.HEADER, - FontWeight.MEDIUM, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - ) - gui_label( - rl.Rectangle(state_rect.x + 40, state_rect.y + 60, state_rect.width - 80, 44), body, - 19, - AetherListColors.SUBTEXT, - FontWeight.NORMAL, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, + title_size=26, + body_size=19, + body_inset_x=40, + title_top_padding=24, + body_height=44, + border=rl.Color(255, 255, 255, 10), + style=MAPS_PANEL_STYLE, ) def _render_region_rows(self, rect: rl.Rectangle, regions: list[dict]): @@ -436,29 +438,23 @@ class MapBrowserCard(Widget): action_width=142, action_pill=True, action_text_size=15, - action_pill_height=36, + action_pill_height=40, action_pill_width=112 if selected else 84, title_size=26, subtitle_size=17, + row_separator=MAPS_PANEL_STYLE.divider_color, + current_bg=MAPS_PANEL_STYLE.current_fill, + current_border=MAPS_PANEL_STYLE.current_border, action_fill=rl.Color(94, 168, 130, 18) if selected else rl.Color(255, 255, 255, 8), action_border=rl.Color(94, 168, 130, 38) if selected else rl.Color(255, 255, 255, 24), - action_text_color=AetherListColors.SUBTEXT if selected else AetherListColors.HEADER, + action_text_color=AetherListColors.HEADER, ) def _active_browse_regions(self) -> list[dict]: return self._controller._browse_regions_for_active_group() def _render_section_header(self, rect: rl.Rectangle, title: str, *, count_text: str | None = None): - del title - if count_text: - gui_label( - rl.Rectangle(rect.x, rect.y + (rect.height - 22) / 2, rect.width, 22), - count_text, - 20, - AetherListColors.SUBTEXT, - FontWeight.NORMAL, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, - ) + draw_section_header(rect, title, trailing_text=count_text or "", title_size=24, trailing_size=20, style=MAPS_PANEL_STYLE) def _measure_height(self, width: float) -> float: if self._controller._showing_source_picker(): @@ -466,8 +462,8 @@ class MapBrowserCard(Widget): return BROWSER_CONTEXT_TAB_HEIGHT + 20 total = 10.0 - total += self._measure_context_tabs_height(width) + 12 - total += BROWSER_SECTION_HEADER_HEIGHT + 2 + total += self._measure_context_tabs_height(width) + BROWSER_SECTION_HEADER_GAP + total += BROWSER_SECTION_HEADER_HEIGHT + BROWSER_SECTION_HEADER_GAP region_count = len(self._active_browse_regions()) total += self._row_height(region_count, BROWSER_REGION_ROW_HEIGHT) if region_count else BROWSER_EMPTY_STATE_HEIGHT return total + 10 @@ -491,14 +487,14 @@ class MapBrowserCard(Widget): context_tabs_h = self._measure_context_tabs_height(content_w) self._render_context_tabs(rl.Rectangle(content_x, y, content_w, context_tabs_h)) - y += context_tabs_h + 12 + y += context_tabs_h + BROWSER_SECTION_HEADER_GAP self._render_section_header( rl.Rectangle(content_x, y, content_w, BROWSER_SECTION_HEADER_HEIGHT), - "", + tr("Regions"), count_text=self._controller._active_group_count_text(), ) - y += BROWSER_SECTION_HEADER_HEIGHT + 2 + y += BROWSER_SECTION_HEADER_HEIGHT + BROWSER_SECTION_HEADER_GAP regions = self._active_browse_regions() region_h = self._row_height(len(regions), BROWSER_REGION_ROW_HEIGHT) if regions else BROWSER_EMPTY_STATE_HEIGHT @@ -1130,15 +1126,16 @@ class StarPilotMapsLayout(StarPilotPanel): hdr = frame.header title_y = hdr.y + HEADER_TOP_OFFSET + subtitle_y = hdr.y + 48 gui_label(rl.Rectangle(hdr.x, title_y, hdr.width, HEADER_TITLE_HEIGHT), tr("Map Data"), 40, AetherListColors.HEADER, FontWeight.SEMI_BOLD) - gui_label(rl.Rectangle(hdr.x, title_y + 40, hdr.width * 0.72, HEADER_SUBTITLE_HEIGHT), tr("Use offline maps for speed-limit control and keep only the regions you need."), 22, AetherListColors.SUBTEXT, FontWeight.NORMAL) + gui_label(rl.Rectangle(hdr.x, subtitle_y, hdr.width * 0.60, HEADER_SUBTITLE_HEIGHT), tr("Use offline maps for speed-limit control and keep only the regions you need."), 22, AetherListColors.SUBTEXT, FontWeight.NORMAL) - header_status_y = title_y + HEADER_TITLE_HEIGHT + HEADER_SUBTITLE_HEIGHT + 8 + header_status_y = subtitle_y + 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) scroll_rect = frame.scroll - content_width = scroll_rect.width - 18 + content_width = scroll_rect.width - AETHER_LIST_METRICS.content_right_gutter 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) self._scroll_panel.set_enabled(self.is_visible)