mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-06-27 17:42:04 +08:00
BigUI WIP: Standarize Breadcrumbs
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user