BigUI WIP: Sounds/Sliders

This commit is contained in:
firestarsdog
2026-04-14 17:28:18 -04:00
parent 19656bd01f
commit 2e20cd061f
3 changed files with 133 additions and 114 deletions
@@ -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):
@@ -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
@@ -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