From fc77ac706e3cc1e62e392db5b99bcabd5dc4e35a Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Mon, 17 Feb 2025 04:47:22 +0800 Subject: [PATCH] python ui: implement inertial scrolling for GuiScrollPanel (#34596) implement inertial scrolling for GuiScrollPanel --- system/ui/lib/scroll_panel.py | 24 +++++++++++++++++++++++- system/ui/text.py | 4 +++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/system/ui/lib/scroll_panel.py b/system/ui/lib/scroll_panel.py index b8fa211ec1..0854ad954c 100644 --- a/system/ui/lib/scroll_panel.py +++ b/system/ui/lib/scroll_panel.py @@ -2,6 +2,9 @@ import pyray as rl from cffi import FFI MOUSE_WHEEL_SCROLL_SPEED = 30 +INERTIA_FRICTION = 0.95 # The rate at which the inertia slows down +MIN_VELOCITY = 0.1 # Minimum velocity before stopping the inertia + class GuiScrollPanel: def __init__(self, bounds: rl.Rectangle, content: rl.Rectangle, show_vertical_scroll_bar: bool = False): @@ -12,22 +15,28 @@ class GuiScrollPanel: self._scroll = 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 - def handle_scroll(self)-> rl.Vector2: + def handle_scroll(self) -> rl.Vector2: mouse_pos = rl.get_mouse_position() + + # Handle dragging logic if rl.check_collision_point_rec(mouse_pos, self._bounds) and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): if not self._dragging: self._dragging = True self._last_mouse_y = mouse_pos.y + self._velocity_y = 0.0 # Reset velocity when drag starts if self._dragging: if rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT): delta_y = mouse_pos.y - self._last_mouse_y self._scroll.y += delta_y self._last_mouse_y = mouse_pos.y + self._velocity_y = delta_y # Update velocity during drag else: self._dragging = False + # Handle mouse wheel scrolling wheel_move = rl.get_mouse_wheel_move() if self._show_vertical_scroll_bar: self._scroll.y += wheel_move * (MOUSE_WHEEL_SCROLL_SPEED - 20) @@ -37,4 +46,17 @@ class GuiScrollPanel: max_scroll_y = self._content.height - self._bounds.height self._scroll.y = max(min(self._scroll.y, 0), -max_scroll_y) + # Apply inertia (continue scrolling after mouse release) + if not self._dragging: + self._scroll.y += self._velocity_y + self._velocity_y *= INERTIA_FRICTION # Slow down velocity over time + + # Stop scrolling when velocity is low + if abs(self._velocity_y) < MIN_VELOCITY: + self._velocity_y = 0.0 + + # Ensure scrolling doesn't go beyond bounds + max_scroll_y = max(self._content.height - self._bounds.height, 0) + self._scroll.y = max(min(self._scroll.y, 0), -max_scroll_y) + return self._scroll diff --git a/system/ui/text.py b/system/ui/text.py index ce8af9ee50..9777102252 100755 --- a/system/ui/text.py +++ b/system/ui/text.py @@ -14,7 +14,7 @@ LINE_HEIGHT = 64 BUTTON_SIZE = rl.Vector2(310, 160) DEMO_TEXT = """This is a sample text that will be wrapped and scrolled if necessary. - The text is long enough to demonstrate scrolling and word wrapping.""" * 20 + The text is long enough to demonstrate scrolling and word wrapping.""" * 30 def wrap_text(text, font_size, max_width): lines = [] @@ -50,6 +50,8 @@ def main(): rl.begin_scissor_mode(int(textarea_rect.x), int(textarea_rect.y), int(textarea_rect.width), int(textarea_rect.height)) for i, line in enumerate(wrapped_lines): position = rl.Vector2(textarea_rect.x + scroll.x, textarea_rect.y + scroll.y + i * LINE_HEIGHT) + if position.y + LINE_HEIGHT < textarea_rect.y or position.y > textarea_rect.y + textarea_rect.height: + continue rl.draw_text_ex(gui_app.font(), line.strip(), position, FONT_SIZE, 0, rl.WHITE) rl.end_scissor_mode()