BigUP WIP: Sound Polish

This commit is contained in:
firestarsdog
2026-05-31 13:53:06 -04:00
parent 86d059bdb4
commit a7d8d7a544
3 changed files with 345 additions and 72 deletions
@@ -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
@@ -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
+43 -1
View File
@@ -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()