diff --git a/selfdrive/ui/layouts/settings/starpilot/vehicle.py b/selfdrive/ui/layouts/settings/starpilot/vehicle.py index de50e6e96b..cbb94c98c5 100644 --- a/selfdrive/ui/layouts/settings/starpilot/vehicle.py +++ b/selfdrive/ui/layouts/settings/starpilot/vehicle.py @@ -1,8 +1,8 @@ from __future__ import annotations +import math import threading import time -from dataclasses import replace import pyray as rl @@ -32,8 +32,9 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( draw_rounded_fill, draw_rounded_stroke, draw_settings_list_row, - draw_soft_card, - mix_colors, + draw_text_fit_common, + snap_rect, + with_alpha, ) from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state from openpilot.selfdrive.ui.lib.fingerprint_catalog import ( @@ -443,9 +444,6 @@ class VehicleSettingsManagerView(PanelManagerView): class ButtonActionComboDialog(Widget): - _DIALOG_W = 660 - _DIALOG_H = 440 - def __init__(self, controller: "StarPilotVehicleSettingsLayout", title: str, keys: tuple[str, ...], labels: tuple[str, ...]): super().__init__() @@ -453,60 +451,54 @@ class ButtonActionComboDialog(Widget): self._title = title self._keys = keys self._labels = labels - self._pressed_idx: int | None = None - self._pressed_close = False + self._snapshot = {key: controller._params.get_int(key) for key in keys} + self._pressed_zone: str | None = None self._font_title = gui_app.font(FontWeight.BOLD) self._font_btn = gui_app.font(FontWeight.SEMI_BOLD) - - def _layout(self): - sw = gui_app.width - sh = gui_app.height - dx = (sw - self._DIALOG_W) / 2 - dy = (sh - self._DIALOG_H) / 2 - d_rect = rl.Rectangle(int(dx), int(dy), int(self._DIALOG_W), int(self._DIALOG_H)) - - rows_area_y = dy + 72 - rows_area_h = len(self._keys) * 88 + 12 - rows_area = rl.Rectangle(int(dx + 40), int(rows_area_y), int(self._DIALOG_W - 80), int(rows_area_h)) - - row_rects = [] - for i in range(len(self._keys)): - row_rects.append(rl.Rectangle( - int(dx + 40), int(rows_area_y + i * 88), - int(self._DIALOG_W - 80), 88, - )) - - close_w = 320 - close_h = 64 - close_rect = rl.Rectangle( - int(dx + (self._DIALOG_W - close_w) / 2), - int(dy + self._DIALOG_H - 36 - close_h), - close_w, close_h, - ) - return d_rect, rows_area, row_rects, close_rect + self._ok_offset = 0.0 + self._cancel_offset = 0.0 + self._ok_target = 0.0 + self._cancel_target = 0.0 + self._row_rects: list[rl.Rectangle] = [] + self._cancel_rect = rl.Rectangle(0, 0, 0, 0) + self._ok_rect = rl.Rectangle(0, 0, 0, 0) def _handle_mouse_press(self, mouse_pos): - _, _, row_rects, close_rect = self._layout() - if rl.check_collision_point_rec(mouse_pos, close_rect): - self._pressed_close = True - return - for i, rect in enumerate(row_rects): + for i, rect in enumerate(self._row_rects): if rl.check_collision_point_rec(mouse_pos, rect): - self._pressed_idx = i + self._pressed_zone = f"row:{i}" return + if rl.check_collision_point_rec(mouse_pos, self._cancel_rect): + self._pressed_zone = "cancel" + self._cancel_target = 1.0 + return + if rl.check_collision_point_rec(mouse_pos, self._ok_rect): + self._pressed_zone = "ok" + self._ok_target = 1.0 def _handle_mouse_release(self, mouse_pos): - if self._pressed_idx is not None: - idx = self._pressed_idx - self._pressed_idx = None - _, _, row_rects, _ = self._layout() - if idx < len(row_rects) and rl.check_collision_point_rec(mouse_pos, row_rects[idx]): + zone = self._pressed_zone + self._pressed_zone = None + if not zone: + return + + if zone.startswith("row:"): + idx = int(zone.split(":")[1]) + if idx < len(self._row_rects) and rl.check_collision_point_rec(mouse_pos, self._row_rects[idx]): self._controller._show_action_picker(self._keys[idx]) return - if self._pressed_close: - self._pressed_close = False - _, _, _, close_rect = self._layout() - if rl.check_collision_point_rec(mouse_pos, close_rect): + + if zone == "cancel": + self._cancel_target = 0.0 + if rl.check_collision_point_rec(mouse_pos, self._cancel_rect): + for k, v in self._snapshot.items(): + self._controller._params.put_int(k, v) + gui_app.pop_widget() + return + + if zone == "ok": + self._ok_target = 0.0 + if rl.check_collision_point_rec(mouse_pos, self._ok_rect): gui_app.pop_widget() def _render(self, rect): @@ -514,63 +506,119 @@ class ButtonActionComboDialog(Widget): sh = gui_app.height rl.draw_rectangle(0, 0, sw, sh, rl.Color(0, 0, 0, 160)) - d_rect, rows_area, row_rects, close_rect = self._layout() + dialog_w = min(2320, int(rect.width - 40)) + dialog_h = max(min(1015, int(rect.height - 40)), 968) + dx = rect.x + (rect.width - dialog_w) / 2 + dy = rect.y + (rect.height - dialog_h) / 2 + d_rect = snap_rect(rl.Rectangle(int(dx), int(dy), dialog_w, dialog_h)) + + btn_y = int(dy + dialog_h - 160 - 87) + btn_pair_w = 600 + 40 + 600 + btn_start_x = int(dx + (dialog_w - btn_pair_w) / 2) + self._cancel_rect = rl.Rectangle(btn_start_x, btn_y, 600, 160) + self._ok_rect = rl.Rectangle(btn_start_x + 600 + 40, btn_y, 600, 160) + + row_margin = 80 + self._row_rects = [] + for i in range(len(self._keys)): + self._row_rects.append(rl.Rectangle( + int(dx + row_margin), int(dy + 130 + i * (170 + 40)), + dialog_w - 2 * row_margin, 170, + )) + mouse_pos = gui_app.last_mouse_event.pos - 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, 4), PANEL_STYLE.accent) + draw_rounded_fill(d_rect, rl.Color(10, 12, 16, 255), radius_px=35) + draw_rounded_stroke(d_rect, rl.Color(255, 255, 255, 16), radius_px=35) + rl.draw_rectangle_rec(rl.Rectangle(d_rect.x, d_rect.y, d_rect.width, 3), PANEL_STYLE.accent) - title_size = 32 + title_size = 64 ts = measure_text_cached(self._font_title, self._title, title_size) rl.draw_text_ex(self._font_title, self._title, - rl.Vector2(int(d_rect.x + (d_rect.width - ts.x) / 2), - int(d_rect.y + (64 - title_size) / 2)), + rl.Vector2(int(dx + (dialog_w - ts.x) / 2), int(dy + 87)), title_size, 0, rl.WHITE) - draw_list_group_shell(rows_area, style=PANEL_STYLE) + font_label = gui_app.font(FontWeight.MEDIUM) + font_value = gui_app.font(FontWeight.MEDIUM) + value_fs = 48 + title_fs = 48 for i in range(len(self._keys)): - row_rect = row_rects[i] + row_rect = self._row_rects[i] hovered = rl.check_collision_point_rec(mouse_pos, row_rect) - pressed = self._pressed_idx == i + pressed = self._pressed_zone == f"row:{i}" - draw_settings_list_row( - row_rect, - title=self._labels[i], - value=self._controller._get_action_name(self._keys[i]), - enabled=True, - hovered=hovered and (self._pressed_idx is None), - pressed=pressed, - is_last=(i == len(self._keys) - 1), - show_chevron=True, - title_size=46, - value_size=42, - style=PANEL_STYLE, - ) + bg = rl.Color(255, 255, 255, 4) + if pressed: + bg = rl.Color(255, 255, 255, 12) + elif hovered: + bg = rl.Color(255, 255, 255, 8) + draw_rounded_fill(row_rect, bg, radius_px=8) + draw_rounded_stroke(row_rect, rl.Color(255, 255, 255, 6), radius_px=8) - close_hovered = rl.check_collision_point_rec(mouse_pos, close_rect) - close_pressed = self._pressed_close + if i < len(self._keys) - 1: + sep_y = int(row_rect.y + row_rect.height - 1) + rl.draw_line(int(row_rect.x + 24), sep_y, int(row_rect.x + row_rect.width - 24), sep_y, rl.Color(255, 255, 255, 16)) - if close_pressed: - close_fill = mix_colors(PANEL_STYLE.accent, rl.Color(0, 0, 0, 255), 0.2) - close_border = PANEL_STYLE.accent - elif close_hovered: - close_fill = PANEL_STYLE.accent - close_border = PANEL_STYLE.accent + title_x = int(row_rect.x + 24) + title_y = int(row_rect.y + (row_rect.height - title_fs) / 2) + rl.draw_text_ex(self._font_title, self._labels[i], rl.Vector2(title_x, title_y), title_fs, 0, rl.WHITE) + + action_name = self._controller._get_action_name(self._keys[i]) + value_left = int(title_x + 350) + value_right = int(row_rect.x + row_rect.width - 24) + available_w = value_right - value_left + text_w = measure_text_cached(font_label, action_name, value_fs).x + if text_w <= available_w: + val_x = value_right - text_w + val_y = int(row_rect.y + (row_rect.height - value_fs) / 2) + rl.draw_text_ex(font_value, action_name, rl.Vector2(val_x, val_y), value_fs, 0, rl.WHITE) + else: + draw_text_fit_common( + font_value, action_name, + rl.Vector2(value_left, int(row_rect.y + (row_rect.height - value_fs) / 2)), + available_w, value_fs, color=rl.WHITE, + ) + + dt = rl.get_frame_time() + self._ok_offset += (self._ok_target - self._ok_offset) * (1 - math.exp(-dt / 0.060)) + self._cancel_offset += (self._cancel_target - self._cancel_offset) * (1 - math.exp(-dt / 0.060)) + + c_y = self._cancel_rect.y + min(1.0, 10 * self._cancel_offset * 0.1) + c_face = snap_rect(rl.Rectangle(self._cancel_rect.x, c_y, 600, 160)) + c_hovered = rl.check_collision_point_rec(mouse_pos, self._cancel_rect) + c_pressed = self._pressed_zone == "cancel" + + if c_pressed: + c_fill = rl.Color(50, 54, 64, 255) + c_border = rl.Color(255, 255, 255, 40) + elif c_hovered: + c_fill = rl.Color(40, 44, 54, 255) + c_border = rl.Color(255, 255, 255, 30) else: - close_fill = rl.Color(255, 255, 255, 14) - close_border = rl.Color(255, 255, 255, 28) + c_fill = rl.Color(34, 38, 48, 255) + c_border = rl.Color(255, 255, 255, 20) - draw_soft_card(close_rect, close_fill, close_border) + draw_rounded_fill(c_face, c_fill, radius_px=41) + draw_rounded_stroke(c_face, c_border, radius_px=41) + c_text = tr("CANCEL") + cts = measure_text_cached(self._font_btn, c_text, 49) + rl.draw_text_ex(self._font_btn, c_text, + rl.Vector2(int(c_face.x + (c_face.width - cts.x) / 2), + int(c_face.y + (c_face.height - cts.y) / 2)), + 49, 0, rl.WHITE) - close_text = tr("Close") - close_size = 22 - cts = measure_text_cached(self._font_btn, close_text, close_size) - rl.draw_text_ex(self._font_btn, close_text, - rl.Vector2(int(close_rect.x + (close_rect.width - cts.x) / 2), - int(close_rect.y + (close_rect.height - cts.y) / 2)), - close_size, 0, rl.WHITE) + o_y = self._ok_rect.y + min(1.0, 10 * self._ok_offset * 0.1) + o_face = snap_rect(rl.Rectangle(self._ok_rect.x, o_y, 600, 160)) + + draw_rounded_fill(o_face, PANEL_STYLE.accent, radius_px=41) + draw_rounded_stroke(o_face, with_alpha(PANEL_STYLE.accent, 150), radius_px=41) + o_text = tr("OK") + ots = measure_text_cached(self._font_btn, o_text, 49) + rl.draw_text_ex(self._font_btn, o_text, + rl.Vector2(int(o_face.x + (o_face.width - ots.x) / 2), + int(o_face.y + (o_face.height - ots.y) / 2)), + 49, 0, rl.WHITE) class StarPilotVehicleSettingsLayout(_SettingsPage): diff --git a/system/ui/widgets/button.py b/system/ui/widgets/button.py index a4e37c7bf4..f6011ed8ab 100644 --- a/system/ui/widgets/button.py +++ b/system/ui/widgets/button.py @@ -47,7 +47,7 @@ BUTTON_DISABLED_TEXT_COLORS = { BUTTON_BACKGROUND_COLORS = { ButtonStyle.NORMAL: rl.Color(51, 51, 51, 255), - ButtonStyle.PRIMARY: rl.Color(70, 91, 234, 255), + ButtonStyle.PRIMARY: rl.Color(139, 92, 246, 255), ButtonStyle.DANGER: rl.Color(226, 44, 44, 255), ButtonStyle.TRANSPARENT: rl.BLACK, ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.BLANK, @@ -61,7 +61,7 @@ BUTTON_BACKGROUND_COLORS = { BUTTON_PRESSED_BACKGROUND_COLORS = { ButtonStyle.NORMAL: rl.Color(74, 74, 74, 255), - ButtonStyle.PRIMARY: rl.Color(48, 73, 244, 255), + ButtonStyle.PRIMARY: rl.Color(120, 78, 220, 255), ButtonStyle.DANGER: rl.Color(255, 36, 36, 255), ButtonStyle.TRANSPARENT: rl.BLACK, ButtonStyle.TRANSPARENT_WHITE_TEXT: rl.BLANK, diff --git a/system/ui/widgets/option_dialog.py b/system/ui/widgets/option_dialog.py index 8e6fe56d55..60282f3c37 100644 --- a/system/ui/widgets/option_dialog.py +++ b/system/ui/widgets/option_dialog.py @@ -46,7 +46,7 @@ class MultiOptionDialog(Widget): def _render(self, rect): dialog_rect = rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - 2 * MARGIN, rect.height - 2 * MARGIN) - rl.draw_rectangle_rounded(dialog_rect, 0.02, 20, rl.Color(30, 30, 30, 255)) + rl.draw_rectangle_rounded(dialog_rect, 0.02, 20, rl.Color(10, 12, 16, 255)) content_rect = rl.Rectangle(dialog_rect.x + MARGIN, dialog_rect.y + MARGIN, dialog_rect.width - 2 * MARGIN, dialog_rect.height - 2 * MARGIN)