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.

197 lines
7.4 KiB

import pyray as rl
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
MIN_VELOCITY = 0.5 # Minimum velocity before stopping the inertia
DRAG_THRESHOLD = 5 # 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
self._last_frame_time = rl.get_time()
self._velocity_history: list[float] = []
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()
delta_time = current_time - self._last_frame_time
self._last_frame_time = current_time
# Prevent large jumps
delta_time = min(delta_time, 0.05)
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
self._velocity_history = []
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:
drag_velocity = delta_y / time_since_last_drag / 60.0
self._velocity_history.append(drag_velocity)
if len(self._velocity_history) > VELOCITY_HISTORY_SIZE:
self._velocity_history.pop(0)
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:
self._offset.y += self._velocity_y
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
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
# 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))