From 8d83e0cdcd3b623fe4f6a4244a89137002471997 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:05:33 -0500 Subject: [PATCH] Backport Mici UI polish from SP-Dev-Dom (exclude wifi_manager) --- .../settings/horizontal_scroll_indicator.png | Bin 0 -> 2176 bytes .../ui/mici/layouts/settings/developer.py | 2 +- selfdrive/ui/mici/layouts/settings/device.py | 15 +++- .../mici/layouts/settings/network/__init__.py | 12 ++- .../ui/mici/layouts/settings/settings.py | 2 +- selfdrive/ui/mici/layouts/settings/toggles.py | 2 +- selfdrive/ui/mici/widgets/button.py | 21 ++++- selfdrive/ui/mici/widgets/dialog.py | 10 ++- selfdrive/ui/qt/offroad/settings.cc | 22 ++++- selfdrive/ui/qt/offroad/settings.h | 1 + system/ui/mici_setup.py | 12 ++- system/ui/widgets/label.py | 55 +++++++++++- system/ui/widgets/scroller.py | 81 ++++++++++++++++-- system/ui/widgets/slider.py | 54 ++++++++---- 14 files changed, 252 insertions(+), 37 deletions(-) create mode 100644 selfdrive/assets/icons_mici/settings/horizontal_scroll_indicator.png diff --git a/selfdrive/assets/icons_mici/settings/horizontal_scroll_indicator.png b/selfdrive/assets/icons_mici/settings/horizontal_scroll_indicator.png new file mode 100644 index 0000000000000000000000000000000000000000..4b8486578e5647f54bc114ac5059232c2be70e8b GIT binary patch literal 2176 zcmV-`2!Hp9P)U}FOr?|wLen&rwLbEwTXR>R zjS=m&UA%Mev$0$AXuadr@xy(3^JME%K6?GD@sv@VBI=OqNxzbe zQRuyESCT%bDN{hEuz1&0z<^RFDQ63sQ7=pd(-N}I=M+6cj>k+%8I`2Y^564}>4eo! zC(Et=?p>$X{Wb#O}l;in= z+9RBY=(wd(Zh3-J0+O+Su^6CjoPa~fYnrLeBK;TP@9m+K$RA3$nr)K9^~l-c*L0C* zZPQakk4B-W*Kc&33}RMVB(3##dX9eVL6f}O8QSE2?chJd38xVJg_73)X(gN`^#Zp3 z#iUjE^OoPS3;VwHziiJfXv7nO=rHB*UWGqzeM9(Mo|B|wPl*sR<+yjP_Gd|hCM)2M zqs8AT-EdR{s12{T68<9wG!9z|Jt6!nb_Hm19-*)V)Um}`_-Dco&=1Tl%V(&$ZHX;E zGb)Y?5ibtBTod7Iz-0x55+ePBwjvmgE;7CoMo;h~0s1|Z1zNWH8;%Ni#nH2G@PQm! ze+a@7fE6w>;je=KwxuXvw_H8tf;u<@^-iWouH9Khw6>rQ?-^<$e?={P$LpFdIC{!s zzA6Z#UL-3Z1j<4V!LR8%az$utxZvyKz}eWK*w2=Kle7w+*W~8bq%ie73-CEWaoGjm zPy`G92bH5f6m-E+5j-Bx%K~%dk9bJ-Ud!qMw=I9gQTQ*L3|>(0aF%4OgYX}PLl8i-j53_~IOpV$?^ zp1{nzK>XFzQYynoz)$RQ0(N*~0crbf%dcBM-G0aXRUCL;M<=(S?m(s6P!7gFC=XBI zcyJZwVj;gv2^l&34}m(P;caOMKZ0|101b*0^Ml+r)CFUOBBumagzk8?2{S;gfJ_;z1XhNg5u69;p(@+syM23W*Mxy*#eWW*&f--$ zE-7?7RC$!c=&yrczcd!OZ7T1U9CPe3-2ZPN_5cmAEcCE;y=rlB6Q~stZq@%0?w25}4G33U7BPJ&zx_v8JsGN{(5b7ofGs#D%4(~i#p9FX}L z@ayFN7|&J> z90ebq;l3HW7=VP?-$A-whMG_SD?t^Zu_KEQK~)fr2CYE54t_y>_W{Fre5%whWDGw# z_l(bn8}+b^I0md`4#IpLy{1+8>(}@b z<&P+06oPb4w`PuY3`M`TGJAuI4Fz5aZ-wvxISXhEUQJq;*IZ3^w{20fD5!- z|B)CHZP<%}-D2Ix8C8S^;w%n0Xvg!Luxv#&XD?8}smsRxKf5K&ZVFW>5I$OM6-v%3 z#jQD00vp=RDShI-0yO3_@$dhmSsg$)0QIblYbQXz&CGE2{_p35KcfRB(G0u!_KYx^EW7E^9{MAR=y+b0jzZ^TOb z=l^a2Y&9;ht~5)X_8AePZU*#uWUP;PQMPsm%LN^!xnIm5&H-pMlGQnQVaA-*j|f)V zmx1R@iK8B)RFXa|K_hvt1pkDf()jlT4Ooi148AnW;A@l${1~J0A+UQIYzwp%oAKCs zZj$?lA=L+&0fn%~uxnLA;eB*QDB1h7bBFT$%f?P|GXMFdHwKqy*N`=(yCl_O7DF`G zIxWW self._rect.width + self._scroll_offset = 0 + self._scroll_timer = 0 + self._scroll_state = ScrollState.PRE_SCROLL + + def _set_label_font_size_override(self, font_size: int | None): + self._label_font_size_override = font_size + self._refresh_label_metrics() + def set_rotate_icon(self, rotate: bool): if rotate and self._rotate_icon_t is not None: return @@ -165,10 +178,12 @@ class BigButton(Widget): def set_text(self, text: str): self.text = text self._label.set_text(text) + self._refresh_label_metrics() def set_value(self, value: str): self.value = value self._sub_label.set_text(value) + self._refresh_label_metrics() def get_value(self) -> str: return self.value @@ -256,7 +271,7 @@ class BigToggle(BigButton): self._checked = initial_state self._toggle_callback = toggle_callback - self._label.set_font_size(48) + self._set_label_font_size_override(48) def _load_images(self): super()._load_images() @@ -296,8 +311,8 @@ class BigMultiToggle(BigToggle): self._select_callback = select_callback self._label.set_width(int(self._rect.width - LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width)) - # TODO: why isn't this automatic? - self._label.set_font_size(self._get_label_font_size()) + # Keep the title size stable when the selected option changes. + self._set_label_font_size_override(self._get_label_font_size()) self.set_value(self._options[0]) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index 34760925..5f201766 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -113,6 +113,14 @@ class BigConfirmationDialogV2(BigDialogBase): self._slider = BigSlider(title, icon_txt, confirm_callback=self._on_confirm) self._slider.set_enabled(lambda: not self._swiping_away) + def show_event(self): + super().show_event() + self._slider.show_event() + + def hide_event(self): + super().hide_event() + self._slider.hide_event() + def _on_confirm(self): if self._confirm_callback: self._confirm_callback() @@ -122,7 +130,7 @@ class BigConfirmationDialogV2(BigDialogBase): def _update_state(self): super()._update_state() if self._swiping_away and not self._slider.confirmed: - self._slider.reset() + self._slider.reset(reset_shimmer=False) def _render(self, _) -> DialogResult: self._slider.render(self._rect) diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index e54c6f6f..65568e71 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -363,6 +363,26 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { connect(dcamBtn, &ButtonControl::clicked, [=]() { emit showDriverView(); }); addItem(dcamBtn); + resetDmCalibBtn = new ButtonControl( + tr("Reset Driver Monitoring"), + tr("RESET"), + tr("Clears the saved driver monitoring wheel-side calibration if the device thinks you're seated on the wrong side. " + "Resetting will restart openpilot if the car is powered on.") + ); + connect(resetDmCalibBtn, &ButtonControl::clicked, [&]() { + if (!uiState()->engaged()) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to reset driver monitoring calibration?"), tr("Reset"), this)) { + if (!uiState()->engaged()) { + params.remove("IsRhdDetected"); + params.putBool("OnroadCycleRequested", true); + } + } + } else { + ConfirmationDialog::alert(tr("Disengage to Reset Driver Monitoring"), this); + } + }); + addItem(resetDmCalibBtn); + resetCalibBtn = new ButtonControl(tr("Reset Calibration"), tr("RESET"), ""); connect(resetCalibBtn, &ButtonControl::showDescriptionEvent, this, &DevicePanel::updateCalibDescription); connect(resetCalibBtn, &ButtonControl::clicked, [&]() { @@ -420,7 +440,7 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { }); QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { for (auto btn : findChildren()) { - if (btn != pair_device && btn != resetCalibBtn) { + if (btn != pair_device && btn != resetCalibBtn && btn != resetDmCalibBtn) { btn->setEnabled(offroad); } } diff --git a/selfdrive/ui/qt/offroad/settings.h b/selfdrive/ui/qt/offroad/settings.h index 438ee0eb..252b5062 100644 --- a/selfdrive/ui/qt/offroad/settings.h +++ b/selfdrive/ui/qt/offroad/settings.h @@ -94,6 +94,7 @@ private: ButtonControl *pair_galaxy; QPushButton *galaxy_qr_btn; ButtonControl *resetCalibBtn; + ButtonControl *resetDmCalibBtn; }; class TogglesPanel : public ListWidget { diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 3624fd77..079267b6 100644 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -133,11 +133,17 @@ class SoftwareSelectionPage(Widget): super().__init__() self._openpilot_slider = LargerSlider("slide to use\nstarpilot", use_openpilot_callback) - self._custom_software_slider = LargerSlider("slide to use\ncustom software", use_custom_software_callback, green=False) + self._custom_software_slider = LargerSlider("slide to use\ncustom software", use_custom_software_callback, + green=False, shimmer_offset=0.4) + + def show_event(self): + super().show_event() + self._openpilot_slider.show_event() + self._custom_software_slider.show_event() def reset(self): - self._openpilot_slider.reset() - self._custom_software_slider.reset() + self._openpilot_slider.reset(reset_shimmer=False) + self._custom_software_slider.reset(reset_shimmer=False) def _render(self, rect: rl.Rectangle): self._openpilot_slider.set_opacity(1.0 - self._custom_software_slider.slider_percentage) diff --git a/system/ui/widgets/label.py b/system/ui/widgets/label.py index 97b29308..7bfbd6dc 100644 --- a/system/ui/widgets/label.py +++ b/system/ui/widgets/label.py @@ -1,3 +1,4 @@ +import math from enum import IntEnum from collections.abc import Callable from itertools import zip_longest @@ -401,6 +402,12 @@ class UnifiedLabel(Widget): - Proper multiline vertical alignment - Height calculation for layout purposes """ + SHIMMER_BAND_WIDTH = 0.3 + SHIMMER_BLUR_RADIUS = 0.12 + SHIMMER_CYCLE_PERIOD = 2.5 + SHIMMER_SWEEP_FRACTION = 0.9 + SHIMMER_LOW_OPACITY = 0.65 + def __init__(self, text: str | Callable[[], str], font_size: int = DEFAULT_TEXT_SIZE, @@ -414,7 +421,8 @@ class UnifiedLabel(Widget): wrap_text: bool = True, scroll: bool = False, line_height: float = 1.0, - letter_spacing: float = 0.0): + letter_spacing: float = 0.0, + shimmer: bool = False): super().__init__() self._text = text self._font_size = font_size @@ -431,6 +439,8 @@ class UnifiedLabel(Widget): self._line_height = line_height * 0.9 self._letter_spacing = letter_spacing # 0.1 = 10% self._spacing_pixels = font_size * letter_spacing + self._shimmer = shimmer + self._shimmer_start_time = 0.0 # Scroll state self._scroll = scroll @@ -489,6 +499,13 @@ class UnifiedLabel(Widget): self._spacing_pixels = self._font_size * letter_spacing self._cached_text = None # Invalidate cache + def set_line_height(self, line_height: float): + """Update line height (multiplier, e.g., 1.0 = default).""" + new_line_height = line_height * 0.9 + if self._line_height != new_line_height: + self._line_height = new_line_height + self._cached_text = None + def set_font_weight(self, font_weight: FontWeight): """Update the font weight.""" if self._font_weight != font_weight: @@ -510,6 +527,9 @@ class UnifiedLabel(Widget): self._scroll_pause_t = None self._scroll_state = ScrollState.STARTING + def reset_shimmer(self, offset: float = 0.0): + self._shimmer_start_time = rl.get_time() + offset + def set_max_width(self, max_width: int | None): """Set the maximum width constraint for wrapping/eliding.""" if self._max_width != max_width: @@ -627,6 +647,25 @@ class UnifiedLabel(Widget): return self._cached_total_height return 0.0 + def _compute_shimmer_alpha(self, char_center_x: float, text_left: float, text_width: float) -> float: + if text_width <= 0: + return self.SHIMMER_LOW_OPACITY + + elapsed = rl.get_time() - self._shimmer_start_time + sigma = text_width * self.SHIMMER_BLUR_RADIUS + + t_raw = (elapsed % self.SHIMMER_CYCLE_PERIOD) / self.SHIMMER_CYCLE_PERIOD + t_clamped = max(0.0, min(t_raw / self.SHIMMER_SWEEP_FRACTION, 1.0)) + t = t_clamped * t_clamped * (3.0 - 2.0 * t_clamped) + + margin = text_width * self.SHIMMER_BAND_WIDTH + text_right = text_left + text_width + center = text_right + margin - t * (text_width + 2.0 * margin) + + d = char_center_x - center + shimmer = math.exp(-0.5 * d * d / (sigma * sigma)) if sigma > 0 else 0.0 + return self.SHIMMER_LOW_OPACITY + (1.0 - self.SHIMMER_LOW_OPACITY) * shimmer + def _render(self, _): """Render the label.""" if self._rect.width <= 0 or self._rect.height <= 0: @@ -770,6 +809,20 @@ class UnifiedLabel(Widget): line_x = self._rect.x + self._text_padding line_x += self._scroll_offset + x_offset + if self._shimmer and not emojis and line: + base_alpha = self._text_color.a / 255.0 + text_width = max(size.x, 1.0) + cursor_x = line_x + for char in line: + char_width = measure_text_cached(self._font, char, self._font_size, self._spacing_pixels).x + char_center_x = cursor_x + char_width / 2.0 + shimmer_alpha = self._compute_shimmer_alpha(char_center_x, line_x, text_width) + char_alpha = int(255 * base_alpha * shimmer_alpha) + char_color = rl.Color(self._text_color.r, self._text_color.g, self._text_color.b, char_alpha) + rl.draw_text_ex(self._font, char, rl.Vector2(cursor_x, current_y), self._font_size, self._spacing_pixels, char_color) + cursor_x += char_width + return + # Render line with emojis line_pos = rl.Vector2(line_x, current_y) prev_index = 0 diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index f33ba941..e941486a 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -11,6 +11,7 @@ ITEM_SPACING = 20 LINE_COLOR = rl.GRAY LINE_PADDING = 40 ANIMATION_SCALE = 0.6 +EDGE_SHADOW_WIDTH = 20 MIN_ZOOM_ANIMATION_TIME = 0.075 # seconds DO_ZOOM = False @@ -33,9 +34,50 @@ class LineSeparator(Widget): LINE_COLOR) +class ScrollIndicator(Widget): + def __init__(self): + super().__init__() + self._txt_scroll_indicator = gui_app.texture("icons_mici/settings/horizontal_scroll_indicator.png", 96, 48) + self._scroll_offset: float = 0.0 + self._content_size: float = 0.0 + self._viewport: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) + + def update(self, scroll_offset: float, content_size: float, viewport: rl.Rectangle) -> None: + self._scroll_offset = scroll_offset + self._content_size = content_size + self._viewport = viewport + + def _render(self, _): + if self._viewport.width <= 0 or self._viewport.height <= 0: + return + + indicator_w = min(float(np.interp(self._content_size, [1000, 3000], [300, 100])), self._viewport.width) + max_scroll = self._content_size - self._viewport.width + if max_scroll > 0: + scroll_ratio = -self._scroll_offset / max_scroll + slide_range = max(self._viewport.width - indicator_w, 0.0) + x = self._viewport.x + scroll_ratio * slide_range + else: + x = self._viewport.x + (self._viewport.width - indicator_w) / 2 + y = max(self._viewport.y, 0) + self._viewport.height - self._txt_scroll_indicator.height / 2 + + dest_left = max(x, self._viewport.x) + dest_right = min(x + indicator_w, self._viewport.x + self._viewport.width) + dest_w = max(indicator_w / 2, dest_right - dest_left) + + dest_left = min(dest_left, self._viewport.x + self._viewport.width - dest_w) + dest_left = max(dest_left, self._viewport.x) + + src_rec = rl.Rectangle(0, 0, self._txt_scroll_indicator.width, self._txt_scroll_indicator.height) + dest_rec = rl.Rectangle(dest_left, y, dest_w, self._txt_scroll_indicator.height) + rl.draw_texture_pro(self._txt_scroll_indicator, src_rec, dest_rec, rl.Vector2(0, 0), 0.0, + rl.Color(255, 255, 255, int(255 * 0.45))) + + class Scroller(Widget): def __init__(self, items: list[Widget], horizontal: bool = True, snap_items: bool = True, spacing: int = ITEM_SPACING, - line_separator: bool = False, pad_start: int = ITEM_SPACING, pad_end: int = ITEM_SPACING): + line_separator: bool = False, pad_start: int = ITEM_SPACING, pad_end: int = ITEM_SPACING, + scroll_indicator: bool = False, edge_shadows: bool = False): super().__init__() self._items: list[Widget] = [] self._horizontal = horizontal @@ -65,11 +107,18 @@ class Scroller(Widget): self.scroll_panel = GuiScrollPanel2(self._horizontal, handle_out_of_bounds=not self._snap_items) self._scroll_enabled: bool | Callable[[], bool] = True - self._txt_scroll_indicator = gui_app.texture("icons_mici/settings/vertical_scroll_indicator.png", 40, 80) + self._txt_vertical_scroll_indicator = gui_app.texture("icons_mici/settings/vertical_scroll_indicator.png", 40, 80) + self._show_scroll_indicator = scroll_indicator and self._horizontal + self._scroll_indicator = ScrollIndicator() + self._edge_shadows = edge_shadows and self._horizontal for item in items: self.add_widget(item) + @property + def items(self) -> list[Widget]: + return self._items + def set_reset_scroll_at_show(self, scroll: bool): self._reset_scroll_at_show = scroll @@ -93,6 +142,14 @@ class Scroller(Widget): self._items.append(item) item.set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid() and self.enabled) + def move_item(self, from_index: int, to_index: int) -> None: + if from_index == to_index: + return + if not (0 <= from_index < len(self._items) and 0 <= to_index < len(self._items)): + return + item = self._items.pop(from_index) + self._items.insert(to_index, item) + def set_scrolling_enabled(self, enabled: bool | Callable[[], bool]) -> None: """Set whether scrolling is enabled (does not affect widget enabled state).""" self._scroll_enabled = enabled @@ -243,13 +300,27 @@ class Scroller(Widget): # Draw scroll indicator if SCROLL_BAR and not self._horizontal and len(self._visible_items) > 0: - _real_content_size = self._content_size - self._rect.height + self._txt_scroll_indicator.height + _real_content_size = self._content_size - self._rect.height + self._txt_vertical_scroll_indicator.height scroll_bar_y = -self._scroll_offset / _real_content_size * self._rect.height - scroll_bar_y = min(max(scroll_bar_y, self._rect.y), self._rect.y + self._rect.height - self._txt_scroll_indicator.height) - rl.draw_texture_ex(self._txt_scroll_indicator, rl.Vector2(self._rect.x, scroll_bar_y), 0, 1.0, rl.WHITE) + scroll_bar_y = min(max(scroll_bar_y, self._rect.y), self._rect.y + self._rect.height - self._txt_vertical_scroll_indicator.height) + rl.draw_texture_ex(self._txt_vertical_scroll_indicator, rl.Vector2(self._rect.x, scroll_bar_y), 0, 1.0, rl.WHITE) rl.end_scissor_mode() + if self._edge_shadows: + rl.draw_rectangle_gradient_h(int(self._rect.x), int(self._rect.y), + EDGE_SHADOW_WIDTH, int(self._rect.height), + rl.Color(0, 0, 0, 166), rl.BLANK) + + right_x = int(self._rect.x + self._rect.width - EDGE_SHADOW_WIDTH) + rl.draw_rectangle_gradient_h(right_x, int(self._rect.y), + EDGE_SHADOW_WIDTH, int(self._rect.height), + rl.BLANK, rl.Color(0, 0, 0, 166)) + + if self._show_scroll_indicator and len(self._visible_items) > 0: + self._scroll_indicator.update(self._scroll_offset, self._content_size, self._rect) + self._scroll_indicator.render() + def show_event(self): super().show_event() if self._reset_scroll_at_show: diff --git a/system/ui/widgets/slider.py b/system/ui/widgets/slider.py index 455cdeef..e606c5c3 100644 --- a/system/ui/widgets/slider.py +++ b/system/ui/widgets/slider.py @@ -5,17 +5,19 @@ import pyray as rl from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter class SmallSlider(Widget): HORIZONTAL_PADDING = 8 CONFIRM_DELAY = 0.2 + PRESSED_SCALE = 1.07 - def __init__(self, title: str, confirm_callback: Callable | None = None): + def __init__(self, title: str, confirm_callback: Callable | None = None, shimmer_offset: float = 0.0): # TODO: unify this with BigConfirmationDialogV2 super().__init__() self._confirm_callback = confirm_callback + self._shimmer_offset = shimmer_offset self._font = gui_app.font(FontWeight.DISPLAY) @@ -30,29 +32,40 @@ class SmallSlider(Widget): self._start_x_circle = 0.0 self._scroll_x_circle = 0.0 self._scroll_x_circle_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) + self._circle_scale_filter = BounceFilter(1.0, 0.1, 1 / gui_app.target_fps) + self._circle_press_time: float | None = None self._is_dragging_circle = False - self._label = UnifiedLabel(title, font_size=36, font_weight=FontWeight.MEDIUM, text_color=rl.Color(255, 255, 255, int(255 * 0.65)), + self._label = UnifiedLabel(title, font_size=36, font_weight=FontWeight.SEMI_BOLD, text_color=rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, line_height=0.9) + alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, line_height=0.9, shimmer=True) def _load_assets(self): self.set_rect(rl.Rectangle(0, 0, 316 + self.HORIZONTAL_PADDING * 2, 100)) self._bg_txt = gui_app.texture("icons_mici/setup/small_slider/slider_bg.png", 316, 100) self._circle_bg_txt = gui_app.texture("icons_mici/setup/small_slider/slider_red_circle.png", 100, 100) + self._circle_bg_pressed_txt = self._circle_bg_txt self._circle_arrow_txt = gui_app.texture("icons_mici/setup/small_slider/slider_arrow.png", 37, 32) @property def confirmed(self) -> bool: return self._confirmed_time > 0.0 - def reset(self): + def show_event(self): + super().show_event() + self.reset() + + def reset(self, reset_shimmer: bool = True): # reset all slider state self._is_dragging_circle = False self._confirmed_time = 0.0 self._confirm_callback_called = False + self._circle_press_time = None + self._circle_scale_filter.x = 1.0 + if reset_shimmer: + self._label.reset_shimmer(self._shimmer_offset) def set_opacity(self, opacity: float, smooth: bool = False): if smooth: @@ -83,6 +96,7 @@ class SmallSlider(Widget): if rl.check_collision_point_rec(mouse_event.pos, circle_button_rect): self._start_x_circle = mouse_event.pos.x self._is_dragging_circle = True + self._circle_press_time = rl.get_time() elif mouse_event.left_released: # swiped to left @@ -129,8 +143,9 @@ class SmallSlider(Widget): btn_x = bg_txt_x + self._bg_txt.width - self._circle_bg_txt.width + self._scroll_x_circle_filter.x btn_y = self._rect.y + (self._rect.height - self._circle_bg_txt.height) / 2 - if self._confirmed_time == 0.0 or self._scroll_x_circle > 0: - self._label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.65 * (1.0 - self.slider_percentage) * self._opacity_filter.x))) + label_alpha = int(255 * (1.0 - self.slider_percentage) * self._opacity_filter.x) + if label_alpha > 0: + self._label.set_text_color(rl.Color(255, 255, 255, label_alpha)) label_rect = rl.Rectangle( self._rect.x + 20, self._rect.y, @@ -139,18 +154,24 @@ class SmallSlider(Widget): ) self._label.render(label_rect) - # circle and arrow - rl.draw_texture_ex(self._circle_bg_txt, rl.Vector2(btn_x, btn_y), 0.0, 1.0, white) + circle_pressed = self._is_dragging_circle or self.confirmed or ( + self._circle_press_time is not None and rl.get_time() - self._circle_press_time < 0.075 + ) + circle_bg_txt = self._circle_bg_pressed_txt if circle_pressed else self._circle_bg_txt + scale = self._circle_scale_filter.update(self.PRESSED_SCALE if circle_pressed else 1.0) + scaled_btn_x = btn_x + (self._circle_bg_txt.width * (1 - scale)) / 2 + scaled_btn_y = btn_y + (self._circle_bg_txt.height * (1 - scale)) / 2 + rl.draw_texture_ex(circle_bg_txt, rl.Vector2(scaled_btn_x, scaled_btn_y), 0.0, scale, white) - arrow_x = btn_x + (self._circle_bg_txt.width - self._circle_arrow_txt.width) / 2 - arrow_y = btn_y + (self._circle_bg_txt.height - self._circle_arrow_txt.height) / 2 + arrow_x = scaled_btn_x + (self._circle_bg_txt.width * scale - self._circle_arrow_txt.width) / 2 + arrow_y = scaled_btn_y + (self._circle_bg_txt.height * scale - self._circle_arrow_txt.height) / 2 rl.draw_texture_ex(self._circle_arrow_txt, rl.Vector2(arrow_x, arrow_y), 0.0, 1.0, white) class LargerSlider(SmallSlider): - def __init__(self, title: str, confirm_callback: Callable | None = None, green: bool = True): + def __init__(self, title: str, confirm_callback: Callable | None = None, green: bool = True, shimmer_offset: float = 0.0): self._green = green - super().__init__(title, confirm_callback=confirm_callback) + super().__init__(title, confirm_callback=confirm_callback, shimmer_offset=shimmer_offset) def _load_assets(self): self.set_rect(rl.Rectangle(0, 0, 520 + self.HORIZONTAL_PADDING * 2, 115)) @@ -158,6 +179,7 @@ class LargerSlider(SmallSlider): self._bg_txt = gui_app.texture("icons_mici/setup/small_slider/slider_bg_larger.png", 520, 115) circle_fn = "slider_green_rounded_rectangle" if self._green else "slider_black_rounded_rectangle" self._circle_bg_txt = gui_app.texture(f"icons_mici/setup/small_slider/{circle_fn}.png", 180, 115) + self._circle_bg_pressed_txt = self._circle_bg_txt self._circle_arrow_txt = gui_app.texture("icons_mici/setup/small_slider/slider_arrow.png", 64, 55) @@ -165,15 +187,16 @@ class BigSlider(SmallSlider): def __init__(self, title: str, icon: rl.Texture, confirm_callback: Callable | None = None): self._icon = icon super().__init__(title, confirm_callback=confirm_callback) - self._label = UnifiedLabel(title, font_size=48, font_weight=FontWeight.DISPLAY, text_color=rl.Color(255, 255, 255, int(255 * 0.65)), + self._label = UnifiedLabel(title, font_size=48, font_weight=FontWeight.DISPLAY, text_color=rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, - line_height=0.875) + line_height=0.875, shimmer=True) def _load_assets(self): self.set_rect(rl.Rectangle(0, 0, 520 + self.HORIZONTAL_PADDING * 2, 180)) self._bg_txt = gui_app.texture("icons_mici/buttons/slider_bg.png", 520, 180) self._circle_bg_txt = gui_app.texture("icons_mici/buttons/button_circle.png", 180, 180) + self._circle_bg_pressed_txt = gui_app.texture("icons_mici/buttons/button_circle_hover.png", 180, 180) self._circle_arrow_txt = self._icon @@ -183,4 +206,5 @@ class RedBigSlider(BigSlider): self._bg_txt = gui_app.texture("icons_mici/buttons/slider_bg.png", 520, 180) self._circle_bg_txt = gui_app.texture("icons_mici/buttons/button_circle_red.png", 180, 180) + self._circle_bg_pressed_txt = gui_app.texture("icons_mici/buttons/button_circle_red_hover.png", 180, 180) self._circle_arrow_txt = self._icon