Recognize short deliberate bookmark swipes

This commit is contained in:
firestar5683
2026-10-29 21:14:56 -05:00
parent b817c3c26e
commit c073497d44
2 changed files with 60 additions and 3 deletions
@@ -44,6 +44,10 @@ CAM_Y_OFFSET = 20
class BookmarkIcon(Widget):
PEEK_THRESHOLD = 150 # If icon peeks out this much, snap it fully visible
FLICK_DISTANCE = 60
FLICK_VELOCITY = 800
FLICK_DURATION = 0.15
FLICK_PAUSE = 0.06
FULL_VISIBLE_OFFSET = 200 # How far onscreen when fully visible
HIDDEN_OFFSET = -50 # How far offscreen when hidden
@@ -68,6 +72,9 @@ class BookmarkIcon(Widget):
self._triggered_time: float = 0.0
self._triggered_duration = 1.5
self._swipe_expired = False
self._swipe_start_t = 0.0
self._last_swipe_move = (0.0, 0.0)
self._swipe_moves = 0
def is_swiping_left(self) -> bool:
"""Check if currently swiping left (for scroller to disable)."""
@@ -154,6 +161,9 @@ class BookmarkIcon(Widget):
self._is_swiping = True
self._is_swiping_left = False
self._swipe_expired = False
self._swipe_start_t = mouse_event.t
self._last_swipe_move = (mouse_event.pos.x, mouse_event.t)
self._swipe_moves = 0
elif (mouse_event.left_down or mouse_event.left_released) and self._is_swiping:
self._swipe_current_x = mouse_event.pos.x
@@ -162,6 +172,12 @@ class BookmarkIcon(Widget):
if swipe_offset < -12 or (vertical > 12 and vertical > abs(swipe_offset)):
self.cancel_gesture()
return
if mouse_event.left_down and not mouse_event.left_released:
if mouse_event.t <= self._last_swipe_move[1]:
self._swipe_moves = 0
else:
self._swipe_moves += 1
self._last_swipe_move = (mouse_event.pos.x, mouse_event.t)
self._is_swiping_left = swipe_offset > 12
if self._is_swiping_left:
self._interacting = True
@@ -174,7 +190,14 @@ class BookmarkIcon(Widget):
swipe_distance = self._swipe_start_x - self._swipe_current_x
# If peeking past threshold, transition to animating to fully visible and bookmark
if swipe_distance > self.PEEK_THRESHOLD:
move_x, move_t = self._last_swipe_move
elapsed = move_t - self._swipe_start_t
held_distance = self._swipe_start_x - move_x
flick = (self._swipe_moves >= 2 and 0.01 <= elapsed <= self.FLICK_DURATION and
0 <= mouse_event.t - move_t <= self.FLICK_PAUSE and
held_distance >= self.FLICK_DISTANCE and swipe_distance >= self.FLICK_DISTANCE and
swipe_distance >= held_distance - 12 and held_distance / elapsed >= self.FLICK_VELOCITY)
if swipe_distance > self.PEEK_THRESHOLD or flick:
self._state = BookmarkState.TRIGGERED
self._triggered_time = rl.get_time()
self._triggered_duration = 1.5
@@ -49,8 +49,8 @@ def navigation(monkeypatch):
with patch.object(type(road.gui_app), 'frame', new_callable=PropertyMock, return_value=frame_number), \
patch.object(type(road.gui_app), 'mouse_events', new_callable=PropertyMock, return_value=list(events)):
layout.render()
def event(x, y=100, *, pressed=False, down=False, released=False):
return MouseEvent(MousePos(x, y), 0, pressed, released, down, frame_number / 60)
def event(x, y=100, *, pressed=False, down=False, released=False, timestamp=None):
return MouseEvent(MousePos(x, y), 0, pressed, released, down, frame_number / 60 if timestamp is None else timestamp)
with patch.object(road, 'ui_state', NS(started=True)), patch.object(bookmark, '_render'), \
patch.object(rl, 'begin_scissor_mode'), patch.object(rl, 'end_scissor_mode'), \
patch.object(rl, 'get_frame_time', return_value=1/60):
@@ -125,3 +125,37 @@ def test_bookmark_requires_full_swipe_threshold(navigation, distance, expected):
for event in (n.event(400, pressed=True, down=True), n.event(400-distance, released=True)):
n.frame(event)
assert n.callback.call_count == expected
@pytest.mark.parametrize('batched', [False, True])
def test_short_fast_left_swipe_bookmarks_without_dragging_page(navigation, batched):
n = navigation
events = [n.event(400, pressed=True, down=True, timestamp=10),
n.event(360, down=True, timestamp=10.04), n.event(310, down=True, timestamp=10.08),
n.event(305, released=True, timestamp=10.09)]
if batched:
n.frame(*events)
else:
for event in events:
n.frame(event)
n.callback.assert_called_once()
assert n.page.rect.x == 0
n.frame(n.event(305, released=True, timestamp=10.1))
n.callback.assert_called_once()
@pytest.mark.parametrize('positions,times', [
([400, 360, 310, 305], [10, 10.4, 10.8, 10.81]),
([400, 380, 360, 355], [10, 10.02, 10.04, 10.05]),
([400, 390, 385, 300], [10, 10.02, 10.04, 10.05]),
([400, 360, 310, 305], [10, 10.04, 10.08, 10.3]),
([400, 360, 310, 330], [10, 10.04, 10.08, 10.09]),
([400, 360, 310, 305], [10, 10, 10, 10]),
([400, 360, 310, 305], [10, 9.9, 9.8, 9.7]),
])
def test_short_swipe_rejects_slow_drag_jitter_release_jump_pause_reversal_and_bad_clock(navigation, positions, times):
n = navigation
for index, (x, timestamp) in enumerate(zip(positions, times, strict=True)):
n.frame(n.event(x, pressed=index == 0, down=index != 3, released=index == 3, timestamp=timestamp))
n.callback.assert_not_called()
assert n.page.rect.x == 0