From 2e20cd061fb9faece4fe731d40aef87776472ef7 Mon Sep 17 00:00:00 2001 From: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Date: Tue, 14 Apr 2026 17:28:18 -0400 Subject: [PATCH] BigUI WIP: Sounds/Sliders --- .../layouts/settings/starpilot/aethergrid.py | 226 ++++++++++-------- .../ui/layouts/settings/starpilot/panel.py | 3 +- .../ui/layouts/settings/starpilot/sounds.py | 18 +- 3 files changed, 133 insertions(+), 114 deletions(-) diff --git a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py index 4a40948b4..9e9db1da9 100644 --- a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py +++ b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py @@ -1,5 +1,6 @@ from __future__ import annotations import math +import time import pyray as rl from collections.abc import Callable from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos @@ -522,125 +523,148 @@ class ValueTile(AetherTile): class SliderTile(AetherTile): - def __init__( - self, - title: str, - get_value: Callable[[], float], - set_value: Callable[[float], None], - min_val: float, - max_val: float, - step: float, - icon_path: str | None = None, - bg_color: rl.Color | str | None = None, - is_enabled: Callable[[], bool] | None = None, - desc: str = "", - unit: str = "", - labels: dict[float, str] | None = None, - ): - super().__init__(surface_color=bg_color) - self.title = title - self.desc = desc - self.get_value = get_value - self.set_value = set_value - self.min_val = min_val - self.max_val = max_val - self.step = step - self.unit = unit - self.labels = labels or {} - self.set_enabled(is_enabled or (lambda: True)) - self._icon = starpilot_texture(icon_path, 80, 80) if icon_path else None - self._font = gui_app.font(FontWeight.BOLD) - self._font_desc = gui_app.font(FontWeight.NORMAL) - self._active_color = self.surface_color - self._disabled_color = rl.Color(120, 120, 120, 255) + LONG_PRESS_THRESHOLD = 0.5 + DRAG_THRESHOLD = 10 - self._is_dragging = False - self._last_mouse_x = 0.0 - self._velocity = 0.0 - self._smooth_value = get_value() + def __init__( + self, + title: str, + get_value: Callable[[], float], + set_value: Callable[[float], None], + min_val: float, + max_val: float, + step: float, + icon_path: str | None = None, + bg_color: rl.Color | str | None = None, + is_enabled: Callable[[], bool] | None = None, + desc: str = "", + unit: str = "", + labels: dict[float, str] | None = None, + on_test: Callable[[], None] | None = None, + ): + super().__init__(surface_color=bg_color) + self.title = title + self.desc = desc + self.get_value = get_value + self.set_value = set_value + self.min_val = min_val + self.max_val = max_val + self.step = step + self.unit = unit + self.labels = labels or {} + self.on_test = on_test + self.set_enabled(is_enabled or (lambda: True)) + self._icon = starpilot_texture(icon_path, 80, 80) if icon_path else None + self._font = gui_app.font(FontWeight.BOLD) + self._font_desc = gui_app.font(FontWeight.NORMAL) + self._active_color = self.surface_color + self._disabled_color = rl.Color(120, 120, 120, 255) - def _handle_mouse_press(self, mouse_pos: MousePos): - if rl.check_collision_point_rec(mouse_pos, self._hit_rect) and self.enabled: - self._is_pressed = True - self._is_dragging = True - self._last_mouse_x = mouse_pos.x - self._velocity = 0.0 + self._is_dragging = False + self._last_mouse_x = 0.0 + self._velocity = 0.0 + self._smooth_value = get_value() + self._press_start_x = 0.0 + self._press_start_time: float | None = None + self._long_press_triggered = False - def _handle_mouse_release(self, mouse_pos: MousePos): - if self._is_dragging: - self._is_dragging = False - self._is_pressed = False + def _handle_mouse_press(self, mouse_pos: MousePos): + if rl.check_collision_point_rec(mouse_pos, self._hit_rect) and self.enabled: + self._is_pressed = True + self._last_mouse_x = mouse_pos.x + self._velocity = 0.0 + self._press_start_x = mouse_pos.x + self._press_start_time = time.monotonic() + self._long_press_triggered = False - def _handle_mouse_event(self, mouse_event): - if not rl.check_collision_point_rec(mouse_event.pos, self._hit_rect): - if not self._is_dragging: - self._plate_target = 0.0 + def _handle_mouse_release(self, mouse_pos: MousePos): + self._is_dragging = False + self._is_pressed = False + self._press_start_time = None - if self._is_dragging: - dt = rl.get_frame_time() - current_val = self.get_value() - mouse_pos = mouse_event.pos - dx = mouse_pos.x - self._last_mouse_x - self._velocity = dx / max(dt, 0.001) - self._last_mouse_x = mouse_pos.x + def _handle_mouse_event(self, mouse_event): + if not rl.check_collision_point_rec(mouse_event.pos, self._hit_rect): + if not self._is_dragging and not self._press_start_time: + self._plate_target = 0.0 - rect_w = self._rect.width - if rect_w > 0: - val_range = self.max_val - self.min_val - val_dx = (dx / rect_w) * val_range - new_val = current_val + val_dx + if self._press_start_time and not self._is_dragging and not self._long_press_triggered: + dx = abs(mouse_event.pos.x - self._press_start_x) + if dx > self.DRAG_THRESHOLD: + self._is_dragging = True + self._press_start_time = None + else: + elapsed = time.monotonic() - self._press_start_time + if elapsed >= self.LONG_PRESS_THRESHOLD: + self._long_press_triggered = True + self._press_start_time = None + if self.on_test: + self.on_test() - abs_vel = abs(self._velocity) - snap_threshold = 800 - coarse_step = 10 if val_range >= 100 else self.step * 5 - dynamic_step = coarse_step if abs_vel > snap_threshold else self.step + if self._is_dragging: + dt = rl.get_frame_time() + current_val = self.get_value() + mouse_pos = mouse_event.pos + dx = mouse_pos.x - self._last_mouse_x + self._velocity = dx / max(dt, 0.001) + self._last_mouse_x = mouse_pos.x - snapped = round(new_val / dynamic_step) * dynamic_step - snapped = max(self.min_val, min(self.max_val, snapped)) + rect_w = self._rect.width + if rect_w > 0: + val_range = self.max_val - self.min_val + val_dx = (dx / rect_w) * val_range + new_val = current_val + val_dx - if abs(snapped - current_val) >= self.step: - self.set_value(float(snapped)) + abs_vel = abs(self._velocity) + snap_threshold = 800 + coarse_step = 10 if val_range >= 100 else self.step * 5 + dynamic_step = coarse_step if abs_vel > snap_threshold else self.step - def _render(self, rect: rl.Rectangle): - enabled = self.enabled - current_val = self.get_value() - dt = rl.get_frame_time() + snapped = round(new_val / dynamic_step) * dynamic_step + snapped = max(self.min_val, min(self.max_val, snapped)) - self._smooth_value += (current_val - self._smooth_value) * (1 - math.exp(-dt / 0.1)) + if abs(snapped - current_val) >= self.step: + self.set_value(float(snapped)) - self.surface_color = self._active_color if enabled else self._disabled_color - if not enabled: - self._plate_offset = 0.0 - self._plate_target = 0.0 + def _render(self, rect: rl.Rectangle): + enabled = self.enabled + current_val = self.get_value() + dt = rl.get_frame_time() - face = self._render_layers(rect) + self._smooth_value += (current_val - self._smooth_value) * (1 - math.exp(-dt / 0.1)) - frac = (self._smooth_value - self.min_val) / (self.max_val - self.min_val) - fill_w = face.width * frac - if fill_w > 1: - fill_rect = rl.Rectangle(face.x, face.y, fill_w, face.height) - fill_color = rl.Color( - min(self.surface_color.r + 30, 255), - min(self.surface_color.g + 30, 255), - min(self.surface_color.b + 30, 255), - 140 - ) - rl.draw_rectangle_rounded(fill_rect, TILE_RADIUS, 10, fill_color) - edge_x = face.x + fill_w - rl.draw_rectangle_rec(rl.Rectangle(edge_x - 3, face.y, 3, face.height), rl.Color(255, 255, 255, 80)) + self.surface_color = self._active_color if enabled else self._disabled_color + if not enabled: + self._plate_offset = 0.0 + self._plate_target = 0.0 - line_heights = [28, 28] - _, ty = self._centered_content(face, self._icon, 0.75, 28, len(line_heights), line_heights) - content_pad = SPACING.tile_content - max_w = face.width - (content_pad * 2) + face = self._render_layers(rect) - self._draw_text_fit(self._font, self.title, rl.Vector2(face.x + content_pad, ty), max_w, 28, align_center=True, uppercase=True) + frac = (self._smooth_value - self.min_val) / (self.max_val - self.min_val) + fill_w = face.width * frac + if fill_w > 1: + fill_rect = rl.Rectangle(face.x, face.y, fill_w, face.height) + fill_color = rl.Color( + min(self.surface_color.r + 30, 255), + min(self.surface_color.g + 30, 255), + min(self.surface_color.b + 30, 255), + 140, + ) + rl.draw_rectangle_rounded(fill_rect, TILE_RADIUS, 10, fill_color) + edge_x = face.x + fill_w + rl.draw_rectangle_rec(rl.Rectangle(edge_x - 3, face.y, 3, face.height), rl.Color(255, 255, 255, 80)) - val_str = self.labels.get(current_val, f"{int(current_val)}{self.unit}") - self._draw_text_fit(self._font, val_str, rl.Vector2(face.x + content_pad, ty + 28 + SPACING.line_gap), max_w, 28, align_center=True, uppercase=True) + line_heights = [28, 28] + _, ty = self._centered_content(face, self._icon, 0.75, 28, len(line_heights), line_heights) + content_pad = SPACING.tile_content + max_w = face.width - (content_pad * 2) - if self.desc: - self._draw_text_fit(self._font_desc, self.desc, rl.Vector2(face.x + content_pad, ty + 28 + SPACING.line_gap + 34), max_w, 18, align_center=True) + self._draw_text_fit(self._font, self.title, rl.Vector2(face.x + content_pad, ty), max_w, 28, align_center=True, uppercase=True) + + val_str = self.labels.get(current_val, f"{int(current_val)}{self.unit}") + self._draw_text_fit(self._font, val_str, rl.Vector2(face.x + content_pad, ty + 28 + SPACING.line_gap), max_w, 28, align_center=True, uppercase=True) + + if self.desc: + self._draw_text_fit(self._font_desc, self.desc, rl.Vector2(face.x + content_pad, ty + 28 + SPACING.line_gap + 34), max_w, 18, align_center=True) class AetherSlider(Widget): diff --git a/selfdrive/ui/layouts/settings/starpilot/panel.py b/selfdrive/ui/layouts/settings/starpilot/panel.py index 0c98e1d72..9f16513e5 100644 --- a/selfdrive/ui/layouts/settings/starpilot/panel.py +++ b/selfdrive/ui/layouts/settings/starpilot/panel.py @@ -106,7 +106,8 @@ class StarPilotPanel(Widget): icon_path=cat.get("icon"), bg_color=cat.get("color"), is_enabled=cat.get("is_enabled"), - desc=tr(cat.get("desc", "")) + desc=tr(cat.get("desc", "")), + on_test=cat.get("on_test"), ) return None diff --git a/selfdrive/ui/layouts/settings/starpilot/sounds.py b/selfdrive/ui/layouts/settings/starpilot/sounds.py index 50ab27ec6..8d91143ee 100644 --- a/selfdrive/ui/layouts/settings/starpilot/sounds.py +++ b/selfdrive/ui/layouts/settings/starpilot/sounds.py @@ -80,8 +80,6 @@ class StarPilotVolumeControlLayout(StarPilotPanel): def __init__(self): super().__init__() self._init_sound_player() - self._pending_sound_test = None - self._last_sound_change_time = 0.0 self.SECTIONS = [ { @@ -95,12 +93,6 @@ class StarPilotVolumeControlLayout(StarPilotPanel): ] self._rebuild_grid() - def _update_state(self): - super()._update_state() - if self._pending_sound_test and (time.monotonic() - self._last_sound_change_time) > 0.8: - self._test_sound(self._pending_sound_test) - self._pending_sound_test = None - def _build_volume_categories(self): cats = [] for key in StarPilotSoundsLayout.VOLUME_KEYS: @@ -114,21 +106,23 @@ class StarPilotVolumeControlLayout(StarPilotPanel): if new_v != 101 and new_v < self.VOLUME_INFO[k]["min"]: new_v = self.VOLUME_INFO[k]["min"] self._params.put_int(k, new_v) - self._pending_sound_test = k - self._last_sound_change_time = time.monotonic() + + def test_cb(k=key): + self._test_sound(k) cats.append({ "title": info["title"], "type": "slider", "get_value": get_val, "set_value": set_val, + "on_test": test_cb, "min_val": 0, "max_val": 101, "step": 1, "unit": "%", "labels": {0: tr("Muted"), 101: tr("Auto")}, "icon": info["icon"], - "color": "#3B82F6" + "color": "#3B82F6", }) return cats @@ -150,7 +144,7 @@ class StarPilotVolumeControlLayout(StarPilotPanel): "unit": " " + tr("min"), "labels": {0: tr("Off"), 1: tr("1 minute")}, "icon": self.COOLDOWN_INFO["icon"], - "color": "#3B82F6" + "color": "#3B82F6", }] @classmethod