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
pull/36207/head
Shane Smiskol 2 days ago committed by GitHub
parent 63e0e038fa
commit 5f33b2fb2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      selfdrive/ui/layouts/settings/firehose.py
  2. 4
      selfdrive/ui/widgets/offroad_alerts.py
  3. 245
      system/ui/lib/scroll_panel.py
  4. 6
      system/ui/setup.py
  5. 8
      system/ui/text.py
  6. 4
      system/ui/widgets/html_render.py
  7. 29
      system/ui/widgets/network.py
  8. 4
      system/ui/widgets/option_dialog.py
  9. 5
      system/ui/widgets/scroller.py

@ -71,7 +71,7 @@ class FirehoseLayout(Widget):
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height) content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height)
# Handle scrolling and render with clipping # 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)) rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
self._render_content(rect, scroll_offset) self._render_content(rect, scroll_offset)
rl.end_scissor_mode() rl.end_scissor_mode()
@ -106,9 +106,9 @@ class FirehoseLayout(Widget):
return height 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) 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) w = int(rect.width - 80)
# Title # Title

@ -109,7 +109,7 @@ class AbstractAlert(Widget, ABC):
def _render_scrollable_content(self): def _render_scrollable_content(self):
content_total_height = self.get_content_height() content_total_height = self.get_content_height()
content_bounds = rl.Rectangle(0, 0, self.scroll_panel_rect.width, content_total_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( rl.begin_scissor_mode(
int(self.scroll_panel_rect.x), int(self.scroll_panel_rect.x),
@ -120,7 +120,7 @@ class AbstractAlert(Widget, ABC):
content_rect_with_scroll = rl.Rectangle( content_rect_with_scroll = rl.Rectangle(
self.scroll_panel_rect.x, 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, self.scroll_panel_rect.width,
content_total_height, content_total_height,
) )

@ -1,189 +1,124 @@
import time import math
import pyray as rl import pyray as rl
from collections import deque
from enum import IntEnum 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 # Scroll constants for smooth scrolling behavior
MOUSE_WHEEL_SCROLL_SPEED = 30 MOUSE_WHEEL_SCROLL_SPEED = 50
INERTIA_FRICTION = 0.92 # The rate at which the inertia slows down BOUNCE_RETURN_RATE = 5 # ~0.92 at 60fps
MIN_VELOCITY = 0.5 # Minimum velocity before stopping the inertia MIN_VELOCITY = 2 # px/s, changes from auto scroll to steady state
DRAG_THRESHOLD = 12 # Pixels of movement to consider it a drag, not a click MIN_VELOCITY_FOR_CLICKING = 2 * 60 # px/s, accepts clicks while auto scrolling below this velocity
BOUNCE_FACTOR = 0.2 # Elastic bounce when scrolling past boundaries DRAG_THRESHOLD = 12 # pixels of movement to consider it a drag, not a click
BOUNCE_RETURN_SPEED = 0.15 # How quickly it returns from the bounce
MAX_BOUNCE_DISTANCE = 150 # Maximum distance for bounce effect DEBUG = False
FLICK_MULTIPLIER = 1.8 # Multiplier for flick gestures
VELOCITY_HISTORY_SIZE = 5 # Track velocity over multiple frames for smoother motion
class ScrollState(IntEnum): class ScrollState(IntEnum):
IDLE = 0 IDLE = 0 # Not dragging, content may be bouncing or scrolling with inertia
DRAGGING_CONTENT = 1 DRAGGING_CONTENT = 1 # User is actively dragging the content
DRAGGING_SCROLLBAR = 2
BOUNCING = 3
class GuiScrollPanel: class GuiScrollPanel:
def __init__(self, show_vertical_scroll_bar: bool = False): def __init__(self):
self._scroll_state: ScrollState = ScrollState.IDLE self._scroll_state: ScrollState = ScrollState.IDLE
self._last_mouse_y: float = 0.0 self._last_mouse_y: float = 0.0
self._start_mouse_y: float = 0.0 # Track the initial mouse position for drag detection self._start_mouse_y: float = 0.0 # Track the initial mouse position for drag detection
self._offset = rl.Vector2(0, 0) self._offset_filter_y = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
self._view = rl.Rectangle(0, 0, 0, 0) self._velocity_filter_y = FirstOrderFilter(0.0, 0.05, 1 / gui_app.target_fps)
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._last_drag_time: float = 0.0 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: def update(self, bounds: rl.Rectangle, content: rl.Rectangle) -> float:
# 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:
for mouse_event in gui_app.mouse_events or [MouseEvent(MousePos(0, 0), 0, False, False, False, time.monotonic())]:
if mouse_event.slot == 0: if mouse_event.slot == 0:
self._handle_mouse_event(mouse_event, bounds, content) 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): self._update_state(bounds, content)
# 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
# Handle active dragging return float(self._offset_filter_y.x)
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
# Track velocity for inertia def _update_state(self, bounds: rl.Rectangle, content: rl.Rectangle):
time_since_last_drag = mouse_event.t - self._last_drag_time if DEBUG:
if time_since_last_drag > 0: rl.draw_rectangle_lines(0, 0, abs(int(self._velocity_filter_y.x)), 10, rl.RED)
# 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
# Handle mouse wheel # Handle mouse wheel
wheel_move = rl.get_mouse_wheel_move() self._offset_filter_y.x += rl.get_mouse_wheel_move() * MOUSE_WHEEL_SCROLL_SPEED
if wheel_move != 0:
self._velocity_y = 0.0 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: # Decay velocity when idle
self._offset.y += wheel_move * (MOUSE_WHEEL_SCROLL_SPEED - 20) if abs(self._velocity_filter_y.x) > MIN_VELOCITY:
rl.gui_scroll_panel(bounds, rl.ffi.NULL, content, self._offset, self._view) # 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: 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: elif self._scroll_state == ScrollState.DRAGGING_CONTENT:
self._scroll_state = ScrollState.BOUNCING # 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 self._scroll_state == ScrollState.IDLE:
if abs(self._velocity_y) > MIN_VELOCITY: if mouse_event.left_pressed:
self._offset.y += self._velocity_y self._start_mouse_y = mouse_event.pos.y
self._velocity_y *= INERTIA_FRICTION # 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: if mouse_event.left_down:
self._scroll_state = ScrollState.BOUNCING if abs(mouse_event.pos.y - self._start_mouse_y) > DRAG_THRESHOLD:
else: self._scroll_state = ScrollState.DRAGGING_CONTENT
self._velocity_y = 0.0 # Start velocity at initial measurement for more immediate response
self._velocity_filter_y.initialized = False
# Handle bouncing effect
elif self._scroll_state == ScrollState.BOUNCING: elif self._scroll_state == ScrollState.DRAGGING_CONTENT:
target_y = 0.0 if mouse_event.left_released:
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
self._scroll_state = ScrollState.IDLE 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 self._offset_filter_y.x += delta_y
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)
def is_touch_valid(self): # Track velocity for inertia
return not self._is_dragging 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: # TODO: just store last mouse event!
"""Returns the current scroll position as a value from 0.0 to 1.0""" self._last_drag_time = mouse_event.t
if not self._content_rect or not self._bounds_rect: self._last_mouse_y = mouse_event.pos.y
return 0.0
max_scroll_y = max(self._content_rect.height - self._bounds_rect.height, 0) def _check_bounds(self, bounds: rl.Rectangle, content: rl.Rectangle) -> tuple[bool, bool]:
if max_scroll_y == 0: max_scroll_distance = max(0, content.height - bounds.height)
return 0.0 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 def is_touch_valid(self):
return max(0.0, min(1.0, normalized)) return self._scroll_state == ScrollState.IDLE and abs(self._velocity_filter_y.x) < MIN_VELOCITY_FOR_CLICKING

@ -299,20 +299,20 @@ class Setup(Widget):
def render_custom_software_warning(self, rect: rl.Rectangle): def render_custom_software_warning(self, rect: rl.Rectangle):
warn_rect = rl.Rectangle(rect.x, rect.y, rect.width, 1500) 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_width = (rect.width - MARGIN * 3) / 2
button_y = rect.height - MARGIN - BUTTON_HEIGHT 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)) 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_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)) 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() 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_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)) 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_enabled(True)
self._custom_software_warning_continue_button.set_text("Continue") self._custom_software_warning_continue_button.set_text("Continue")

@ -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._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._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._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 = GuiScrollPanel()
self._scroll_panel._offset.y = -max(self._content_rect.height - self._textarea_rect.height, 0) self._scroll_panel._offset_filter_y.x = -max(self._content_rect.height - self._textarea_rect.height, 0)
def _render(self, rect: rl.Rectangle): 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)) 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): 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: if position.y + LINE_HEIGHT < self._textarea_rect.y or position.y > self._textarea_rect.y + self._textarea_rect.height:
continue continue
rl.draw_text_ex(gui_app.font(), line, position, FONT_SIZE, 0, rl.WHITE) rl.draw_text_ex(gui_app.font(), line, position, FONT_SIZE, 0, rl.WHITE)

@ -116,10 +116,10 @@ class HtmlRenderer(Widget):
total_height = self.get_total_height(int(scrollable_rect.width)) 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_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)) 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() rl.end_scissor_mode()
button_width = (rect.width - 3 * 50) // 3 button_width = (rect.width - 3 * 50) // 3

@ -339,24 +339,23 @@ class WifiManagerUI(Widget):
def _draw_network_list(self, rect: rl.Rectangle): def _draw_network_list(self, rect: rl.Rectangle):
content_rect = rl.Rectangle(rect.x, rect.y, rect.width, len(self._networks) * ITEM_HEIGHT) content_rect = rl.Rectangle(rect.x, rect.y, rect.width, len(self._networks) * ITEM_HEIGHT)
offset = self.scroll_panel.handle_scroll(rect, content_rect) offset = self.scroll_panel.update(rect, content_rect)
clicked = self.scroll_panel.is_touch_valid() and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT)
rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
for i, network in enumerate(self._networks): 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) item_rect = rl.Rectangle(rect.x, y_offset, rect.width, ITEM_HEIGHT)
if not rl.check_collision_recs(item_rect, rect): if not rl.check_collision_recs(item_rect, rect):
continue continue
self._draw_network_item(item_rect, network, clicked) self._draw_network_item(item_rect, network)
if i < len(self._networks) - 1: if i < len(self._networks) - 1:
line_y = int(item_rect.y + item_rect.height - 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.draw_line(int(item_rect.x), int(line_y), int(item_rect.x + item_rect.width), line_y, rl.LIGHTGRAY)
rl.end_scissor_mode() rl.end_scissor_mode()
def _draw_network_item(self, rect, network: Network, clicked: bool): def _draw_network_item(self, rect, network: Network):
spacing = 50 spacing = 50
ssid_rect = rl.Rectangle(rect.x, rect.y, rect.width - self.btn_width * 2, ITEM_HEIGHT) 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) 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) self._draw_signal_strength_icon(signal_icon_rect, network)
def _networks_buttons_callback(self, 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:
if not network.is_saved and network.security_type != SecurityType.OPEN: self.state = UIState.NEEDS_AUTH
self.state = UIState.NEEDS_AUTH self._state_network = network
self._state_network = network self._password_retry = False
self._password_retry = False elif not network.is_connected:
elif not network.is_connected: self.connect_to_network(network)
self.connect_to_network(network)
def _forget_networks_buttons_callback(self, network): def _forget_networks_buttons_callback(self, network):
if self.scroll_panel.is_touch_valid(): self.state = UIState.SHOW_FORGET_CONFIRM
self.state = UIState.SHOW_FORGET_CONFIRM self._state_network = network
self._state_network = network
def _draw_status_icon(self, rect, network: Network): def _draw_status_icon(self, rect, network: Network):
"""Draw the status icon based on network's connection state""" """Draw the status icon based on network's connection state"""
@ -449,8 +446,10 @@ class WifiManagerUI(Widget):
for n in self._networks: 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, 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) 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, self._forget_networks_buttons[n.ssid] = Button("Forget", partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI,
font_size=45) 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): def _on_need_auth(self, ssid):
network = next((n for n in self._networks if n.ssid == ssid), None) network = next((n for n in self._networks if n.ssid == ssid), None)

@ -41,12 +41,12 @@ class MultiOptionDialog(Widget):
list_content_rect = rl.Rectangle(content_rect.x, options_y, content_rect.width, content_h) list_content_rect = rl.Rectangle(content_rect.x, options_y, content_rect.width, content_h)
# Scroll and render options # 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) 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)) 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): 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) item_rect = rl.Rectangle(view_rect.x, item_y, view_rect.width, ITEM_HEIGHT)
if rl.check_collision_recs(item_rect, view_rect): if rl.check_collision_recs(item_rect, view_rect):

@ -52,7 +52,7 @@ class Scroller(Widget):
content_height = sum(item.rect.height for item in visible_items) + self._spacing * (len(visible_items)) content_height = sum(item.rect.height for item in visible_items) + self._spacing * (len(visible_items))
if not self._pad_end: if not self._pad_end:
content_height -= self._spacing 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), rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y),
int(self._rect.width), int(self._rect.height)) int(self._rect.width), int(self._rect.height))
@ -68,8 +68,7 @@ class Scroller(Widget):
cur_height += item.rect.height + self._spacing * (idx != 0) cur_height += item.rect.height + self._spacing * (idx != 0)
# Consider scroll # Consider scroll
x += scroll.x y += scroll
y += scroll.y
# Update item state # Update item state
item.set_position(x, y) item.set_position(x, y)

Loading…
Cancel
Save