Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 39 additions & 34 deletions system/ui/widgets/scroller.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
DO_ZOOM = False
DO_JELLO = False
SCROLL_BAR = False

SNAP_VELOCITY_THRESHOLD = 100.0

class LineSeparator(Widget):
def __init__(self, height: int = 1):
Expand Down Expand Up @@ -56,6 +56,7 @@ def __init__(self, items: list[Widget], horizontal: bool = True, snap_items: boo

# when not pressed, snap to closest item to be center
self._scroll_snap_filter = FirstOrderFilter(0.0, 0.05, 1 / gui_app.target_fps)
self._last_snap_target_offset: float = 0.0

self.scroll_panel = GuiScrollPanel2(self._horizontal, handle_out_of_bounds=not self._snap_items)
self._scroll_enabled: bool | Callable[[], bool] = True
Expand Down Expand Up @@ -123,40 +124,44 @@ def _get_scroll(self, visible_items: list[Widget], content_size: float) -> float
scroll_enabled = self._scroll_enabled() if callable(self._scroll_enabled) else self._scroll_enabled
self.scroll_panel.set_enabled(scroll_enabled and self.enabled)
self.scroll_panel.update(self._rect, content_size)
if not self._snap_items:
return self.scroll_panel.get_offset()

# Snap closest item to center
center_pos = self._rect.x + self._rect.width / 2 if self._horizontal else self._rect.y + self._rect.height / 2
closest_delta_pos = float('inf')
scroll_snap_idx: int | None = None
for idx, item in enumerate(visible_items):
if self._horizontal:
delta_pos = (item.rect.x + item.rect.width / 2) - center_pos
else:
delta_pos = (item.rect.y + item.rect.height / 2) - center_pos
if abs(delta_pos) < abs(closest_delta_pos):
closest_delta_pos = delta_pos
scroll_snap_idx = idx

if scroll_snap_idx is not None:
snap_item = visible_items[scroll_snap_idx]
if self.is_pressed:
# no snapping until released
self._scroll_snap_filter.x = 0
else:
# TODO: this doesn't handle two small buttons at the edges well
if self._horizontal:
snap_delta_pos = (center_pos - (snap_item.rect.x + snap_item.rect.width / 2)) / 10
snap_delta_pos = min(snap_delta_pos, -self.scroll_panel.get_offset() / 10)
snap_delta_pos = max(snap_delta_pos, (self._rect.width - self.scroll_panel.get_offset() - content_size) / 10)
else:
snap_delta_pos = (center_pos - (snap_item.rect.y + snap_item.rect.height / 2)) / 10
snap_delta_pos = min(snap_delta_pos, -self.scroll_panel.get_offset() / 10)
snap_delta_pos = max(snap_delta_pos, (self._rect.height - self.scroll_panel.get_offset() - content_size) / 10)
self._scroll_snap_filter.update(snap_delta_pos)

self.scroll_panel.set_offset(self.scroll_panel.get_offset() + self._scroll_snap_filter.x)
current_offset = self.scroll_panel.get_offset()
if not self._snap_items or not visible_items:
return current_offset

# Stable at target
if self.scroll_panel.state == ScrollState.STEADY and abs(current_offset - self._last_snap_target_offset) < 1.0:
self.scroll_panel.set_offset(self._last_snap_target_offset) # Ensure offset is exactly the target
return self._last_snap_target_offset

# Interaction or fast fling disables snapping
is_interacting = self.is_pressed or self.scroll_panel.state == ScrollState.PRESSED
is_flinging = (self.scroll_panel.state == ScrollState.AUTO_SCROLL and abs(self.scroll_panel._velocity) > SNAP_VELOCITY_THRESHOLD)
if is_interacting or is_flinging:
self._scroll_snap_filter.x = current_offset # Reset filter state to current offset during active motion
return current_offset

# Viewport center
if self._horizontal:
viewport_size, viewport_center = self._rect.width, self._rect.x + self._rect.width / 2
def get_center(r): return r.x + r.width / 2
else:
viewport_size, viewport_center = self._rect.height, self._rect.y + self._rect.height / 2
def get_center(r): return r.y + r.height / 2

# Find closest item to center
snap_item = min(visible_items, key=lambda w: abs(get_center(w.rect) - viewport_center))
item_center = get_center(snap_item.rect) - current_offset
target_offset = viewport_center - item_center

# Clamp offset
min_scroll = -(content_size - viewport_size)
target_offset = max(min_scroll, min(0.0, target_offset))

# Smooth snap
self._scroll_snap_filter.update(target_offset)
self._last_snap_target_offset = target_offset
self.scroll_panel.set_offset(self._scroll_snap_filter.x)

return self.scroll_panel.get_offset()

Expand Down
Loading