From 5f33b2fb2dafb9957f539d9d94fc51cd4a82244a Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 30 Sep 2025 22:25:43 -0700 Subject: [PATCH] raylib: frame independent scroller (#36227) * rm that * almost * yess * some work * more * todo * okay viber is good once in a while * temp * chadder can't do this * revert * this was broken anyway * fixes * mouse wheel scroll * some clean up * kinda works * way better * can tap to stop * more clean up * more clean up * revert last mouse * fix * debug only * no print * ahh setup.py fps doesn't affect DEFAULT_FPS ofc * rest * fix text * fix touch valid for network --- selfdrive/ui/layouts/settings/firehose.py | 6 +- selfdrive/ui/widgets/offroad_alerts.py | 4 +- system/ui/lib/scroll_panel.py | 245 ++++++++-------------- system/ui/setup.py | 6 +- system/ui/text.py | 8 +- system/ui/widgets/html_render.py | 4 +- system/ui/widgets/network.py | 29 ++- system/ui/widgets/option_dialog.py | 4 +- system/ui/widgets/scroller.py | 5 +- 9 files changed, 122 insertions(+), 189 deletions(-) diff --git a/selfdrive/ui/layouts/settings/firehose.py b/selfdrive/ui/layouts/settings/firehose.py index b3db1fa5f0..7e1d3b61ba 100644 --- a/selfdrive/ui/layouts/settings/firehose.py +++ b/selfdrive/ui/layouts/settings/firehose.py @@ -71,7 +71,7 @@ class FirehoseLayout(Widget): content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height) # Handle scrolling and render with clipping - scroll_offset = self.scroll_panel.handle_scroll(rect, content_rect) + scroll_offset = self.scroll_panel.update(rect, content_rect) rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) self._render_content(rect, scroll_offset) rl.end_scissor_mode() @@ -106,9 +106,9 @@ class FirehoseLayout(Widget): return height - def _render_content(self, rect: rl.Rectangle, scroll_offset: rl.Vector2): + def _render_content(self, rect: rl.Rectangle, scroll_offset: float): x = int(rect.x + 40) - y = int(rect.y + 40 + scroll_offset.y) + y = int(rect.y + 40 + scroll_offset) w = int(rect.width - 80) # Title diff --git a/selfdrive/ui/widgets/offroad_alerts.py b/selfdrive/ui/widgets/offroad_alerts.py index da0ea287cf..6ca0ca2c40 100644 --- a/selfdrive/ui/widgets/offroad_alerts.py +++ b/selfdrive/ui/widgets/offroad_alerts.py @@ -109,7 +109,7 @@ class AbstractAlert(Widget, ABC): def _render_scrollable_content(self): content_total_height = self.get_content_height() content_bounds = rl.Rectangle(0, 0, self.scroll_panel_rect.width, content_total_height) - scroll_offset = self.scroll_panel.handle_scroll(self.scroll_panel_rect, content_bounds) + scroll_offset = self.scroll_panel.update(self.scroll_panel_rect, content_bounds) rl.begin_scissor_mode( int(self.scroll_panel_rect.x), @@ -120,7 +120,7 @@ class AbstractAlert(Widget, ABC): content_rect_with_scroll = rl.Rectangle( self.scroll_panel_rect.x, - self.scroll_panel_rect.y + scroll_offset.y, + self.scroll_panel_rect.y + scroll_offset, self.scroll_panel_rect.width, content_total_height, ) diff --git a/system/ui/lib/scroll_panel.py b/system/ui/lib/scroll_panel.py index e2296fd5ed..5dacae8210 100644 --- a/system/ui/lib/scroll_panel.py +++ b/system/ui/lib/scroll_panel.py @@ -1,189 +1,124 @@ -import time +import math import pyray as rl -from collections import deque from enum import IntEnum -from openpilot.system.ui.lib.application import gui_app, MouseEvent, MousePos +from openpilot.system.ui.lib.application import gui_app, MouseEvent +from openpilot.common.filter_simple import FirstOrderFilter # Scroll constants for smooth scrolling behavior -MOUSE_WHEEL_SCROLL_SPEED = 30 -INERTIA_FRICTION = 0.92 # The rate at which the inertia slows down -MIN_VELOCITY = 0.5 # Minimum velocity before stopping the inertia -DRAG_THRESHOLD = 12 # Pixels of movement to consider it a drag, not a click -BOUNCE_FACTOR = 0.2 # Elastic bounce when scrolling past boundaries -BOUNCE_RETURN_SPEED = 0.15 # How quickly it returns from the bounce -MAX_BOUNCE_DISTANCE = 150 # Maximum distance for bounce effect -FLICK_MULTIPLIER = 1.8 # Multiplier for flick gestures -VELOCITY_HISTORY_SIZE = 5 # Track velocity over multiple frames for smoother motion +MOUSE_WHEEL_SCROLL_SPEED = 50 +BOUNCE_RETURN_RATE = 5 # ~0.92 at 60fps +MIN_VELOCITY = 2 # px/s, changes from auto scroll to steady state +MIN_VELOCITY_FOR_CLICKING = 2 * 60 # px/s, accepts clicks while auto scrolling below this velocity +DRAG_THRESHOLD = 12 # pixels of movement to consider it a drag, not a click + +DEBUG = False class ScrollState(IntEnum): - IDLE = 0 - DRAGGING_CONTENT = 1 - DRAGGING_SCROLLBAR = 2 - BOUNCING = 3 + IDLE = 0 # Not dragging, content may be bouncing or scrolling with inertia + DRAGGING_CONTENT = 1 # User is actively dragging the content class GuiScrollPanel: - def __init__(self, show_vertical_scroll_bar: bool = False): + def __init__(self): self._scroll_state: ScrollState = ScrollState.IDLE self._last_mouse_y: float = 0.0 self._start_mouse_y: float = 0.0 # Track the initial mouse position for drag detection - self._offset = rl.Vector2(0, 0) - self._view = rl.Rectangle(0, 0, 0, 0) - self._show_vertical_scroll_bar: bool = show_vertical_scroll_bar - self._velocity_y = 0.0 # Velocity for inertia - self._is_dragging: bool = False - self._bounce_offset: float = 0.0 - self._velocity_history: deque[float] = deque(maxlen=VELOCITY_HISTORY_SIZE) + self._offset_filter_y = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) + self._velocity_filter_y = FirstOrderFilter(0.0, 0.05, 1 / gui_app.target_fps) self._last_drag_time: float = 0.0 - self._content_rect: rl.Rectangle | None = None - self._bounds_rect: rl.Rectangle | None = None - def handle_scroll(self, bounds: rl.Rectangle, content: rl.Rectangle) -> rl.Vector2: - # TODO: HACK: this class is driven by mouse events, so we need to ensure we have at least one event to process - for mouse_event in gui_app.mouse_events or [MouseEvent(MousePos(0, 0), 0, False, False, False, time.monotonic())]: + def update(self, bounds: rl.Rectangle, content: rl.Rectangle) -> float: + for mouse_event in gui_app.mouse_events: if mouse_event.slot == 0: self._handle_mouse_event(mouse_event, bounds, content) - return self._offset - def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, content: rl.Rectangle): - # Store rectangles for reference - self._content_rect = content - self._bounds_rect = bounds - - max_scroll_y = max(content.height - bounds.height, 0) - - # Start dragging on mouse press - if rl.check_collision_point_rec(mouse_event.pos, bounds) and mouse_event.left_pressed: - if self._scroll_state == ScrollState.IDLE or self._scroll_state == ScrollState.BOUNCING: - self._scroll_state = ScrollState.DRAGGING_CONTENT - if self._show_vertical_scroll_bar: - scrollbar_width = rl.gui_get_style(rl.GuiControl.LISTVIEW, rl.GuiListViewProperty.SCROLLBAR_WIDTH) - scrollbar_x = bounds.x + bounds.width - scrollbar_width - if mouse_event.pos.x >= scrollbar_x: - self._scroll_state = ScrollState.DRAGGING_SCROLLBAR - - # TODO: hacky - # when clicking while moving, go straight into dragging - self._is_dragging = abs(self._velocity_y) > MIN_VELOCITY - self._last_mouse_y = mouse_event.pos.y - self._start_mouse_y = mouse_event.pos.y - self._last_drag_time = mouse_event.t - self._velocity_history.clear() - self._velocity_y = 0.0 - self._bounce_offset = 0.0 + self._update_state(bounds, content) - # Handle active dragging - if self._scroll_state == ScrollState.DRAGGING_CONTENT or self._scroll_state == ScrollState.DRAGGING_SCROLLBAR: - if mouse_event.left_down: - delta_y = mouse_event.pos.y - self._last_mouse_y + return float(self._offset_filter_y.x) - # Track velocity for inertia - time_since_last_drag = mouse_event.t - self._last_drag_time - if time_since_last_drag > 0: - # TODO: HACK: /2 since we usually get two touch events per frame - drag_velocity = delta_y / time_since_last_drag / 60.0 / 2 # TODO: shouldn't be hardcoded - self._velocity_history.append(drag_velocity) - - self._last_drag_time = mouse_event.t - - # Detect actual dragging - total_drag = abs(mouse_event.pos.y - self._start_mouse_y) - if total_drag > DRAG_THRESHOLD: - self._is_dragging = True - - if self._scroll_state == ScrollState.DRAGGING_CONTENT: - # Add resistance at boundaries - if (self._offset.y > 0 and delta_y > 0) or (self._offset.y < -max_scroll_y and delta_y < 0): - delta_y *= BOUNCE_FACTOR - - self._offset.y += delta_y - elif self._scroll_state == ScrollState.DRAGGING_SCROLLBAR: - scroll_ratio = content.height / bounds.height - self._offset.y -= delta_y * scroll_ratio - - self._last_mouse_y = mouse_event.pos.y - - elif mouse_event.left_released: - # Calculate flick velocity - if self._velocity_history: - total_weight = 0 - weighted_velocity = 0.0 - - for i, v in enumerate(self._velocity_history): - weight = i + 1 - weighted_velocity += v * weight - total_weight += weight - - if total_weight > 0: - avg_velocity = weighted_velocity / total_weight - self._velocity_y = avg_velocity * FLICK_MULTIPLIER - - # Check bounds - if self._offset.y > 0 or self._offset.y < -max_scroll_y: - self._scroll_state = ScrollState.BOUNCING - else: - self._scroll_state = ScrollState.IDLE + def _update_state(self, bounds: rl.Rectangle, content: rl.Rectangle): + if DEBUG: + rl.draw_rectangle_lines(0, 0, abs(int(self._velocity_filter_y.x)), 10, rl.RED) # Handle mouse wheel - wheel_move = rl.get_mouse_wheel_move() - if wheel_move != 0: - self._velocity_y = 0.0 + self._offset_filter_y.x += rl.get_mouse_wheel_move() * MOUSE_WHEEL_SCROLL_SPEED + + max_scroll_distance = max(0, content.height - bounds.height) + if self._scroll_state == ScrollState.IDLE: + above_bounds, below_bounds = self._check_bounds(bounds, content) - if self._show_vertical_scroll_bar: - self._offset.y += wheel_move * (MOUSE_WHEEL_SCROLL_SPEED - 20) - rl.gui_scroll_panel(bounds, rl.ffi.NULL, content, self._offset, self._view) + # Decay velocity when idle + if abs(self._velocity_filter_y.x) > MIN_VELOCITY: + # Faster decay if bouncing back from out of bounds + friction = math.exp(-BOUNCE_RETURN_RATE * 1 / gui_app.target_fps) + self._velocity_filter_y.x *= friction ** 2 if (above_bounds or below_bounds) else friction else: - self._offset.y += wheel_move * MOUSE_WHEEL_SCROLL_SPEED + self._velocity_filter_y.x = 0.0 + + if above_bounds or below_bounds: + if above_bounds: + self._offset_filter_y.update(0) + else: + self._offset_filter_y.update(-max_scroll_distance) + + self._offset_filter_y.x += self._velocity_filter_y.x / gui_app.target_fps - if self._offset.y > 0 or self._offset.y < -max_scroll_y: - self._scroll_state = ScrollState.BOUNCING + elif self._scroll_state == ScrollState.DRAGGING_CONTENT: + # Mouse not moving, decay velocity + if not len(gui_app.mouse_events): + self._velocity_filter_y.update(0.0) - # Apply inertia (continue scrolling after mouse release) + # Settle to exact bounds + if abs(self._offset_filter_y.x) < 1e-2: + self._offset_filter_y.x = 0.0 + elif abs(self._offset_filter_y.x + max_scroll_distance) < 1e-2: + self._offset_filter_y.x = -max_scroll_distance + + def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, content: rl.Rectangle): if self._scroll_state == ScrollState.IDLE: - if abs(self._velocity_y) > MIN_VELOCITY: - self._offset.y += self._velocity_y - self._velocity_y *= INERTIA_FRICTION + if mouse_event.left_pressed: + self._start_mouse_y = mouse_event.pos.y + # Interrupt scrolling with new drag + # TODO: stop scrolling with any tap, need to fix is_touch_valid + if abs(self._velocity_filter_y.x) > MIN_VELOCITY_FOR_CLICKING: + self._scroll_state = ScrollState.DRAGGING_CONTENT + # Start velocity at initial measurement for more immediate response + self._velocity_filter_y.initialized = False - if self._offset.y > 0 or self._offset.y < -max_scroll_y: - self._scroll_state = ScrollState.BOUNCING - else: - self._velocity_y = 0.0 - - # Handle bouncing effect - elif self._scroll_state == ScrollState.BOUNCING: - target_y = 0.0 - if self._offset.y < -max_scroll_y: - target_y = -max_scroll_y - - distance = target_y - self._offset.y - bounce_step = distance * BOUNCE_RETURN_SPEED - self._offset.y += bounce_step - self._velocity_y *= INERTIA_FRICTION * 0.8 - - if abs(distance) < 0.5 and abs(self._velocity_y) < MIN_VELOCITY: - self._offset.y = target_y - self._velocity_y = 0.0 + if mouse_event.left_down: + if abs(mouse_event.pos.y - self._start_mouse_y) > DRAG_THRESHOLD: + self._scroll_state = ScrollState.DRAGGING_CONTENT + # Start velocity at initial measurement for more immediate response + self._velocity_filter_y.initialized = False + + elif self._scroll_state == ScrollState.DRAGGING_CONTENT: + if mouse_event.left_released: self._scroll_state = ScrollState.IDLE + else: + delta_y = mouse_event.pos.y - self._last_mouse_y + above_bounds, below_bounds = self._check_bounds(bounds, content) + # Rubber banding effect when out of bands + if above_bounds or below_bounds: + delta_y /= 3 - # Limit bounce distance - if self._scroll_state != ScrollState.DRAGGING_CONTENT: - if self._offset.y > MAX_BOUNCE_DISTANCE: - self._offset.y = MAX_BOUNCE_DISTANCE - elif self._offset.y < -(max_scroll_y + MAX_BOUNCE_DISTANCE): - self._offset.y = -(max_scroll_y + MAX_BOUNCE_DISTANCE) + self._offset_filter_y.x += delta_y - def is_touch_valid(self): - return not self._is_dragging + # Track velocity for inertia + dt = mouse_event.t - self._last_drag_time + if dt > 0: + drag_velocity = delta_y / dt + self._velocity_filter_y.update(drag_velocity) - def get_normalized_scroll_position(self) -> float: - """Returns the current scroll position as a value from 0.0 to 1.0""" - if not self._content_rect or not self._bounds_rect: - return 0.0 + # TODO: just store last mouse event! + self._last_drag_time = mouse_event.t + self._last_mouse_y = mouse_event.pos.y - max_scroll_y = max(self._content_rect.height - self._bounds_rect.height, 0) - if max_scroll_y == 0: - return 0.0 + def _check_bounds(self, bounds: rl.Rectangle, content: rl.Rectangle) -> tuple[bool, bool]: + max_scroll_distance = max(0, content.height - bounds.height) + above_bounds = self._offset_filter_y.x > 0 + below_bounds = self._offset_filter_y.x < -max_scroll_distance + return above_bounds, below_bounds - normalized = -self._offset.y / max_scroll_y - return max(0.0, min(1.0, normalized)) + def is_touch_valid(self): + return self._scroll_state == ScrollState.IDLE and abs(self._velocity_filter_y.x) < MIN_VELOCITY_FOR_CLICKING diff --git a/system/ui/setup.py b/system/ui/setup.py index a985e783be..e0d737cb1c 100755 --- a/system/ui/setup.py +++ b/system/ui/setup.py @@ -299,20 +299,20 @@ class Setup(Widget): def render_custom_software_warning(self, rect: rl.Rectangle): warn_rect = rl.Rectangle(rect.x, rect.y, rect.width, 1500) - offset = self._custom_software_warning_body_scroll_panel.handle_scroll(rect, warn_rect) + offset = self._custom_software_warning_body_scroll_panel.update(rect, warn_rect) button_width = (rect.width - MARGIN * 3) / 2 button_y = rect.height - MARGIN - BUTTON_HEIGHT rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(button_y - BODY_FONT_SIZE)) - y_offset = rect.y + offset.y + y_offset = rect.y + offset self._custom_software_warning_title_label.render(rl.Rectangle(rect.x + 50, y_offset + 150, rect.width - 265, TITLE_FONT_SIZE)) self._custom_software_warning_body_label.render(rl.Rectangle(rect.x + 50, y_offset + 200 , rect.width - 50, BODY_FONT_SIZE * 3)) rl.end_scissor_mode() self._custom_software_warning_back_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT)) self._custom_software_warning_continue_button.render(rl.Rectangle(rect.x + MARGIN * 2 + button_width, button_y, button_width, BUTTON_HEIGHT)) - if offset.y < (rect.height - warn_rect.height): + if offset < (rect.height - warn_rect.height): self._custom_software_warning_continue_button.set_enabled(True) self._custom_software_warning_continue_button.set_text("Continue") diff --git a/system/ui/text.py b/system/ui/text.py index 61ac043b72..3db930eb26 100755 --- a/system/ui/text.py +++ b/system/ui/text.py @@ -53,14 +53,14 @@ class TextWindow(Widget): self._textarea_rect = rl.Rectangle(MARGIN, MARGIN, gui_app.width - MARGIN * 2, gui_app.height - MARGIN * 2) self._wrapped_lines = wrap_text(text, FONT_SIZE, self._textarea_rect.width - 20) self._content_rect = rl.Rectangle(0, 0, self._textarea_rect.width - 20, len(self._wrapped_lines) * LINE_HEIGHT) - self._scroll_panel = GuiScrollPanel(show_vertical_scroll_bar=True) - self._scroll_panel._offset.y = -max(self._content_rect.height - self._textarea_rect.height, 0) + self._scroll_panel = GuiScrollPanel() + self._scroll_panel._offset_filter_y.x = -max(self._content_rect.height - self._textarea_rect.height, 0) def _render(self, rect: rl.Rectangle): - scroll = self._scroll_panel.handle_scroll(self._textarea_rect, self._content_rect) + scroll = self._scroll_panel.update(self._textarea_rect, self._content_rect) rl.begin_scissor_mode(int(self._textarea_rect.x), int(self._textarea_rect.y), int(self._textarea_rect.width), int(self._textarea_rect.height)) for i, line in enumerate(self._wrapped_lines): - position = rl.Vector2(self._textarea_rect.x + scroll.x, self._textarea_rect.y + scroll.y + i * LINE_HEIGHT) + position = rl.Vector2(self._textarea_rect.x, self._textarea_rect.y + scroll + i * LINE_HEIGHT) if position.y + LINE_HEIGHT < self._textarea_rect.y or position.y > self._textarea_rect.y + self._textarea_rect.height: continue rl.draw_text_ex(gui_app.font(), line, position, FONT_SIZE, 0, rl.WHITE) diff --git a/system/ui/widgets/html_render.py b/system/ui/widgets/html_render.py index d68ea58990..bb7eeb7c5c 100644 --- a/system/ui/widgets/html_render.py +++ b/system/ui/widgets/html_render.py @@ -116,10 +116,10 @@ class HtmlRenderer(Widget): total_height = self.get_total_height(int(scrollable_rect.width)) scroll_content_rect = rl.Rectangle(scrollable_rect.x, scrollable_rect.y, scrollable_rect.width, total_height) - scroll_offset = self._scroll_panel.handle_scroll(scrollable_rect, scroll_content_rect) + scroll_offset = self._scroll_panel.update(scrollable_rect, scroll_content_rect) rl.begin_scissor_mode(int(scrollable_rect.x), int(scrollable_rect.y), int(scrollable_rect.width), int(scrollable_rect.height)) - self._render_content(scrollable_rect, scroll_offset.y) + self._render_content(scrollable_rect, scroll_offset) rl.end_scissor_mode() button_width = (rect.width - 3 * 50) // 3 diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 03385609d6..3dd2a4d654 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -339,24 +339,23 @@ class WifiManagerUI(Widget): def _draw_network_list(self, rect: rl.Rectangle): content_rect = rl.Rectangle(rect.x, rect.y, rect.width, len(self._networks) * ITEM_HEIGHT) - offset = self.scroll_panel.handle_scroll(rect, content_rect) - clicked = self.scroll_panel.is_touch_valid() and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) + offset = self.scroll_panel.update(rect, content_rect) rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) for i, network in enumerate(self._networks): - y_offset = rect.y + i * ITEM_HEIGHT + offset.y + y_offset = rect.y + i * ITEM_HEIGHT + offset item_rect = rl.Rectangle(rect.x, y_offset, rect.width, ITEM_HEIGHT) if not rl.check_collision_recs(item_rect, rect): continue - self._draw_network_item(item_rect, network, clicked) + self._draw_network_item(item_rect, network) if i < len(self._networks) - 1: line_y = int(item_rect.y + item_rect.height - 1) rl.draw_line(int(item_rect.x), int(line_y), int(item_rect.x + item_rect.width), line_y, rl.LIGHTGRAY) rl.end_scissor_mode() - def _draw_network_item(self, rect, network: Network, clicked: bool): + def _draw_network_item(self, rect, network: Network): spacing = 50 ssid_rect = rl.Rectangle(rect.x, rect.y, rect.width - self.btn_width * 2, ITEM_HEIGHT) signal_icon_rect = rl.Rectangle(rect.x + rect.width - ICON_SIZE, rect.y + (ITEM_HEIGHT - ICON_SIZE) / 2, ICON_SIZE, ICON_SIZE) @@ -396,18 +395,16 @@ class WifiManagerUI(Widget): self._draw_signal_strength_icon(signal_icon_rect, network) def _networks_buttons_callback(self, network): - if self.scroll_panel.is_touch_valid(): - if not network.is_saved and network.security_type != SecurityType.OPEN: - self.state = UIState.NEEDS_AUTH - self._state_network = network - self._password_retry = False - elif not network.is_connected: - self.connect_to_network(network) + if not network.is_saved and network.security_type != SecurityType.OPEN: + self.state = UIState.NEEDS_AUTH + self._state_network = network + self._password_retry = False + elif not network.is_connected: + self.connect_to_network(network) def _forget_networks_buttons_callback(self, network): - if self.scroll_panel.is_touch_valid(): - self.state = UIState.SHOW_FORGET_CONFIRM - self._state_network = network + self.state = UIState.SHOW_FORGET_CONFIRM + self._state_network = network def _draw_status_icon(self, rect, network: Network): """Draw the status icon based on network's connection state""" @@ -449,8 +446,10 @@ class WifiManagerUI(Widget): for n in self._networks: self._networks_buttons[n.ssid] = Button(n.ssid, partial(self._networks_buttons_callback, n), font_size=55, text_alignment=TextAlignment.LEFT, button_style=ButtonStyle.TRANSPARENT_WHITE) + self._networks_buttons[n.ssid].set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid()) self._forget_networks_buttons[n.ssid] = Button("Forget", partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI, font_size=45) + self._forget_networks_buttons[n.ssid].set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid()) def _on_need_auth(self, ssid): network = next((n for n in self._networks if n.ssid == ssid), None) diff --git a/system/ui/widgets/option_dialog.py b/system/ui/widgets/option_dialog.py index 8f33124b5c..cbab024f09 100644 --- a/system/ui/widgets/option_dialog.py +++ b/system/ui/widgets/option_dialog.py @@ -41,12 +41,12 @@ class MultiOptionDialog(Widget): list_content_rect = rl.Rectangle(content_rect.x, options_y, content_rect.width, content_h) # Scroll and render options - offset = self.scroll.handle_scroll(view_rect, list_content_rect) + offset = self.scroll.update(view_rect, list_content_rect) valid_click = self.scroll.is_touch_valid() and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) rl.begin_scissor_mode(int(view_rect.x), int(options_y), int(view_rect.width), int(options_h)) for i, option in enumerate(self.options): - item_y = options_y + i * (ITEM_HEIGHT + LIST_ITEM_SPACING) + offset.y + item_y = options_y + i * (ITEM_HEIGHT + LIST_ITEM_SPACING) + offset item_rect = rl.Rectangle(view_rect.x, item_y, view_rect.width, ITEM_HEIGHT) if rl.check_collision_recs(item_rect, view_rect): diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 757500a71c..c76f30d196 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -52,7 +52,7 @@ class Scroller(Widget): content_height = sum(item.rect.height for item in visible_items) + self._spacing * (len(visible_items)) if not self._pad_end: content_height -= self._spacing - scroll = self.scroll_panel.handle_scroll(self._rect, rl.Rectangle(0, 0, self._rect.width, content_height)) + scroll = self.scroll_panel.update(self._rect, rl.Rectangle(0, 0, self._rect.width, content_height)) rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height)) @@ -68,8 +68,7 @@ class Scroller(Widget): cur_height += item.rect.height + self._spacing * (idx != 0) # Consider scroll - x += scroll.x - y += scroll.y + y += scroll # Update item state item.set_position(x, y)