BigUI WIP: Finish nuking nav from ui

This commit is contained in:
firestarsdog
2026-04-25 23:40:39 -04:00
parent a2c39aa2ed
commit ff67d2eedd
3 changed files with 262 additions and 241 deletions
+254 -81
View File
@@ -3,108 +3,211 @@ from __future__ import annotations
import shutil
from pathlib import Path
from openpilot.system.ui.lib.application import gui_app
import pyray as rl
from openpilot.system.ui.lib.application import FontWeight, MousePos, gui_app
from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2
from openpilot.system.ui.widgets import DialogResult, Widget
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
from openpilot.starpilot.common.maps_catalog import (
COUNTRY_REGION_GROUPS,
MAP_SCHEDULE_LABELS,
MAP_SECTIONS,
STATE_REGION_GROUPS,
sanitize_selected_locations_csv,
schedule_label,
schedule_param_value,
from openpilot.system.ui.widgets.label import gui_label
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import (
AetherChip,
AetherScrollbar,
HubTile,
TileGrid,
ToggleTile,
ValueTile,
build_list_panel_frame,
draw_action_pill,
draw_list_panel_shell,
draw_list_scroll_fades,
draw_soft_card,
)
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
from openpilot.starpilot.common.maps_catalog import MAPS_CATALOG, MAP_SCHEDULE_LABELS, sanitize_selected_locations_csv, schedule_label, schedule_param_value
class StarPilotMapRegionLayout(StarPilotPanel):
def __init__(self, region_map: dict[str, str], prefix: str):
TOP_ACTION_ROW_HEIGHT = 132
GROUP_HEADER_HEIGHT = 52
GROUP_BODY_GAP = 14
GROUP_CARD_GAP = 18
GROUP_TILE_HEIGHT = 124
class MapGroupCard(Widget):
def __init__(self, controller: "StarPilotMapsLayout", section_title: str, group: dict):
super().__init__()
self._prefix = prefix
self.CATEGORIES = []
self._controller = controller
self._title = group["title"]
self._regions = group["regions"]
self._tile_grid = self._child(TileGrid(columns=None, padding=14, uniform_width=True))
self._action_rect = rl.Rectangle(0, 0, 0, 0)
self._action_pressed = False
for key, name in sorted(region_map.items(), key=lambda item: item[1]):
self.CATEGORIES.append({
"title": name,
"type": "toggle",
"get_state": lambda k=key: self._get_map_state(k),
"set_state": lambda s, k=key: self._set_map_state(k, s),
"color": "#68ACA3"
})
self._rebuild_grid()
for region in self._regions:
token = region["token"]
self._tile_grid.add_tile(
ToggleTile(
title=tr_noop(region["label"]),
get_state=lambda token=token: self._controller._get_map_state(token),
set_state=lambda state, token=token: self._controller._set_map_state(token, state),
bg_color="#68ACA3",
)
)
def _get_map_state(self, key):
selected = set(sanitize_selected_locations_csv(self._params.get("MapsSelected", encoding="utf-8") or "").split(","))
selected.discard("")
return f"{self._prefix}{key}" in selected
def _selected_count(self) -> int:
return sum(1 for region in self._regions if self._controller._get_map_state(region["token"]))
def _set_map_state(self, key, state):
selected = set(sanitize_selected_locations_csv(self._params.get("MapsSelected", encoding="utf-8") or "").split(","))
selected.discard("")
def _all_selected(self) -> bool:
return self._selected_count() == len(self._regions)
prefixed_key = f"{self._prefix}{key}"
if state:
selected.add(prefixed_key)
else:
selected.discard(prefixed_key)
def _toggle_all(self):
state = not self._all_selected()
for region in self._regions:
self._controller._set_map_state(region["token"], state)
self._params.put("MapsSelected", sanitize_selected_locations_csv(sorted(selected)))
def _measure_height(self, width: float) -> float:
content_w = max(0.0, width - 32)
rows = self._tile_grid.get_row_count(available_width=content_w)
body_h = rows * GROUP_TILE_HEIGHT + self._tile_grid.gap * max(0, rows - 1)
return GROUP_HEADER_HEIGHT + GROUP_BODY_GAP + body_h + 28
def _handle_mouse_press(self, mouse_pos: MousePos):
if rl.check_collision_point_rec(mouse_pos, self._action_rect):
self._action_pressed = True
class StarPilotMapCountriesLayout(StarPilotPanel):
def __init__(self):
super().__init__()
self.CATEGORIES = [
{"title": tr_noop(group["title"]), "panel": group["key"], "icon": "toggle_icons/icon_map.png", "color": "#68ACA3"}
for group in COUNTRY_REGION_GROUPS
]
self._rebuild_grid()
def _handle_mouse_release(self, mouse_pos: MousePos):
if self._action_pressed:
self._action_pressed = False
if rl.check_collision_point_rec(mouse_pos, self._action_rect):
self._toggle_all()
def _render(self, rect: rl.Rectangle):
self.set_rect(rect)
draw_soft_card(rect, rl.Color(255, 255, 255, 4), rl.Color(255, 255, 255, 15), radius=0.08, segments=18)
class StarPilotMapStatesLayout(StarPilotPanel):
def __init__(self):
super().__init__()
self.CATEGORIES = [
{"title": tr_noop(group["title"]), "panel": group["key"], "icon": "toggle_icons/icon_map.png", "color": "#68ACA3"}
for group in STATE_REGION_GROUPS
]
self._rebuild_grid()
header_rect = rl.Rectangle(rect.x + 16, rect.y + 12, rect.width - 32, GROUP_HEADER_HEIGHT)
title_w = max(0.0, header_rect.width - 160)
title_rect = rl.Rectangle(header_rect.x, header_rect.y, title_w, 28)
gui_label(title_rect, self._title, 28, rl.Color(236, 242, 250, 255), FontWeight.SEMI_BOLD)
count = self._selected_count()
count_label = tr(f"{count}/{len(self._regions)} selected")
chip = AetherChip(count_label, rl.Color(89, 116, 151, 26), rl.Color(116, 136, 168, 52), rl.Color(236, 242, 250, 255), pill=True)
chip_w = min(136, max(96, header_rect.width * 0.26))
chip_rect = rl.Rectangle(header_rect.x + max(0.0, header_rect.width - chip_w - 68), header_rect.y + 2, chip_w, 34)
chip.render(chip_rect)
action_text = tr("Clear All") if self._all_selected() else tr("Select All")
self._action_rect = rl.Rectangle(header_rect.x + header_rect.width - 60, header_rect.y + 2, 60, 34)
draw_action_pill(self._action_rect, action_text, rl.Color(89, 116, 151, 20), rl.Color(255, 255, 255, 26), rl.Color(236, 242, 250, 255), font_size=16)
content_x = rect.x + 16
content_y = header_rect.y + GROUP_HEADER_HEIGHT + GROUP_BODY_GAP
content_w = rect.width - 32
rows = self._tile_grid.get_row_count(available_width=content_w)
body_h = rows * GROUP_TILE_HEIGHT + self._tile_grid.gap * max(0, rows - 1)
grid_rect = rl.Rectangle(content_x, content_y, content_w, body_h)
self._tile_grid.set_parent_rect(rect)
self._tile_grid.render(grid_rect)
class StarPilotMapsLayout(StarPilotPanel):
def __init__(self):
super().__init__()
self._scroll_panel = GuiScrollPanel2(horizontal=False)
self._scrollbar = AetherScrollbar()
self._content_height = 0.0
self._scroll_offset = 0.0
self._selected_count = 0
self._storage_text = "0 MB"
self._storage_updated_at = 0.0
self._sub_panels = {
"countries": StarPilotMapCountriesLayout(),
"states": StarPilotMapStatesLayout(),
}
for section in MAP_SECTIONS:
for group in section["groups"]:
self._sub_panels[group["key"]] = StarPilotMapRegionLayout(group["regions"], section["prefix"])
self.CATEGORIES = [
{"title": tr_noop("Download Maps"), "type": "hub", "on_click": self._on_download, "icon": "toggle_icons/icon_map.png", "color": "#68ACA3"},
{"title": tr_noop("Auto Update Schedule"), "type": "value", "get_value": lambda: schedule_label(self._params.get("PreferredSchedule")), "on_click": self._on_schedule, "icon": "toggle_icons/icon_calendar.png", "color": "#68ACA3"},
{"title": tr_noop("Countries"), "panel": "countries", "icon": "toggle_icons/icon_map.png", "color": "#68ACA3"},
{"title": tr_noop("U.S. States"), "panel": "states", "icon": "toggle_icons/icon_map.png", "color": "#68ACA3"},
{"title": tr_noop("Storage Used"), "type": "value", "get_value": self._get_storage, "on_click": lambda: None, "icon": "toggle_icons/icon_system.png", "color": "#68ACA3"},
{"title": tr_noop("Remove Maps"), "type": "hub", "on_click": self._on_remove, "icon": "toggle_icons/icon_map.png", "color": "#68ACA3"},
self._action_grid = TileGrid(columns=None, padding=16, uniform_width=True)
self._action_tiles = [
HubTile(
title=tr_noop("Download Maps"),
desc=tr_noop("Download the selected regions."),
icon_path="toggle_icons/icon_map.png",
on_click=self._on_download,
starpilot_icon=True,
bg_color="#68ACA3",
),
ValueTile(
title=tr_noop("Auto Update Schedule"),
get_value=lambda: schedule_label(self._params.get("PreferredSchedule")),
on_click=self._on_schedule,
icon_path="toggle_icons/icon_calendar.png",
bg_color="#68ACA3",
),
ValueTile(
title=tr_noop("Storage Used"),
get_value=self._get_storage,
on_click=lambda: None,
icon_path="toggle_icons/icon_system.png",
bg_color="#68ACA3",
),
HubTile(
title=tr_noop("Remove Maps"),
desc=tr_noop("Delete downloaded map files."),
icon_path="toggle_icons/icon_map.png",
on_click=self._on_remove,
starpilot_icon=True,
bg_color="#A64D5A",
),
]
for tile in self._action_tiles:
self._action_grid.add_tile(self._child(tile))
for _, panel in self._sub_panels.items():
if hasattr(panel, "set_navigate_callback"):
panel.set_navigate_callback(self._navigate_to)
if hasattr(panel, "set_back_callback"):
panel.set_back_callback(self._go_back)
self._sections: list[tuple[str, list[MapGroupCard]]] = []
for section in MAPS_CATALOG:
cards = [self._child(MapGroupCard(self, section["title"], group)) for group in section["groups"]]
self._sections.append((section["title"], cards))
self._rebuild_grid()
self._refresh_storage_cache(force=True)
self._sync_selected_count()
def _get_storage(self) -> str:
def show_event(self):
super().show_event()
self._scroll_offset = 0.0
self._refresh_storage_cache(force=True)
self._sync_selected_count()
def hide_event(self):
super().hide_event()
self._scroll_offset = 0.0
def _refresh_storage_cache(self, force: bool = False):
now = rl.get_time()
if not force and (now - self._storage_updated_at) < 5.0:
return
self._storage_text = self._calculate_storage_used()
self._storage_updated_at = now
def _sync_selected_count(self):
selected = set(sanitize_selected_locations_csv(self._params.get("MapsSelected", encoding="utf-8") or "").split(","))
selected.discard("")
self._selected_count = len(selected)
def _get_map_state(self, token: str) -> bool:
selected = set(sanitize_selected_locations_csv(self._params.get("MapsSelected", encoding="utf-8") or "").split(","))
selected.discard("")
return token in selected
def _set_map_state(self, token: str, state: bool):
selected = set(sanitize_selected_locations_csv(self._params.get("MapsSelected", encoding="utf-8") or "").split(","))
selected.discard("")
if state:
selected.add(token)
else:
selected.discard(token)
self._params.put("MapsSelected", sanitize_selected_locations_csv(sorted(selected)))
self._sync_selected_count()
def _calculate_storage_used(self) -> str:
maps_path = Path("/data/media/0/osm/offline")
if not maps_path.exists():
return "0 MB"
@@ -114,6 +217,10 @@ class StarPilotMapsLayout(StarPilotPanel):
return f"{(mb / 1024):.2f} GB"
return f"{mb:.2f} MB"
def _get_storage(self) -> str:
self._refresh_storage_cache()
return self._storage_text
def _on_schedule(self):
options = list(MAP_SCHEDULE_LABELS.values())
current = schedule_label(self._params.get("PreferredSchedule"))
@@ -122,7 +229,6 @@ class StarPilotMapsLayout(StarPilotPanel):
def on_select(res):
if res == DialogResult.CONFIRM and dialog.selection:
self._params.put("PreferredSchedule", schedule_param_value(dialog.selection))
self._rebuild_grid()
gui_app.push_widget(dialog, callback=on_select)
@@ -131,8 +237,7 @@ class StarPilotMapsLayout(StarPilotPanel):
selected_raw = sanitize_selected_locations_csv(current_selected)
if selected_raw != current_selected:
self._params.put("MapsSelected", selected_raw)
selected = [k.strip() for k in selected_raw.split(",") if k.strip()]
if not selected:
if not selected_raw:
gui_app.push_widget(alert_dialog(tr("Please select at least one region or state first!")))
return
@@ -150,6 +255,74 @@ class StarPilotMapsLayout(StarPilotPanel):
if maps_path.exists():
shutil.rmtree(maps_path, ignore_errors=True)
gui_app.push_widget(alert_dialog(tr("Maps removed.")))
self._rebuild_grid()
self._refresh_storage_cache(force=True)
gui_app.push_widget(ConfirmDialog(tr("Delete all downloaded map data?"), tr("Remove"), on_close=on_confirm))
def _measure_content_height(self, width: float) -> float:
action_rows = self._action_grid.get_row_count(available_width=width)
total = action_rows * TOP_ACTION_ROW_HEIGHT + self._action_grid.gap * max(0, action_rows - 1)
total += 30
for _section_title, cards in self._sections:
total += 34 + GROUP_CARD_GAP
for card in cards:
total += card._measure_height(width) + GROUP_CARD_GAP
total += 12
return total
def _draw_section_title(self, rect: rl.Rectangle, title: str):
gui_label(rect, title, 26, rl.Color(236, 242, 250, 230), FontWeight.SEMI_BOLD)
def _draw_scroll_content(self, rect: rl.Rectangle, width: float):
y = rect.y + self._scroll_offset
action_rows = self._action_grid.get_row_count(available_width=width)
action_h = action_rows * TOP_ACTION_ROW_HEIGHT + self._action_grid.gap * max(0, action_rows - 1)
self._action_grid.render(rl.Rectangle(rect.x, y, width, action_h))
y += action_h + 30
for section_title, cards in self._sections:
title_rect = rl.Rectangle(rect.x, y, width, 34)
self._draw_section_title(title_rect, section_title)
y += 34 + GROUP_CARD_GAP
for card in cards:
card_h = card._measure_height(width)
card.render(rl.Rectangle(rect.x, y, width, card_h))
y += card_h + GROUP_CARD_GAP
y += 12
def _render(self, rect: rl.Rectangle):
self.set_rect(rect)
frame = build_list_panel_frame(rect)
draw_list_panel_shell(frame)
hdr = frame.header
chip_w = min(140, max(104, int(hdr.width * 0.18)))
title_w = max(0, hdr.width - chip_w - 20)
gui_label(rl.Rectangle(hdr.x, hdr.y + 4, title_w, 40), tr("Map Data"), 40, rl.Color(236, 242, 250, 255), FontWeight.SEMI_BOLD)
gui_label(
rl.Rectangle(hdr.x, hdr.y + 48, title_w, 36),
tr("Select regions, schedule updates, and manage offline map storage."),
24,
rl.Color(164, 177, 196, 255),
FontWeight.NORMAL,
)
chip = AetherChip(tr(f"{self._selected_count} selected"), rl.Color(89, 116, 151, 26), rl.Color(116, 136, 168, 52), rl.Color(236, 242, 250, 255), pill=True)
chip.render(rl.Rectangle(hdr.x + max(0.0, hdr.width - chip_w), hdr.y + 12, chip_w, 34))
scroll_rect = frame.scroll
content_width = scroll_rect.width - 18
self._content_height = self._measure_content_height(content_width)
self._scroll_panel.set_enabled(self.is_visible)
self._scroll_offset = self._scroll_panel.update(scroll_rect, max(self._content_height, scroll_rect.height))
rl.begin_scissor_mode(int(scroll_rect.x), int(scroll_rect.y), int(scroll_rect.width), int(scroll_rect.height))
self._draw_scroll_content(scroll_rect, content_width)
rl.end_scissor_mode()
if self._content_height > scroll_rect.height:
self._scrollbar.render(scroll_rect, self._content_height, self._scroll_offset)
draw_list_scroll_fades(scroll_rect, self._content_height, self._scroll_offset, rl.Color(8, 8, 10, 255))
@@ -1,151 +0,0 @@
from __future__ import annotations
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog
from openpilot.system.ui.widgets.keyboard import Keyboard
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
class StarPilotNavigationLayout(StarPilotPanel):
def __init__(self):
super().__init__()
self._keyboard = Keyboard(min_text_size=1)
self._sub_panels = {
"mapbox": StarPilotMapboxLayout(),
}
self.CATEGORIES = [
{"title": tr_noop("Mapbox Credentials"), "panel": "mapbox", "icon": "toggle_icons/icon_navigate.png", "color": "#68ACA3"},
{"title": tr_noop("Setup Instructions"), "type": "hub", "on_click": self._on_setup, "icon": "toggle_icons/icon_navigate.png", "color": "#68ACA3"},
{
"title": tr_noop("Speed Limit Filler"),
"type": "toggle",
"get_state": lambda: self._params.get_bool("SpeedLimitFiller"),
"set_state": lambda s: self._params.put_bool("SpeedLimitFiller", s),
"icon": "toggle_icons/icon_speed_limit.png",
"color": "#68ACA3",
},
{"title": tr_noop("Search Destination"), "type": "hub", "on_click": self._on_search, "icon": "toggle_icons/icon_navigate.png", "color": "#68ACA3"},
{
"title": tr_noop("Home Address"),
"type": "value",
"get_value": lambda: self._params.get("HomeAddress", encoding='utf-8') or tr("Not set"),
"on_click": self._on_home,
"icon": "toggle_icons/icon_navigate.png",
"color": "#68ACA3",
},
{
"title": tr_noop("Work Address"),
"type": "value",
"get_value": lambda: self._params.get("WorkAddress", encoding='utf-8') or tr("Not set"),
"on_click": self._on_work,
"icon": "toggle_icons/icon_navigate.png",
"color": "#68ACA3",
},
]
for name, panel in self._sub_panels.items():
if hasattr(panel, 'set_navigate_callback'):
panel.set_navigate_callback(self._navigate_to)
if hasattr(panel, 'set_back_callback'):
panel.set_back_callback(self._go_back)
self._rebuild_grid()
def _on_setup(self):
gui_app.push_widget(
alert_dialog(tr("Mapbox Setup:\n1. Create account at mapbox.com\n2. Generate Public/Secret keys\n3. Add keys in 'Mapbox Credentials'"))
)
def _on_search(self):
def on_close(res, text):
if res == DialogResult.CONFIRM and text:
self._params.put("SearchAddress", text)
self._keyboard.reset(min_text_size=1)
self._keyboard.set_title(tr("Search Destination"), "")
self._keyboard.set_text("")
self._keyboard.set_callback(lambda result: on_close(result, self._keyboard.text))
gui_app.push_widget(self._keyboard)
def _on_home(self):
current = self._params.get("HomeAddress", encoding='utf-8') or ""
def on_close(res, text):
if res == DialogResult.CONFIRM:
self._params.put("HomeAddress", text)
self._rebuild_grid()
self._keyboard.reset(min_text_size=0)
self._keyboard.set_title(tr("Home Address"), "")
self._keyboard.set_text(current)
self._keyboard.set_callback(lambda result: on_close(result, self._keyboard.text))
gui_app.push_widget(self._keyboard)
def _on_work(self):
current = self._params.get("WorkAddress", encoding='utf-8') or ""
def on_close(res, text):
if res == DialogResult.CONFIRM:
self._params.put("WorkAddress", text)
self._rebuild_grid()
self._keyboard.reset(min_text_size=0)
self._keyboard.set_title(tr("Work Address"), "")
self._keyboard.set_text(current)
self._keyboard.set_callback(lambda result: on_close(result, self._keyboard.text))
gui_app.push_widget(self._keyboard)
class StarPilotMapboxLayout(StarPilotPanel):
def __init__(self):
super().__init__()
self._keyboard = Keyboard(min_text_size=1)
self.CATEGORIES = [
{
"title": tr_noop("Public Mapbox Key"),
"type": "value",
"get_value": self._get_key_display,
"on_click": lambda: self._on_key("MapboxPublicKey", "pk."),
"color": "#68ACA3",
},
{
"title": tr_noop("Secret Mapbox Key"),
"type": "value",
"get_value": self._get_secret_display,
"on_click": lambda: self._on_key("MapboxSecretKey", "sk."),
"color": "#68ACA3",
},
]
self._rebuild_grid()
def _get_key_display(self):
v = self._params.get("MapboxPublicKey", encoding='utf-8') or ""
return f"{v[:8]}..." if v else tr("Not set")
def _get_secret_display(self):
v = self._params.get("MapboxSecretKey", encoding='utf-8') or ""
return "********" if v else tr("Not set")
def _on_key(self, key, prefix):
current = self._params.get(key, encoding='utf-8') or ""
if current:
def on_remove(res):
if res == DialogResult.CONFIRM:
self._params.remove(key)
self._rebuild_grid()
gui_app.push_widget(ConfirmDialog(tr(f"Remove your {key.replace('Mapbox', '')} key?"), tr("Remove"), on_close=on_remove))
else:
def on_close(res, text):
if res == DialogResult.CONFIRM and text:
if not text.startswith(prefix):
text = prefix + text
self._params.put(key, text)
self._rebuild_grid()
self._keyboard.reset(min_text_size=1)
self._keyboard.set_title(tr(f"Enter {key.replace('Mapbox', 'Mapbox ')}"), "")
self._keyboard.set_text("")
self._keyboard.set_callback(lambda result: on_close(result, self._keyboard.text))
gui_app.push_widget(self._keyboard)
@@ -19,15 +19,14 @@ class StarPilotPanelType(IntEnum):
LONGITUDINAL = 3
LATERAL = 4
MAPS = 5
NAVIGATION = 6
DATA = 7
DEVICE = 8
UTILITIES = 9
VISUALS = 10
THEMES = 11
VEHICLE = 12
WHEEL = 13
SYSTEM = 14
DATA = 6
DEVICE = 7
UTILITIES = 8
VISUALS = 9
THEMES = 10
VEHICLE = 11
WHEEL = 12
SYSTEM = 13
@dataclass