BigUI WIP: Standardize Sliders

This commit is contained in:
firestarsdog
2026-05-11 01:09:15 -04:00
parent ee3d33526b
commit 5c412cea96
2 changed files with 122 additions and 215 deletions
@@ -791,11 +791,30 @@ def draw_settings_list_row(
separator_inset=separator_inset,
)
label_rect = rl.Rectangle(draw_rect.x + 24, draw_rect.y + 14, draw_rect.width * 0.55, 28)
subtitle_rect = rl.Rectangle(draw_rect.x + 24, draw_rect.y + 46, draw_rect.width * 0.60, 24)
gui_label(label_rect, title, title_size, resolved_title_color, FontWeight.MEDIUM)
# Compute text width based on right-side elements — no truncation, font-size reduces gracefully
text_left = draw_rect.x + 24
if toggle_value is not None:
text_right = draw_rect.x + draw_rect.width - AETHER_LIST_METRICS.toggle_width - AETHER_LIST_METRICS.toggle_right_inset - 12
elif value:
text_right = draw_rect.x + draw_rect.width - AETHER_LIST_METRICS.utility_value_right - 12
elif show_chevron:
text_right = draw_rect.x + draw_rect.width - AETHER_LIST_METRICS.utility_chevron_right - 26 - 12
else:
text_right = draw_rect.x + draw_rect.width - 24
text_width = max(100.0, text_right - text_left)
_draw_text_fit_common(
gui_app.font(FontWeight.MEDIUM), title,
rl.Vector2(text_left, draw_rect.y + 16),
text_width, title_size,
color=resolved_title_color,
)
if subtitle:
gui_label(subtitle_rect, subtitle, subtitle_size, resolved_subtitle_color, FontWeight.NORMAL)
_draw_text_fit_common(
gui_app.font(FontWeight.NORMAL), subtitle,
rl.Vector2(text_left, draw_rect.y + 54),
text_width, subtitle_size,
color=resolved_subtitle_color,
)
if toggle_value is not None:
draw_toggle_switch(draw_rect, bool(toggle_value), is_enabled=enabled, track_color=style.accent)
@@ -3080,122 +3099,6 @@ class TileGrid(Widget):
tile.render(_snap_rect(rl.Rectangle(row_x + c * (row_tile_w + self._gap), rect.y + r * (tile_h + self._gap), row_tile_w, tile_h)))
tile_idx += 1
class AetherContinuousSlider(Widget):
def __init__(self, min_val: float, max_val: float, step: float, current_val: float, on_change, title: str = "", unit: str = "", labels: dict | None = None, color: rl.Color | None = None):
super().__init__()
self.min_val = min_val
self.max_val = max_val
self.base_step = step
self.current_val = current_val
self.on_change = on_change
self.title = title
self.unit = unit
self.labels = labels or {}
self.color = color or rl.Color(54, 77, 239, 255)
self._is_dragging = False
self._pending_drag = False
self._press_start_x = 0.0
self._last_mouse_x = 0.0
self._smooth_value = current_val
self._font = gui_app.font(FontWeight.BOLD)
def _handle_mouse_press(self, mouse_pos: MousePos):
if rl.check_collision_point_rec(mouse_pos, self._rect):
self._pending_drag = True
self._is_dragging = False
self._press_start_x = mouse_pos.x
self._last_mouse_x = mouse_pos.x
def _handle_mouse_release(self, mouse_pos: MousePos):
del mouse_pos
self._pending_drag = False
if self._is_dragging:
self._is_dragging = False
def _handle_mouse_event(self, mouse_event: MouseEvent):
if not self._touch_valid():
self._pending_drag = False
self._is_dragging = False
return
if self._pending_drag and not self._is_dragging:
if abs(mouse_event.pos.x - self._press_start_x) > 12:
self._pending_drag = False
self._is_dragging = True
self._last_mouse_x = mouse_event.pos.x
else:
return
if self._is_dragging:
dt = rl.get_frame_time()
dx = mouse_event.pos.x - self._last_mouse_x
self._last_mouse_x = mouse_event.pos.x
velocity = abs(dx / max(dt, 0.001))
if velocity > 1500:
step = self.base_step * 10
elif velocity > 500:
step = self.base_step * 5
else:
step = self.base_step
self._update_val_from_absolute(mouse_event.pos.x, step)
def _update_val_from_absolute(self, mouse_x: float, step: float):
track_w = self._rect.width
if track_w <= 0: return
rel_x = max(0.0, min(1.0, (mouse_x - self._rect.x) / track_w))
val = self.min_val + rel_x * (self.max_val - self.min_val)
self._set_snapped_val(val, step)
def _set_snapped_val(self, val: float, step: float):
if step <= 0:
snapped = max(self.min_val, min(self.max_val, val))
if snapped != self.current_val:
self.current_val = snapped
self.on_change(self.current_val)
return
snapped = round((val - self.min_val) / step) * step + self.min_val
snapped = max(self.min_val, min(self.max_val, snapped))
if snapped != self.current_val:
self.current_val = snapped
self.on_change(self.current_val)
def _render(self, rect: rl.Rectangle):
rect = _snap_rect(rect)
self.set_rect(rect)
dt = rl.get_frame_time()
self._smooth_value += (self.current_val - self._smooth_value) * (1 - math.exp(-dt / 0.060))
_draw_rounded_fill(rect, rl.Color(28, 32, 40, 255), radius_px=18)
_draw_rounded_stroke(rect, rl.Color(255, 255, 255, 18), radius_px=18)
value_range = self.max_val - self.min_val
frac = 0.0 if value_range == 0 else max(0.0, min(1.0, (self._smooth_value - self.min_val) / value_range))
meter_h = max(4, int(rect.height * 0.12))
meter_rect = _snap_rect(rl.Rectangle(rect.x + 20, rect.y + rect.height - meter_h - 10, rect.width - 40, meter_h))
fill_w = frac * meter_rect.width
rl.draw_rectangle_rec(meter_rect, rl.Color(255, 255, 255, 14))
if fill_w > 0:
fill_rect = _snap_rect(rl.Rectangle(meter_rect.x, meter_rect.y, fill_w, meter_rect.height))
rl.draw_rectangle_rec(fill_rect, _with_alpha(self.color, 190))
title_size = max(18, min(24, int(rect.height * 0.34)))
value_size = max(18, min(24, int(rect.height * 0.34)))
title_y = rect.y + (rect.height - title_size) / 2 - 5
rl.draw_text_ex(self._font, self.title, rl.Vector2(round(rect.x + 24), round(title_y)), title_size, 0, AetherListColors.SUBTEXT)
val_str = self.labels.get(self.current_val, f"{int(self.current_val)}{self.unit}")
ts = measure_text_cached(self._font, val_str, value_size)
text_color = AetherListColors.HEADER
text_x = rect.x + rect.width - ts.x - 24
text_y = rect.y + (rect.height - ts.y) / 2 - 5
rl.draw_text_ex(self._font, val_str, rl.Vector2(round(text_x), round(text_y)), value_size, 0, text_color)
def draw_toggle_pill(rect: rl.Rectangle, is_on: bool, is_enabled: bool, title: str, status_str: str, hovered: bool, pressed: bool):
rect = _snap_rect(rect)
@@ -6,7 +6,7 @@ import pyray as rl
from openpilot.common.basedir import BASEDIR
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.application import gui_app, MouseEvent, MousePos
from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.ui_state import ui_state
@@ -14,7 +14,7 @@ from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import (
AETHER_LIST_METRICS,
AetherContinuousSlider,
AetherAdjustorRow,
AetherListColors,
DEFAULT_PANEL_STYLE,
_point_hits,
@@ -33,72 +33,88 @@ class SoundsManagerView(Widget):
super().__init__()
self._controller = controller
self._pressed_target: str | None = None
self._sliders: dict[str, AetherContinuousSlider] = {}
self._slider_was_dragging: dict[str, bool] = {}
self._adjustor_rows: dict[str, AetherAdjustorRow] = {}
self._was_interacting: dict[str, bool] = {}
self._toggle_rects: dict[str, rl.Rectangle] = {}
self._active_adjustor_key: str | None = None
self._init_sliders()
self._init_adjustors()
def _init_sliders(self):
def _set_active_adjustor(self, key: str, active: bool):
if active:
if self._active_adjustor_key and self._active_adjustor_key != key:
old = self._adjustor_rows.get(self._active_adjustor_key)
if old:
old.reset_interaction()
self._active_adjustor_key = key
elif self._active_adjustor_key == key:
self._active_adjustor_key = None
def _init_adjustors(self):
for key in self._controller.VOLUME_KEYS:
val = self._controller._params.get_int(key, return_default=True, default=100)
def on_change(v, k=key):
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 < self._controller.VOLUME_INFO[k]["min"]:
new_v = self._controller.VOLUME_INFO[k]["min"]
if new_v != 101 and new_v < min_v:
new_v = min_v
self._controller._params.put_int(k, new_v)
slider = AetherContinuousSlider(
min_val=0.0,
max_val=101.0,
step=1.0,
current_val=float(val),
adjustor = AetherAdjustorRow(
tr(info["title"]),
"",
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,
title=tr(self._controller.VOLUME_INFO[key]["title"]),
on_commit=None,
unit="%",
labels={0.0: tr("Muted"), 101.0: tr("Auto")},
color=AetherListColors.PRIMARY
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),
style=PANEL_STYLE,
color=AetherListColors.PRIMARY,
)
self._sliders[key] = slider
self._slider_was_dragging[key] = False
self._adjustor_rows[key] = adjustor
cd_val = self._controller._params.get_int(self._controller.COOLDOWN_KEY, return_default=True, default=0)
def on_cd_change(v):
self._controller._params.put_int(self._controller.COOLDOWN_KEY, int(v))
cd_slider = AetherContinuousSlider(
min_val=0.0,
max_val=float(self._controller.COOLDOWN_INFO["max"]),
step=1.0,
current_val=float(cd_val),
on_change=on_cd_change,
title=tr(self._controller.COOLDOWN_INFO["title"]),
cd_key = self._controller.COOLDOWN_KEY
def on_cd_commit(v):
self._controller._params.put_int(cd_key, int(v))
cd_adjustor = AetherAdjustorRow(
tr(self._controller.COOLDOWN_INFO["title"]),
"",
0.0, float(self._controller.COOLDOWN_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,
unit=" " + tr("min"),
labels={0.0: tr("Off"), 1.0: tr("1 minute")},
color=AetherListColors.PRIMARY
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),
style=PANEL_STYLE,
color=AetherListColors.PRIMARY,
)
self._sliders[self._controller.COOLDOWN_KEY] = cd_slider
self._slider_was_dragging[self._controller.COOLDOWN_KEY] = False
self._adjustor_rows[cd_key] = cd_adjustor
def _handle_mouse_press(self, mouse_pos: MousePos):
self._pressed_target = self._target_at(mouse_pos)
for slider in self._sliders.values():
slider._handle_mouse_press(mouse_pos)
for adjustor in self._adjustor_rows.values():
adjustor._handle_mouse_press(mouse_pos)
def _handle_mouse_release(self, mouse_pos: MousePos):
for slider in self._sliders.values():
slider._handle_mouse_release(mouse_pos)
for adjustor in self._adjustor_rows.values():
adjustor._handle_mouse_release(mouse_pos)
target = self._target_at(mouse_pos)
if self._pressed_target is not None and self._pressed_target == target:
self._activate_target(target)
self._pressed_target = None
def _handle_mouse_event(self, mouse_event: MouseEvent):
for slider in self._sliders.values():
slider._handle_mouse_event(mouse_event)
for adjustor in self._adjustor_rows.values():
adjustor._handle_mouse_event(mouse_event)
def _target_at(self, mouse_pos: MousePos) -> str | None:
for key, rect in self._toggle_rects.items():
@@ -127,78 +143,66 @@ class SoundsManagerView(Widget):
content_y = frame.header.y + actual_header_height
content_h = (frame.shell.y + frame.shell.height) - content_y - metrics.panel_padding_bottom
content_rect = rl.Rectangle(
frame.scroll.x,
content_y,
frame.scroll.width,
content_h
)
col_width = (content_rect.width - SECTION_GAP) / 2
left_col = rl.Rectangle(content_rect.x, content_rect.y, col_width, content_rect.height)
right_col = rl.Rectangle(content_rect.x + col_width + SECTION_GAP, content_rect.y, col_width, content_rect.height)
col_width = (frame.scroll.width - SECTION_GAP) / 2
col_left = rl.Rectangle(frame.scroll.x, content_y, col_width, content_h)
col_right = rl.Rectangle(frame.scroll.x + col_width + SECTION_GAP, content_y, col_width, content_h)
self._draw_volume_section(left_col)
self._draw_utility_section(right_col)
self._draw_volume_column(col_left)
self._draw_utility_column(col_right)
for key, slider in self._sliders.items():
is_dragging = slider._is_dragging
if self._slider_was_dragging[key] and not is_dragging:
if key in self._controller.VOLUME_KEYS:
self._controller._test_sound(key)
self._slider_was_dragging[key] = is_dragging
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)
self._was_interacting[key] = is_interacting
def _draw_header(self, rect: rl.Rectangle):
draw_settings_panel_header(rect, tr("Sounds & Alerts"), tr("Manage system volumes and custom alert toggles."), subtitle_size=24)
def _draw_volume_section(self, rect: rl.Rectangle):
num_volumes = len(self._controller.VOLUME_KEYS)
vol_row_h = rect.height / num_volumes
def _draw_volume_column(self, rect: rl.Rectangle):
current_y = rect.y
for key in self._controller.VOLUME_KEYS:
adjustor = self._adjustor_rows[key]
row_h = adjustor.measure_height(rect.width)
row_rect = rl.Rectangle(rect.x, current_y, rect.width, row_h)
adjustor.set_is_last(True)
adjustor.set_parent_rect(rect)
adjustor.render(row_rect)
current_y += row_h
for index, key in enumerate(self._controller.VOLUME_KEYS):
row_rect = rl.Rectangle(rect.x, rect.y + index * vol_row_h, rect.width, vol_row_h)
self._draw_slider_row(row_rect, key, self._controller.VOLUME_INFO[key])
def _draw_utility_column(self, rect: rl.Rectangle):
current_y = rect.y
def _draw_utility_section(self, rect: rl.Rectangle):
total_elements = 7 # 1 cooldown + 6 alerts
row_h = rect.height / total_elements
cd_key = self._controller.COOLDOWN_KEY
adjustor = self._adjustor_rows[cd_key]
row_h = adjustor.measure_height(rect.width)
adjustor.set_is_last(True)
adjustor.set_parent_rect(rect)
adjustor.render(rl.Rectangle(rect.x, current_y, rect.width, row_h))
current_y += row_h
# Cooldown Slider (Index 0)
cd_row_rect = rl.Rectangle(rect.x, rect.y, rect.width, row_h)
self._draw_slider_row(cd_row_rect, self._controller.COOLDOWN_KEY, self._controller.COOLDOWN_INFO)
# Custom Alert Toggle Pills (Indices 1 to 6)
for index, key in enumerate(self._controller.CUSTOM_ALERTS_KEYS):
row_rect = rl.Rectangle(rect.x, rect.y + (index + 1) * row_h, rect.width, row_h)
toggle_h = AETHER_LIST_METRICS.utility_row_height
for i, key in enumerate(self._controller.CUSTOM_ALERTS_KEYS):
row_rect = rl.Rectangle(rect.x, current_y, rect.width, toggle_h)
self._draw_toggle_row(row_rect, key, self._controller.ALERT_INFO[key])
def _draw_slider_row(self, rect: rl.Rectangle, key: str, info: dict):
slider = self._sliders[key]
padded_rect = rl.Rectangle(rect.x, rect.y + 4, rect.width - 12, rect.height - 8)
if not slider._is_dragging:
current_param = self._controller._params.get_int(key, return_default=True, default=100 if key != self._controller.COOLDOWN_KEY else 0)
if slider.current_val != current_param:
slider.current_val = float(current_param)
slider.render(padded_rect)
current_y += toggle_h
def _draw_toggle_row(self, rect: rl.Rectangle, key: str, info: dict):
padded_rect = rl.Rectangle(rect.x, rect.y + 4, rect.width - 12, rect.height - 8)
current_val = self._controller._params.get_bool(key)
is_enabled = info.get("is_enabled", lambda: True)()
mouse_pos = gui_app.last_mouse_event.pos
hovered = _point_hits(mouse_pos, padded_rect, pad_x=6, pad_y=6)
pressed = self._pressed_target == f"toggle:{key}"
status_str = tr("ON") if current_val else tr("OFF")
if not is_enabled: status_str = tr(info.get("disabled_label", "UNAVAILABLE"))
draw_toggle_pill(padded_rect, current_val, is_enabled, tr(info["title"]), status_str, hovered, pressed)
self._toggle_rects[key] = padded_rect
@@ -247,7 +251,7 @@ class StarPilotSoundsLayout(StarPilotPanel):
"GreenLightAlert": {"title": tr_noop("Green Light")},
"LeadDepartingAlert": {"title": tr_noop("Lead Departure")},
"LoudBlindspotAlert": {
"title": tr_noop("Loud Blindspot"),
"title": tr_noop("Loud Blindspot"),
"is_enabled": lambda: starpilot_state.car_state.hasBSM,
"disabled_label": tr_noop("Needs BSM")
},