openpilot is an open source driver assistance system. openpilot performs the functions of Automated Lane Centering and Adaptive Cruise Control for over 200 supported car makes and models.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

190 lines
7.2 KiB

import pyray as rl
2 days ago
from collections import deque
from enum import IntEnum
# Scroll constants for smooth scrolling behavior
MOUSE_WHEEL_SCROLL_SPEED = 30
INERTIA_FRICTION = 0.92 # The rate at which the inertia slows down
2 days ago
MIN_VELOCITY = 0.5 * 60 # 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
class ScrollState(IntEnum):
IDLE = 0
DRAGGING_CONTENT = 1
DRAGGING_SCROLLBAR = 2
BOUNCING = 3
class GuiScrollPanel:
def __init__(self, show_vertical_scroll_bar: bool = False):
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
2 days ago
self._velocity_history: deque[float] = deque(maxlen=VELOCITY_HISTORY_SIZE)
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:
# Store rectangles for reference
self._content_rect = content
self._bounds_rect = bounds
# Calculate time delta
current_time = rl.get_time()
2 days ago
fps_scale = 60.0 / rl.get_fps()
mouse_pos = rl.get_mouse_position()
max_scroll_y = max(content.height - bounds.height, 0)
# Start dragging on mouse press
if rl.check_collision_point_rec(mouse_pos, bounds) and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT):
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_pos.x >= scrollbar_x:
self._scroll_state = ScrollState.DRAGGING_SCROLLBAR
self._last_mouse_y = mouse_pos.y
self._start_mouse_y = mouse_pos.y
self._last_drag_time = current_time
2 days ago
self._velocity_history.clear()
self._velocity_y = 0.0
self._bounce_offset = 0.0
self._is_dragging = False
# Handle active dragging
if self._scroll_state == ScrollState.DRAGGING_CONTENT or self._scroll_state == ScrollState.DRAGGING_SCROLLBAR:
if rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT):
delta_y = mouse_pos.y - self._last_mouse_y
# Track velocity for inertia
time_since_last_drag = current_time - self._last_drag_time
if time_since_last_drag > 0:
2 days ago
drag_velocity = delta_y / time_since_last_drag # / 60.0 * fps_scale
self._velocity_history.append(drag_velocity)
self._last_drag_time = current_time
# Detect actual dragging
total_drag = abs(mouse_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_pos.y
elif rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT):
# 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
wheel_move = rl.get_mouse_wheel_move()
if wheel_move != 0:
self._velocity_y = 0.0
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)
else:
self._offset.y += wheel_move * MOUSE_WHEEL_SCROLL_SPEED
if self._offset.y > 0 or self._offset.y < -max_scroll_y:
self._scroll_state = ScrollState.BOUNCING
# Apply inertia (continue scrolling after mouse release)
if self._scroll_state == ScrollState.IDLE:
if abs(self._velocity_y) > MIN_VELOCITY:
2 days ago
self._offset.y += self._velocity_y * rl.get_frame_time()
self._velocity_y *= INERTIA_FRICTION
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
2 days ago
bounce_step = distance * BOUNCE_RETURN_SPEED * fps_scale
self._offset.y += bounce_step
2 days ago
self._velocity_y *= pow(INERTIA_FRICTION, fps_scale) * 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
# 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)
return self._offset
def is_click_valid(self) -> bool:
# Check if this is a click rather than a drag
return (
self._scroll_state == ScrollState.IDLE
and not self._is_dragging
and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT)
)
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
max_scroll_y = max(self._content_rect.height - self._bounds_rect.height, 0)
if max_scroll_y == 0:
return 0.0
normalized = -self._offset.y / max_scroll_y
return max(0.0, min(1.0, normalized))