Skip to content

Commit 0403bea

Browse files
committed
enhance scroll snapping and fix physic jitter in Scroller
1 parent 4917853 commit 0403bea

File tree

1 file changed

+39
-34
lines changed

1 file changed

+39
-34
lines changed

system/ui/widgets/scroller.py

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
DO_ZOOM = False
1717
DO_JELLO = False
1818
SCROLL_BAR = False
19-
19+
SNAP_VELOCITY_THRESHOLD = 100.0
2020

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

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

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

129-
# Snap closest item to center
130-
center_pos = self._rect.x + self._rect.width / 2 if self._horizontal else self._rect.y + self._rect.height / 2
131-
closest_delta_pos = float('inf')
132-
scroll_snap_idx: int | None = None
133-
for idx, item in enumerate(visible_items):
134-
if self._horizontal:
135-
delta_pos = (item.rect.x + item.rect.width / 2) - center_pos
136-
else:
137-
delta_pos = (item.rect.y + item.rect.height / 2) - center_pos
138-
if abs(delta_pos) < abs(closest_delta_pos):
139-
closest_delta_pos = delta_pos
140-
scroll_snap_idx = idx
141-
142-
if scroll_snap_idx is not None:
143-
snap_item = visible_items[scroll_snap_idx]
144-
if self.is_pressed:
145-
# no snapping until released
146-
self._scroll_snap_filter.x = 0
147-
else:
148-
# TODO: this doesn't handle two small buttons at the edges well
149-
if self._horizontal:
150-
snap_delta_pos = (center_pos - (snap_item.rect.x + snap_item.rect.width / 2)) / 10
151-
snap_delta_pos = min(snap_delta_pos, -self.scroll_panel.get_offset() / 10)
152-
snap_delta_pos = max(snap_delta_pos, (self._rect.width - self.scroll_panel.get_offset() - content_size) / 10)
153-
else:
154-
snap_delta_pos = (center_pos - (snap_item.rect.y + snap_item.rect.height / 2)) / 10
155-
snap_delta_pos = min(snap_delta_pos, -self.scroll_panel.get_offset() / 10)
156-
snap_delta_pos = max(snap_delta_pos, (self._rect.height - self.scroll_panel.get_offset() - content_size) / 10)
157-
self._scroll_snap_filter.update(snap_delta_pos)
158-
159-
self.scroll_panel.set_offset(self.scroll_panel.get_offset() + self._scroll_snap_filter.x)
128+
current_offset = self.scroll_panel.get_offset()
129+
if not self._snap_items or not visible_items:
130+
return current_offset
131+
132+
# Stable at target
133+
if self.scroll_panel.state == ScrollState.STEADY and abs(current_offset - self._last_snap_target_offset) < 1.0:
134+
self.scroll_panel.set_offset(self._last_snap_target_offset) # Ensure offset is exactly the target
135+
return self._last_snap_target_offset
136+
137+
# Interaction or fast fling disables snapping
138+
is_interacting = self.is_pressed or self.scroll_panel.state == ScrollState.PRESSED
139+
is_flinging = (self.scroll_panel.state == ScrollState.AUTO_SCROLL and abs(self.scroll_panel._velocity) > SNAP_VELOCITY_THRESHOLD)
140+
if is_interacting or is_flinging:
141+
self._scroll_snap_filter.x = current_offset # Reset filter state to current offset during active motion
142+
return current_offset
143+
144+
# Viewport center
145+
if self._horizontal:
146+
viewport_size, viewport_center = self._rect.width, self._rect.x + self._rect.width / 2
147+
def get_center(r): return r.x + r.width / 2
148+
else:
149+
viewport_size, viewport_center = self._rect.height, self._rect.y + self._rect.height / 2
150+
def get_center(r): return r.y + r.height / 2
151+
152+
# Find closest item to center
153+
snap_item = min(visible_items, key=lambda w: abs(get_center(w.rect) - viewport_center))
154+
item_center = get_center(snap_item.rect) - current_offset
155+
target_offset = viewport_center - item_center
156+
157+
# Clamp offset
158+
min_scroll = -(content_size - viewport_size)
159+
target_offset = max(min_scroll, min(0.0, target_offset))
160+
161+
# Smooth snap
162+
self._scroll_snap_filter.update(target_offset)
163+
self._last_snap_target_offset = target_offset
164+
self.scroll_panel.set_offset(self._scroll_snap_filter.x)
160165

161166
return self.scroll_panel.get_offset()
162167

0 commit comments

Comments
 (0)