From a91a4c11dd48a75afbd078484f98e130ef3b7adf Mon Sep 17 00:00:00 2001 From: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:04:21 -0400 Subject: [PATCH] BigUI WIP: Extraction into Aethergrid --- .../layouts/settings/starpilot/aethergrid.py | 278 +++++++++++++++++- .../settings/starpilot/driving_model.py | 231 +++++---------- 2 files changed, 335 insertions(+), 174 deletions(-) diff --git a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py index 9e9db1da9..a2cf6e6dd 100644 --- a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py +++ b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py @@ -1,4 +1,5 @@ from __future__ import annotations +from dataclasses import dataclass import math import time import pyray as rl @@ -15,7 +16,6 @@ GEOMETRY_OFFSET = 10 PLATE_TAU = 0.060 TILE_RADIUS = 0.25 TILE_SEGMENTS = 10 -TILE_PADDING = 20 SLIDER_BUTTON_SIZE = 60 @@ -41,15 +41,6 @@ def hex_to_color(hex_str: str) -> rl.Color: return rl.Color(int(hex_str[0:2], 16), int(hex_str[2:4], 16), int(hex_str[4:6], 16), 255) -_SURFACE_MAP = { - "#E63956": hex_to_color("#E63956"), - "#3B82F6": hex_to_color("#3B82F6"), - "#10B981": hex_to_color("#10B981"), - "#D946EF": hex_to_color("#D946EF"), - "#8B5CF6": hex_to_color("#8B5CF6"), - "#64748B": hex_to_color("#64748B"), -} - _SUBSTRATE_MAP = { "#E63956": hex_to_color("#5A0B1A"), "#3B82F6": hex_to_color("#0B1C4A"), @@ -69,7 +60,7 @@ def _resolve_value(value, 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)))) + return rl.Color(color.r, color.g, color.b, max(0, min(color.a, int(alpha)))) class AetherListColors: @@ -98,6 +89,271 @@ class AetherListColors: SCROLL_THUMB = rl.Color(255, 255, 255, 68) +@dataclass(frozen=True) +class AetherListMetrics: + max_content_width: int = 1560 + outer_margin_x: int = 18 + outer_margin_y: int = 24 + panel_padding_x: int = 16 + panel_padding_top: int = 28 + panel_padding_bottom: int = 22 + header_height: int = 210 + section_gap: int = 28 + section_header_height: int = 34 + section_header_gap: int = 12 + row_height: int = 122 + utility_row_height: int = 88 + row_radius: float = 0.12 + action_width: int = 188 + header_button_height: int = 58 + header_button_gap: int = 10 + fade_height: int = 24 + content_right_gutter: int = 18 + toggle_width: int = 78 + toggle_height: int = 42 + toggle_right_inset: int = 34 + utility_value_right: int = 270 + utility_value_width: int = 220 + utility_chevron_right: int = 62 + menu_button_font_size: int = 18 + menu_button_roundness: float = 0.35 + menu_button_segments: int = 12 + + +@dataclass(frozen=True) +class AetherListFrame: + shell: rl.Rectangle + header: rl.Rectangle + scroll: rl.Rectangle + + +AETHER_LIST_METRICS = AetherListMetrics() + + +def build_list_panel_frame(rect: rl.Rectangle, metrics: AetherListMetrics = AETHER_LIST_METRICS) -> AetherListFrame: + shell_w = min(rect.width - metrics.outer_margin_x * 2, metrics.max_content_width) + shell_x = rect.x + (rect.width - shell_w) / 2 + shell_y = rect.y + metrics.outer_margin_y + shell_h = rect.height - metrics.outer_margin_y * 2 + shell_rect = rl.Rectangle(shell_x, shell_y, shell_w, shell_h) + + header_rect = rl.Rectangle( + shell_x + metrics.panel_padding_x, + shell_y + metrics.panel_padding_top, + shell_w - metrics.panel_padding_x * 2, + metrics.header_height, + ) + + scroll_rect = rl.Rectangle( + shell_x + metrics.panel_padding_x, + header_rect.y + header_rect.height, + shell_w - metrics.panel_padding_x * 2, + shell_h - metrics.header_height - metrics.panel_padding_top - metrics.panel_padding_bottom, + ) + + return AetherListFrame(shell_rect, header_rect, scroll_rect) + + +def draw_list_panel_shell(frame: AetherListFrame, *, bg: rl.Color = AetherListColors.PANEL_BG, border: rl.Color = AetherListColors.PANEL_BORDER, glow: rl.Color = AetherListColors.PANEL_GLOW): + rl.draw_rectangle_rounded(frame.shell, 0.055, 18, bg) + rl.draw_rectangle_rounded_lines_ex(frame.shell, 0.055, 18, 1, border) + glow_rect = rl.Rectangle(frame.shell.x + 2, frame.shell.y + 2, frame.shell.width - 4, frame.shell.height - 4) + rl.draw_rectangle_rounded_lines_ex(glow_rect, 0.055, 18, 1, glow) + + +def draw_soft_card(rect: rl.Rectangle, fill: rl.Color, border: rl.Color, radius: float = 0.08, segments: int = 18): + rl.draw_rectangle_rounded(rect, radius, segments, fill) + rl.draw_rectangle_rounded_lines_ex(rect, radius, segments, 1, border) + + +def draw_list_row_shell( + rect: rl.Rectangle, + *, + current: bool = False, + hovered: bool = False, + pressed: bool = False, + is_last: bool = False, + alpha: int = 255, + row_bg: rl.Color = AetherListColors.ROW_BG, + row_border: rl.Color = AetherListColors.ROW_BORDER, + row_separator: rl.Color = AetherListColors.ROW_SEPARATOR, + row_hover: rl.Color = AetherListColors.ROW_HOVER, + current_bg: rl.Color = AetherListColors.CURRENT_BG, + current_border: rl.Color = AetherListColors.CURRENT_BORDER, + row_radius: float = AetherListMetrics.row_radius, + segments: int = 18, + separator_inset: int = 22, +): + bg = current_bg if current else row_bg + border = current_border if current else row_border + if hovered: + bg = rl.Color(bg.r, bg.g, bg.b, min(bg.a + row_hover.a, 255)) + if pressed: + bg = rl.Color(bg.r, bg.g, bg.b, min(bg.a + 8, 255)) + + if bg.a > 0: + rl.draw_rectangle_rounded(rect, row_radius, segments, _with_alpha(bg, alpha)) + if current and border.a > 0: + rl.draw_rectangle_rounded_lines_ex(rect, row_radius, segments, 1, _with_alpha(border, alpha)) + if not is_last: + line_y = int(rect.y + rect.height - 1) + rl.draw_line(int(rect.x + separator_inset), line_y, int(rect.x + rect.width - separator_inset), line_y, _with_alpha(row_separator, alpha)) + + +def draw_action_rail( + rect: rl.Rectangle, + action_width: int, + *, + current: bool = False, + alpha: int = 255, + fill: rl.Color = AetherListColors.ACTION_BG, + current_fill: rl.Color = rl.Color(255, 255, 255, 6), + separator: rl.Color = AetherListColors.ACTION_SEPARATOR, + inset_y: int = 18, +): + action_x = rect.x + rect.width - action_width + action_rect = rl.Rectangle(action_x, rect.y, action_width, rect.height) + action_fill = current_fill if current else fill + if action_fill.a > 0: + rl.draw_rectangle_rec(action_rect, _with_alpha(action_fill, alpha)) + rl.draw_line(int(action_x), int(rect.y + inset_y), int(action_x), int(rect.y + rect.height - inset_y), _with_alpha(separator, alpha)) + return action_rect + + +def draw_list_scroll_fades( + scroll_rect: rl.Rectangle, + content_height: float, + scroll_offset: float, + bg_color: rl.Color, + *, + fade_height: int = AETHER_LIST_METRICS.fade_height, + right_trim: int = 12, + threshold: int = 4, +): + if content_height <= scroll_rect.height + threshold: + return + + fade_h = min(fade_height, int(scroll_rect.height / 4)) + if scroll_offset < -threshold: + rl.draw_rectangle_gradient_v( + int(scroll_rect.x), int(scroll_rect.y), int(scroll_rect.width - right_trim), fade_h, _with_alpha(bg_color, 255), _with_alpha(bg_color, 0) + ) + + if (-scroll_offset + scroll_rect.height) < (content_height - threshold): + bottom_y = int(scroll_rect.y + scroll_rect.height - fade_h) + rl.draw_rectangle_gradient_v( + int(scroll_rect.x), bottom_y, int(scroll_rect.width - right_trim), fade_h, _with_alpha(bg_color, 0), _with_alpha(bg_color, 255) + ) + + +def draw_busy_ring( + center: rl.Vector2, + phase: float, + accent_color: rl.Color, + *, + track_color: rl.Color = rl.Color(255, 255, 255, 26), + inner_radius: float = 20, + outer_radius: float = 26, + sweep: float = 260, + thickness: int = 48, +): + rl.draw_ring(center, inner_radius, outer_radius, 0, 360, thickness, track_color) + rl.draw_ring(center, inner_radius, outer_radius, phase, phase + sweep, thickness, accent_color) + + +def draw_toggle_switch( + rect: rl.Rectangle, + enabled: bool, + *, + track_color: rl.Color = AetherListColors.PRIMARY, + off_track_color: rl.Color = rl.Color(255, 255, 255, 24), + knob_color: rl.Color = rl.WHITE, + width: int = AETHER_LIST_METRICS.toggle_width, + height: int = AETHER_LIST_METRICS.toggle_height, + right_inset: int = AETHER_LIST_METRICS.toggle_right_inset, + knob_offset: int = 20, +): + toggle_rect = rl.Rectangle(rect.x + rect.width - width - right_inset, rect.y + (rect.height - height) / 2, width, height) + track = track_color if enabled else off_track_color + knob_x = toggle_rect.x + toggle_rect.width - knob_offset if enabled else toggle_rect.x + knob_offset + 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, knob_color) + + +def draw_action_pill( + rect: rl.Rectangle, + text: str, + fill: rl.Color, + border: rl.Color, + text_color: rl.Color, + *, + font_size: int = AETHER_LIST_METRICS.menu_button_font_size, + roundness: float = AETHER_LIST_METRICS.menu_button_roundness, + segments: int = AETHER_LIST_METRICS.menu_button_segments, +): + rl.draw_rectangle_rounded(rect, roundness, segments, fill) + rl.draw_rectangle_rounded_lines_ex(rect, roundness, segments, 1, border) + gui_label(rect, text, font_size, text_color, FontWeight.SEMI_BOLD, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + + +def draw_status_led(center: rl.Vector2, enabled: bool): + if enabled: + led_color = rl.Color(110, 175, 245, 255) + rl.draw_circle(int(center.x), int(center.y), 18, rl.Color(110, 175, 245, 18)) + rl.draw_circle(int(center.x), int(center.y), 12, rl.Color(110, 175, 245, 48)) + rl.draw_circle(int(center.x), int(center.y), 7, rl.Color(110, 175, 245, 100)) + rl.draw_circle(int(center.x), int(center.y), 6, led_color) + rl.draw_circle(int(center.x - 2), int(center.y - 2), 2, rl.Color(210, 235, 255, 200)) + else: + rl.draw_circle(int(center.x), int(center.y), 8, rl.Color(10, 10, 14, 230)) + rl.draw_circle(int(center.x), int(center.y), 6, rl.Color(35, 40, 50, 255)) + rl.draw_ring(center, 6, 7, 0, 360, 24, rl.Color(70, 78, 95, 140)) + + +def draw_overflow_dots(center: rl.Vector2, color: rl.Color): + dot_r = 4 + gap = 12 + for i in range(3): + rl.draw_circle(int(center.x + (i - 1) * gap), int(center.y), dot_r, color) + + +def draw_trash_icon(center: rl.Vector2, color: rl.Color): + bin_rect = rl.Rectangle(center.x - 12, center.y - 12, 24, 24) + lid_rect = rl.Rectangle(center.x - 14, center.y - 18, 28, 5) + handle_rect = rl.Rectangle(center.x - 4, center.y - 22, 8, 4) + rl.draw_rectangle_rounded(bin_rect, 0.2, 8, color) + rl.draw_rectangle_rounded(lid_rect, 0.5, 8, color) + rl.draw_rectangle_rounded(handle_rect, 0.5, 8, color) + stripe = _with_alpha(AetherListColors.PANEL_BG, 120) + rl.draw_line(int(center.x - 6), int(center.y - 8), int(center.x - 6), int(center.y + 8), stripe) + rl.draw_line(int(center.x), int(center.y - 8), int(center.x), int(center.y + 8), stripe) + rl.draw_line(int(center.x + 6), int(center.y - 8), int(center.x + 6), int(center.y + 8), stripe) + + +def draw_heart_icon(center: rl.Vector2, color: rl.Color): + rl.draw_circle(int(center.x - 5), int(center.y - 3), 7, color) + rl.draw_circle(int(center.x + 5), int(center.y - 3), 7, color) + rl.draw_triangle( + rl.Vector2(center.x + 13, center.y + 1), + rl.Vector2(center.x - 13, center.y + 1), + rl.Vector2(center.x, center.y + 13), + color, + ) + + +def draw_download_icon(center: rl.Vector2, color: rl.Color): + shaft_top = rl.Vector2(center.x, center.y - 18) + shaft_bottom = rl.Vector2(center.x, center.y + 8) + left_head = rl.Vector2(center.x - 11, center.y - 2) + right_head = rl.Vector2(center.x + 11, center.y - 2) + tray_left = rl.Vector2(center.x - 14, center.y + 18) + tray_right = rl.Vector2(center.x + 14, center.y + 18) + rl.draw_line_ex(shaft_top, shaft_bottom, 4, color) + rl.draw_line_ex(left_head, shaft_bottom, 4, color) + rl.draw_line_ex(right_head, shaft_bottom, 4, color) + rl.draw_line_ex(tray_left, tray_right, 4, color) + + class AetherButton(Widget): def __init__( self, diff --git a/selfdrive/ui/layouts/settings/starpilot/driving_model.py b/selfdrive/ui/layouts/settings/starpilot/driving_model.py index 01776e9c5..812edce83 100644 --- a/selfdrive/ui/layouts/settings/starpilot/driving_model.py +++ b/selfdrive/ui/layouts/settings/starpilot/driving_model.py @@ -31,12 +31,30 @@ 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 AetherButton, AetherChip, AetherListColors, AetherScrollbar, AetherSliderDialog +from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( + AETHER_LIST_METRICS, + AetherButton, + AetherChip, + AetherListColors, + AetherScrollbar, + AetherSliderDialog, + draw_action_rail, + draw_action_pill, + draw_busy_ring, + draw_download_icon, + draw_heart_icon, + draw_list_panel_shell, + build_list_panel_frame, + draw_list_row_shell, + draw_list_scroll_fades, + draw_soft_card, + draw_status_led, + draw_toggle_switch, + draw_overflow_dots, +) 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 @@ -52,31 +70,17 @@ 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 -OUTER_MARGIN_Y = 24 -PANEL_RADIUS = 0.055 -PANEL_PADDING_X = 16 -PANEL_PADDING_TOP = 28 -PANEL_PADDING_BOTTOM = 22 -HEADER_HEIGHT = 210 -SECTION_GAP = 28 -SECTION_HEADER_HEIGHT = 34 -SECTION_HEADER_GAP = 12 -ROW_HEIGHT = 122 -UTILITY_ROW_HEIGHT = 88 -ROW_RADIUS = 0.12 -ACTION_WIDTH = 188 -ROW_TEXT_GAP = 8 -BUTTON_HEIGHT = 58 -BUTTON_GAP = 16 -FADE_HEIGHT = 24 +SECTION_GAP = AETHER_LIST_METRICS.section_gap +SECTION_HEADER_HEIGHT = AETHER_LIST_METRICS.section_header_height +SECTION_HEADER_GAP = AETHER_LIST_METRICS.section_header_gap +ROW_HEIGHT = AETHER_LIST_METRICS.row_height +UTILITY_ROW_HEIGHT = AETHER_LIST_METRICS.utility_row_height +ROW_RADIUS = AETHER_LIST_METRICS.row_radius +ACTION_WIDTH = AETHER_LIST_METRICS.action_width +BUTTON_HEIGHT = AETHER_LIST_METRICS.header_button_height +FADE_HEIGHT = AETHER_LIST_METRICS.fade_height CONFIRM_TIMEOUT_SECONDS = 3.0 TRANSITION_SECONDS = 0.24 @@ -99,10 +103,6 @@ def _clean_model_name(name: str) -> str: return str(name or "").replace("_default", "").replace("(Default)", "").strip() -def _with_alpha(color: rl.Color, alpha: int) -> rl.Color: - return rl.Color(color.r, color.g, color.b, max(0, min(color.a, int(alpha)))) - - def _ease(current: float, target: float, tau: float = 0.085) -> float: dt = max(rl.get_frame_time(), 1 / max(gui_app.target_fps, 1)) return current + (target - current) * (1 - math.exp(-dt / tau)) @@ -307,29 +307,17 @@ class DrivingModelManagerView(Widget): self._utility_rects.clear() self._menu_sub_rects.clear() - shell_w = min(rect.width - OUTER_MARGIN_X * 2, MAX_CONTENT_WIDTH) - shell_x = rect.x + (rect.width - shell_w) / 2 - shell_y = rect.y + OUTER_MARGIN_Y - shell_h = rect.height - OUTER_MARGIN_Y * 2 - self._shell_rect = rl.Rectangle(shell_x, shell_y, shell_w, shell_h) + frame = build_list_panel_frame(rect) + self._shell_rect = frame.shell + draw_list_panel_shell(frame) - header_rect = rl.Rectangle( - shell_x + PANEL_PADDING_X, - shell_y + PANEL_PADDING_TOP, - shell_w - PANEL_PADDING_X * 2, - HEADER_HEIGHT, - ) + header_rect = frame.header self._draw_header(header_rect) - scroll_rect = rl.Rectangle( - shell_x + PANEL_PADDING_X, - header_rect.y + header_rect.height, - shell_w - PANEL_PADDING_X * 2, - shell_h - HEADER_HEIGHT - PANEL_PADDING_TOP - PANEL_PADDING_BOTTOM, - ) + scroll_rect = frame.scroll self._scroll_rect = scroll_rect - content_width = scroll_rect.width - 18 + content_width = scroll_rect.width - AETHER_LIST_METRICS.content_right_gutter self._content_height = self._measure_content_height(content_width) self._scroll_panel.set_enabled(lambda: not self._controller._is_download_active()) self._scroll_offset = self._scroll_panel.update(scroll_rect, max(self._content_height, scroll_rect.height)) @@ -341,18 +329,7 @@ class DrivingModelManagerView(Widget): if self._content_height > scroll_rect.height: self._draw_scrollbar(scroll_rect) - if self._content_height > scroll_rect.height + 4: - 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) - ) + draw_list_scroll_fades(scroll_rect, self._content_height, self._scroll_offset, MODEL_PANEL_BG, fade_height=FADE_HEIGHT) def _draw_header(self, rect: rl.Rectangle): title_rect = rl.Rectangle(rect.x, rect.y + 4, rect.width * 0.55, 40) @@ -386,19 +363,7 @@ class DrivingModelManagerView(Widget): led_x = int(random_rect.x + random_rect.width - 26) led_y = int(random_rect.y + random_rect.height / 2) randomizer_on = self._controller._params.get_bool("ModelRandomizer") - if randomizer_on: - # Lit: bright theme-blue LED with layered glow rings - led_color = rl.Color(110, 175, 245, 255) - rl.draw_circle(led_x, led_y, 18, rl.Color(110, 175, 245, 18)) - rl.draw_circle(led_x, led_y, 12, rl.Color(110, 175, 245, 48)) - rl.draw_circle(led_x, led_y, 7, rl.Color(110, 175, 245, 100)) - rl.draw_circle(led_x, led_y, 6, led_color) - rl.draw_circle(led_x - 2, led_y - 2, 2, rl.Color(210, 235, 255, 200)) - else: - # Unlit: dark socket with faint ring - rl.draw_circle(led_x, led_y, 8, rl.Color(10, 10, 14, 230)) - rl.draw_circle(led_x, led_y, 6, rl.Color(35, 40, 50, 255)) - rl.draw_ring(rl.Vector2(led_x, led_y), 6, 7, 0, 360, 24, rl.Color(70, 78, 95, 140)) + draw_status_led(rl.Vector2(led_x, led_y), randomizer_on) def _measure_content_height(self, width: float) -> float: sections = self._build_sections(width) @@ -447,8 +412,7 @@ class DrivingModelManagerView(Widget): def _draw_empty_state(self, rect: rl.Rectangle): 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)) + draw_soft_card(state_rect, rl.Color(255, 255, 255, 5), rl.Color(255, 255, 255, 14), radius=0.08, segments=18) gui_label( rl.Rectangle(state_rect.x, state_rect.y + 42, state_rect.width, 40), self._controller.empty_state_title(), @@ -486,32 +450,29 @@ class DrivingModelManagerView(Widget): removable = self._controller.is_model_removable(entry.key) is_menu_open = (self._confirm_key == entry.key) - row_bg = MODEL_CURRENT_BG if current else MODEL_ROW_BG - row_border = MODEL_CURRENT_BORDER if current else MODEL_ROW_BORDER - if row_hovered: - row_bg = rl.Color(row_bg.r, row_bg.g, row_bg.b, min(row_bg.a + MODEL_ROW_HOVER.a, 255)) - if pressed: - 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 ) - if row_bg.a > 0: - rl.draw_rectangle_rounded(draw_rect, ROW_RADIUS, 18, _with_alpha(row_bg, alpha)) - if current and row_border.a > 0: - rl.draw_rectangle_rounded_lines_ex(draw_rect, ROW_RADIUS, 18, 1, _with_alpha(row_border, alpha)) - if not is_last: - line_y = int(draw_rect.y + draw_rect.height - 1) - rl.draw_line(int(draw_rect.x + 22), line_y, int(draw_rect.x + draw_rect.width - 22), line_y, _with_alpha(MODEL_ROW_SEPARATOR, alpha)) + draw_list_row_shell( + draw_rect, + current=current, + hovered=row_hovered, + pressed=pressed, + is_last=is_last, + alpha=alpha, + row_bg=MODEL_ROW_BG, + row_border=MODEL_ROW_BORDER, + row_separator=MODEL_ROW_SEPARATOR, + row_hover=MODEL_ROW_HOVER, + current_bg=MODEL_CURRENT_BG, + current_border=MODEL_CURRENT_BORDER, + row_radius=ROW_RADIUS, + separator_inset=22, + ) - action_x = draw_rect.x + draw_rect.width - ACTION_WIDTH - action_rect = rl.Rectangle(action_x, draw_rect.y, ACTION_WIDTH, draw_rect.height) - action_fill = rl.Color(255, 255, 255, 6 if current else MODEL_ACTION_BG.a) - if action_fill.a > 0: - rl.draw_rectangle_rec(action_rect, _with_alpha(action_fill, alpha)) - rl.draw_line(int(action_x), int(draw_rect.y + 18), int(action_x), int(draw_rect.y + draw_rect.height - 18), _with_alpha(MODEL_ACTION_SEPARATOR, alpha)) + action_rect = draw_action_rail(draw_rect, ACTION_WIDTH, current=current, alpha=alpha, fill=MODEL_ACTION_BG, separator=MODEL_ACTION_SEPARATOR, inset_y=18) info_rect = rl.Rectangle(draw_rect.x + 24, draw_rect.y + 18, draw_rect.width - ACTION_WIDTH - 42, draw_rect.height - 36) row_touchable = entry.installed and not self._controller._params.get_bool("ModelRandomizer") @@ -540,7 +501,7 @@ class DrivingModelManagerView(Widget): if entry.user_favorite: heart_color = rl.Color(210, 100, 130, 230) heart_center = rl.Vector2(rect.x + 15, rect.y + 17) - self._draw_heart_icon(heart_center, heart_color) + draw_heart_icon(heart_center, heart_color) heart_offset = 34 title_rect = rl.Rectangle(rect.x + heart_offset, rect.y, rect.width - heart_offset, 34) gui_label(title_rect, entry.name, 34, MODEL_HEADER_TEXT, FontWeight.MEDIUM) @@ -569,7 +530,7 @@ class DrivingModelManagerView(Widget): def _draw_download_action(self, rect: rl.Rectangle): 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) + 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"), @@ -582,8 +543,7 @@ class DrivingModelManagerView(Widget): 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) phase = (time.monotonic() * 240.0) % 360.0 - rl.draw_ring(center, 20, 26, 0, 360, 48, rl.Color(255, 255, 255, 26)) - rl.draw_ring(center, 20, 26, phase, phase + 260, 48, MODEL_PRIMARY) + draw_busy_ring(center, phase, MODEL_PRIMARY) label = progress_text if progress_text else tr("Downloading") gui_label( @@ -600,10 +560,7 @@ class DrivingModelManagerView(Widget): # Three-dot menu indicator center_x = rect.x + rect.width / 2 center_y = rect.y + rect.height / 2 - 10 - dot_r = 4 - gap = 12 - for i in range(3): - rl.draw_circle(int(center_x + (i - 1) * gap), int(center_y), dot_r, _with_alpha(MODEL_HEADER_TEXT, 200)) + draw_overflow_dots(rl.Vector2(center_x, center_y), rl.Color(MODEL_HEADER_TEXT.r, MODEL_HEADER_TEXT.g, MODEL_HEADER_TEXT.b, min(MODEL_HEADER_TEXT.a, 200))) gui_label( rl.Rectangle(rect.x + 16, rect.y + rect.height - 38, rect.width - 32, 22), tr("Options"), @@ -626,19 +583,15 @@ class DrivingModelManagerView(Widget): self._menu_sub_rects[f"{entry.key}:favorite"] = fav_rect # Delete button - rl.draw_rectangle_rounded(delete_rect, 0.35, 12, MODEL_DANGER_SOFT) - rl.draw_rectangle_rounded_lines_ex(delete_rect, 0.35, 12, 1, _with_alpha(MODEL_DANGER, 70)) - gui_label(delete_rect, tr("Delete"), 18, MODEL_DANGER, FontWeight.SEMI_BOLD, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + draw_action_pill(delete_rect, tr("Delete"), MODEL_DANGER_SOFT, rl.Color(MODEL_DANGER.r, MODEL_DANGER.g, MODEL_DANGER.b, min(MODEL_DANGER.a, 70)), MODEL_DANGER) # Favorite toggle button is_fav = entry.user_favorite fav_fill = rl.Color(210, 100, 130, 44) if is_fav else MODEL_PRIMARY_SOFT - fav_border = _with_alpha(rl.Color(210, 100, 130, 255) if is_fav else MODEL_PRIMARY, 70) + fav_border = rl.Color((210 if is_fav else MODEL_PRIMARY.r), (100 if is_fav else MODEL_PRIMARY.g), (130 if is_fav else MODEL_PRIMARY.b), min((255 if is_fav else MODEL_PRIMARY.a), 70)) fav_text_color = rl.Color(210, 100, 130, 255) if is_fav else MODEL_PRIMARY fav_label = tr("Unfavorite") if is_fav else tr("Favorite") - rl.draw_rectangle_rounded(fav_rect, 0.35, 12, fav_fill) - rl.draw_rectangle_rounded_lines_ex(fav_rect, 0.35, 12, 1, fav_border) - gui_label(fav_rect, fav_label, 18, fav_text_color, FontWeight.SEMI_BOLD, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + draw_action_pill(fav_rect, fav_label, fav_fill, fav_border, fav_text_color) 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) @@ -648,50 +601,13 @@ class DrivingModelManagerView(Widget): chip_rect = rl.Rectangle(rect.x + 20, rect.y + (rect.height - 42) / 2, rect.width - 40, 42) 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) - lid_rect = rl.Rectangle(center.x - 14, center.y - 18, 28, 5) - handle_rect = rl.Rectangle(center.x - 4, center.y - 22, 8, 4) - rl.draw_rectangle_rounded(bin_rect, 0.2, 8, color) - rl.draw_rectangle_rounded(lid_rect, 0.5, 8, color) - rl.draw_rectangle_rounded(handle_rect, 0.5, 8, color) - stripe = _with_alpha(MODEL_PANEL_BG, 120) - rl.draw_line(int(center.x - 6), int(center.y - 8), int(center.x - 6), int(center.y + 8), stripe) - rl.draw_line(int(center.x), int(center.y - 8), int(center.x), int(center.y + 8), stripe) - rl.draw_line(int(center.x + 6), int(center.y - 8), int(center.x + 6), int(center.y + 8), stripe) - - def _draw_heart_icon(self, center: rl.Vector2, color: rl.Color): - # Two rounded bumps at top - rl.draw_circle(int(center.x - 5), int(center.y - 3), 7, color) - rl.draw_circle(int(center.x + 5), int(center.y - 3), 7, color) - # Downward triangle for the bottom point - rl.draw_triangle( - rl.Vector2(center.x + 13, center.y + 1), - rl.Vector2(center.x - 13, center.y + 1), - rl.Vector2(center.x, center.y + 13), - color, - ) - - def _draw_download_icon(self, center: rl.Vector2, color: rl.Color): - shaft_top = rl.Vector2(center.x, center.y - 18) - shaft_bottom = rl.Vector2(center.x, center.y + 8) - left_head = rl.Vector2(center.x - 11, center.y - 2) - right_head = rl.Vector2(center.x + 11, center.y - 2) - tray_left = rl.Vector2(center.x - 14, center.y + 18) - tray_right = rl.Vector2(center.x + 14, center.y + 18) - rl.draw_line_ex(shaft_top, shaft_bottom, 4, color) - rl.draw_line_ex(left_head, shaft_bottom, 4, color) - rl.draw_line_ex(right_head, shaft_bottom, 4, color) - rl.draw_line_ex(tray_left, tray_right, 4, color) - def _draw_utility_section(self, x: float, y: float, width: float, rows: list[dict]): title_rect = rl.Rectangle(x, y, width - 18, SECTION_HEADER_HEIGHT) gui_label(title_rect, tr("Automation and Tuning"), 26, MODEL_SUBTEXT, FontWeight.MEDIUM) y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP container_rect = rl.Rectangle(x, y, width - 18, len(rows) * UTILITY_ROW_HEIGHT) - rl.draw_rectangle_rounded(container_rect, 0.055, 18, rl.Color(255, 255, 255, 4)) - rl.draw_rectangle_rounded_lines_ex(container_rect, 0.055, 18, 1, rl.Color(255, 255, 255, 15)) + draw_soft_card(container_rect, rl.Color(255, 255, 255, 4), rl.Color(255, 255, 255, 15), radius=0.055, segments=18) for index, row in enumerate(rows): row_rect = rl.Rectangle(x, y + index * UTILITY_ROW_HEIGHT, width - 18, UTILITY_ROW_HEIGHT) @@ -701,9 +617,7 @@ class DrivingModelManagerView(Widget): mouse_pos = gui_app.last_mouse_event.pos hovered = rl.check_collision_point_rec(mouse_pos, rect) pressed = self._pressed_target == f"utility:{row['id']}" - bg = rl.Color(255, 255, 255, 0) - if hovered: - bg = rl.Color(255, 255, 255, 8) + bg = rl.Color(255, 255, 255, 8 if hovered else 0) if pressed: bg = rl.Color(255, 255, 255, 14) if bg.a: @@ -721,23 +635,14 @@ class DrivingModelManagerView(Widget): gui_label(subtitle_rect, row["subtitle"], 20, MODEL_SUBTEXT, FontWeight.NORMAL) if row["type"] == "toggle": - self._draw_toggle(rect, bool(row["value"])) + draw_toggle_switch(rect, bool(row["value"])) else: - value_rect = rl.Rectangle(rect.x + rect.width - 270, rect.y + 20, 220, 28) + value_rect = rl.Rectangle(rect.x + rect.width - AETHER_LIST_METRICS.utility_value_right, rect.y + 20, AETHER_LIST_METRICS.utility_value_width, 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 + rl.Rectangle(rect.x + rect.width - AETHER_LIST_METRICS.utility_chevron_right, 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 - toggle_h = 42 - toggle_rect = rl.Rectangle(rect.x + rect.width - toggle_w - 34, rect.y + (rect.height - toggle_h) / 2, toggle_w, toggle_h) - track = MODEL_PRIMARY if enabled else rl.Color(255, 255, 255, 24) - knob_x = toggle_rect.x + toggle_rect.width - 20 if enabled else toggle_rect.x + 20 - 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_scrollbar(self, rect: rl.Rectangle): self._scrollbar.render(rect, self._content_height, self._scroll_offset)