Big UI : We can sort models now

This commit is contained in:
firestarsdog
2026-07-20 17:19:13 -04:00
parent c3d1f727c0
commit 7a76325e67
2 changed files with 162 additions and 72 deletions
@@ -196,7 +196,7 @@ def draw_text_fit_common(
size = measure_text_cached(font, text, base_font_size, spacing=spacing)
actual_font_size = base_font_size
if size.x > max_width:
MIN_FONT = 8
MIN_FONT = 16
hi = max(MIN_FONT, int(round(font_size * (max_width / size.x))))
lo = MIN_FONT
actual_font_size = hi
@@ -55,6 +55,8 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import (
draw_settings_panel_header,
draw_status_led,
draw_overflow_dots,
draw_rounded_fill,
draw_rounded_stroke,
init_list_panel,
draw_interactive_rect,
resolve_interactive_target,
@@ -77,6 +79,15 @@ PANEL_STYLE = DEFAULT_PANEL_STYLE
BANNER_HEIGHT = ROW_HEIGHT
HEADER_BAR_HEIGHT = 107.0
MANAGEMENT_STRIP_HEIGHT = 56.0
_SORT_MODES = ("alphabetical", "date", "date_oldest", "favorites", "community_picks")
_SORT_LABELS = {
"alphabetical": "Alphabetical",
"date": "Date (Newest)",
"date_oldest": "Date (Oldest)",
"favorites": "Favorites",
"community_picks": "Community Picks",
}
_SORT_PILLS = ("alphabetical", "date", "favorites", "community_picks")
@dataclass
@@ -204,7 +215,7 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
if point_hits(mouse_pos, rect, self._scroll_rect, pad_x=6, pad_y=pad_y):
return target_id
for target_id, rect in self._interactive_rects.items():
if target_id.startswith("mgmt:"):
if target_id.startswith("sortopt:") or target_id.startswith("mgmt:"):
if point_hits(mouse_pos, rect, self._shell_rect, pad_x=6, pad_y=6):
return target_id
return None
@@ -268,6 +279,14 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
self._controller._on_scores_clicked()
return
if target.startswith("sortopt:"):
mode = target.split(":", 1)[1]
if mode == "date":
current = self._controller._get_sort_mode()
mode = "date_oldest" if current == "date" else "date"
self._controller._params.put("ModelSortMode", mode)
return
def _render(self, rect: rl.Rectangle):
self.set_rect(rect)
self._interactive_rects.clear()
@@ -289,11 +308,10 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
scroll_rect.width, scroll_rect.height - HEADER_BAR_HEIGHT)
randomizer_on = self._controller._params.get_bool("ModelRandomizer")
if randomizer_on:
mgmt_y = scroll_rect.y
self._draw_management_strip(scroll_rect.x, mgmt_y, content_width)
scroll_rect = rl.Rectangle(scroll_rect.x, scroll_rect.y + MANAGEMENT_STRIP_HEIGHT,
scroll_rect.width, scroll_rect.height - MANAGEMENT_STRIP_HEIGHT)
mgmt_y = scroll_rect.y
self._draw_sort_strip(scroll_rect.x, mgmt_y, content_width, randomizer_on)
scroll_rect = rl.Rectangle(scroll_rect.x, scroll_rect.y + MANAGEMENT_STRIP_HEIGHT,
scroll_rect.width, scroll_rect.height - MANAGEMENT_STRIP_HEIGHT)
self._scroll_rect = scroll_rect
@@ -357,32 +375,63 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
randomizer_on = self._controller._params.get_bool("ModelRandomizer")
draw_status_led(rl.Vector2(led_x, led_y), randomizer_on)
def _draw_management_strip(self, x: float, y: float, width: float):
def _draw_sort_strip(self, x: float, y: float, width: float, randomizer_on: bool):
pill_h = 48.0
pill_y = y + (MANAGEMENT_STRIP_HEIGHT - pill_h) / 2
left = x + 16
gap = 14.0
gap = 8.0
usable = width - 32.0
sort_mode = self._controller._get_sort_mode()
n = len(_SORT_PILLS)
blacklisted = [m.strip() for m in (self._controller._params.get("BlacklistedModels", encoding="utf-8") or "").split(",") if m.strip()]
bl_label = tr(f"Blacklist: {len(blacklisted)} blocked") if blacklisted else tr("Blacklist")
if randomizer_on:
half = (usable - gap) / 2.0
seg_w = (half - gap * (n - 1)) / n
mgmt_left = left + half + gap
else:
seg_w = (usable - gap * (n - 1)) / n
mgmt_left = None
bl_w = min(420.0, usable * 0.62)
rt_w = min(280.0, usable - bl_w - gap)
for i, mode in enumerate(_SORT_PILLS):
seg_x = left + i * (seg_w + gap)
seg_rect = rl.Rectangle(seg_x, pill_y, seg_w, pill_h)
if mode == "date":
is_active = sort_mode in ("date", "date_oldest")
label = tr("Date (Oldest)") if sort_mode == "date_oldest" else tr("Date (Newest)")
else:
is_active = (mode == sort_mode)
label = tr(_SORT_LABELS[mode])
pressed = self._pressed_target == f"sortopt:{mode}"
if pressed and is_active:
fill = with_alpha(AetherListColors.PRIMARY, 80)
elif pressed:
fill = rl.Color(255, 255, 255, 18)
elif is_active:
fill = with_alpha(AetherListColors.PRIMARY, 28)
else:
fill = rl.Color(255, 255, 255, 8)
border = with_alpha(AetherListColors.PRIMARY, 80) if is_active else rl.Color(255, 255, 255, 24)
draw_action_pill(seg_rect, label, fill, border, AetherListColors.HEADER, font_size=28, roundness=0.3)
self._interactive_rects[f"sortopt:{mode}"] = seg_rect
bl_pill = rl.Rectangle(left, pill_y, bl_w, pill_h)
draw_action_pill(bl_pill, bl_label,
with_alpha(AetherListColors.PRIMARY, 18),
with_alpha(AetherListColors.PRIMARY, 50),
AetherListColors.HEADER, font_size=28, roundness=0.35)
self._interactive_rects["mgmt:blacklist"] = bl_pill
rt_pill = rl.Rectangle(left + bl_w + gap, pill_y, rt_w, pill_h)
draw_action_pill(rt_pill, tr("Ratings"),
with_alpha(AetherListColors.PRIMARY, 18),
with_alpha(AetherListColors.PRIMARY, 50),
AetherListColors.HEADER, font_size=28, roundness=0.35)
self._interactive_rects["mgmt:ratings"] = rt_pill
if randomizer_on:
blacklisted = [m.strip() for m in (self._controller._params.get("BlacklistedModels", encoding="utf-8") or "").split(",") if m.strip()]
bl_label = tr(f"Blacklist: {len(blacklisted)} blocked") if blacklisted else tr("Blacklist")
mgmt_w = usable - half - gap
bl_w = min(420.0, mgmt_w * 0.62)
rt_w = min(280.0, mgmt_w - bl_w - gap)
bl_pill = rl.Rectangle(mgmt_left, pill_y, bl_w, pill_h)
draw_action_pill(bl_pill, bl_label,
with_alpha(AetherListColors.PRIMARY, 18),
with_alpha(AetherListColors.PRIMARY, 50),
AetherListColors.HEADER, font_size=28, roundness=0.35)
self._interactive_rects["mgmt:blacklist"] = bl_pill
rt_pill = rl.Rectangle(mgmt_left + bl_w + gap, pill_y, rt_w, pill_h)
draw_action_pill(rt_pill, tr("Ratings"),
with_alpha(AetherListColors.PRIMARY, 18),
with_alpha(AetherListColors.PRIMARY, 50),
AetherListColors.HEADER, font_size=28, roundness=0.35)
self._interactive_rects["mgmt:ratings"] = rt_pill
def _measure_content_height(self, width: float) -> float:
sections = self._build_sections(width)
@@ -392,24 +441,34 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
def _build_sections(self, width: float) -> list[tuple[str, float]]:
sections: list[tuple[str, float]] = []
sort_mode = self._controller._get_sort_mode()
installed = self._controller.installed_entries()
available = self._controller.available_entries()
utility_rows = self._controller.utility_rows()
if self._controller._params.get_bool("ModelRandomizer"):
available = []
utility_rows = []
if installed:
installed_count = len(installed)
if self._controller.current_entry() is not None:
installed_count = max(installed_count - 1, 0)
sections.append(("installed", SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP + installed_count * ROW_HEIGHT))
if available:
sections.append(("available", SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP + len(available) * ROW_HEIGHT))
if utility_rows:
utility_height = SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP + len(utility_rows) * UTILITY_ROW_HEIGHT
sections.append(("utility", utility_height))
if sort_mode == "favorites":
fav = self._controller.favorites_entries()
if fav:
count = len(fav)
if self._controller.current_entry() is not None and self._controller.current_entry().user_favorite:
count = max(count - 1, 0)
sections.append(("favorites", SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP + count * ROW_HEIGHT))
elif sort_mode == "community_picks":
community = self._controller.community_picks_entries()
if community:
count = len(community)
if self._controller.current_entry() is not None and self._controller.current_entry().community_favorite:
count = max(count - 1, 0)
sections.append(("community_picks", SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP + count * ROW_HEIGHT))
else:
installed = self._controller.installed_entries()
available = self._controller.available_entries()
if self._controller._params.get_bool("ModelRandomizer"):
available = []
if installed:
installed_count = len(installed)
if self._controller.current_entry() is not None:
installed_count = max(installed_count - 1, 0)
sections.append(("installed", SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP + installed_count * ROW_HEIGHT))
if available:
sections.append(("available", SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP + len(available) * ROW_HEIGHT))
if not sections:
sections.append(("empty", 240))
@@ -417,18 +476,37 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
return [(key, height + SECTION_GAP) for key, height in sections]
def _draw_scroll_content(self, rect: rl.Rectangle, width: float):
sort_mode = self._controller._get_sort_mode()
y = rect.y + self._scroll_offset
if sort_mode == "favorites":
entries = self._controller.favorites_entries()
if self._controller.current_entry() is not None:
entries = [e for e in entries if not self._controller.is_current_model(e.key)]
if entries:
y = self._draw_model_section(rect.x, y, width, tr("Favorites"), entries)
else:
self._draw_empty_state(rl.Rectangle(rect.x, y + 36, width, 200))
return
if sort_mode == "community_picks":
entries = self._controller.community_picks_entries()
if self._controller.current_entry() is not None:
entries = [e for e in entries if not self._controller.is_current_model(e.key)]
if entries:
y = self._draw_model_section(rect.x, y, width, tr("Community Picks"), entries)
else:
self._draw_empty_state(rl.Rectangle(rect.x, y + 36, width, 200))
return
installed = self._controller.installed_entries()
if self._controller.current_entry() is not None:
installed = [e for e in installed if not self._controller.is_current_model(e.key)]
available = self._controller.available_entries()
utility_rows = self._controller.utility_rows()
if self._controller._params.get_bool("ModelRandomizer"):
available = []
utility_rows = []
y = rect.y + self._scroll_offset
if not installed and not available and not utility_rows:
if not installed and not available:
self._draw_empty_state(rl.Rectangle(rect.x, y + 36, width, 200))
return
@@ -438,8 +516,6 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
if available:
y = self._draw_model_section(rect.x, y, width, tr("Available to Download"), available)
y += SECTION_GAP
if utility_rows:
self._draw_utility_section(rect.x, y, width, utility_rows)
def _draw_empty_state(self, rect: rl.Rectangle):
draw_empty_state_card(
@@ -447,7 +523,7 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
self._controller.empty_state_title(),
self._controller.empty_state_body(),
title_size=46,
body_size=35,
body_size=36,
body_inset_x=48,
title_top_padding=42,
body_height=72,
@@ -584,7 +660,7 @@ class DrivingModelManagerView(AetherInteractiveMixin, Widget):
gui_label(
rl.Rectangle(rect.x + 16, rect.y + rect.height - 58, rect.width - 32, 32),
label,
24,
28,
AetherListColors.SUBTEXT,
FontWeight.MEDIUM,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
@@ -924,29 +1000,39 @@ class StarPilotDrivingModelLayout(_SettingsPage):
def installed_entries(self) -> list[ModelCatalogEntry]:
entries = [entry for entry in self._catalog_entries.values() if entry.installed]
return sorted(
entries,
key=lambda entry: (
0 if self.is_current_model(entry.key) else 1,
0 if entry.builtin else 1,
0 if entry.user_favorite else 1,
0 if entry.community_favorite else 1,
self._model_file_to_name_processed.get(entry.key, entry.name).lower(),
entry.key,
),
)
sort_mode = self._get_sort_mode()
if sort_mode == "date":
return sorted(entries, key=lambda e: (e.released or "0000-00-00", self._model_file_to_name_processed.get(e.key, e.name).lower(), e.key), reverse=True)
if sort_mode == "date_oldest":
return sorted(entries, key=lambda e: (e.released or "0000-00-00", self._model_file_to_name_processed.get(e.key, e.name).lower(), e.key))
return sorted(entries, key=lambda e: (0 if e.builtin else 1, self._model_file_to_name_processed.get(e.key, e.name).lower(), e.key))
def available_entries(self) -> list[ModelCatalogEntry]:
entries = [entry for entry in self._catalog_entries.values() if not entry.installed]
return sorted(
entries,
key=lambda entry: (
0 if entry.user_favorite else 1,
0 if entry.community_favorite else 1,
self._model_file_to_name_processed.get(entry.key, entry.name).lower(),
entry.key,
),
)
sort_mode = self._get_sort_mode()
if sort_mode == "date":
return sorted(entries, key=lambda e: (e.released or "0000-00-00", self._model_file_to_name_processed.get(e.key, e.name).lower(), e.key), reverse=True)
if sort_mode == "date_oldest":
return sorted(entries, key=lambda e: (e.released or "0000-00-00", self._model_file_to_name_processed.get(e.key, e.name).lower(), e.key))
return sorted(entries, key=lambda e: (self._model_file_to_name_processed.get(e.key, e.name).lower(), e.key))
def community_picks_entries(self) -> list[ModelCatalogEntry]:
entries = [entry for entry in self._catalog_entries.values() if entry.community_favorite]
if self._params.get_bool("ModelRandomizer"):
entries = [e for e in entries if e.installed]
return sorted(entries, key=lambda e: (
self._model_file_to_name_processed.get(e.key, e.name).lower(),
e.key
))
def favorites_entries(self) -> list[ModelCatalogEntry]:
entries = [entry for entry in self._catalog_entries.values() if entry.user_favorite]
if self._params.get_bool("ModelRandomizer"):
entries = [e for e in entries if e.installed]
return sorted(entries, key=lambda e: (
self._model_file_to_name_processed.get(e.key, e.name).lower(),
e.key
))
def is_current_model(self, model_key: str) -> bool:
return canonical_model_key(model_key) == self._current_model_key
@@ -954,6 +1040,10 @@ class StarPilotDrivingModelLayout(_SettingsPage):
def current_entry(self) -> ModelCatalogEntry | None:
return self._catalog_entries.get(self._current_model_key)
def _get_sort_mode(self) -> str:
mode = (self._params.get("ModelSortMode", encoding="utf-8") or "").strip()
return mode if mode in _SORT_MODES else _SORT_MODES[0]
def is_model_removable(self, model_key: str) -> bool:
key = canonical_model_key(model_key)
if not key: