From a7d8d7a5445eff3c96074d03d325b2eb4cdfee46 Mon Sep 17 00:00:00 2001 From: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Date: Sun, 31 May 2026 13:53:06 -0400 Subject: [PATCH] BigUP WIP: Sound Polish --- .../layouts/settings/starpilot/aethergrid.py | 277 +++++++++++++++--- .../ui/layouts/settings/starpilot/sounds.py | 96 ++++-- selfdrive/ui/tests/test_aethergrid.py | 44 ++- 3 files changed, 345 insertions(+), 72 deletions(-) diff --git a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py index 23076ac14..a3185ea9f 100644 --- a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py +++ b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py @@ -3061,91 +3061,288 @@ class AetherSliderDialog(Widget): step: float, current_val: float, on_close: Callable, + presets: list[float] | None = None, unit: str = "", labels: dict[float, str] | None = None, color: rl.Color | str = "#F57371", + on_change: Callable[[float], None] | None = None, ): super().__init__() self.title, self._user_callback = title, on_close + self._on_change = on_change self._color = hex_to_color(color) if isinstance(color, str) else color self._font_title, self._font_btn = gui_app.font(FontWeight.BOLD), gui_app.font(FontWeight.BOLD) - self._slider = AetherSlider(min_val, max_val, step, current_val, self._on_slider_change, unit, labels, self._color) - self._current_val, self._is_pressed_ok, self._is_pressed_cancel = current_val, False, False - self._ok_offset: float = 0.0 - self._cancel_offset: float = 0.0 - self._ok_target: float = 0.0 - self._cancel_target: float = 0.0 + self._font_value = gui_app.font(FontWeight.BOLD) + self._current_val = current_val + self.min_val = min_val + self.max_val = max_val + self.step = step + self._presets = presets or [] + self._unit = unit + self._labels = labels or {} + self._preset_rects: list[tuple[float, rl.Rectangle]] = [] + self._pressed_zone: str | None = None + self._is_pressed_ok = False + self._is_pressed_cancel = False + self._ok_offset = 0.0 + self._cancel_offset = 0.0 + self._ok_target = 0.0 + self._cancel_target = 0.0 + self._is_dragging = False + self._val_on_press = current_val - def _on_slider_change(self, val): - self._current_val = val + self._ok_rect = rl.Rectangle(0, 0, 0, 0) + self._cancel_rect = rl.Rectangle(0, 0, 0, 0) + self._minus_rect = rl.Rectangle(0, 0, 0, 0) + self._plus_rect = rl.Rectangle(0, 0, 0, 0) + self._track_rect = rl.Rectangle(0, 0, 0, 0) + self._thumb_rect = rl.Rectangle(0, 0, 0, 0) + + def _value_fraction(self, value: float) -> float: + val_range = self.max_val - self.min_val + if val_range == 0: + return 0.0 + return max(0.0, min(1.0, (value - self.min_val) / val_range)) + + def _clamp_and_snap(self, val: float) -> float: + if self.step <= 0: + return max(self.min_val, min(self.max_val, val)) + snapped = round((val - self.min_val) / self.step) * self.step + self.min_val + return max(self.min_val, min(self.max_val, snapped)) + + def _update_val_from_mouse(self, mouse_pos: MousePos) -> None: + if self._track_rect.width <= 0: + return + rel_x = max(0.0, min(1.0, (mouse_pos.x - self._track_rect.x) / self._track_rect.width)) + val = self.min_val + rel_x * (self.max_val - self.min_val) + self._current_val = self._clamp_and_snap(val) + + def formatted_value(self) -> str: + return format_adjustor_value(self._current_val, step=self.step, unit=self._unit, labels=self._labels) def _handle_mouse_press(self, mouse_pos: MousePos): - self._slider._handle_mouse_press(mouse_pos) + self._val_on_press = self._current_val if rl.check_collision_point_rec(mouse_pos, self._ok_rect): self._is_pressed_ok = True self._ok_target = 1.0 + return if rl.check_collision_point_rec(mouse_pos, self._cancel_rect): self._is_pressed_cancel = True self._cancel_target = 1.0 + return + + for value, preset_rect in self._preset_rects: + if rl.check_collision_point_rec(mouse_pos, preset_rect): + self._pressed_zone = f"preset:{value}" + return + + if rl.check_collision_point_rec(mouse_pos, self._minus_rect): + self._pressed_zone = "minus" + return + if rl.check_collision_point_rec(mouse_pos, self._plus_rect): + self._pressed_zone = "plus" + return + + hit_track = _inflate_rect(self._track_rect, 0, 24) + if rl.check_collision_point_rec(mouse_pos, hit_track): + self._is_dragging = True + self._update_val_from_mouse(mouse_pos) + return def _handle_mouse_release(self, mouse_pos: MousePos): - self._slider._handle_mouse_release(mouse_pos) + is_ok = self._is_pressed_ok + is_cancel = self._is_pressed_cancel + if self._is_pressed_ok: self._ok_target = 0.0 if rl.check_collision_point_rec(mouse_pos, self._ok_rect): gui_app.pop_widget() self._user_callback(DialogResult.CONFIRM, self._current_val) self._is_pressed_ok = False - if self._is_pressed_cancel: + elif self._is_pressed_cancel: self._cancel_target = 0.0 if rl.check_collision_point_rec(mouse_pos, self._cancel_rect): gui_app.pop_widget() self._user_callback(DialogResult.CANCEL, self._current_val) self._is_pressed_cancel = False + elif self._pressed_zone: + if self._pressed_zone.startswith("preset:"): + try: + preset_value = float(self._pressed_zone.split(":", 1)[1]) + except ValueError: + preset_value = None + if preset_value is not None: + for value, preset_rect in self._preset_rects: + if value == preset_value and rl.check_collision_point_rec(mouse_pos, preset_rect): + self._current_val = preset_value + break + elif self._pressed_zone == "minus": + if rl.check_collision_point_rec(mouse_pos, self._minus_rect): + self._current_val = self._clamp_and_snap(self._current_val - self.step) + elif self._pressed_zone == "plus": + if rl.check_collision_point_rec(mouse_pos, self._plus_rect): + self._current_val = self._clamp_and_snap(self._current_val + self.step) + self._pressed_zone = None + + if self._is_dragging: + self._is_dragging = False + + if not is_ok and not is_cancel: + if self._current_val != self._val_on_press and self._on_change: + self._on_change(self._current_val) + + def _handle_mouse_event(self, mouse_event): + if self._is_dragging: + self._update_val_from_mouse(mouse_event.pos) + + def _render_preset_chip(self, rect: rl.Rectangle, text: str, *, current: bool, pressed: bool): + fill = rl.Color(255, 255, 255, 5) + border = rl.Color(255, 255, 255, 14) + text_color = AetherListColors.SUBTEXT + if current: + fill = _mix_colors(rl.Color(18, 22, 28, 255), self._color, 0.22, alpha=255) + border = _with_alpha(self._color, 72) + text_color = AetherListColors.HEADER + elif pressed: + fill = rl.Color(255, 255, 255, 10) + border = rl.Color(255, 255, 255, 22) + + _draw_rounded_fill(rect, fill, radius_px=16) + _draw_rounded_stroke(rect, border, radius_px=16) + _draw_text_fit_common( + gui_app.font(FontWeight.MEDIUM), + text, + rl.Vector2(rect.x + 10, rect.y + (rect.height - 18) / 2), + max(1.0, rect.width - 20), + 18, + align_center=True, + color=text_color, + ) def _render(self, rect: rl.Rectangle): dt = rl.get_frame_time() self._ok_offset += (self._ok_target - self._ok_offset) * (1 - math.exp(-dt / PLATE_TAU)) self._cancel_offset += (self._cancel_target - self._cancel_offset) * (1 - math.exp(-dt / PLATE_TAU)) rl.draw_rectangle(0, 0, gui_app.width, gui_app.height, rl.Color(0, 0, 0, 160)) - dialog_margin = SPACING.xxl - dialog_w = min(1000, max(640, rect.width - dialog_margin * 2)) - dialog_h = min(500, max(360, rect.height - dialog_margin * 2)) - button_height = min(80, max(64, int(dialog_h * 0.16))) - button_width = min(350, max(180, int((dialog_w - SPACING.lg * 3) / 2))) + + has_presets = len(self._presets) > 0 + dialog_w = 880 + dialog_h = 500 if has_presets else 440 + button_height = 72 + button_width = 320 + dx, dy = rect.x + (rect.width - dialog_w) / 2, rect.y + (rect.height - dialog_h) / 2 - self._ok_rect = rl.Rectangle(dx + dialog_w - button_width - SPACING.lg, dy + dialog_h - button_height - SPACING.lg, button_width, button_height) - self._cancel_rect = rl.Rectangle(dx + SPACING.lg, dy + dialog_h - button_height - SPACING.lg, button_width, button_height) + self._ok_rect = rl.Rectangle(dx + dialog_w - button_width - 48, dy + dialog_h - button_height - 36, button_width, button_height) + self._cancel_rect = rl.Rectangle(dx + 48, dy + dialog_h - button_height - 36, button_width, button_height) + d_rect = _snap_rect(rl.Rectangle(dx, dy, dialog_w, dialog_h)) - _draw_rounded_fill(d_rect, rl.Color(13, 16, 22, 255), radius_px=24) - _draw_rounded_stroke(d_rect, rl.Color(255, 255, 255, 24), radius_px=24) - rl.draw_rectangle_rec(rl.Rectangle(d_rect.x, d_rect.y, d_rect.width, 2), _with_alpha(self._color, 40)) - title_size = max(30, min(50, int(dialog_w * 0.05))) + _draw_rounded_fill(d_rect, rl.Color(10, 12, 16, 255), radius_px=24) + _draw_rounded_stroke(d_rect, rl.Color(255, 255, 255, 16), radius_px=24) + rl.draw_rectangle_rec(rl.Rectangle(d_rect.x, d_rect.y, d_rect.width, 3), self._color) + + title_size = 28 ts = measure_text_cached(self._font_title, self.title, title_size) - rl.draw_text_ex(self._font_title, self.title, rl.Vector2(round(dx + (dialog_w - ts.x) / 2), round(dy + SPACING.xxl)), title_size, 0, rl.WHITE) - slider_y = dy + max(120, int(dialog_h * 0.38)) - slider_h = min(100, max(72, int(dialog_h * 0.22))) - slider_rect = rl.Rectangle(dx + SPACING.xxl, slider_y, dialog_w - (SPACING.xxl * 2), slider_h) - self._slider.render(slider_rect) + rl.draw_text_ex(self._font_title, self.title, rl.Vector2(round(dx + (dialog_w - ts.x) / 2), round(dy + 32)), title_size, 0, rl.WHITE) + + # Large value display below title + val_str = self.formatted_value() + val_size = 48 + vts = measure_text_cached(self._font_value, val_str, val_size) + rl.draw_text_ex(self._font_value, val_str, rl.Vector2(round(dx + (dialog_w - vts.x) / 2), round(dy + 80)), val_size, 0, self._color) + + # Render presets below the value display (if any) + presets_y = dy + 154 + self._preset_rects.clear() + if has_presets: + chip_h = 52.0 + chip_gap = 16.0 + chip_w = max(68.0, (dialog_w - 48 * 2 - chip_gap * (len(self._presets) - 1)) / max(1, len(self._presets))) + for index, val in enumerate(self._presets): + chip_x = dx + 48 + index * (chip_w + chip_gap) + chip_rect = _snap_rect(rl.Rectangle(chip_x, presets_y, chip_w, chip_h)) + self._preset_rects.append((val, chip_rect)) + formatted_label = format_adjustor_value(val, step=self.step, unit=self._unit, labels=self._labels) + self._render_preset_chip( + chip_rect, + formatted_label, + current=abs(self._current_val - val) <= 0.5 * self.step, + pressed=self._pressed_zone == f"preset:{val}", + ) + slider_y = dy + 254 + else: + slider_y = dy + 180 + + # Slider + btn_size = 54 + self._minus_rect = _snap_rect(rl.Rectangle(dx + 48, slider_y - btn_size / 2, btn_size, btn_size)) + self._plus_rect = _snap_rect(rl.Rectangle(dx + dialog_w - 48 - btn_size, slider_y - btn_size / 2, btn_size, btn_size)) + + # Draw minus button + minus_pressed = self._pressed_zone == "minus" + _draw_rounded_fill(self._minus_rect, rl.Color(255, 255, 255, 14 if minus_pressed else 8), radius_px=27) + _draw_rounded_stroke(self._minus_rect, rl.Color(255, 255, 255, 28 if minus_pressed else 18), radius_px=27) + mts = measure_text_cached(self._font_btn, "-", 24) + rl.draw_text_ex(self._font_btn, "-", rl.Vector2(round(self._minus_rect.x + (btn_size - mts.x) / 2), round(self._minus_rect.y + (btn_size - mts.y) / 2)), 24, 0, rl.WHITE) + + # Draw plus button + plus_pressed = self._pressed_zone == "plus" + _draw_rounded_fill(self._plus_rect, rl.Color(255, 255, 255, 14 if plus_pressed else 8), radius_px=27) + _draw_rounded_stroke(self._plus_rect, rl.Color(255, 255, 255, 28 if plus_pressed else 18), radius_px=27) + pts = measure_text_cached(self._font_btn, "+", 24) + rl.draw_text_ex(self._font_btn, "+", rl.Vector2(round(self._plus_rect.x + (btn_size - pts.x) / 2), round(self._plus_rect.y + (btn_size - pts.y) / 2)), 24, 0, rl.WHITE) + + # Draw track + track_x = self._minus_rect.x + btn_size + 24 + track_w = self._plus_rect.x - 24 - track_x + track_h = 6 + track_y = slider_y - track_h / 2 + self._track_rect = _snap_rect(rl.Rectangle(track_x, track_y, track_w, track_h)) + + _draw_rounded_fill(self._track_rect, rl.Color(255, 255, 255, 14), radius_px=3) + _draw_rounded_stroke(self._track_rect, rl.Color(255, 255, 255, 8), radius_px=3) + + # Draw ticks at preset values (or custom ticks if no presets) + ticks_to_draw = self._presets + if not has_presets: + ticks_to_draw = [self.min_val, (self.min_val + self.max_val) / 2, self.max_val] + + for val in ticks_to_draw: + frac = self._value_fraction(val) + tick_x = track_x + frac * track_w + rl.draw_rectangle_rec(rl.Rectangle(tick_x - 1, track_y - 5, 2, 16), rl.Color(255, 255, 255, 28)) + + # Draw active fill + fill_frac = self._value_fraction(self._current_val) + fill_w = fill_frac * track_w + if fill_w > 0: + fill_rect = _snap_rect(rl.Rectangle(track_x, track_y, fill_w, track_h)) + _draw_rounded_fill(fill_rect, self._color, radius_px=3) + + # Draw thumb + thumb_w = 16 + thumb_h = 36 + thumb_x = track_x + fill_frac * track_w + self._thumb_rect = _snap_rect(rl.Rectangle(thumb_x - thumb_w / 2, slider_y - thumb_h / 2, thumb_w, thumb_h)) + _draw_rounded_fill(self._thumb_rect, rl.WHITE, radius_px=8) + _draw_rounded_stroke(self._thumb_rect, rl.Color(20, 22, 28, 46), radius_px=8) + + # Cancel Button c_face_x = self._cancel_rect.x c_face_y = self._cancel_rect.y + min(1.0, GEOMETRY_OFFSET * self._cancel_offset * 0.1) c_face = _snap_rect(rl.Rectangle(c_face_x, c_face_y, button_width, button_height)) - _draw_rounded_fill(c_face, rl.Color(34, 38, 48, 255), radius_px=16) - _draw_rounded_stroke(c_face, rl.Color(255, 255, 255, 24), radius_px=16) - rl.draw_rectangle_rec(rl.Rectangle(c_face.x, c_face.y, c_face.width, 1), rl.Color(255, 255, 255, 12)) - button_text_size = max(24, min(35, int(button_height * 0.42))) - cts = measure_text_cached(self._font_btn, tr("CANCEL"), button_text_size) - cancel_text_pos = rl.Vector2(c_face_x + (button_width - cts.x) / 2, c_face_y + (button_height - cts.y) / 2) - rl.draw_text_ex(self._font_btn, tr("CANCEL"), rl.Vector2(round(cancel_text_pos.x), round(cancel_text_pos.y)), button_text_size, 0, rl.WHITE) + _draw_rounded_fill(c_face, rl.Color(34, 38, 48, 255), radius_px=18) + _draw_rounded_stroke(c_face, rl.Color(255, 255, 255, 20), radius_px=18) + cts = measure_text_cached(self._font_btn, tr("CANCEL"), 24) + rl.draw_text_ex(self._font_btn, tr("CANCEL"), rl.Vector2(round(c_face_x + (button_width - cts.x) / 2), round(c_face_y + (button_height - cts.y) / 2)), 24, 0, rl.WHITE) + + # OK Button o_face_x = self._ok_rect.x o_face_y = self._ok_rect.y + min(1.0, GEOMETRY_OFFSET * self._ok_offset * 0.1) o_face = _snap_rect(rl.Rectangle(o_face_x, o_face_y, button_width, button_height)) - _draw_rounded_fill(o_face, _mix_colors(rl.Color(34, 38, 48, 255), self._color, 0.40), radius_px=16) - _draw_rounded_stroke(o_face, _with_alpha(self._color, 130), radius_px=16) - rl.draw_rectangle_rec(rl.Rectangle(o_face.x, o_face.y, o_face.width, 1), rl.Color(255, 255, 255, 18)) - ots = measure_text_cached(self._font_btn, tr("OK"), button_text_size) - ok_text_pos = rl.Vector2(o_face_x + (button_width - ots.x) / 2, o_face_y + (button_height - ots.y) / 2) - rl.draw_text_ex(self._font_btn, tr("OK"), rl.Vector2(round(ok_text_pos.x), round(ok_text_pos.y)), button_text_size, 0, rl.WHITE) + _draw_rounded_fill(o_face, self._color, radius_px=18) + _draw_rounded_stroke(o_face, _with_alpha(self._color, 150), radius_px=18) + ots = measure_text_cached(self._font_btn, tr("OK"), 24) + rl.draw_text_ex(self._font_btn, tr("OK"), rl.Vector2(round(o_face_x + (button_width - ots.x) / 2), round(o_face_y + (button_height - ots.y) / 2)), 24, 0, rl.WHITE) return DialogResult.NO_ACTION diff --git a/selfdrive/ui/layouts/settings/starpilot/sounds.py b/selfdrive/ui/layouts/settings/starpilot/sounds.py index cbf9dc63a..0d2bb10e3 100644 --- a/selfdrive/ui/layouts/settings/starpilot/sounds.py +++ b/selfdrive/ui/layouts/settings/starpilot/sounds.py @@ -1,5 +1,6 @@ from __future__ import annotations from dataclasses import replace +import math import subprocess from pathlib import Path @@ -10,7 +11,8 @@ from openpilot.starpilot.common.starpilot_variables import ACTIVE_THEME_PATH from openpilot.system.ui.lib.application import gui_app, FontWeight, MouseEvent, MousePos from openpilot.system.ui.lib.multilang import tr, tr_noop from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 -from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets import Widget, DialogResult +from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets.label import gui_label from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state @@ -31,6 +33,7 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( draw_section_header, draw_settings_panel_header, init_list_panel, + AetherSliderDialog, ) PANEL_STYLE = panel_style_from_color("#E63956") @@ -58,8 +61,6 @@ class SoundsManagerView(Widget): self._controller = controller self._pressed_target: str | None = None self._adjustor_rows: dict[str, AetherAdjustorRow] = {} - self._was_interacting: dict[str, bool] = {} - self._active_adjustor_key: str | None = None self._can_click = True self._reset_rect = rl.Rectangle(0, 0, 0, 0) @@ -75,10 +76,6 @@ class SoundsManagerView(Widget): self._forward_touch_valid() def _forward_touch_valid(self): - for adjustor in self._adjustor_rows.values(): - adjustor.set_touch_valid_callback( - lambda: self._scroll_panel.is_touch_valid() or adjustor.is_interacting - ) self._toggle_grid.set_touch_valid_callback( lambda: self._scroll_panel.is_touch_valid() ) @@ -113,24 +110,18 @@ class SoundsManagerView(Widget): for key in self._controller.VOLUME_KEYS: info = self._controller.VOLUME_INFO[key] - def on_change(v, k=key, min_v=info["min"]): - new_v = int(v) - if new_v != 101 and new_v < min_v: - new_v = min_v - self._controller._params.put_int(k, new_v) - adjustor = AetherAdjustorRow( tr(info["title"]), tr(info["subtitle"]), 0.0, 101.0, 1.0, get_value=lambda k=key: float(self._controller._params.get_int(k, return_default=True, default=100)), - on_change=on_change, + on_change=lambda _v: None, on_commit=None, unit="%", labels={0.0: tr("Muted"), 101.0: tr("Auto")}, presets=[0, 25, 50, 75, 101], - is_active=lambda k=key: self._active_adjustor_key == k, - set_active=lambda active, k=key: self._set_active_adjustor(k, active), + is_active=lambda: False, + set_active=lambda active, k=key: self._show_volume_slider(k) if active else None, style=PANEL_STYLE, color=PANEL_STYLE.accent, ) @@ -138,8 +129,9 @@ class SoundsManagerView(Widget): cd_key = self._controller.COOLDOWN_KEY cd_info = self._controller.COOLDOWN_INFO - def on_cd_commit(v): - self._controller._params.put_int(cd_key, int(v)) + def on_cd_close(res, val): + if res == DialogResult.CONFIRM: + self._controller._params.put_int(cd_key, int(val)) cd_adjustor = AetherAdjustorRow( tr(cd_info["title"]), @@ -147,17 +139,69 @@ class SoundsManagerView(Widget): 0.0, float(cd_info["max"]), 1.0, get_value=lambda: float(self._controller._params.get_int(cd_key, return_default=True, default=0)), on_change=lambda _v: None, - on_commit=on_cd_commit, + on_commit=None, unit=" " + tr("min"), labels={0.0: tr("Off"), 1.0: tr("1 min")}, presets=[0, 1, 5, 10, 20, 30], - is_active=lambda: self._active_adjustor_key == cd_key, - set_active=lambda active: self._set_active_adjustor(cd_key, active), + is_active=lambda: False, + set_active=lambda active: gui_app.push_widget( + AetherSliderDialog( + title=tr(cd_info["title"]), + min_val=0.0, + max_val=float(cd_info["max"]), + step=1.0, + current_val=float(self._controller._params.get_int(cd_key, return_default=True, default=0)), + on_close=on_cd_close, + presets=[0.0, 1.0, 5.0, 10.0, 20.0, 30.0], + unit=" " + tr("min"), + labels={0.0: tr("Off"), 1.0: tr("1 min")}, + color=PANEL_STYLE.accent, + ) + ) if active else None, style=PANEL_STYLE, color=PANEL_STYLE.accent, ) self._adjustor_rows[cd_key] = cd_adjustor + def _show_volume_slider(self, key: str): + info = self._controller.VOLUME_INFO[key] + min_v = info["min"] + original_val = self._controller._params.get_int(key, return_default=True, default=100) + + def on_close(res, val): + if res == DialogResult.CONFIRM: + new_v = int(val) + if new_v != 101 and new_v < min_v: + new_v = min_v + self._controller._params.put_int(key, new_v) + else: + self._controller._params.put_int(key, original_val) + + def on_change(val): + new_v = int(val) + if new_v != 101 and new_v < min_v: + new_v = min_v + self._controller._params.put_int(key, new_v) + self._controller._test_sound(key) + + current_val = float(original_val) + dialog_title = tr(info["title"]) + gui_app.push_widget( + AetherSliderDialog( + title=dialog_title, + min_val=0.0, + max_val=101.0, + step=1.0, + current_val=current_val, + on_close=on_close, + presets=[0.0, 25.0, 50.0, 75.0, 101.0], + unit="%", + labels={0.0: tr("Muted"), 101.0: tr("Auto")}, + color=PANEL_STYLE.accent, + on_change=on_change, + ) + ) + def _handle_mouse_press(self, mouse_pos: MousePos): self._pressed_target = self._target_at(mouse_pos) self._can_click = True @@ -230,16 +274,6 @@ class SoundsManagerView(Widget): draw_list_scroll_fades(scroll_rect, self._content_height, self._scroll_offset, AetherListColors.PANEL_BG) - for key in self._controller.VOLUME_KEYS: - adjustor = self._adjustor_rows[key] - is_interacting = adjustor.is_interacting - if self._was_interacting.get(key, False) and not is_interacting: - self._controller._test_sound(key) - if adjustor._preset_applied: - adjustor._preset_applied = False - self._controller._test_sound(key) - self._was_interacting[key] = is_interacting - def _measure_content_height(self, content_width: float) -> float: col_width = (content_width - SECTION_GAP) / 2 left_h = 0.0 diff --git a/selfdrive/ui/tests/test_aethergrid.py b/selfdrive/ui/tests/test_aethergrid.py index 1a395cc2e..3511560fb 100644 --- a/selfdrive/ui/tests/test_aethergrid.py +++ b/selfdrive/ui/tests/test_aethergrid.py @@ -127,6 +127,10 @@ def _install_aethergrid_stubs(): widgets_mod.DialogResult = types.SimpleNamespace(CONFIRM=1, CANCEL=0, NO_ACTION=-1) sys.modules["openpilot.system.ui.widgets"] = widgets_mod + option_dialog_mod = types.ModuleType("openpilot.system.ui.widgets.option_dialog") + option_dialog_mod.MultiOptionDialog = type("MultiOptionDialog", (), {}) + sys.modules["openpilot.system.ui.widgets.option_dialog"] = option_dialog_mod + label_mod = types.ModuleType("openpilot.system.ui.widgets.label") label_mod.gui_label = lambda *a, **k: None sys.modules["openpilot.system.ui.widgets.label"] = label_mod @@ -143,7 +147,7 @@ def _install_panel_stubs(aethergrid): _register_modules({ SECTIONED_PANEL_MODULE_NAME: sectioned_mod, - "openpilot.common.params": types.SimpleNamespace(Params=type("Params", (), {})), + "openpilot.common.params": types.SimpleNamespace(Params=type("Params", (), {}), UnknownKeyName=Exception), MODULE_NAME: aethergrid, }) @@ -354,6 +358,44 @@ class TestAethergridContracts(unittest.TestCase): self.assertGreaterEqual(row_h, 0) self.assertLessEqual(title_h, layout._title_height) + def test_slider_dialog_on_change_callback(self): + mod = _import_aethergrid() + captured_changes = [] + captured_close = [] + + dialog = mod.AetherSliderDialog( + "Test", 0, 10, 1, 5, + on_close=lambda res, val: captured_close.append((res, val)), + color="#8B5CF6", + on_change=lambda val: captured_changes.append(val) + ) + + # Run render to compute button and track rects + dialog._render(mod.rl.Rectangle(0, 0, 1920, 1080)) + + # Mock mouse position hitting the plus rect and press it + plus_center = mod.rl.Vector2( + dialog._plus_rect.x + dialog._plus_rect.width / 2, + dialog._plus_rect.y + dialog._plus_rect.height / 2 + ) + + # Mock collision detection to return True when checking the plus button + old_collision = mod.rl.check_collision_point_rec + def mock_collision(point, rec): + if rec == dialog._plus_rect: + return True + return False + mod.rl.check_collision_point_rec = mock_collision + + try: + dialog._handle_mouse_press(plus_center) + dialog._handle_mouse_release(plus_center) + finally: + mod.rl.check_collision_point_rec = old_collision + + self.assertEqual(dialog._current_val, 6) + self.assertEqual(captured_changes, [6]) + if __name__ == "__main__": unittest.main()