diff --git a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py index df04eeaee..061eba25b 100644 --- a/selfdrive/ui/layouts/settings/starpilot/aethergrid.py +++ b/selfdrive/ui/layouts/settings/starpilot/aethergrid.py @@ -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) diff --git a/selfdrive/ui/layouts/settings/starpilot/sounds.py b/selfdrive/ui/layouts/settings/starpilot/sounds.py index 0feba95e1..acf9aaaaa 100644 --- a/selfdrive/ui/layouts/settings/starpilot/sounds.py +++ b/selfdrive/ui/layouts/settings/starpilot/sounds.py @@ -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") },