StarPilot

This commit is contained in:
firestar5683
2026-03-12 01:49:47 -05:00
parent 0e9ef526f7
commit d0e1db6766
2171 changed files with 590688 additions and 247754 deletions
+7 -7
View File
@@ -8,8 +8,10 @@ from openpilot.system.ui.lib.application import gui_app, MousePos, MAX_TOUCH_SLO
try:
from openpilot.selfdrive.ui.ui_state import device
except ImportError:
class Device:
awake = True
device = Device() # type: ignore
@@ -38,8 +40,7 @@ class Widget(abc.ABC):
return self._rect
def set_rect(self, rect: rl.Rectangle) -> None:
changed = (self._rect.x != rect.x or self._rect.y != rect.y or
self._rect.width != rect.width or self._rect.height != rect.height)
changed = self._rect.x != rect.x or self._rect.y != rect.y or self._rect.width != rect.width or self._rect.height != rect.height
self._rect = rect
if changed:
self._update_layout_rects()
@@ -79,7 +80,7 @@ class Widget(abc.ABC):
return self._touch_valid_callback() if self._touch_valid_callback else True
def set_position(self, x: float, y: float) -> None:
changed = (self._rect.x != x or self._rect.y != y)
changed = self._rect.x != x or self._rect.y != y
self._rect = rl.Rectangle(x, y, self._rect.width, self._rect.height)
if changed:
self._update_layout_rects()
@@ -228,6 +229,7 @@ class NavWidget(Widget, abc.ABC):
"""
A full screen widget that supports back navigation by swiping down from the top.
"""
BACK_TOUCH_AREA_PERCENTAGE = 0.65
def __init__(self):
@@ -318,12 +320,10 @@ class NavWidget(Widget, abc.ABC):
self._set_up = True
if hasattr(self, '_scroller'):
original_enabled = self._scroller._enabled
self._scroller.set_enabled(lambda: not self._swiping_away and (original_enabled() if callable(original_enabled) else
original_enabled))
self._scroller.set_enabled(lambda: not self._swiping_away and (original_enabled() if callable(original_enabled) else original_enabled))
elif hasattr(self, '_scroll_panel'):
original_enabled = self._scroll_panel.enabled
self._scroll_panel.set_enabled(lambda: not self._swiping_away and (original_enabled() if callable(original_enabled) else
original_enabled))
self._scroll_panel.set_enabled(lambda: not self._swiping_away and (original_enabled() if callable(original_enabled) else original_enabled))
if self._trigger_animate_in:
self._pos_filter.x = self._rect.height
+8 -1
View File
@@ -1,3 +1,4 @@
from collections.abc import Callable
import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
@@ -17,7 +18,7 @@ BACKGROUND_COLOR = rl.Color(27, 27, 27, 255)
class ConfirmDialog(Widget):
def __init__(self, text: str, confirm_text: str, cancel_text: str | None = None, rich: bool = False):
def __init__(self, text: str, confirm_text: str, cancel_text: str | None = None, rich: bool = False, on_close: Callable[[DialogResult], None] | None = None):
super().__init__()
if cancel_text is None:
cancel_text = tr("Cancel")
@@ -27,6 +28,7 @@ class ConfirmDialog(Widget):
self._confirm_button = Button(confirm_text, self._confirm_button_callback, button_style=ButtonStyle.PRIMARY)
self._rich = rich
self._dialog_result = DialogResult.NO_ACTION
self._on_close = on_close
self._cancel_text = cancel_text
self._scroller = Scroller([self._html_renderer], line_separator=False, spacing=0)
@@ -38,12 +40,15 @@ class ConfirmDialog(Widget):
def reset(self):
self._dialog_result = DialogResult.NO_ACTION
self._on_close = on_close
def _cancel_button_callback(self):
self._dialog_result = DialogResult.CANCEL
if self._on_close: self._on_close(self._dialog_result)
def _confirm_button_callback(self):
self._dialog_result = DialogResult.CONFIRM
if self._on_close: self._on_close(self._dialog_result)
def _render(self, rect: rl.Rectangle):
dialog_x = OUTER_MARGIN if not self._rich else RICH_OUTER_MARGIN
@@ -74,8 +79,10 @@ class ConfirmDialog(Widget):
if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER):
self._dialog_result = DialogResult.CONFIRM
if self._on_close: self._on_close(self._dialog_result)
elif rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
self._dialog_result = DialogResult.CANCEL
if self._on_close: self._on_close(self._dialog_result)
if self._cancel_text:
self._confirm_button.render(confirm_button)
+118
View File
@@ -0,0 +1,118 @@
import pyray as rl
from collections.abc import Callable
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets import Widget, DialogResult
from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.widgets.label import Label, FontWeight
from openpilot.system.ui.widgets.keyboard import Keyboard, KeyboardLayout
MARGIN = 50
BUTTON_HEIGHT = 160
OUTER_MARGIN_X = 200
OUTER_MARGIN_Y = 150
BACKGROUND_COLOR = rl.Color(27, 27, 27, 255)
class InputDialog(Widget):
def __init__(self, title: str, default_text: str = "", hint_text: str = "", on_close: Callable[[DialogResult, str], None] | None = None):
super().__init__()
self._title = title
self._text = default_text
self._hint = hint_text
self._on_close = on_close
self._dialog_result = DialogResult.NO_ACTION
self._title_label = Label(title, 70, FontWeight.BOLD, text_color=rl.Color(201, 201, 201, 255))
self._cancel_button = Button("Cancel", self._cancel_button_callback)
self._confirm_button = Button("Confirm", self._confirm_button_callback, button_style=ButtonStyle.PRIMARY)
self._keyboard = Keyboard(self._on_key_pressed, self._on_keyboard_done, layout=KeyboardLayout.QWERTY)
self._font = gui_app.font(FontWeight.MEDIUM)
def _on_key_pressed(self, key: str):
if key == "\b":
self._text = self._text[:-1]
else:
self._text += key
def _on_keyboard_done(self):
self._confirm_button_callback()
def _cancel_button_callback(self):
self._dialog_result = DialogResult.CANCEL
if self._on_close:
self._on_close(self._dialog_result, self._text)
def _confirm_button_callback(self):
self._dialog_result = DialogResult.CONFIRM
if self._on_close:
self._on_close(self._dialog_result, self._text)
@property
def result(self) -> DialogResult:
return self._dialog_result
@property
def text(self) -> str:
return self._text
def show_event(self):
super().show_event()
self._dialog_result = DialogResult.NO_ACTION
def _render(self, rect: rl.Rectangle):
# Dim background
rl.draw_rectangle(0, 0, int(rect.width), int(rect.height), rl.Color(0, 0, 0, 200))
# Dialog Box
dialog_rect = rl.Rectangle(
rect.x + OUTER_MARGIN_X,
rect.y + OUTER_MARGIN_Y,
rect.width - 2 * OUTER_MARGIN_X,
rect.height - 2 * OUTER_MARGIN_Y,
)
rl.draw_rectangle_rounded(dialog_rect, 0.05, 10, BACKGROUND_COLOR)
# Title
title_rect = rl.Rectangle(dialog_rect.x + MARGIN, dialog_rect.y + MARGIN, dialog_rect.width - 2 * MARGIN, 100)
self._title_label.render(title_rect)
# Text Input Field
input_rect = rl.Rectangle(dialog_rect.x + MARGIN, title_rect.y + title_rect.height + 40, dialog_rect.width - 2 * MARGIN, 120)
rl.draw_rectangle_rounded(input_rect, 0.1, 10, rl.Color(40, 40, 40, 255))
display_text = self._text
text_color = rl.WHITE
if not display_text:
display_text = self._hint
text_color = rl.Color(128, 128, 128, 255)
text_size = rl.measure_text_ex(self._font, display_text, 50, 0)
text_pos = rl.Vector2(input_rect.x + 40, input_rect.y + (input_rect.height - text_size.y) / 2)
rl.draw_text_ex(self._font, display_text, text_pos, 50, 0, text_color)
# Blinking cursor
if (rl.get_time() % 1.0) < 0.5:
cursor_x = text_pos.x + (text_size.x if self._text else 0) + 5
rl.draw_rectangle(int(cursor_x), int(text_pos.y), 4, 50, rl.WHITE)
# Keyboard
keyboard_rect = rl.Rectangle(
dialog_rect.x + MARGIN,
input_rect.y + input_rect.height + 40,
dialog_rect.width - 2 * MARGIN,
400
)
self._keyboard.render(keyboard_rect)
# Buttons
btn_y = dialog_rect.y + dialog_rect.height - BUTTON_HEIGHT - MARGIN
btn_width = (dialog_rect.width - 3 * MARGIN) / 2
cancel_rect = rl.Rectangle(dialog_rect.x + MARGIN, btn_y, btn_width, BUTTON_HEIGHT)
confirm_rect = rl.Rectangle(dialog_rect.x + 2 * MARGIN + btn_width, btn_y, btn_width, BUTTON_HEIGHT)
self._cancel_button.render(cancel_rect)
self._confirm_button.render(confirm_rect)
File diff suppressed because it is too large Load Diff
+317
View File
@@ -0,0 +1,317 @@
from enum import IntEnum
from collections.abc import Callable
import pyray as rl
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget, DialogResult
from openpilot.system.ui.widgets.button import Button, ButtonStyle
from openpilot.system.ui.widgets.label import Label
from openpilot.system.ui.widgets.scroller_tici import Scroller
SELECTION_COLOR = rl.Color(70, 91, 234, 255) # #465BEA
HEADER_BG = rl.Color(51, 51, 51, 255) # #333333
BACKGROUND_COLOR = rl.Color(27, 27, 27, 255) # #1B1B1B
BORDER_COLOR = rl.Color(80, 80, 80, 255)
MARGIN = 40
OUTER_MARGIN_X = 100
OUTER_MARGIN_Y = 80
BUTTON_HEIGHT = 90
class SortMode(IntEnum):
ALPHABETICAL = 0
DATE_NEWEST = 1
DATE_OLDEST = 2
FAVORITES = 3
class SelectionHeader(Widget):
def __init__(self, text: str, is_expanded: bool, callback: Callable[[str], None]):
super().__init__()
self._text = text
self._is_expanded = is_expanded
self._callback = callback
self._font = gui_app.font(FontWeight.BOLD)
self._font_size = 40
self._pressed = False
self.set_rect(rl.Rectangle(0, 0, 0, 70))
def set_parent_rect(self, parent_rect: rl.Rectangle) -> None:
super().set_parent_rect(parent_rect)
self._rect.width = parent_rect.width
def _render(self, rect: rl.Rectangle):
# Header background - Match Qt .series-header {#333333}
bg_color = rl.Color(64, 64, 64, 255) if self._pressed else HEADER_BG
rl.draw_rectangle_rounded(rect, 0.1, 10, bg_color)
# Arrow - Match Qt text-based arrows
arrow = "" if self._is_expanded else ""
arrow_pos = rl.Vector2(rect.x + 30, rect.y + (rect.height - self._font_size) / 2)
rl.draw_text_ex(self._font, arrow, arrow_pos, self._font_size, 0, rl.WHITE)
# Text - Match Qt padding-left: 80px
text_pos = rl.Vector2(rect.x + 80, rect.y + (rect.height - self._font_size) / 2)
rl.draw_text_ex(self._font, self._text, text_pos, self._font_size, 0, rl.WHITE)
def _handle_mouse_press(self, mouse_pos):
if rl.check_collision_point_rec(mouse_pos, self._hit_rect):
self._pressed = True
def _handle_mouse_release(self, mouse_pos):
if self._pressed and rl.check_collision_point_rec(mouse_pos, self._hit_rect):
if self._callback:
self._callback(self._text)
self._pressed = False
class SelectionItem(Widget):
def __init__(self, text: str, is_selected: bool, is_favorite: bool, callback: Callable[[str], None], fav_callback: Callable[[str], None] = None):
super().__init__()
self._text = text
self._is_selected = is_selected
self._is_favorite = is_favorite
self._callback = callback
self._fav_callback = fav_callback
self._font = gui_app.font(FontWeight.MEDIUM)
self._font_size = 48
self._pressed = False
self._fav_pressed = False
self.set_rect(rl.Rectangle(0, 0, 0, 110))
def set_parent_rect(self, parent_rect: rl.Rectangle) -> None:
super().set_parent_rect(parent_rect)
self._rect.width = parent_rect.width
def _render(self, rect: rl.Rectangle):
# Background for item - Match Qt .model-option:checked {#465BEA}
if self._is_selected:
bg_color = rl.Color(70, 91, 234, 255) # #465BEA
else:
bg_color = rl.Color(90, 90, 90, 255) if self._pressed else rl.Color(79, 79, 79, 255) # #4F4F4F
rl.draw_rectangle_rounded(rect, 0.1, 10, bg_color)
# Selection Border - Match Qt {3px WHITE}
if self._is_selected:
rl.draw_rectangle_rounded_lines_ex(rect, 0.1, 10, 3, rl.WHITE)
# Favorite Star - Left side
star = "" if self._is_favorite else ""
star_pos = rl.Vector2(rect.x + 25, rect.y + (rect.height - self._font_size) / 2)
rl.draw_text_ex(self._font, star, star_pos, self._font_size + 10, 0, rl.WHITE)
# Text
text_size = rl.measure_text_ex(self._font, self._text, self._font_size, 0)
text_pos = rl.Vector2(rect.x + 90, rect.y + (rect.height - text_size.y) / 2)
rl.draw_text_ex(self._font, self._text, text_pos, self._font_size, 0, rl.WHITE)
# Indicator (Dot for selection instead of radio)
if self._is_selected:
circle_center = rl.Vector2(rect.x + rect.width - 50, rect.y + rect.height / 2)
rl.draw_circle_v(circle_center, 12, rl.WHITE)
@property
def _fav_rect(self) -> rl.Rectangle:
return rl.Rectangle(self._rect.x, self._rect.y, 80, self._rect.height)
def _handle_mouse_press(self, mouse_pos):
if rl.check_collision_point_rec(mouse_pos, self._fav_rect):
self._fav_pressed = True
elif rl.check_collision_point_rec(mouse_pos, self._hit_rect):
self._pressed = True
def _handle_mouse_release(self, mouse_pos):
if self._fav_pressed and rl.check_collision_point_rec(mouse_pos, self._fav_rect):
if self._fav_callback:
self._fav_callback(self._text)
elif self._pressed and rl.check_collision_point_rec(mouse_pos, self._hit_rect):
if self._callback:
self._callback(self._text)
self._pressed = False
self._fav_pressed = False
def _handle_mouse_press(self, mouse_pos):
if rl.check_collision_point_rec(mouse_pos, self._hit_rect):
self._pressed = True
def _handle_mouse_release(self, mouse_pos):
if self._pressed and rl.check_collision_point_rec(mouse_pos, self._hit_rect):
if self._callback:
self._callback(self._text)
self._pressed = False
class SelectionDialog(Widget):
def __init__(self, title: str, options, current_selection: str = "",
on_close: Callable[[DialogResult, str], None] | None = None,
model_released_dates: dict[str, str] | None = None,
model_file_to_name: dict[str, str] | None = None,
user_favorites: list[str] | None = None,
community_favorites: list[str] | None = None,
on_favorite_toggled: Callable[[str], None] | None = None):
super().__init__()
self._title = title
self._options_raw = options
self._selected_value = current_selection
self._on_close = on_close
self._model_released_dates = model_released_dates or {}
self._name_to_file = {v: k for k, v in (model_file_to_name or {}).items()}
self._user_favorites = user_favorites or []
self._community_favorites = community_favorites or []
self._on_favorite_toggled = on_favorite_toggled
self._sort_mode = SortMode.ALPHABETICAL
self._expanded_series = {s: True for s in (options.keys() if isinstance(options, dict) else [])}
self._title_label = Label(title, 60, FontWeight.BOLD, text_color=rl.WHITE)
self._sort_button = Button("Alphabetical", self._toggle_sort, button_style=ButtonStyle.NORMAL)
self._cancel_button = Button("Cancel", self._cancel_button_callback)
self._confirm_button = Button("Select", self._confirm_button_callback, button_style=ButtonStyle.PRIMARY)
self._scroller = None
self._build_scroller()
def _toggle_sort(self):
self._sort_mode = SortMode((int(self._sort_mode) + 1) % 4)
modes = ["Alphabetical", "Date (Newest)", "Date (Oldest)", "Favorites First"]
self._sort_button.set_text(modes[int(self._sort_mode)])
self._build_scroller()
def _toggle_series(self, series: str):
self._expanded_series[series] = not self._expanded_series.get(series, True)
self._build_scroller()
def _build_scroller(self):
items = []
if isinstance(self._options_raw, dict):
series_keys = list(self._options_raw.keys())
priority_series = ["FrogPilot", "Comma", "Experimental"]
sorted_series_keys = []
for p in priority_series:
if p in series_keys:
sorted_series_keys.append(p)
series_keys.remove(p)
sorted_series_keys.extend(sorted(series_keys))
for series in sorted_series_keys:
models = self._options_raw[series]
if not models:
continue
items.append(SelectionHeader(series, self._expanded_series.get(series, True), self._toggle_series))
if self._expanded_series.get(series, True):
sorted_models = list(models)
if self._sort_mode == SortMode.ALPHABETICAL:
sorted_models.sort()
elif self._sort_mode == SortMode.DATE_NEWEST:
def get_date(m):
key = self._name_to_file.get(m, m)
return self._model_released_dates.get(key, "0000-00-00")
sorted_models.sort(key=get_date, reverse=True)
elif self._sort_mode == SortMode.DATE_OLDEST:
def get_date(m):
key = self._name_to_file.get(m, m)
return self._model_released_dates.get(key, "9999-99-99")
sorted_models.sort(key=get_date)
elif self._sort_mode == SortMode.FAVORITES:
def is_fav(m):
key = self._name_to_file.get(m, m)
return key in self._user_favorites or key in self._community_favorites
sorted_models.sort(key=is_fav, reverse=True)
for model in sorted_models:
key = self._name_to_file.get(model, model)
is_selected = (model == self._selected_value or key == self._selected_value)
is_fav = key in self._user_favorites or key in self._community_favorites
items.append(SelectionItem(
text=model,
is_selected=is_selected,
is_favorite=is_fav,
callback=self._on_item_selected,
fav_callback=self._toggle_favorite
))
else:
for option in self._options_raw:
items.append(SelectionItem(
text=option,
is_selected=(option == self._selected_value),
is_favorite=False,
callback=self._on_item_selected
))
self._scroller = Scroller(items, line_separator=False, spacing=10)
self._scroller.show_event()
def _toggle_favorite(self, model_name: str):
key = self._name_to_file.get(model_name, model_name)
if self._on_favorite_toggled:
self._on_favorite_toggled(key)
# Update local state for instant feedback
if key in self._user_favorites:
self._user_favorites.remove(key)
else:
self._user_favorites.append(key)
self._build_scroller()
def _on_item_selected(self, val):
self._selected_value = val
# Instant visual update
if self._scroller:
for item in self._scroller._items:
if isinstance(item, SelectionItem):
item._is_selected = (item._text == val)
def _cancel_button_callback(self):
if self._on_close:
self._on_close(DialogResult.CANCEL, "")
gui_app.set_modal_overlay(None)
def _confirm_button_callback(self):
if self._on_close:
self._on_close(DialogResult.CONFIRM, self._selected_value)
gui_app.set_modal_overlay(None)
def show_event(self):
super().show_event()
if self._scroller:
self._scroller.show_event()
def _render(self, rect: rl.Rectangle):
# Dim background
rl.draw_rectangle(0, 0, int(rl.get_screen_width()), int(rl.get_screen_height()), rl.Color(0, 0, 0, 180))
# Dialog Box
dialog_rect = rl.Rectangle(
rect.x + OUTER_MARGIN_X,
rect.y + OUTER_MARGIN_Y,
rect.width - 2 * OUTER_MARGIN_X,
rect.height - 2 * OUTER_MARGIN_Y,
)
rl.draw_rectangle_rounded(dialog_rect, 0.04, 12, BACKGROUND_COLOR)
rl.draw_rectangle_rounded_lines_ex(dialog_rect, 0.04, 12, 2, BORDER_COLOR)
# Title
title_width = dialog_rect.width - 2 * MARGIN - 260
self._title_label.render(rl.Rectangle(dialog_rect.x + MARGIN, dialog_rect.y + MARGIN, title_width, 80))
# Sort Button
self._sort_button.render(rl.Rectangle(dialog_rect.x + dialog_rect.width - MARGIN - 240, dialog_rect.y + MARGIN, 240, 80))
# Bottom Buttons
btn_y = dialog_rect.y + dialog_rect.height - BUTTON_HEIGHT - MARGIN
btn_width = (dialog_rect.width - 3 * MARGIN) / 2
self._cancel_button.render(rl.Rectangle(dialog_rect.x + MARGIN, btn_y, btn_width, BUTTON_HEIGHT))
self._confirm_button.render(rl.Rectangle(dialog_rect.x + 2 * MARGIN + btn_width, btn_y, btn_width, BUTTON_HEIGHT))
# Scrollable Options List
scroller_y = dialog_rect.y + MARGIN + 80 + 20
scroller_rect = rl.Rectangle(
dialog_rect.x + MARGIN,
scroller_y,
dialog_rect.width - 2 * MARGIN,
btn_y - scroller_y - 20
)
self._scroller.render(scroller_rect)
return DialogResult.NO_ACTION