From d016071df366eba47a74d4a0761494ed97469997 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Sat, 28 Feb 2026 03:26:18 -0800 Subject: [PATCH] NavWidget: clean up scroller access (#37480) * clean up * more * great clean ups * better name * remove useless _can_swipe_away * reorder * rename * state machine is nice but might be too much * Revert "state machine is nice but might be too much" This reverts commit f8952969243a2eac3ed5f84793ba7b0c0cdf24bf. * got a better name out of it though * clean up * clean up * rm! * rm * and this * and * clean up --- selfdrive/ui/mici/layouts/settings/device.py | 10 +---- .../ui/mici/layouts/settings/firehose.py | 10 ++--- system/ui/widgets/nav_widget.py | 42 +++++++++---------- system/ui/widgets/scroller.py | 23 ++++++++++ 4 files changed, 48 insertions(+), 37 deletions(-) diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index d7313cb5a..b6f6f71d6 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -7,8 +7,7 @@ from collections.abc import Callable from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.time_helpers import system_time_valid -from openpilot.system.ui.widgets.scroller import NavScroller -from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 +from openpilot.system.ui.widgets.scroller import NavRawScrollPanel, NavScroller from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2 from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog @@ -17,21 +16,16 @@ from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.widgets.label import MiciLabel from openpilot.system.ui.widgets.html_render import HtmlModal, HtmlRenderer from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID -class MiciFccModal(NavWidget): - BACK_TOUCH_AREA_PERCENTAGE = 0.1 - +class MiciFccModal(NavRawScrollPanel): def __init__(self, file_path: str | None = None, text: str | None = None): super().__init__() self._content = HtmlRenderer(file_path=file_path, text=text) - self._scroll_panel = GuiScrollPanel2(horizontal=False) - self._scroll_panel.set_enabled(lambda: self.enabled and not self._dragging_down) self._fcc_logo = gui_app.texture("icons_mici/settings/device/fcc_logo.png", 76, 64) def _render(self, rect: rl.Rectangle): diff --git a/selfdrive/ui/mici/layouts/settings/firehose.py b/selfdrive/ui/mici/layouts/settings/firehose.py index 9ad43a6a4..e5b6301ac 100644 --- a/selfdrive/ui/mici/layouts/settings/firehose.py +++ b/selfdrive/ui/mici/layouts/settings/firehose.py @@ -14,7 +14,7 @@ from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.system.ui.lib.multilang import tr, trn, tr_noop from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.nav_widget import NavWidget +from openpilot.system.ui.widgets.scroller import NavRawScrollPanel TITLE = tr_noop("Firehose Mode") DESCRIPTION = tr_noop( @@ -218,9 +218,5 @@ class FirehoseLayoutBase(Widget): time.sleep(self.UPDATE_INTERVAL) -class FirehoseLayout(FirehoseLayoutBase, NavWidget): - BACK_TOUCH_AREA_PERCENTAGE = 0.1 - - def __init__(self): - super().__init__() - self._scroll_panel.set_enabled(lambda: self.enabled and not self._dragging_down) +class FirehoseLayout(NavRawScrollPanel, FirehoseLayoutBase): + pass diff --git a/system/ui/widgets/nav_widget.py b/system/ui/widgets/nav_widget.py index d7d394215..acbec5eb6 100644 --- a/system/ui/widgets/nav_widget.py +++ b/system/ui/widgets/nav_widget.py @@ -14,11 +14,12 @@ NAV_BAR_MARGIN = 6 NAV_BAR_WIDTH = 205 NAV_BAR_HEIGHT = 8 -DISMISS_PUSH_OFFSET = 50 + NAV_BAR_MARGIN + NAV_BAR_HEIGHT # px extra to push down when dismissing -DISMISS_TIME_SECONDS = 2.0 +DISMISS_PUSH_OFFSET = NAV_BAR_MARGIN + NAV_BAR_HEIGHT + 50 # px extra to push down when dismissing class NavBar(Widget): + FADE_AFTER_SECONDS = 2.0 + def __init__(self): super().__init__() self.set_rect(rl.Rectangle(0, 0, NAV_BAR_WIDTH, NAV_BAR_HEIGHT)) @@ -37,7 +38,7 @@ class NavBar(Widget): self._fade_time = rl.get_time() def _render(self, _): - if rl.get_time() - self._fade_time > DISMISS_TIME_SECONDS: + if rl.get_time() - self._fade_time > self.FADE_AFTER_SECONDS: self._alpha = 0.0 alpha = self._alpha_filter.update(self._alpha) @@ -54,42 +55,37 @@ class NavWidget(Widget, abc.ABC): def __init__(self): super().__init__() + # State self._drag_start_pos: MousePos | None = None # cleared after certain amount of horizontal movement self._dragging_down = False # swiped down enough to trigger dismissing on release self._playing_dismiss_animation = False # released and animating away self._y_pos_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps, bounce=1) + # TODO: move this state into NavBar self._nav_bar = NavBar() self._nav_bar_show_time = 0.0 self._nav_bar_y_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) + def _back_enabled(self) -> bool: + # Children can override this to block swipe away, like when not at + # the top of a vertical scroll panel to prevent erroneous swipes + return True + def _handle_mouse_event(self, mouse_event: MouseEvent) -> None: - # FIXME: disabling this widget on new push_widget still causes this widget to track mouse events without mouse down super()._handle_mouse_event(mouse_event) if mouse_event.left_pressed: - # user is able to swipe away if starting near top of screen, or anywhere if scroller is at top + # user is able to swipe away if starting near top of screen self._y_pos_filter.update_alpha(0.04) in_dismiss_area = mouse_event.pos.y < self._rect.height * self.BACK_TOUCH_AREA_PERCENTAGE - # TODO: remove vertical scrolling and then this hacky logic to check if scroller is at top - scroller_at_top = False - vertical_scroller = False - # TODO: -20? snapping in WiFi dialog can make offset not be positive at the top - if hasattr(self, '_scroller'): - scroller_at_top = self._scroller.scroll_panel.get_offset() >= -20 and not self._scroller._horizontal - vertical_scroller = not self._scroller._horizontal - elif hasattr(self, '_scroll_panel'): - scroller_at_top = self._scroll_panel.get_offset() >= -20 and not self._scroll_panel._horizontal - vertical_scroller = not self._scroll_panel._horizontal - - # Vertical scrollers need to be at the top to swipe away to prevent erroneous swipes - if (not vertical_scroller and in_dismiss_area) or scroller_at_top: + if in_dismiss_area and self._back_enabled(): self._drag_start_pos = mouse_event.pos elif mouse_event.left_down: if self._drag_start_pos is not None: # block swiping away if too much horizontal or upward movement + # block (lock-in) threshold is higher than start dismissing horizontal_movement = abs(mouse_event.pos.x - self._drag_start_pos.x) > BLOCK_SWIPE_AWAY_THRESHOLD upward_movement = mouse_event.pos.y - self._drag_start_pos.y < -BLOCK_SWIPE_AWAY_THRESHOLD @@ -102,7 +98,9 @@ class NavWidget(Widget, abc.ABC): self._drag_start_pos = None elif mouse_event.left_released: + # reset rc for either slide up or down animation self._y_pos_filter.update_alpha(0.1) + # if far enough, trigger back navigation callback if self._drag_start_pos is not None: if mouse_event.pos.y - self._drag_start_pos.y > SWIPE_AWAY_THRESHOLD: @@ -116,10 +114,13 @@ class NavWidget(Widget, abc.ABC): new_y = 0.0 + if self._dragging_down: + self._nav_bar.set_alpha(1.0) + + # FIXME: disabling this widget on new push_widget still causes this widget to track mouse events without mouse down if not self.enabled: self._drag_start_pos = None - # TODO: why is this not in handle_mouse_event? have to hack above if self._drag_start_pos is not None: last_mouse_event = gui_app.last_mouse_event # push entire widget as user drags it away @@ -127,9 +128,6 @@ class NavWidget(Widget, abc.ABC): if new_y < SWIPE_AWAY_THRESHOLD: new_y /= 2 # resistance until mouse release would dismiss widget - if self._dragging_down: - self._nav_bar.set_alpha(1.0) - if self._playing_dismiss_animation: new_y = self._rect.height + DISMISS_PUSH_OFFSET diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index ca6492ae1..fb47f690b 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -444,3 +444,26 @@ class NavScroller(NavWidget, Scroller): super().__init__(**kwargs) # pass down enabled to child widget for nav stack + disable while swiping away NavWidget self._scroller.set_enabled(lambda: self.enabled and not self._dragging_down) + + def _back_enabled(self) -> bool: + # Vertical scrollers need to be at the top to swipe away to prevent erroneous swipes + # TODO: only used for offroad alerts, remove when horizontal + return self._scroller._horizontal or self._scroller.scroll_panel.get_offset() >= -20 # some tolerance + + +# TODO: only used for a few vertical scrollers, remove when horizontal +class NavRawScrollPanel(NavWidget): + # can swipe anywhere, only when at top + BACK_TOUCH_AREA_PERCENTAGE = 1.0 + + def __init__(self): + super().__init__() + self._scroll_panel = GuiScrollPanel2(horizontal=False) + self._scroll_panel.set_enabled(lambda: self.enabled and not self._dragging_down) + + def show_event(self): + super().show_event() + self._scroll_panel.set_offset(0) + + def _back_enabled(self) -> bool: + return self._scroll_panel.get_offset() >= -20