mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-10 20:02:13 +08:00
BigUI WIP: Some reusable stuff
This commit is contained in:
@@ -6,6 +6,7 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget, DialogResult
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.asset_loader import starpilot_texture
|
||||
|
||||
|
||||
@@ -60,6 +61,149 @@ _SUBSTRATE_MAP = {
|
||||
_DEFAULT_SUBSTRATE = hex_to_color("#1E0A4D")
|
||||
|
||||
|
||||
def _resolve_value(value, default=""):
|
||||
if callable(value):
|
||||
return value()
|
||||
return value if value is not None else default
|
||||
|
||||
|
||||
def _with_alpha(color: rl.Color, alpha: int) -> rl.Color:
|
||||
return rl.Color(color.r, color.g, color.b, max(0, min(255, int(alpha))))
|
||||
|
||||
|
||||
class AetherListColors:
|
||||
PANEL_BG = rl.Color(8, 8, 10, 255)
|
||||
PANEL_BORDER = rl.Color(255, 255, 255, 22)
|
||||
PANEL_GLOW = rl.Color(92, 116, 151, 34)
|
||||
HEADER = rl.Color(236, 242, 250, 255)
|
||||
SUBTEXT = rl.Color(164, 177, 196, 255)
|
||||
MUTED = rl.Color(126, 139, 158, 255)
|
||||
ROW_BG = rl.Color(255, 255, 255, 0)
|
||||
ROW_BORDER = rl.Color(255, 255, 255, 0)
|
||||
ROW_SEPARATOR = rl.Color(255, 255, 255, 16)
|
||||
ROW_HOVER = rl.Color(255, 255, 255, 8)
|
||||
CURRENT_BG = rl.Color(89, 116, 151, 18)
|
||||
CURRENT_BORDER = rl.Color(116, 136, 168, 44)
|
||||
ACTION_BG = rl.Color(255, 255, 255, 0)
|
||||
ACTION_SEPARATOR = rl.Color(255, 255, 255, 18)
|
||||
PRIMARY = hex_to_color("#597497")
|
||||
PRIMARY_SOFT = rl.Color(89, 116, 151, 48)
|
||||
DANGER = rl.Color(173, 78, 90, 255)
|
||||
DANGER_SOFT = rl.Color(173, 78, 90, 44)
|
||||
SUCCESS = rl.Color(94, 168, 130, 255)
|
||||
SUCCESS_SOFT = rl.Color(94, 168, 130, 44)
|
||||
WARNING = rl.Color(204, 158, 83, 255)
|
||||
SCROLL_TRACK = rl.Color(255, 255, 255, 10)
|
||||
SCROLL_THUMB = rl.Color(255, 255, 255, 68)
|
||||
|
||||
|
||||
class AetherButton(Widget):
|
||||
def __init__(
|
||||
self,
|
||||
text: str | Callable[[], str],
|
||||
click_callback: Callable[[], None] | None = None,
|
||||
enabled: bool | Callable[[], bool] = True,
|
||||
emphasized: bool = False,
|
||||
font_size: int = 24,
|
||||
):
|
||||
super().__init__()
|
||||
self._text = text
|
||||
self._emphasized = emphasized
|
||||
self._font_size = font_size
|
||||
self.set_click_callback(click_callback)
|
||||
self.set_enabled(enabled)
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
return str(_resolve_value(self._text, ""))
|
||||
|
||||
def set_text(self, text: str | Callable[[], str]):
|
||||
self._text = text
|
||||
|
||||
def set_emphasized(self, emphasized: bool):
|
||||
self._emphasized = emphasized
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
enabled = self.enabled
|
||||
hovered = enabled and rl.check_collision_point_rec(gui_app.last_mouse_event.pos, rect)
|
||||
pressed = enabled and self.is_pressed
|
||||
|
||||
if self._emphasized:
|
||||
bg = AetherListColors.PRIMARY if enabled else rl.Color(AetherListColors.PRIMARY.r, AetherListColors.PRIMARY.g, AetherListColors.PRIMARY.b, 80)
|
||||
border = _with_alpha(AetherListColors.PRIMARY, 190 if enabled else 70)
|
||||
else:
|
||||
bg = rl.Color(255, 255, 255, 10 if enabled else 5)
|
||||
border = rl.Color(255, 255, 255, 22 if enabled else 10)
|
||||
|
||||
if hovered:
|
||||
bg = rl.Color(min(bg.r + 10, 255), min(bg.g + 10, 255), min(bg.b + 10, 255), bg.a)
|
||||
if pressed:
|
||||
bg = rl.Color(max(bg.r - 8, 0), max(bg.g - 8, 0), max(bg.b - 8, 0), bg.a)
|
||||
|
||||
rl.draw_rectangle_rounded(rect, 0.32, 16, bg)
|
||||
rl.draw_rectangle_rounded_lines_ex(rect, 0.32, 16, 1, border)
|
||||
gui_label(
|
||||
rect,
|
||||
self.text,
|
||||
self._font_size,
|
||||
AetherListColors.HEADER if enabled else AetherListColors.MUTED,
|
||||
FontWeight.MEDIUM,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
)
|
||||
|
||||
|
||||
class AetherChip:
|
||||
def __init__(self, text: str | Callable[[], str], fill: rl.Color, border: rl.Color, text_color: rl.Color, pill: bool = False, font_size: int = 18):
|
||||
self._text = text
|
||||
self._fill = fill
|
||||
self._border = border
|
||||
self._text_color = text_color
|
||||
self._pill = pill
|
||||
self._font_size = font_size
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
return str(_resolve_value(self._text, ""))
|
||||
|
||||
def render(self, rect: rl.Rectangle):
|
||||
roundness = 1.0 if self._pill else 0.4
|
||||
rl.draw_rectangle_rounded(rect, roundness, 18, self._fill)
|
||||
rl.draw_rectangle_rounded_lines_ex(rect, roundness, 18, 1, _with_alpha(self._border, 110))
|
||||
gui_label(rect, self.text, 20 if self._pill else self._font_size, self._text_color, FontWeight.MEDIUM, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
|
||||
|
||||
class AetherScrollbar:
|
||||
def __init__(
|
||||
self,
|
||||
track_color: rl.Color = AetherListColors.SCROLL_TRACK,
|
||||
thumb_color: rl.Color = AetherListColors.SCROLL_THUMB,
|
||||
track_width: int = 4,
|
||||
track_inset_x: int = 7,
|
||||
track_inset_y: int = 8,
|
||||
min_thumb_height: float = 46.0,
|
||||
):
|
||||
self._track_color = track_color
|
||||
self._thumb_color = thumb_color
|
||||
self._track_width = track_width
|
||||
self._track_inset_x = track_inset_x
|
||||
self._track_inset_y = track_inset_y
|
||||
self._min_thumb_height = min_thumb_height
|
||||
|
||||
def render(self, rect: rl.Rectangle, content_height: float, scroll_offset: float):
|
||||
if content_height <= 0 or content_height <= rect.height:
|
||||
return
|
||||
|
||||
track_rect = rl.Rectangle(rect.x + rect.width - self._track_inset_x, rect.y + self._track_inset_y, self._track_width, rect.height - self._track_inset_y * 2)
|
||||
rl.draw_rectangle_rounded(track_rect, 1.0, 12, self._track_color)
|
||||
|
||||
max_scroll = max(content_height - rect.height, 1.0)
|
||||
thumb_height = max(self._min_thumb_height, track_rect.height * (rect.height / content_height))
|
||||
thumb_range = max(track_rect.height - thumb_height, 0.0)
|
||||
thumb_y = track_rect.y + (-scroll_offset / max_scroll) * thumb_range
|
||||
thumb_rect = rl.Rectangle(track_rect.x, thumb_y, track_rect.width, thumb_height)
|
||||
rl.draw_rectangle_rounded(thumb_rect, 1.0, 12, self._thumb_color)
|
||||
|
||||
|
||||
class AetherTile(Widget):
|
||||
def __init__(self, surface_color: rl.Color | str | None = None, substrate_color: rl.Color | str | None = None, on_click: Callable | None = None):
|
||||
super().__init__()
|
||||
@@ -81,8 +225,10 @@ class AetherTile(Widget):
|
||||
@property
|
||||
def _hit_rect(self) -> rl.Rectangle:
|
||||
return rl.Rectangle(
|
||||
self._rect.x - GEOMETRY_OFFSET, self._rect.y - GEOMETRY_OFFSET,
|
||||
self._rect.width + 2 * GEOMETRY_OFFSET, self._rect.height + 2 * GEOMETRY_OFFSET,
|
||||
self._rect.x - GEOMETRY_OFFSET,
|
||||
self._rect.y - GEOMETRY_OFFSET,
|
||||
self._rect.width + 2 * GEOMETRY_OFFSET,
|
||||
self._rect.height + 2 * GEOMETRY_OFFSET,
|
||||
)
|
||||
|
||||
def _handle_mouse_press(self, mouse_pos: MousePos):
|
||||
@@ -134,7 +280,18 @@ class AetherTile(Widget):
|
||||
|
||||
return surf_rect
|
||||
|
||||
def _draw_text_fit(self, font: rl.Font, text: str, pos: rl.Vector2, max_width: float, font_size: float, align_center: bool = False, align_right: bool = False, letter_spacing: float = 0, uppercase: bool = False):
|
||||
def _draw_text_fit(
|
||||
self,
|
||||
font: rl.Font,
|
||||
text: str,
|
||||
pos: rl.Vector2,
|
||||
max_width: float,
|
||||
font_size: float,
|
||||
align_center: bool = False,
|
||||
align_right: bool = False,
|
||||
letter_spacing: float = 0,
|
||||
uppercase: bool = False,
|
||||
):
|
||||
if uppercase:
|
||||
text = text.upper()
|
||||
spacing = round(letter_spacing if letter_spacing > 0 else font_size * 0.15)
|
||||
@@ -158,7 +315,9 @@ class AetherTile(Widget):
|
||||
rl.draw_text_ex(font, text, shadow_pos, actual_font_size, spacing, rl.Color(0, 0, 0, 90))
|
||||
rl.draw_text_ex(font, text, rl.Vector2(round(draw_x), round(pos.y + nudge_y)), actual_font_size, spacing, rl.WHITE)
|
||||
|
||||
def _centered_content(self, face: rl.Rectangle, icon: rl.Texture2D | None, icon_scale: float, title_font_size: float, text_lines: int, line_heights: list[float]):
|
||||
def _centered_content(
|
||||
self, face: rl.Rectangle, icon: rl.Texture2D | None, icon_scale: float, title_font_size: float, text_lines: int, line_heights: list[float]
|
||||
):
|
||||
line_spacing = SPACING.line_gap
|
||||
total_h = sum(line_heights) + line_spacing * (text_lines - 1)
|
||||
icon_w = icon.width * icon_scale if icon else 0
|
||||
@@ -168,7 +327,9 @@ class AetherTile(Widget):
|
||||
group_top = face.y + (face.height - total_h) / 2
|
||||
if icon:
|
||||
ix = face.x + (face.width - icon_w) / 2
|
||||
rl.draw_texture_pro(icon, rl.Rectangle(0, 0, icon.width, icon.height), rl.Rectangle(ix + 1, group_top + 2, icon_w, icon_h), rl.Vector2(0, 0), 0, rl.Color(0, 0, 0, 90))
|
||||
rl.draw_texture_pro(
|
||||
icon, rl.Rectangle(0, 0, icon.width, icon.height), rl.Rectangle(ix + 1, group_top + 2, icon_w, icon_h), rl.Vector2(0, 0), 0, rl.Color(0, 0, 0, 90)
|
||||
)
|
||||
rl.draw_texture_pro(icon, rl.Rectangle(0, 0, icon.width, icon.height), rl.Rectangle(ix, group_top, icon_w, icon_h), rl.Vector2(0, 0), 0, rl.WHITE)
|
||||
ty = group_top + icon_h + line_spacing
|
||||
else:
|
||||
@@ -199,7 +360,16 @@ class AetherTile(Widget):
|
||||
|
||||
|
||||
class HubTile(AetherTile):
|
||||
def __init__(self, title: str, desc: str, icon_path: str, on_click: Callable | None = None, starpilot_icon: bool = False, bg_color: rl.Color | str | None = None, get_status: Callable[[], str] | None = None):
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
desc: str,
|
||||
icon_path: str,
|
||||
on_click: Callable | None = None,
|
||||
starpilot_icon: bool = False,
|
||||
bg_color: rl.Color | str | None = None,
|
||||
get_status: Callable[[], str] | None = None,
|
||||
):
|
||||
if bg_color:
|
||||
super().__init__(surface_color=bg_color, on_click=on_click)
|
||||
else:
|
||||
@@ -208,24 +378,28 @@ class HubTile(AetherTile):
|
||||
self.title = title
|
||||
self.desc = desc
|
||||
if icon_path:
|
||||
if starpilot_icon: self._icon = starpilot_texture(icon_path, 100, 100)
|
||||
else: self._icon = gui_app.texture(icon_path, 100, 100)
|
||||
else: self._icon = None
|
||||
if starpilot_icon:
|
||||
self._icon = starpilot_texture(icon_path, 100, 100)
|
||||
else:
|
||||
self._icon = gui_app.texture(icon_path, 100, 100)
|
||||
else:
|
||||
self._icon = None
|
||||
self._font_title = gui_app.font(FontWeight.BOLD)
|
||||
self._font_desc = gui_app.font(FontWeight.NORMAL)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
face = self._render_layers(rect)
|
||||
|
||||
|
||||
status_text = self.get_status() if self.get_status else ""
|
||||
if status_text:
|
||||
import re
|
||||
|
||||
m = re.search(r'(\d+)%$', status_text)
|
||||
if m:
|
||||
ratio = min(1.0, max(0.0, float(m.group(1)) / 100.0))
|
||||
if ratio > 0.05:
|
||||
fill_rect = rl.Rectangle(face.x, face.y, face.width * ratio, face.height)
|
||||
rl.draw_rectangle_rounded(fill_rect, TILE_RADIUS, 10, rl.Color(255, 255, 255, 40))
|
||||
fill_rect = rl.Rectangle(face.x, face.y, face.width * ratio, face.height)
|
||||
rl.draw_rectangle_rounded(fill_rect, TILE_RADIUS, 10, rl.Color(255, 255, 255, 40))
|
||||
|
||||
content_pad = SPACING.tile_content
|
||||
max_w = face.width - (content_pad * 2)
|
||||
@@ -246,10 +420,21 @@ class HubTile(AetherTile):
|
||||
|
||||
|
||||
class ToggleTile(AetherTile):
|
||||
def __init__(self, title: str, get_state: Callable[[], bool], set_state: Callable[[bool], None], icon_path: str | None = None,
|
||||
bg_color: rl.Color | str | None = None, desc: str = "", is_enabled: Callable[[], bool] | None = None, disabled_label: str = ""):
|
||||
if bg_color: super().__init__(surface_color=bg_color)
|
||||
else: super().__init__(surface_color=rl.Color(0, 163, 0, 255))
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
get_state: Callable[[], bool],
|
||||
set_state: Callable[[bool], None],
|
||||
icon_path: str | None = None,
|
||||
bg_color: rl.Color | str | None = None,
|
||||
desc: str = "",
|
||||
is_enabled: Callable[[], bool] | None = None,
|
||||
disabled_label: str = "",
|
||||
):
|
||||
if bg_color:
|
||||
super().__init__(surface_color=bg_color)
|
||||
else:
|
||||
super().__init__(surface_color=rl.Color(0, 163, 0, 255))
|
||||
self.title = title
|
||||
self.desc = desc
|
||||
self.get_state = get_state
|
||||
@@ -296,8 +481,16 @@ class ToggleTile(AetherTile):
|
||||
|
||||
|
||||
class ValueTile(AetherTile):
|
||||
def __init__(self, title: str, get_value: Callable[[], str], on_click: Callable, icon_path: str | None = None,
|
||||
bg_color: rl.Color | str | None = None, is_enabled: Callable[[], bool] | None = None, desc: str = ""):
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
get_value: Callable[[], str],
|
||||
on_click: Callable,
|
||||
icon_path: str | None = None,
|
||||
bg_color: rl.Color | str | None = None,
|
||||
is_enabled: Callable[[], bool] | None = None,
|
||||
desc: str = "",
|
||||
):
|
||||
super().__init__(surface_color=bg_color, on_click=on_click)
|
||||
self.title = title
|
||||
self.desc = desc
|
||||
@@ -329,7 +522,17 @@ class ValueTile(AetherTile):
|
||||
|
||||
|
||||
class AetherSlider(Widget):
|
||||
def __init__(self, min_val: float, max_val: float, step: float, current_val: float, on_change: Callable[[float], None], unit: str = "", labels: dict[float, str] | None = None, color: rl.Color = rl.Color(54, 77, 239, 255)):
|
||||
def __init__(
|
||||
self,
|
||||
min_val: float,
|
||||
max_val: float,
|
||||
step: float,
|
||||
current_val: float,
|
||||
on_change: Callable[[float], None],
|
||||
unit: str = "",
|
||||
labels: dict[float, str] | None = None,
|
||||
color: rl.Color = rl.Color(54, 77, 239, 255),
|
||||
):
|
||||
super().__init__()
|
||||
self.min_val, self.max_val, self.step, self.current_val = min_val, max_val, step, current_val
|
||||
self.on_change, self.unit, self.labels, self.color = on_change, unit, labels or {}, color
|
||||
@@ -408,7 +611,9 @@ class AetherSlider(Widget):
|
||||
t_face_rect = rl.Rectangle(thumb_x + thumb_offset, thumb_y + thumb_offset, thumb_w, thumb_h)
|
||||
rl.draw_rectangle_rounded(t_face_rect, 0.2, 10, rl.WHITE)
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(t_face_rect.x + 1, t_face_rect.y + 1, t_face_rect.width - 2, t_face_rect.height - 2), 0.2, 10, rl.Color(0, 0, 0, 80))
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(t_face_rect.x, t_face_rect.y, t_face_rect.width - 1.5, t_face_rect.height - 1.5), 0.2, 10, rl.Color(255, 255, 255, 110))
|
||||
rl.draw_rectangle_rounded(
|
||||
rl.Rectangle(t_face_rect.x, t_face_rect.y, t_face_rect.width - 1.5, t_face_rect.height - 1.5), 0.2, 10, rl.Color(255, 255, 255, 110)
|
||||
)
|
||||
val_str = self.labels.get(self.current_val, f"{self.current_val:.2f}".rstrip('0').rstrip('.') + self.unit)
|
||||
ts = measure_text_cached(self._font, val_str, 35)
|
||||
val_pos = rl.Vector2(thumb_x + (thumb_w - ts.x) / 2, thumb_y - 45)
|
||||
@@ -454,7 +659,9 @@ class AetherSlider(Widget):
|
||||
self.on_change(self.current_val)
|
||||
if self._plus_pressed:
|
||||
self._plus_pressed = False
|
||||
if rl.check_collision_point_rec(mouse_pos, rl.Rectangle(self._rect.x + self._rect.width - SLIDER_BUTTON_SIZE, self._rect.y, SLIDER_BUTTON_SIZE, self._rect.height)):
|
||||
if rl.check_collision_point_rec(
|
||||
mouse_pos, rl.Rectangle(self._rect.x + self._rect.width - SLIDER_BUTTON_SIZE, self._rect.y, SLIDER_BUTTON_SIZE, self._rect.height)
|
||||
):
|
||||
new_val = self._clamp_and_snap(self.current_val + self.step)
|
||||
if new_val != self.current_val:
|
||||
self.current_val = new_val
|
||||
@@ -477,7 +684,18 @@ class AetherSlider(Widget):
|
||||
|
||||
|
||||
class AetherSliderDialog(Widget):
|
||||
def __init__(self, title: str, min_val: float, max_val: float, step: float, current_val: float, on_close: Callable, unit: str = "", labels: dict[float, str] | None = None, color: rl.Color | str = "#F57371"):
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
min_val: float,
|
||||
max_val: float,
|
||||
step: float,
|
||||
current_val: float,
|
||||
on_close: Callable,
|
||||
unit: str = "",
|
||||
labels: dict[float, str] | None = None,
|
||||
color: rl.Color | str = "#F57371",
|
||||
):
|
||||
super().__init__()
|
||||
self.title, self._user_callback = title, on_close
|
||||
self._color = hex_to_color(color) if isinstance(color, str) else color
|
||||
@@ -536,7 +754,12 @@ class AetherSliderDialog(Widget):
|
||||
slider_rect = rl.Rectangle(dx + SPACING.xxl, dy + 200, dialog_w - (SPACING.xxl * 2), 100)
|
||||
self._slider.render(slider_rect)
|
||||
c_shadow_alpha = int(255 * (1.0 - 0.9 * self._cancel_offset))
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(self._cancel_rect.x + GEOMETRY_OFFSET, self._cancel_rect.y + GEOMETRY_OFFSET, button_width, button_height), 0.2, 10, rl.Color(30, 30, 30, c_shadow_alpha))
|
||||
rl.draw_rectangle_rounded(
|
||||
rl.Rectangle(self._cancel_rect.x + GEOMETRY_OFFSET, self._cancel_rect.y + GEOMETRY_OFFSET, button_width, button_height),
|
||||
0.2,
|
||||
10,
|
||||
rl.Color(30, 30, 30, c_shadow_alpha),
|
||||
)
|
||||
c_face_x = self._cancel_rect.x + GEOMETRY_OFFSET * self._cancel_offset
|
||||
c_face_y = self._cancel_rect.y + GEOMETRY_OFFSET * self._cancel_offset
|
||||
c_face = rl.Rectangle(c_face_x, c_face_y, button_width, button_height)
|
||||
@@ -548,7 +771,12 @@ class AetherSliderDialog(Widget):
|
||||
rl.draw_text_ex(self._font_btn, tr("CANCEL"), rl.Vector2(round(cancel_text_pos.x + 1), round(cancel_text_pos.y + 2)), 35, 0, rl.Color(0, 0, 0, 90))
|
||||
rl.draw_text_ex(self._font_btn, tr("CANCEL"), rl.Vector2(round(cancel_text_pos.x), round(cancel_text_pos.y)), 35, 0, rl.WHITE)
|
||||
o_shadow_alpha = int(255 * (1.0 - 0.9 * self._ok_offset))
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(self._ok_rect.x + GEOMETRY_OFFSET, self._ok_rect.y + GEOMETRY_OFFSET, button_width, button_height), 0.2, 10, rl.Color(self._color.r, self._color.g, self._color.b, int(o_shadow_alpha * 0.4)))
|
||||
rl.draw_rectangle_rounded(
|
||||
rl.Rectangle(self._ok_rect.x + GEOMETRY_OFFSET, self._ok_rect.y + GEOMETRY_OFFSET, button_width, button_height),
|
||||
0.2,
|
||||
10,
|
||||
rl.Color(self._color.r, self._color.g, self._color.b, int(o_shadow_alpha * 0.4)),
|
||||
)
|
||||
o_face_x = self._ok_rect.x + GEOMETRY_OFFSET * self._ok_offset
|
||||
o_face_y = self._ok_rect.y + GEOMETRY_OFFSET * self._ok_offset
|
||||
o_face = rl.Rectangle(o_face_x, o_face_y, button_width, button_height)
|
||||
@@ -573,7 +801,8 @@ class RadioTileGroup(Widget):
|
||||
self._option_offsets: list[float] = []
|
||||
self._option_targets: list[float] = []
|
||||
|
||||
def set_index(self, index: int): self.current_index = index
|
||||
def set_index(self, index: int):
|
||||
self.current_index = index
|
||||
|
||||
def _handle_mouse_press(self, mouse_pos: MousePos):
|
||||
for i, r in enumerate(self._option_rects):
|
||||
@@ -621,13 +850,19 @@ class RadioTileGroup(Widget):
|
||||
color = self._active_color if is_active else self._inactive_color
|
||||
offset = self._option_offsets[i] if i < len(self._option_offsets) else 0.0
|
||||
extrusion_alpha = int(255 * (1.0 - 0.9 * offset))
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(r.x + GEOMETRY_OFFSET, r.y + GEOMETRY_OFFSET, r.width, r.height), TILE_RADIUS, 10, rl.Color(30, 30, 30, extrusion_alpha))
|
||||
rl.draw_rectangle_rounded(
|
||||
rl.Rectangle(r.x + GEOMETRY_OFFSET, r.y + GEOMETRY_OFFSET, r.width, r.height), TILE_RADIUS, 10, rl.Color(30, 30, 30, extrusion_alpha)
|
||||
)
|
||||
face_x = r.x + GEOMETRY_OFFSET * offset
|
||||
face_y = r.y + GEOMETRY_OFFSET * offset
|
||||
face_rect = rl.Rectangle(face_x, face_y, r.width, r.height)
|
||||
rl.draw_rectangle_rounded(face_rect, TILE_RADIUS, 10, color)
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(face_rect.x + 1, face_rect.y + 1, face_rect.width - 2, face_rect.height - 2), TILE_RADIUS, 10, rl.Color(0, 0, 0, 80))
|
||||
rl.draw_rectangle_rounded(rl.Rectangle(face_rect.x, face_rect.y, face_rect.width - 1.5, face_rect.height - 1.5), TILE_RADIUS, 10, rl.Color(255, 255, 255, 110))
|
||||
rl.draw_rectangle_rounded(
|
||||
rl.Rectangle(face_rect.x + 1, face_rect.y + 1, face_rect.width - 2, face_rect.height - 2), TILE_RADIUS, 10, rl.Color(0, 0, 0, 80)
|
||||
)
|
||||
rl.draw_rectangle_rounded(
|
||||
rl.Rectangle(face_rect.x, face_rect.y, face_rect.width - 1.5, face_rect.height - 1.5), TILE_RADIUS, 10, rl.Color(255, 255, 255, 110)
|
||||
)
|
||||
font_size = 30
|
||||
spacing = round(font_size * 0.08)
|
||||
max_width = r.width - (SPACING.lg + SPACING.xs)
|
||||
@@ -653,9 +888,11 @@ class TileGrid(Widget):
|
||||
def gap(self) -> int:
|
||||
return self._gap
|
||||
|
||||
def add_tile(self, tile: Widget): self.tiles.append(tile)
|
||||
def add_tile(self, tile: Widget):
|
||||
self.tiles.append(tile)
|
||||
|
||||
def clear(self): self.tiles.clear()
|
||||
def clear(self):
|
||||
self.tiles.clear()
|
||||
|
||||
def get_column_count(self, tile_count: int | None = None) -> int:
|
||||
count = len(self.tiles) if tile_count is None else tile_count
|
||||
@@ -663,11 +900,16 @@ class TileGrid(Widget):
|
||||
return self._columns or 1
|
||||
if self._columns is not None:
|
||||
return self._columns
|
||||
if count == 1: return 1
|
||||
if count == 2: return 2
|
||||
if count == 3: return 3
|
||||
if count == 4: return 2
|
||||
if count <= 6: return 3
|
||||
if count == 1:
|
||||
return 1
|
||||
if count == 2:
|
||||
return 2
|
||||
if count == 3:
|
||||
return 3
|
||||
if count == 4:
|
||||
return 2
|
||||
if count <= 6:
|
||||
return 3
|
||||
return 4
|
||||
|
||||
def get_row_count(self, tile_count: int | None = None) -> int:
|
||||
@@ -694,7 +936,8 @@ class TileGrid(Widget):
|
||||
tile_idx = 0
|
||||
for r in range(rows):
|
||||
remaining = count - tile_idx
|
||||
if remaining <= 0: break
|
||||
if remaining <= 0:
|
||||
break
|
||||
items_in_row = min(cols, remaining)
|
||||
if self._uniform_width:
|
||||
row_tile_w = uniform_tile_w
|
||||
|
||||
@@ -31,32 +31,32 @@ from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dial
|
||||
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.panel import StarPilotPanel
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import AetherSliderDialog, hex_to_color
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import AetherButton, AetherChip, AetherListColors, AetherScrollbar, AetherSliderDialog
|
||||
|
||||
|
||||
MODEL_PANEL_BG = rl.Color(8, 8, 10, 255)
|
||||
MODEL_PANEL_BORDER = rl.Color(255, 255, 255, 22)
|
||||
MODEL_PANEL_GLOW = rl.Color(92, 116, 151, 34)
|
||||
MODEL_HEADER_TEXT = rl.Color(236, 242, 250, 255)
|
||||
MODEL_SUBTEXT = rl.Color(164, 177, 196, 255)
|
||||
MODEL_MUTED = rl.Color(126, 139, 158, 255)
|
||||
MODEL_ROW_BG = rl.Color(255, 255, 255, 0)
|
||||
MODEL_ROW_BORDER = rl.Color(255, 255, 255, 0)
|
||||
MODEL_ROW_SEPARATOR = rl.Color(255, 255, 255, 16)
|
||||
MODEL_ROW_HOVER = rl.Color(255, 255, 255, 8)
|
||||
MODEL_CURRENT_BG = rl.Color(89, 116, 151, 18)
|
||||
MODEL_CURRENT_BORDER = rl.Color(116, 136, 168, 44)
|
||||
MODEL_ACTION_BG = rl.Color(255, 255, 255, 0)
|
||||
MODEL_ACTION_SEPARATOR = rl.Color(255, 255, 255, 18)
|
||||
MODEL_PRIMARY = hex_to_color("#597497")
|
||||
MODEL_PRIMARY_SOFT = rl.Color(89, 116, 151, 48)
|
||||
MODEL_DANGER = rl.Color(173, 78, 90, 255)
|
||||
MODEL_DANGER_SOFT = rl.Color(173, 78, 90, 44)
|
||||
MODEL_SUCCESS = rl.Color(94, 168, 130, 255)
|
||||
MODEL_SUCCESS_SOFT = rl.Color(94, 168, 130, 44)
|
||||
MODEL_WARNING = rl.Color(204, 158, 83, 255)
|
||||
MODEL_SCROLL_TRACK = rl.Color(255, 255, 255, 10)
|
||||
MODEL_SCROLL_THUMB = rl.Color(255, 255, 255, 68)
|
||||
MODEL_PANEL_BG = AetherListColors.PANEL_BG
|
||||
MODEL_PANEL_BORDER = AetherListColors.PANEL_BORDER
|
||||
MODEL_PANEL_GLOW = AetherListColors.PANEL_GLOW
|
||||
MODEL_HEADER_TEXT = AetherListColors.HEADER
|
||||
MODEL_SUBTEXT = AetherListColors.SUBTEXT
|
||||
MODEL_MUTED = AetherListColors.MUTED
|
||||
MODEL_ROW_BG = AetherListColors.ROW_BG
|
||||
MODEL_ROW_BORDER = AetherListColors.ROW_BORDER
|
||||
MODEL_ROW_SEPARATOR = AetherListColors.ROW_SEPARATOR
|
||||
MODEL_ROW_HOVER = AetherListColors.ROW_HOVER
|
||||
MODEL_CURRENT_BG = AetherListColors.CURRENT_BG
|
||||
MODEL_CURRENT_BORDER = AetherListColors.CURRENT_BORDER
|
||||
MODEL_ACTION_BG = AetherListColors.ACTION_BG
|
||||
MODEL_ACTION_SEPARATOR = AetherListColors.ACTION_SEPARATOR
|
||||
MODEL_PRIMARY = AetherListColors.PRIMARY
|
||||
MODEL_PRIMARY_SOFT = AetherListColors.PRIMARY_SOFT
|
||||
MODEL_DANGER = AetherListColors.DANGER
|
||||
MODEL_DANGER_SOFT = AetherListColors.DANGER_SOFT
|
||||
MODEL_SUCCESS = AetherListColors.SUCCESS
|
||||
MODEL_SUCCESS_SOFT = AetherListColors.SUCCESS_SOFT
|
||||
MODEL_WARNING = AetherListColors.WARNING
|
||||
MODEL_SCROLL_TRACK = AetherListColors.SCROLL_TRACK
|
||||
MODEL_SCROLL_THUMB = AetherListColors.SCROLL_THUMB
|
||||
|
||||
MAX_CONTENT_WIDTH = 1560
|
||||
OUTER_MARGIN_X = 18
|
||||
@@ -113,6 +113,7 @@ class DrivingModelManagerView(Widget):
|
||||
super().__init__()
|
||||
self._controller = controller
|
||||
self._scroll_panel = GuiScrollPanel2(horizontal=False)
|
||||
self._scrollbar = AetherScrollbar()
|
||||
self._content_height = 0.0
|
||||
self._scroll_offset = 0.0
|
||||
self._pressed_target: str | None = None
|
||||
@@ -120,7 +121,6 @@ class DrivingModelManagerView(Widget):
|
||||
self._row_rects: dict[str, rl.Rectangle] = {}
|
||||
self._action_rects: dict[str, rl.Rectangle] = {}
|
||||
self._utility_rects: dict[str, rl.Rectangle] = {}
|
||||
self._header_rects: dict[str, rl.Rectangle] = {}
|
||||
self._confirm_key: str | None = None
|
||||
self._confirm_until = 0.0
|
||||
self._confirm_mix: dict[str, float] = {}
|
||||
@@ -130,6 +130,24 @@ class DrivingModelManagerView(Widget):
|
||||
self._shell_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self._scroll_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self._metric_font = gui_app.font(FontWeight.BOLD)
|
||||
self._primary_header_button = self._child(
|
||||
AetherButton(
|
||||
lambda: self._controller.primary_header_button_state()[0],
|
||||
lambda: self._controller.cancel_active_download() if self._controller._is_download_active() else self._controller.download_all_missing(),
|
||||
enabled=lambda: self._controller.primary_header_button_state()[1],
|
||||
emphasized=True,
|
||||
)
|
||||
)
|
||||
self._secondary_header_button = self._child(
|
||||
AetherButton(
|
||||
lambda: self._controller.secondary_header_button_state()[0],
|
||||
self._controller.refresh_manifest,
|
||||
enabled=lambda: self._controller.secondary_header_button_state()[1],
|
||||
emphasized=False,
|
||||
)
|
||||
)
|
||||
self._primary_header_button.set_touch_valid_callback(lambda: self._scroll_panel.is_touch_valid())
|
||||
self._secondary_header_button.set_touch_valid_callback(lambda: self._scroll_panel.is_touch_valid())
|
||||
|
||||
def _clear_ephemeral_state(self):
|
||||
self._pressed_target = None
|
||||
@@ -203,10 +221,6 @@ class DrivingModelManagerView(Widget):
|
||||
self._pressed_target = None
|
||||
|
||||
def _target_at(self, mouse_pos: MousePos) -> str | None:
|
||||
for name, rect in self._header_rects.items():
|
||||
if rl.check_collision_point_rec(mouse_pos, rect):
|
||||
return name
|
||||
|
||||
for key, rect in self._action_rects.items():
|
||||
visible_rect = rl.get_collision_rec(rect, self._scroll_rect)
|
||||
if visible_rect.width > 0 and visible_rect.height > 0 and rl.check_collision_point_rec(mouse_pos, visible_rect):
|
||||
@@ -228,17 +242,6 @@ class DrivingModelManagerView(Widget):
|
||||
if not target:
|
||||
return
|
||||
|
||||
if target == "header:primary":
|
||||
if self._controller._is_download_active():
|
||||
self._controller.cancel_active_download()
|
||||
else:
|
||||
self._controller.download_all_missing()
|
||||
return
|
||||
|
||||
if target == "header:secondary":
|
||||
self._controller.refresh_manifest()
|
||||
return
|
||||
|
||||
if target.startswith("row:"):
|
||||
self._confirm_key = None
|
||||
model_key = target.split(":", 1)[1]
|
||||
@@ -284,7 +287,6 @@ class DrivingModelManagerView(Widget):
|
||||
self._row_rects.clear()
|
||||
self._action_rects.clear()
|
||||
self._utility_rects.clear()
|
||||
self._header_rects.clear()
|
||||
|
||||
shell_w = min(rect.width - OUTER_MARGIN_X * 2, MAX_CONTENT_WIDTH)
|
||||
shell_x = rect.x + (rect.width - shell_w) / 2
|
||||
@@ -321,12 +323,17 @@ class DrivingModelManagerView(Widget):
|
||||
self._draw_scrollbar(scroll_rect)
|
||||
|
||||
if self._content_height > scroll_rect.height + 4:
|
||||
top_fade = min(FADE_HEIGHT, int(scroll_rect.height / 4))
|
||||
bottom_y = int(scroll_rect.y + scroll_rect.height - top_fade)
|
||||
rl.draw_rectangle_gradient_v(int(scroll_rect.x), int(scroll_rect.y), int(scroll_rect.width - 12), top_fade,
|
||||
_with_alpha(MODEL_PANEL_BG, 255), _with_alpha(MODEL_PANEL_BG, 0))
|
||||
rl.draw_rectangle_gradient_v(int(scroll_rect.x), bottom_y, int(scroll_rect.width - 12), top_fade,
|
||||
_with_alpha(MODEL_PANEL_BG, 0), _with_alpha(MODEL_PANEL_BG, 255))
|
||||
fade_height = min(FADE_HEIGHT, int(scroll_rect.height / 4))
|
||||
if self._scroll_offset < -4:
|
||||
rl.draw_rectangle_gradient_v(
|
||||
int(scroll_rect.x), int(scroll_rect.y), int(scroll_rect.width - 12), fade_height, _with_alpha(MODEL_PANEL_BG, 255), _with_alpha(MODEL_PANEL_BG, 0)
|
||||
)
|
||||
|
||||
if (-self._scroll_offset + scroll_rect.height) < (self._content_height - 4):
|
||||
bottom_y = int(scroll_rect.y + scroll_rect.height - fade_height)
|
||||
rl.draw_rectangle_gradient_v(
|
||||
int(scroll_rect.x), bottom_y, int(scroll_rect.width - 12), fade_height, _with_alpha(MODEL_PANEL_BG, 0), _with_alpha(MODEL_PANEL_BG, 255)
|
||||
)
|
||||
|
||||
def _draw_header(self, rect: rl.Rectangle):
|
||||
title_rect = rl.Rectangle(rect.x, rect.y + 4, rect.width * 0.55, 40)
|
||||
@@ -348,14 +355,10 @@ class DrivingModelManagerView(Widget):
|
||||
stack_y = rect.y + 24
|
||||
right_rect = rl.Rectangle(rect.x + rect.width - right_panel_w, stack_y, right_panel_w, stack_height)
|
||||
|
||||
primary_label, primary_enabled = self._controller.primary_header_button_state()
|
||||
secondary_label, secondary_enabled = self._controller.secondary_header_button_state()
|
||||
primary_rect = rl.Rectangle(right_rect.x, right_rect.y, right_rect.width, BUTTON_HEIGHT)
|
||||
secondary_rect = rl.Rectangle(right_rect.x, primary_rect.y + BUTTON_HEIGHT + BUTTON_GAP, right_rect.width, BUTTON_HEIGHT)
|
||||
self._header_rects["header:primary"] = primary_rect if primary_enabled else rl.Rectangle(-1, -1, 0, 0)
|
||||
self._header_rects["header:secondary"] = secondary_rect if secondary_enabled else rl.Rectangle(-1, -1, 0, 0)
|
||||
self._draw_button(primary_rect, primary_label, primary_enabled, emphasized=True)
|
||||
self._draw_button(secondary_rect, secondary_label, secondary_enabled, emphasized=False)
|
||||
self._primary_header_button.render(primary_rect)
|
||||
self._secondary_header_button.render(secondary_rect)
|
||||
|
||||
def _measure_content_height(self, width: float) -> float:
|
||||
sections = self._build_sections(width)
|
||||
@@ -406,10 +409,22 @@ class DrivingModelManagerView(Widget):
|
||||
state_rect = rl.Rectangle(rect.x, rect.y, rect.width - 18, rect.height)
|
||||
rl.draw_rectangle_rounded(state_rect, 0.08, 18, rl.Color(255, 255, 255, 5))
|
||||
rl.draw_rectangle_rounded_lines_ex(state_rect, 0.08, 18, 1, rl.Color(255, 255, 255, 14))
|
||||
gui_label(rl.Rectangle(state_rect.x, state_rect.y + 42, state_rect.width, 40), self._controller.empty_state_title(), 32, MODEL_HEADER_TEXT, FontWeight.MEDIUM,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
gui_label(rl.Rectangle(state_rect.x + 48, state_rect.y + 88, state_rect.width - 96, 72), self._controller.empty_state_body(), 24, MODEL_SUBTEXT, FontWeight.NORMAL,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
gui_label(
|
||||
rl.Rectangle(state_rect.x, state_rect.y + 42, state_rect.width, 40),
|
||||
self._controller.empty_state_title(),
|
||||
32,
|
||||
MODEL_HEADER_TEXT,
|
||||
FontWeight.MEDIUM,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
)
|
||||
gui_label(
|
||||
rl.Rectangle(state_rect.x + 48, state_rect.y + 88, state_rect.width - 96, 72),
|
||||
self._controller.empty_state_body(),
|
||||
24,
|
||||
MODEL_SUBTEXT,
|
||||
FontWeight.NORMAL,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
)
|
||||
|
||||
def _draw_model_section(self, x: float, y: float, width: float, title: str, entries: list[ModelCatalogEntry]) -> float:
|
||||
title_rect = rl.Rectangle(x, y, width - 18, SECTION_HEADER_HEIGHT)
|
||||
@@ -439,7 +454,9 @@ class DrivingModelManagerView(Widget):
|
||||
row_bg = rl.Color(row_bg.r, row_bg.g, row_bg.b, min(row_bg.a + 8, 255))
|
||||
|
||||
alpha, offset_y, scale = self._row_transition_style(entry.key)
|
||||
draw_rect = rl.Rectangle(rect.x + (rect.width * (1 - scale) / 2), rect.y + offset_y + (rect.height * (1 - scale) / 2), rect.width * scale, rect.height * scale)
|
||||
draw_rect = rl.Rectangle(
|
||||
rect.x + (rect.width * (1 - scale) / 2), rect.y + offset_y + (rect.height * (1 - scale) / 2), rect.width * scale, rect.height * scale
|
||||
)
|
||||
|
||||
if row_bg.a > 0:
|
||||
rl.draw_rectangle_rounded(draw_rect, ROW_RADIUS, 18, _with_alpha(row_bg, alpha))
|
||||
@@ -507,8 +524,14 @@ class DrivingModelManagerView(Widget):
|
||||
center_x = rect.x + rect.width / 2
|
||||
center_y = rect.y + rect.height / 2 - 8
|
||||
self._draw_download_icon(rl.Vector2(center_x, center_y), MODEL_HEADER_TEXT)
|
||||
gui_label(rl.Rectangle(rect.x + 16, rect.y + rect.height - 40, rect.width - 32, 22), tr("Download"), 18, MODEL_SUBTEXT, FontWeight.MEDIUM,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
gui_label(
|
||||
rl.Rectangle(rect.x + 16, rect.y + rect.height - 40, rect.width - 32, 22),
|
||||
tr("Download"),
|
||||
18,
|
||||
MODEL_SUBTEXT,
|
||||
FontWeight.MEDIUM,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
)
|
||||
|
||||
def _draw_downloading_action(self, rect: rl.Rectangle, progress_text: str):
|
||||
center = rl.Vector2(rect.x + rect.width / 2, rect.y + rect.height / 2 - 8)
|
||||
@@ -517,8 +540,14 @@ class DrivingModelManagerView(Widget):
|
||||
rl.draw_ring(center, 20, 26, phase, phase + 260, 48, MODEL_PRIMARY)
|
||||
|
||||
label = progress_text if progress_text else tr("Downloading")
|
||||
gui_label(rl.Rectangle(rect.x + 16, rect.y + rect.height - 40, rect.width - 32, 22), label, 17, MODEL_SUBTEXT, FontWeight.MEDIUM,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
gui_label(
|
||||
rl.Rectangle(rect.x + 16, rect.y + rect.height - 40, rect.width - 32, 22),
|
||||
label,
|
||||
17,
|
||||
MODEL_SUBTEXT,
|
||||
FontWeight.MEDIUM,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
)
|
||||
|
||||
def _draw_ready_action(self, rect: rl.Rectangle, confirm_mix: float):
|
||||
confirm_alpha = int(255 * confirm_mix)
|
||||
@@ -535,18 +564,30 @@ class DrivingModelManagerView(Widget):
|
||||
|
||||
ready_label = _with_alpha(MODEL_SUBTEXT, ready_alpha)
|
||||
confirm_label = _with_alpha(MODEL_DANGER, confirm_alpha)
|
||||
gui_label(rl.Rectangle(rect.x + 16, rect.y + rect.height - 40, rect.width - 32, 18), tr("Ready"), 18, ready_label, FontWeight.MEDIUM,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
gui_label(rl.Rectangle(rect.x + 16, rect.y + rect.height - 40, rect.width - 32, 18), tr("Remove"), 18, confirm_label, FontWeight.SEMI_BOLD,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
gui_label(
|
||||
rl.Rectangle(rect.x + 16, rect.y + rect.height - 40, rect.width - 32, 18),
|
||||
tr("Ready"),
|
||||
18,
|
||||
ready_label,
|
||||
FontWeight.MEDIUM,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
)
|
||||
gui_label(
|
||||
rl.Rectangle(rect.x + 16, rect.y + rect.height - 40, rect.width - 32, 18),
|
||||
tr("Remove"),
|
||||
18,
|
||||
confirm_label,
|
||||
FontWeight.SEMI_BOLD,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
)
|
||||
|
||||
def _draw_current_action(self, rect: rl.Rectangle):
|
||||
chip_rect = rl.Rectangle(rect.x + 24, rect.y + (rect.height - 42) / 2, rect.width - 48, 42)
|
||||
self._draw_chip(chip_rect, tr("Current"), rl.Color(89, 116, 151, 26), rl.Color(116, 136, 168, 52), MODEL_HEADER_TEXT)
|
||||
AetherChip(tr("Current"), rl.Color(89, 116, 151, 26), rl.Color(116, 136, 168, 52), MODEL_HEADER_TEXT, font_size=18).render(chip_rect)
|
||||
|
||||
def _draw_protected_action(self, rect: rl.Rectangle):
|
||||
chip_rect = rl.Rectangle(rect.x + 20, rect.y + (rect.height - 42) / 2, rect.width - 40, 42)
|
||||
self._draw_chip(chip_rect, tr("Protected"), rl.Color(255, 255, 255, 10), MODEL_MUTED, MODEL_SUBTEXT)
|
||||
AetherChip(tr("Protected"), rl.Color(255, 255, 255, 10), MODEL_MUTED, MODEL_SUBTEXT, font_size=18).render(chip_rect)
|
||||
|
||||
def _draw_trash_icon(self, center: rl.Vector2, color: rl.Color):
|
||||
bin_rect = rl.Rectangle(center.x - 12, center.y - 12, 24, 24)
|
||||
@@ -613,8 +654,9 @@ class DrivingModelManagerView(Widget):
|
||||
else:
|
||||
value_rect = rl.Rectangle(rect.x + rect.width - 270, rect.y + 20, 220, 28)
|
||||
gui_label(value_rect, row["value"], 24, MODEL_HEADER_TEXT, FontWeight.MEDIUM, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||
gui_label(rl.Rectangle(rect.x + rect.width - 62, rect.y + 18, 26, 26), "›", 32, MODEL_MUTED, FontWeight.MEDIUM,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
gui_label(
|
||||
rl.Rectangle(rect.x + rect.width - 62, rect.y + 18, 26, 26), "›", 32, MODEL_MUTED, FontWeight.MEDIUM, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER
|
||||
)
|
||||
|
||||
def _draw_toggle(self, rect: rl.Rectangle, enabled: bool):
|
||||
toggle_w = 78
|
||||
@@ -625,42 +667,8 @@ class DrivingModelManagerView(Widget):
|
||||
rl.draw_rectangle_rounded(toggle_rect, 1.0, 16, track)
|
||||
rl.draw_circle(int(knob_x), int(toggle_rect.y + toggle_rect.height / 2), 16, rl.WHITE)
|
||||
|
||||
def _draw_button(self, rect: rl.Rectangle, label: str, enabled: bool, emphasized: bool):
|
||||
hovered = enabled and rl.check_collision_point_rec(gui_app.last_mouse_event.pos, rect)
|
||||
pressed = enabled and self._pressed_target == ("header:primary" if emphasized else "header:secondary")
|
||||
if emphasized:
|
||||
bg = MODEL_PRIMARY if enabled else rl.Color(MODEL_PRIMARY.r, MODEL_PRIMARY.g, MODEL_PRIMARY.b, 80)
|
||||
border = _with_alpha(MODEL_PRIMARY, 190 if enabled else 70)
|
||||
else:
|
||||
bg = rl.Color(255, 255, 255, 10 if enabled else 5)
|
||||
border = rl.Color(255, 255, 255, 22 if enabled else 10)
|
||||
|
||||
if hovered:
|
||||
bg = rl.Color(min(bg.r + 10, 255), min(bg.g + 10, 255), min(bg.b + 10, 255), bg.a)
|
||||
if pressed:
|
||||
bg = rl.Color(max(bg.r - 8, 0), max(bg.g - 8, 0), max(bg.b - 8, 0), bg.a)
|
||||
|
||||
rl.draw_rectangle_rounded(rect, 0.32, 16, bg)
|
||||
rl.draw_rectangle_rounded_lines_ex(rect, 0.32, 16, 1, border)
|
||||
label_color = MODEL_HEADER_TEXT if enabled else MODEL_MUTED
|
||||
gui_label(rect, label, 24, label_color, FontWeight.MEDIUM, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
|
||||
def _draw_chip(self, rect: rl.Rectangle, label: str, fill: rl.Color, border: rl.Color, text: rl.Color, pill: bool = False):
|
||||
roundness = 1.0 if pill else 0.4
|
||||
rl.draw_rectangle_rounded(rect, roundness, 18, fill)
|
||||
rl.draw_rectangle_rounded_lines_ex(rect, roundness, 18, 1, _with_alpha(border, 110))
|
||||
gui_label(rect, label, 20 if pill else 18, text, FontWeight.MEDIUM, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
|
||||
|
||||
def _draw_scrollbar(self, rect: rl.Rectangle):
|
||||
track_rect = rl.Rectangle(rect.x + rect.width - 7, rect.y + 8, 4, rect.height - 16)
|
||||
rl.draw_rectangle_rounded(track_rect, 1.0, 12, MODEL_SCROLL_TRACK)
|
||||
|
||||
max_scroll = max(self._content_height - rect.height, 1.0)
|
||||
thumb_height = max(46.0, track_rect.height * (rect.height / self._content_height))
|
||||
thumb_range = max(track_rect.height - thumb_height, 0.0)
|
||||
thumb_y = track_rect.y + (-self._scroll_offset / max_scroll) * thumb_range
|
||||
thumb_rect = rl.Rectangle(track_rect.x, thumb_y, track_rect.width, thumb_height)
|
||||
rl.draw_rectangle_rounded(thumb_rect, 1.0, 12, MODEL_SCROLL_THUMB)
|
||||
self._scrollbar.render(rect, self._content_height, self._scroll_offset)
|
||||
|
||||
def _row_transition_style(self, key: str) -> tuple[int, float, float]:
|
||||
if key not in self._transition_starts:
|
||||
@@ -789,10 +797,12 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
|
||||
]
|
||||
|
||||
if version == "v12":
|
||||
files.extend([
|
||||
f"{key}_driving_off_policy_tinygrad.pkl",
|
||||
f"{key}_driving_off_policy_metadata.pkl",
|
||||
])
|
||||
files.extend(
|
||||
[
|
||||
f"{key}_driving_off_policy_tinygrad.pkl",
|
||||
f"{key}_driving_off_policy_metadata.pkl",
|
||||
]
|
||||
)
|
||||
|
||||
return files
|
||||
|
||||
@@ -841,8 +851,12 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
|
||||
available_versions = [entry.strip() for entry in (self._params.get("ModelVersions", encoding="utf-8") or "").split(",")]
|
||||
released_dates = [entry.strip() for entry in (self._params.get("ModelReleasedDates", encoding="utf-8") or "").split(",")]
|
||||
|
||||
self._community_favorites = {canonical_model_key(entry.strip()) for entry in (self._params.get("CommunityFavorites", encoding="utf-8") or "").split(",") if entry.strip()}
|
||||
self._user_favorites = {canonical_model_key(entry.strip()) for entry in (self._params.get("UserFavorites", encoding="utf-8") or "").split(",") if entry.strip()}
|
||||
self._community_favorites = {
|
||||
canonical_model_key(entry.strip()) for entry in (self._params.get("CommunityFavorites", encoding="utf-8") or "").split(",") if entry.strip()
|
||||
}
|
||||
self._user_favorites = {
|
||||
canonical_model_key(entry.strip()) for entry in (self._params.get("UserFavorites", encoding="utf-8") or "").split(",") if entry.strip()
|
||||
}
|
||||
|
||||
size = min(len(available_models), len(available_names))
|
||||
for i in range(size):
|
||||
@@ -941,23 +955,29 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
|
||||
|
||||
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,
|
||||
))
|
||||
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,
|
||||
),
|
||||
)
|
||||
|
||||
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,
|
||||
))
|
||||
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,
|
||||
),
|
||||
)
|
||||
|
||||
def is_current_model(self, model_key: str) -> bool:
|
||||
return canonical_model_key(model_key) == self._current_model_key
|
||||
@@ -1039,40 +1059,44 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
|
||||
|
||||
if self._params.get_bool("ModelRandomizer"):
|
||||
blacklist_count = len([m.strip() for m in (self._params.get("BlacklistedModels", encoding="utf-8") or "").split(",") if m.strip()])
|
||||
rows.extend([
|
||||
{
|
||||
"id": "blacklist",
|
||||
"title": tr("Blacklist"),
|
||||
"subtitle": tr("Keep specific installed models out of the rotation."),
|
||||
"type": "value",
|
||||
"value": tr(f"{blacklist_count} blocked" if blacklist_count else "Manage"),
|
||||
},
|
||||
{
|
||||
"id": "ratings",
|
||||
"title": tr("Ratings"),
|
||||
"subtitle": tr("Review recorded drives and model score history."),
|
||||
"type": "value",
|
||||
"value": tr("View"),
|
||||
},
|
||||
])
|
||||
rows.extend(
|
||||
[
|
||||
{
|
||||
"id": "blacklist",
|
||||
"title": tr("Blacklist"),
|
||||
"subtitle": tr("Keep specific installed models out of the rotation."),
|
||||
"type": "value",
|
||||
"value": tr(f"{blacklist_count} blocked" if blacklist_count else "Manage"),
|
||||
},
|
||||
{
|
||||
"id": "ratings",
|
||||
"title": tr("Ratings"),
|
||||
"subtitle": tr("Review recorded drives and model score history."),
|
||||
"type": "value",
|
||||
"value": tr("View"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
if self._params.get_int("TuningLevel") == 3:
|
||||
rows.extend([
|
||||
{
|
||||
"id": "recovery_power",
|
||||
"title": tr("Recovery Power"),
|
||||
"subtitle": tr("How assertively the model recenters after disturbances."),
|
||||
"type": "value",
|
||||
"value": f"{self._params.get_float('RecoveryPower'):.1f}x",
|
||||
},
|
||||
{
|
||||
"id": "stop_distance",
|
||||
"title": tr("Stop Distance"),
|
||||
"subtitle": tr("Preferred gap held at a complete stop."),
|
||||
"type": "value",
|
||||
"value": f"{self._params.get_float('StopDistance'):.1f}m",
|
||||
},
|
||||
])
|
||||
rows.extend(
|
||||
[
|
||||
{
|
||||
"id": "recovery_power",
|
||||
"title": tr("Recovery Power"),
|
||||
"subtitle": tr("How assertively the model recenters after disturbances."),
|
||||
"type": "value",
|
||||
"value": f"{self._params.get_float('RecoveryPower'):.1f}x",
|
||||
},
|
||||
{
|
||||
"id": "stop_distance",
|
||||
"title": tr("Stop Distance"),
|
||||
"subtitle": tr("Preferred gap held at a complete stop."),
|
||||
"type": "value",
|
||||
"value": f"{self._params.get_float('StopDistance'):.1f}m",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
return rows
|
||||
|
||||
@@ -1189,14 +1213,18 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
|
||||
if res == DialogResult.CONFIRM:
|
||||
self._params.put_float("RecoveryPower", float(val))
|
||||
|
||||
gui_app.set_modal_overlay(AetherSliderDialog(tr("Recovery Power"), 0.5, 2.0, 0.1, self._params.get_float("RecoveryPower"), on_close, unit="x", color="#597497"))
|
||||
gui_app.set_modal_overlay(
|
||||
AetherSliderDialog(tr("Recovery Power"), 0.5, 2.0, 0.1, self._params.get_float("RecoveryPower"), on_close, unit="x", color="#597497")
|
||||
)
|
||||
|
||||
def _on_stop_distance_clicked(self):
|
||||
def on_close(res, val):
|
||||
if res == DialogResult.CONFIRM:
|
||||
self._params.put_float("StopDistance", float(val))
|
||||
|
||||
gui_app.set_modal_overlay(AetherSliderDialog(tr("Stop Distance"), 4.0, 10.0, 0.1, self._params.get_float("StopDistance"), on_close, unit="m", color="#597497"))
|
||||
gui_app.set_modal_overlay(
|
||||
AetherSliderDialog(tr("Stop Distance"), 4.0, 10.0, 0.1, self._params.get_float("StopDistance"), on_close, unit="m", color="#597497")
|
||||
)
|
||||
|
||||
def _on_blacklist_clicked(self):
|
||||
blacklisted = [m.strip() for m in (self._params.get("BlacklistedModels", encoding="utf-8") or "").split(",") if m.strip()]
|
||||
@@ -1250,6 +1278,7 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
|
||||
is_downloading = bool(model_to_download or download_all)
|
||||
|
||||
if is_downloading and (self._download_thread is None or not self._download_thread.is_alive()):
|
||||
|
||||
def _download_task():
|
||||
try:
|
||||
if download_all:
|
||||
|
||||
Reference in New Issue
Block a user