diff --git a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png index 4d83ed5cd..04ffc2435 100644 --- a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png +++ b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f27352a18194a1c819e9eaea89cfc11d2964402df0a28efa3ba60ae2d972fe67 -size 13108 +oid sha256:b7eb870d01e5bf6c421e204026a4ea08e177731f2d6b5b17c4ad43c90c1c3e78 +size 23549 diff --git a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png index 7aa7f0542..540b2029a 100644 --- a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png +++ b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25d66e42a28a3367eb40724d28652889089aa762438b475645269e0319c46009 -size 1431 +oid sha256:f7b3bb76ee2359076339285ea6bced5b680e5b919a1b7dee163f36cd819c9ea1 +size 1746 diff --git a/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png b/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png new file mode 100644 index 000000000..92993e3e0 --- /dev/null +++ b/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b7dce550c008ff7a65ed19ccf308ecf92cd0118bb544978b7dd7393c5c27ae5 +size 809 diff --git a/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png b/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png new file mode 100644 index 000000000..53a837afb --- /dev/null +++ b/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e102b8b2e71a25d9f818b37d6f75ed958430cb765a07ae50713995779fb6a886 +size 1388 diff --git a/selfdrive/assets/icons_mici/setup/orange_dm.png b/selfdrive/assets/icons_mici/setup/orange_dm.png new file mode 100644 index 000000000..74cce9d97 --- /dev/null +++ b/selfdrive/assets/icons_mici/setup/orange_dm.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38a108f96f85a154b698693b07f2e4214124b8f2545b7c4490cea0aa998d75fd +size 11855 diff --git a/selfdrive/assets/icons_mici/setup/small_button_disabled.png b/selfdrive/assets/icons_mici/setup/small_button_disabled.png new file mode 100644 index 000000000..da8bb3eef --- /dev/null +++ b/selfdrive/assets/icons_mici/setup/small_button_disabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ed258d8e0531c19705953ded065c6d5e14929728a2909d8d4e335898fa5d080 +size 4056 diff --git a/selfdrive/ui/mici/layouts/onboarding.py b/selfdrive/ui/mici/layouts/onboarding.py index 81109fb25..d35881341 100644 --- a/selfdrive/ui/mici/layouts/onboarding.py +++ b/selfdrive/ui/mici/layouts/onboarding.py @@ -1,11 +1,14 @@ from enum import IntEnum -from collections.abc import Callable import weakref +import math +import numpy as np import pyray as rl +from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.button import SmallButton +from openpilot.system.ui.widgets.button import SmallButton, SmallCircleIconButton from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.slider import SmallSlider from openpilot.system.ui.mici_setup import TermsHeader, TermsPage as SetupTermsPage @@ -24,11 +27,12 @@ class OnboardingState(IntEnum): class DriverCameraSetupDialog(DriverCameraDialog): - def __init__(self, confirm_callback: Callable): + def __init__(self): super().__init__(no_escape=True) - self.driver_state_renderer = DriverStateRenderer(confirm_mode=True, confirm_callback=confirm_callback) - self.driver_state_renderer.set_rect(rl.Rectangle(0, 0, 200, 200)) + self.driver_state_renderer = DriverStateRenderer(inset=True) + self.driver_state_renderer.set_rect(rl.Rectangle(0, 0, 120, 120)) self.driver_state_renderer.load_icons() + self.driver_state_renderer.set_force_active(True) def _render(self, rect): rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) @@ -42,11 +46,10 @@ class DriverCameraSetupDialog(DriverCameraDialog): # Position dmoji on opposite side from driver is_rhd = self.driver_state_renderer.is_rhd - driver_state_rect = ( - rect.x if is_rhd else rect.x + rect.width - self.driver_state_renderer.rect.width, - rect.y + (rect.height - self.driver_state_renderer.rect.height) / 2, + self.driver_state_renderer.set_position( + rect.x + 8 if is_rhd else rect.x + rect.width - self.driver_state_renderer.rect.width - 8, + rect.y + 8, ) - self.driver_state_renderer.set_position(*driver_state_rect) self.driver_state_renderer.render() self._draw_face_detection(rect) @@ -89,17 +92,54 @@ class TrainingGuidePreDMTutorial(SetupTermsPage): )) +class DMBadFaceDetected(SetupTermsPage): + def __init__(self, continue_callback, back_callback): + super().__init__(continue_callback, back_callback, continue_text="power off") + self._title_header = TermsHeader("make sure comma four can see your face", gui_app.texture("icons_mici/setup/orange_dm.png", 60, 60)) + self._dm_label = UnifiedLabel("Re-mount if your face is occluded or driver monitoring has difficulty tracking your face.", 42, FontWeight.ROMAN) + + @property + def _content_height(self): + return self._dm_label.rect.y + self._dm_label.rect.height - self._scroll_panel.get_offset() + + def _render_content(self, scroll_offset): + self._title_header.render(rl.Rectangle( + self._rect.x + 16, + self._rect.y + 16 + scroll_offset, + self._title_header.rect.width, + self._title_header.rect.height, + )) + + self._dm_label.render(rl.Rectangle( + self._rect.x + 16, + self._title_header.rect.y + self._title_header.rect.height + 16, + self._rect.width - 32, + self._dm_label.get_content_height(int(self._rect.width - 32)), + )) + + class TrainingGuideDMTutorial(Widget): + PROGRESS_DURATION = 4 + LOOKING_THRESHOLD_DEG = 30.0 + def __init__(self, continue_callback): super().__init__() - self._title_header = TermsHeader("fill the circle to continue", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60)) + self._back_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_question.png", 48, 48)) + self._back_button.set_click_callback(self._show_bad_face_page) + self._good_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 48, 35)) # Wrap the continue callback to restore settings def wrapped_continue_callback(): device.set_offroad_brightness(None) continue_callback() - self._dialog = DriverCameraSetupDialog(wrapped_continue_callback) + self._good_button.set_click_callback(wrapped_continue_callback) + self._good_button.set_enabled(False) + + self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps) + self._dialog = DriverCameraSetupDialog() + self._bad_face_page = DMBadFaceDetected(HARDWARE.shutdown, self._hide_bad_face_page) + self._should_show_bad_face_page = False # Disable driver monitoring model when device times out for inactivity def inactivity_callback(): @@ -107,9 +147,20 @@ class TrainingGuideDMTutorial(Widget): device.add_interactive_timeout_callback(inactivity_callback) + def _show_bad_face_page(self): + self._bad_face_page.show_event() + self.hide_event() + self._should_show_bad_face_page = True + + def _hide_bad_face_page(self): + self._bad_face_page.hide_event() + self.show_event() + self._should_show_bad_face_page = False + def show_event(self): super().show_event() self._dialog.show_event() + self._progress.x = 0.0 device.set_offroad_brightness(100) @@ -118,19 +169,91 @@ class TrainingGuideDMTutorial(Widget): if device.awake: ui_state.params.put_bool("IsDriverViewEnabled", True) + sm = ui_state.sm + if sm.recv_frame.get("driverMonitoringState", 0) == 0: + return + + dm_state = sm["driverMonitoringState"] + driver_data = self._dialog.driver_state_renderer.get_driver_data() + + if len(driver_data.faceOrientation) == 3: + pitch, yaw, _ = driver_data.faceOrientation + looking_center = abs(math.degrees(pitch)) < self.LOOKING_THRESHOLD_DEG and abs(math.degrees(yaw)) < self.LOOKING_THRESHOLD_DEG + else: + looking_center = False + + # stay at 100% once reached + if (dm_state.faceDetected and looking_center) or self._progress.x > 0.99: + slow = self._progress.x < 0.25 + duration = self.PROGRESS_DURATION * 2 if slow else self.PROGRESS_DURATION + self._progress.x += 1.0 / (duration * gui_app.target_fps) + self._progress.x = min(1.0, self._progress.x) + else: + self._progress.update(0.0) + + self._good_button.set_enabled(self._progress.x >= 0.999) + def _render(self, _): + if self._should_show_bad_face_page: + return self._bad_face_page.render(self._rect) + self._dialog.render(self._rect) - rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + self._rect.height - self._title_header.rect.height * 1.5 - 32), - int(self._rect.width), int(self._title_header.rect.height * 1.5 + 32), - rl.BLANK, rl.Color(0, 0, 0, 150)) - self._title_header.render(rl.Rectangle( - self._rect.x + 16, - self._rect.y + self._rect.height - self._title_header.rect.height - 16, - self._title_header.rect.width, - self._title_header.rect.height, + rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + self._rect.height - 80), + int(self._rect.width), 80, rl.BLANK, rl.BLACK) + + # draw white ring around dm icon to indicate progress + ring_thickness = 8 + + # DM icon is 120x120, positioned on opposite side from driver + dm_size = 120 + is_rhd = self._dialog.driver_state_renderer._is_rhd + dm_center_x = (self._rect.x + dm_size / 2 + 8) if is_rhd else (self._rect.x + self._rect.width - dm_size / 2 - 8) + dm_center_y = self._rect.y + dm_size / 2 + 8 + icon_edge_radius = dm_size / 2 + outer_radius = icon_edge_radius + 1 # 2px outward from icon edge + inner_radius = outer_radius - ring_thickness # Inset by ring_thickness + start_angle = 90.0 # Start from bottom + end_angle = start_angle + self._progress.x * 360.0 # Clockwise + + # Fade in alpha + current_angle = end_angle - start_angle + alpha = int(np.interp(current_angle, [0.0, 45.0], [0, 255])) + + # White to green + color_t = np.clip(np.interp(current_angle, [45.0, 360.0], [0.0, 1.0]), 0.0, 1.0) + r = int(np.interp(color_t, [0.0, 1.0], [255, 0])) + g = int(np.interp(color_t, [0.0, 1.0], [255, 255])) + b = int(np.interp(color_t, [0.0, 1.0], [255, 64])) + ring_color = rl.Color(r, g, b, alpha) + + rl.draw_ring( + rl.Vector2(dm_center_x, dm_center_y), + inner_radius, + outer_radius, + start_angle, + end_angle, + 36, + ring_color, + ) + + self._back_button.render(rl.Rectangle( + self._rect.x + 8, + self._rect.y + self._rect.height - self._back_button.rect.height, + self._back_button.rect.width, + self._back_button.rect.height, )) + self._good_button.render(rl.Rectangle( + self._rect.x + self._rect.width - self._good_button.rect.width - 8, + self._rect.y + self._rect.height - self._good_button.rect.height, + self._good_button.rect.width, + self._good_button.rect.height, + )) + + # rounded border + rl.draw_rectangle_rounded_lines_ex(self._rect, 0.2 * 1.02, 10, 50, rl.BLACK) + class TrainingGuideRecordFront(SetupTermsPage): def __init__(self, continue_callback): diff --git a/selfdrive/ui/mici/onroad/driver_camera_dialog.py b/selfdrive/ui/mici/onroad/driver_camera_dialog.py index 150e43656..bab3d6e6f 100644 --- a/selfdrive/ui/mici/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/mici/onroad/driver_camera_dialog.py @@ -94,9 +94,8 @@ class DriverCameraDialog(NavWidget): self._draw_eyes(rect, driver_data) # Position dmoji on opposite side from driver - dm_state = ui_state.sm["driverMonitoringState"] driver_state_rect = ( - rect.x if dm_state.isRHD else rect.x + rect.width - self.driver_state_renderer.rect.width, + rect.x if self.driver_state_renderer.is_rhd else rect.x + rect.width - self.driver_state_renderer.rect.width, rect.y + (rect.height - self.driver_state_renderer.rect.height) / 2, ) self.driver_state_renderer.set_position(*driver_state_rect) @@ -140,7 +139,7 @@ class DriverCameraDialog(NavWidget): # Show first event (only one should be active at a time) event_name_str = str(dm_state.events[0].name).split('.')[-1] - alignment = rl.GuiTextAlignment.TEXT_ALIGN_RIGHT if dm_state.isRHD else rl.GuiTextAlignment.TEXT_ALIGN_LEFT + alignment = rl.GuiTextAlignment.TEXT_ALIGN_RIGHT if self.driver_state_renderer.is_rhd else rl.GuiTextAlignment.TEXT_ALIGN_LEFT shadow_rect = rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height) gui_label(shadow_rect, event_name_str, font_size=40, font_weight=FontWeight.BOLD, diff --git a/selfdrive/ui/mici/onroad/driver_state.py b/selfdrive/ui/mici/onroad/driver_state.py index 3862c299a..356d7ac83 100644 --- a/selfdrive/ui/mici/onroad/driver_state.py +++ b/selfdrive/ui/mici/onroad/driver_state.py @@ -1,9 +1,7 @@ import pyray as rl -from collections.abc import Callable import numpy as np import math from cereal import log -from openpilot.system.hardware import PC from openpilot.common.filter_simple import FirstOrderFilter from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import Widget @@ -22,15 +20,11 @@ class DriverStateRenderer(Widget): LINES_ANGLE_INCREMENT = 5 LINES_STALE_ANGLES = 3.0 # seconds - def __init__(self, lines: bool = False, confirm_mode: bool = False, confirm_callback: Callable | None = None): + def __init__(self, lines: bool = False, inset: bool = False): super().__init__() self.set_rect(rl.Rectangle(0, 0, self.BASE_SIZE, self.BASE_SIZE)) - self._lines = lines or confirm_mode - - # In confirm mode, user must fill out the circle to confirm some action in the UI - self._confirm_mode = confirm_mode - self._confirm_callback = confirm_callback - self._confirm_angles: dict[int, float] = {} # angle: timestamp + self._lines = lines + self._inset = inset # In line mode, track smoothed angles assert 360 % self.LINES_ANGLE_INCREMENT == 0 @@ -54,12 +48,20 @@ class DriverStateRenderer(Widget): def load_icons(self): """Load or reload the driver face icon texture""" - self._dm_person = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_person.png", self._rect.width, self._rect.height) - self._dm_cone = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_cone.png", self._rect.width, self._rect.height) + cone_and_person_size = round(52 / self.BASE_SIZE * self._rect.width) + + # If inset is enabled, push cone and person smaller by 2x the current inset space + if self._inset: + # Current inset space = (rect.width - cone_and_person_size) / 2 + current_inset = (self._rect.width - cone_and_person_size) / 2 + # Reduce size by 2x the current inset (1x on each side) + cone_and_person_size = round(cone_and_person_size - current_inset * 2) + + self._dm_person = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_person.png", cone_and_person_size, cone_and_person_size) + self._dm_cone = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_cone.png", cone_and_person_size, cone_and_person_size) center_size = round(36 / self.BASE_SIZE * self._rect.width) self._dm_center = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_center.png", center_size, center_size) - background_size = round(52 / self.BASE_SIZE * self._rect.width) - self._dm_background = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_background.png", background_size, background_size) + self._dm_background = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_background.png", self._rect.width, self._rect.height) def set_should_draw(self, should_draw: bool): self._should_draw = should_draw @@ -87,11 +89,13 @@ class DriverStateRenderer(Widget): rl.draw_rectangle_lines_ex(self._rect, 1, rl.RED) rl.draw_texture(self._dm_background, - int(self._rect.x + (self._rect.width - self._dm_background.width) / 2), - int(self._rect.y + (self._rect.height - self._dm_background.height) / 2), + int(self._rect.x), + int(self._rect.y), rl.Color(255, 255, 255, int(255 * self._fade_filter.x))) - rl.draw_texture(self._dm_person, int(self._rect.x), int(self._rect.y), + rl.draw_texture(self._dm_person, + int(self._rect.x + (self._rect.width - self._dm_person.width) / 2), + int(self._rect.y + (self._rect.height - self._dm_person.height) / 2), rl.Color(255, 255, 255, int(255 * 0.9 * self._fade_filter.x))) if self.effective_active: @@ -124,38 +128,18 @@ class DriverStateRenderer(Widget): else: # remove old angles - now = rl.get_time() - self._confirm_angles = {angle: t for angle, t in self._confirm_angles.items() if now - t < self.LINES_STALE_ANGLES} - - looking_center = self._looking_center_filter.x > 0.2 for angle, f in self._head_angles.items(): dst_from_current = ((angle - self._rotation_filter.x) % 360) - 180 target = 1.0 if abs(dst_from_current) <= self.LINES_ANGLE_INCREMENT * 5 else 0.0 if not self._face_detected: target = 0.0 - if self._confirm_mode: - # Extra careful to not add angles when looking near center - if target > 0 and not looking_center: - self._confirm_angles[angle] = now - - # User is looking at area already confirmed, reduce target to indicate where they are - if angle in self._confirm_angles and target == 0: - target = 0.65 - # Reduce all line lengths when looking center if self._looking_center: target = np.interp(self._looking_center_filter.x, [0.0, 1.0], [target, 0.45]) f.update(target) - self._draw_line(angle, f, self._looking_center and angle not in self._confirm_angles) - - # if all lines placed, reset for next time and call callback - if self._confirm_mode: - if len(self._confirm_angles) >= 360 // self.LINES_ANGLE_INCREMENT: - self._confirm_angles = {} - if self._confirm_callback is not None: - self._confirm_callback() + self._draw_line(angle, f, self._looking_center) def _draw_line(self, angle: int, f: FirstOrderFilter, grey: bool): line_length = self._rect.width / 6 @@ -226,10 +210,7 @@ class DriverStateRenderer(Widget): rotation = math.degrees(math.atan2(pitch, yaw)) angle_diff = rotation - self._rotation_filter.x angle_diff = ((angle_diff + 180) % 360) - 180 - if PC and self._confirm_mode: - self._rotation_filter.x += 2 - else: - self._rotation_filter.update(self._rotation_filter.x + angle_diff) + self._rotation_filter.update(self._rotation_filter.x + angle_diff) if not self.should_draw: self._fade_filter.update(0.0) diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 84be5bc3d..fda35be6e 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -24,7 +24,7 @@ from openpilot.system.ui.widgets.button import (IconButton, SmallButton, WideRou SmallCircleIconButton, WidishRoundedButton, SmallRedPillButton, FullRoundedButton) from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.system.ui.widgets.slider import LargerSlider +from openpilot.system.ui.widgets.slider import LargerSlider, SmallSlider from openpilot.selfdrive.ui.mici.layouts.settings.network import WifiUIMici from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog @@ -198,15 +198,20 @@ class TermsPage(Widget): self._scroll_panel = GuiScrollPanel2(horizontal=False) self._continue_text = continue_text - self._continue_button: WideRoundedButton | FullRoundedButton - if back_callback is not None: + self._continue_slider: bool = continue_text in ("reboot", "power off") + self._continue_button: WideRoundedButton | FullRoundedButton | SmallSlider + if self._continue_slider: + self._continue_button = SmallSlider(continue_text, confirm_callback=continue_callback) + self._scroll_panel.set_enabled(lambda: not self._continue_button.is_pressed) + elif back_callback is not None: self._continue_button = WideRoundedButton(continue_text) else: self._continue_button = FullRoundedButton(continue_text) self._continue_button.set_enabled(False) self._continue_button.set_opacity(0.0) self._continue_button.set_touch_valid_callback(self._scroll_panel.is_touch_valid) - self._continue_button.set_click_callback(continue_callback) + if not self._continue_slider: + self._continue_button.set_click_callback(continue_callback) self._enable_back = back_callback is not None self._back_button = SmallButton(back_text) @@ -225,6 +230,10 @@ class TermsPage(Widget): self._back_button.set_opacity(0.0) self._scroll_down_indicator.set_opacity(1.0) + def show_event(self): + super().show_event() + self.reset() + @property @abstractmethod def _content_height(self): @@ -265,6 +274,11 @@ class TermsPage(Widget): rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + self._rect.height - 20), int(self._rect.width), 20, rl.BLANK, rl.BLACK) + # fade out back button as slider is moved + if self._continue_slider and scroll_offset <= self._scrolled_down_offset: + self._back_button.set_opacity(1.0 - self._continue_button.slider_percentage) + self._back_button.set_visible(self._continue_button.slider_percentage < 0.99) + self._back_button.render(rl.Rectangle( self._rect.x + 8, self._rect.y + self._rect.height - self._back_button.rect.height, @@ -275,6 +289,8 @@ class TermsPage(Widget): continue_x = self._rect.x + 8 if self._enable_back: continue_x = self._rect.x + self._rect.width - self._continue_button.rect.width - 8 + if self._continue_slider: + continue_x += 8 self._continue_button.render(rl.Rectangle( continue_x, self._rect.y + self._rect.height - self._continue_button.rect.height, diff --git a/system/ui/widgets/button.py b/system/ui/widgets/button.py index 34b2a51a4..9c0ea75b4 100644 --- a/system/ui/widgets/button.py +++ b/system/ui/widgets/button.py @@ -201,6 +201,7 @@ class SmallCircleIconButton(Widget): self._opacity_filter = FirstOrderFilter(1.0, 0.1, 1 / gui_app.target_fps) self._icon_bg_txt = gui_app.texture("icons_mici/setup/small_button.png", 100, 100) self._icon_bg_pressed_txt = gui_app.texture("icons_mici/setup/small_button_pressed.png", 100, 100) + self._icon_bg_disabled_txt = gui_app.texture("icons_mici/setup/small_button_disabled.png", 100, 100) self._icon_txt = icon_txt def set_opacity(self, opacity: float, smooth: bool = False): @@ -210,12 +211,18 @@ class SmallCircleIconButton(Widget): self._opacity_filter.x = opacity def _render(self, _): - bg_txt = self._icon_bg_pressed_txt if self.is_pressed else self._icon_bg_txt white = rl.Color(255, 255, 255, int(255 * self._opacity_filter.x)) + if not self.enabled: + bg_txt = self._icon_bg_disabled_txt + icon_white = rl.Color(255, 255, 255, int(white.a * 0.35)) + else: + bg_txt = self._icon_bg_pressed_txt if self.is_pressed else self._icon_bg_txt + icon_white = white + rl.draw_texture(bg_txt, int(self.rect.x), int(self.rect.y), white) icon_x = self.rect.x + (self.rect.width - self._icon_txt.width) / 2 icon_y = self.rect.y + (self.rect.height - self._icon_txt.height) / 2 - rl.draw_texture(self._icon_txt, int(icon_x), int(icon_y), white) + rl.draw_texture(self._icon_txt, int(icon_x), int(icon_y), icon_white) class SmallButton(Widget): diff --git a/system/ui/widgets/slider.py b/system/ui/widgets/slider.py index b17d8f3b7..455cdeef7 100644 --- a/system/ui/widgets/slider.py +++ b/system/ui/widgets/slider.py @@ -24,7 +24,7 @@ class SmallSlider(Widget): self._drag_threshold = -self._rect.width // 2 # State - self._opacity = 1.0 + self._opacity_filter = FirstOrderFilter(1.0, 0.1, 1 / gui_app.target_fps) self._confirmed_time = 0.0 self._confirm_callback_called = False # we keep dialog open by default, only call once self._start_x_circle = 0.0 @@ -54,8 +54,11 @@ class SmallSlider(Widget): self._confirmed_time = 0.0 self._confirm_callback_called = False - def set_opacity(self, opacity: float): - self._opacity = opacity + def set_opacity(self, opacity: float, smooth: bool = False): + if smooth: + self._opacity_filter.update(opacity) + else: + self._opacity_filter.x = opacity @property def slider_percentage(self): @@ -117,7 +120,7 @@ class SmallSlider(Widget): def _render(self, _): # TODO: iOS text shimmering animation - white = rl.Color(255, 255, 255, int(255 * self._opacity)) + white = rl.Color(255, 255, 255, int(255 * self._opacity_filter.x)) bg_txt_x = self._rect.x + (self._rect.width - self._bg_txt.width) / 2 bg_txt_y = self._rect.y + (self._rect.height - self._bg_txt.height) / 2 @@ -127,11 +130,11 @@ class SmallSlider(Widget): 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))) + self._label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.65 * (1.0 - self.slider_percentage) * self._opacity_filter.x))) label_rect = rl.Rectangle( self._rect.x + 20, self._rect.y, - self._rect.width - self._circle_bg_txt.width - 20 * 3, + self._rect.width - self._circle_bg_txt.width - 20 * 2.5, self._rect.height, ) self._label.render(label_rect)