mici ui: Scroller widget helpers (#37451)

* it's so dumb

* niceeee

* oh this is interesting

* this is actually epic

* clean up

* more clean up

* cmt

* super

* forgot

* top
This commit is contained in:
Shane Smiskol
2026-02-27 02:37:02 -08:00
committed by GitHub
parent fe39ffa55a
commit 1bf0fb3851
11 changed files with 60 additions and 118 deletions
+5 -14
View File
@@ -14,9 +14,9 @@ from openpilot.system.ui.lib.application import gui_app
ONROAD_DELAY = 2.5 # seconds
class MiciMainLayout(Widget):
class MiciMainLayout(Scroller):
def __init__(self):
super().__init__()
super().__init__(snap_items=True, spacing=0, pad=0, scroll_indicator=False, edge_shadows=False)
self._pm = messaging.PubMaster(['bookmarkButton'])
@@ -36,13 +36,12 @@ class MiciMainLayout(Widget):
# TODO: set parent rect and use it if never passed rect from render (like in Scroller)
widget.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
self._scroller = Scroller([
self._scroller.add_widgets([
self._alerts_layout,
self._home_layout,
self._onroad_layout,
], snap_items=True, spacing=0, pad=0, scroll_indicator=False, edge_shadows=False)
])
self._scroller.set_reset_scroll_at_show(False)
self._scroller.set_enabled(lambda: self.enabled) # for nav stack
# Disable scrolling when onroad is interacting with bookmark
self._scroller.set_scrolling_enabled(lambda: not self._onroad_layout.is_swiping_left())
@@ -62,14 +61,6 @@ class MiciMainLayout(Widget):
self._onroad_layout.set_click_callback(lambda: self._scroll_to(self._home_layout))
device.add_interactive_timeout_callback(self._on_interactive_timeout)
def show_event(self):
super().show_event()
self._scroller.show_event()
def hide_event(self):
super().hide_event()
self._scroller.hide_event()
def _scroll_to(self, layout: Widget):
layout_x = int(layout.rect.x)
self._scroller.scroll_to(layout_x, smooth=True)
@@ -83,7 +74,7 @@ class MiciMainLayout(Widget):
self._setup = True
# Render
self._scroller.render(self._rect)
super()._render(self._rect)
self._handle_transitions()
+3 -10
View File
@@ -186,19 +186,17 @@ class AlertItem(Widget):
rl.draw_texture(icon_texture, int(icon_x), int(icon_y), rl.WHITE)
class MiciOffroadAlerts(Widget):
class MiciOffroadAlerts(Scroller):
"""Offroad alerts layout with vertical scrolling."""
def __init__(self):
super().__init__()
# Create vertical scroller
super().__init__(horizontal=False, spacing=12, pad=0)
self.params = Params()
self.sorted_alerts: list[AlertData] = []
self.alert_items: list[AlertItem] = []
self._last_refresh = 0.0
# Create vertical scroller
self._scroller = Scroller([], horizontal=False, spacing=12, pad=0)
# Create empty state label
self._empty_label = UnifiedLabel(tr("no alerts"), 65, FontWeight.DISPLAY, rl.WHITE,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
@@ -290,14 +288,9 @@ class MiciOffroadAlerts(Widget):
def show_event(self):
"""Reset scroll position when shown and refresh alerts."""
super().show_event()
self._scroller.show_event()
self._last_refresh = time.monotonic()
self.refresh()
def hide_event(self):
super().hide_event()
self._scroller.hide_event()
def _update_state(self):
"""Periodically refresh alerts."""
# Refresh alerts periodically, not every frame
@@ -1,17 +1,14 @@
import pyray as rl
from openpilot.common.time_helpers import system_time_valid
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.widgets.scroller import NavScroller
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle, BigParamControl, BigCircleParamControl
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigInputDialog
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets.nav_widget import NavWidget
from openpilot.selfdrive.ui.layouts.settings.common import restart_needed_callback
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.widgets.ssh_key import SshKeyAction
class DeveloperLayoutMici(NavWidget):
class DeveloperLayoutMici(NavScroller):
def __init__(self):
super().__init__()
self.set_back_callback(gui_app.pop_widget)
@@ -61,7 +58,7 @@ class DeveloperLayoutMici(NavWidget):
toggle_callback=lambda checked: (gui_app.set_show_touches(checked),
gui_app.set_show_fps(checked)))
self._scroller = Scroller([
self._scroller.add_widgets([
self._adb_toggle,
self._ssh_toggle,
self._ssh_keys_btn,
@@ -105,16 +102,8 @@ class DeveloperLayoutMici(NavWidget):
def show_event(self):
super().show_event()
self._scroller.show_event()
self._update_toggles()
def hide_event(self):
super().hide_event()
self._scroller.hide_event()
def _render(self, rect: rl.Rectangle):
self._scroller.render(rect)
def _update_toggles(self):
ui_state.update_params()
+4 -14
View File
@@ -7,7 +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 Scroller
from openpilot.system.ui.widgets.scroller import NavScroller
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2
@@ -32,6 +32,7 @@ class MiciFccModal(NavWidget):
self.set_back_callback(gui_app.pop_widget)
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._swiping_away)
self._fcc_logo = gui_app.texture("icons_mici/settings/device/fcc_logo.png", 76, 64)
def _render(self, rect: rl.Rectangle):
@@ -266,7 +267,7 @@ class UpdateOpenpilotBigButton(BigButton):
self._waiting_for_updater_t = None
class DeviceLayoutMici(NavWidget):
class DeviceLayoutMici(NavScroller):
def __init__(self):
super().__init__()
@@ -313,7 +314,7 @@ class DeviceLayoutMici(NavWidget):
review_training_guide_btn.set_click_callback(lambda: gui_app.push_widget(TrainingGuide(completed_callback=gui_app.pop_widget)))
review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad())
self._scroller = Scroller([
self._scroller.add_widgets([
DeviceInfoLayoutMici(),
UpdateOpenpilotBigButton(),
PairBigButton(),
@@ -340,14 +341,3 @@ class DeviceLayoutMici(NavWidget):
def _offroad_transition(self):
self._power_off_btn.set_visible(ui_state.is_offroad())
def show_event(self):
super().show_event()
self._scroller.show_event()
def hide_event(self):
super().hide_event()
self._scroller.hide_event()
def _render(self, rect: rl.Rectangle):
self._scroller.render(rect)
@@ -224,3 +224,4 @@ class FirehoseLayout(FirehoseLayoutBase, NavWidget):
def __init__(self):
super().__init__()
self.set_back_callback(gui_app.pop_widget)
self._scroll_panel.set_enabled(lambda: self.enabled and not self._swiping_away)
@@ -1,13 +1,12 @@
import pyray as rl
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.widgets.scroller import NavScroller
from openpilot.selfdrive.ui.mici.layouts.settings.network.wifi_ui import WifiUIMici, WifiIcon
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigMultiToggle, BigParamControl, BigToggle
from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.lib.prime_state import PrimeType
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets.nav_widget import NavWidget
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, MeteredType, ConnectStatus, SecurityType, normalize_ssid
@@ -65,7 +64,7 @@ class WifiNetworkButton(BigButton):
rl.draw_texture_ex(self._lock_txt, (lock_x, lock_y), 0.0, 1.0, rl.WHITE)
class NetworkLayoutMici(NavWidget):
class NetworkLayoutMici(NavScroller):
def __init__(self):
super().__init__()
@@ -132,7 +131,7 @@ class NetworkLayoutMici(NavWidget):
self._cellular_metered_btn = BigParamControl("cellular metered", "GsmMetered", toggle_callback=self._toggle_cellular_metered)
# Main scroller ----------------------------------
self._scroller = Scroller([
self._scroller.add_widgets([
self._wifi_button,
self._network_metered_btn,
self._tethering_toggle_btn,
@@ -165,14 +164,12 @@ class NetworkLayoutMici(NavWidget):
def show_event(self):
super().show_event()
self._wifi_manager.set_active(True)
self._scroller.show_event()
# Process wifi callbacks while at any point in the nav stack
gui_app.set_nav_stack_tick(self._wifi_manager.process_callbacks)
def hide_event(self):
super().hide_event()
self._scroller.hide_event()
self._wifi_manager.set_active(False)
gui_app.set_nav_stack_tick(None)
@@ -213,6 +210,3 @@ class NetworkLayoutMici(NavWidget):
MeteredType.YES: 'metered',
MeteredType.NO: 'unmetered'
}.get(self._wifi_manager.current_network_metered, 'default'))
def _render(self, rect: rl.Rectangle):
self._scroller.render(rect)
@@ -9,8 +9,7 @@ from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog, BigConfir
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, LABEL_COLOR
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.nav_widget import NavWidget
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.widgets.scroller import NavScroller
from openpilot.system.ui.lib.wifi_manager import WifiManager, Network, SecurityType, normalize_ssid
@@ -271,15 +270,13 @@ class ForgetButton(Widget):
rl.draw_texture_ex(self._trash_txt, (trash_x, trash_y), 0, 1.0, rl.WHITE)
class WifiUIMici(NavWidget):
class WifiUIMici(NavScroller):
def __init__(self, wifi_manager: WifiManager):
super().__init__()
# Set up back navigation
self.set_back_callback(gui_app.pop_widget)
self._scroller = Scroller([])
self._loading_animation = LoadingAnimation()
self._wifi_manager = wifi_manager
@@ -294,17 +291,12 @@ class WifiUIMici(NavWidget):
def show_event(self):
# Clear scroller items and update from latest scan results
super().show_event()
self._scroller.show_event()
self._loading_animation.show_event()
self._wifi_manager.set_active(True)
self._scroller.items.clear()
# trigger button update on latest sorted networks
self._on_network_updated(self._wifi_manager.networks)
def hide_event(self):
super().hide_event()
self._scroller.hide_event()
def _on_network_updated(self, networks: list[Network]):
self._networks = {network.ssid: network for network in networks}
self._update_buttons()
@@ -389,7 +381,7 @@ class WifiUIMici(NavWidget):
self._loading_animation.show_event()
def _render(self, _):
self._scroller.render(self._rect)
super()._render(self._rect)
anim_w = 90
anim_x = self._rect.x + self._rect.width - anim_w
+3 -17
View File
@@ -1,7 +1,5 @@
import pyray as rl
from openpilot.common.params import Params
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.widgets.scroller import NavScroller
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMici
from openpilot.selfdrive.ui.mici.layouts.settings.network import NetworkLayoutMici
@@ -9,7 +7,6 @@ from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici
from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici
from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.widgets.nav_widget import NavWidget
class SettingsBigButton(BigButton):
@@ -17,7 +14,7 @@ class SettingsBigButton(BigButton):
return 64
class SettingsLayout(NavWidget):
class SettingsLayout(NavScroller):
def __init__(self):
super().__init__()
self._params = Params()
@@ -42,7 +39,7 @@ class SettingsLayout(NavWidget):
firehose_btn = SettingsBigButton("firehose", "", "icons_mici/settings/firehose.png", icon_size=(52, 62))
firehose_btn.set_click_callback(lambda: gui_app.push_widget(firehose_panel))
self._scroller = Scroller([
self._scroller.add_widgets([
toggles_btn,
network_btn,
device_btn,
@@ -56,14 +53,3 @@ class SettingsLayout(NavWidget):
self.set_back_callback(gui_app.pop_widget)
self._font_medium = gui_app.font(FontWeight.MEDIUM)
def show_event(self):
super().show_event()
self._scroller.show_event()
def hide_event(self):
super().hide_event()
self._scroller.hide_event()
def _render(self, rect: rl.Rectangle):
self._scroller.render(rect)
+3 -13
View File
@@ -1,17 +1,15 @@
import pyray as rl
from cereal import log
from openpilot.system.ui.widgets.scroller import Scroller
from openpilot.system.ui.widgets.scroller import NavScroller
from openpilot.selfdrive.ui.mici.widgets.button import BigParamControl, BigMultiParamToggle
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets.nav_widget import NavWidget
from openpilot.selfdrive.ui.layouts.settings.common import restart_needed_callback
from openpilot.selfdrive.ui.ui_state import ui_state
PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants
class TogglesLayoutMici(NavWidget):
class TogglesLayoutMici(NavScroller):
def __init__(self):
super().__init__()
self.set_back_callback(gui_app.pop_widget)
@@ -25,7 +23,7 @@ class TogglesLayoutMici(NavWidget):
record_mic = BigParamControl("record & upload mic audio", "RecordAudio", toggle_callback=restart_needed_callback)
enable_openpilot = BigParamControl("enable openpilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback)
self._scroller = Scroller([
self._scroller.add_widgets([
self._personality_toggle,
self._experimental_btn,
is_metric_toggle,
@@ -68,13 +66,8 @@ class TogglesLayoutMici(NavWidget):
def show_event(self):
super().show_event()
self._scroller.show_event()
self._update_toggles()
def hide_event(self):
super().hide_event()
self._scroller.hide_event()
def _update_toggles(self):
ui_state.update_params()
@@ -93,6 +86,3 @@ class TogglesLayoutMici(NavWidget):
# Refresh toggles from params to mirror external changes
for key, item in self._refresh_toggles:
item.set_checked(ui_state.params.get_bool(key))
def _render(self, rect: rl.Rectangle):
self._scroller.render(rect)
+1 -15
View File
@@ -69,8 +69,6 @@ class NavWidget(Widget, abc.ABC):
self._nav_bar_y_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
self._set_up = False
@property
def back_enabled(self) -> bool:
return self._back_enabled() if callable(self._back_enabled) else self._back_enabled
@@ -96,6 +94,7 @@ class NavWidget(Widget, abc.ABC):
self._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
@@ -138,19 +137,6 @@ class NavWidget(Widget, abc.ABC):
def _update_state(self):
super()._update_state()
# Disable self's scroller while swiping away
if not self._set_up:
self._set_up = True
if hasattr(self, '_scroller'):
# TODO: use touch_valid
original_enabled = self._scroller._enabled
self._scroller.set_enabled(lambda: self.enabled and not self._swiping_away and (original_enabled() if callable(original_enabled) else
original_enabled))
elif hasattr(self, '_scroll_panel'):
original_enabled = self._scroll_panel.enabled
self._scroll_panel.set_enabled(lambda: self.enabled and not self._swiping_away and (original_enabled() if callable(original_enabled) else
original_enabled))
if self._trigger_animate_in:
self._pos_filter.x = self._rect.height
self._nav_bar_y_filter.x = -NAV_BAR_MARGIN - NAV_BAR_HEIGHT
+31 -1
View File
@@ -7,6 +7,7 @@ from openpilot.common.swaglog import cloudlog
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2, ScrollState
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.nav_widget import NavWidget
ITEM_SPACING = 20
LINE_COLOR = rl.GRAY
@@ -66,7 +67,8 @@ class ScrollIndicator(Widget):
rl.Color(255, 255, 255, int(255 * 0.45)))
class Scroller(Widget):
class _Scroller(Widget):
"""Should use wrapper below to reduce boilerplate"""
def __init__(self, items: list[Widget], horizontal: bool = True, snap_items: bool = False, spacing: int = ITEM_SPACING,
pad: int = ITEM_SPACING, scroll_indicator: bool = True, edge_shadows: bool = True):
super().__init__()
@@ -414,3 +416,31 @@ class Scroller(Widget):
super().hide_event()
for item in self._items:
item.hide_event()
class Scroller(Widget):
"""Wrapper for _Scroller so that children do not need to call events or pass down enabled for nav stack."""
def __init__(self, **kwargs):
super().__init__()
self._scroller = _Scroller([], **kwargs)
# pass down enabled to child widget for nav stack
self._scroller.set_enabled(lambda: self.enabled)
def show_event(self):
super().show_event()
self._scroller.show_event()
def hide_event(self):
super().hide_event()
self._scroller.hide_event()
def _render(self, _):
self._scroller.render(self._rect)
class NavScroller(NavWidget, Scroller):
"""Full screen Scroller that properly supports nav stack w/ animations"""
def __init__(self, **kwargs):
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._swiping_away)