Skip to main content

repose_ui/
scrollbar_repeat.rs

1//! Track-press auto-repeat for scrollbars.
2//!
3//! Compose parity (`TrackPressScroller` in
4//! `compose/foundation/.../Scrollbar.skiko.kt`): pressing the scrollbar track
5//! outside the thumb pages once immediately, then keeps paging toward the held
6//! point after a 300ms delay, every 100ms, until release.
7//!
8//! The repeat itself cannot live in the scrollbar hit closures: those run on
9//! input dispatch, but repeats must fire on time even when the pointer sits
10//! still and no input arrives. So the track-press closure registers a page
11//! callback here, and the app runtime polls it once per tick next to
12//! `poll_long_press`, with the next fire time feeding `next_wakeup_deadline`
13//! so a sleeping host still wakes up to page.
14
15use std::cell::RefCell;
16use std::rc::Rc;
17
18use web_time::{Duration, Instant};
19
20use repose_core::input::{PointerId, PointerKind};
21
22/// First repeat fires this long after the track press; Compose
23/// `DelayBeforeSecondScrollOnTrackPress`.
24const FIRST_DELAY_MS: u64 = 300;
25/// Subsequent pages fire at this cadence; Compose
26/// `DelayBetweenScrollsOnTrackPress`.
27const REPEAT_MS: u64 = 100;
28
29/// Pointer key for non-touch pointers. Mouse and pen share one active repeat:
30/// there is only ever one system pointer of that kind driving a repeat.
31const MOUSE_POINTER: u64 = u64::MAX;
32
33/// Stable key for the pointer behind an event. Touch fingers keep their id so
34/// two fingers can hold two tracks; mouse and pen collapse to one key.
35pub fn pointer_key(kind: PointerKind, id: PointerId) -> u64 {
36    match kind {
37        PointerKind::Touch => id.0,
38        _ => MOUSE_POINTER,
39    }
40}
41
42/// Same key from the runtime side, which addresses pointers by touch id.
43pub fn key_for_touch(touch: Option<u64>) -> u64 {
44    touch.unwrap_or(MOUSE_POINTER)
45}
46
47struct ActiveRepeat {
48    pointer: u64,
49    next_fire: Instant,
50    page: Rc<dyn Fn()>,
51}
52
53thread_local! {
54    static ACTIVE: RefCell<Option<ActiveRepeat>> = const { RefCell::new(None) };
55}
56
57/// Start (or restart) paging for `pointer`. The caller pages once
58/// immediately; the first repeat fires [`FIRST_DELAY_MS`] later.
59pub fn begin_track_repeat(pointer: u64, page: Rc<dyn Fn()>) {
60    ACTIVE.with(|active| {
61        *active.borrow_mut() = Some(ActiveRepeat {
62            pointer,
63            next_fire: Instant::now() + Duration::from_millis(FIRST_DELAY_MS),
64            page,
65        });
66    });
67}
68
69/// Stop paging for `pointer`. No-op when another pointer holds the repeat.
70pub fn cancel_track_repeat(pointer: u64) {
71    ACTIVE.with(|active| {
72        let mut slot = active.borrow_mut();
73        if slot.as_ref().is_some_and(|repeat| repeat.pointer == pointer) {
74            *slot = None;
75        }
76    });
77}
78
79/// Stop every repeat. Used on global pointer cancel, where per-pointer
80/// bookkeeping may already be gone.
81pub fn cancel_all_track_repeats() {
82    ACTIVE.with(|active| {
83        *active.borrow_mut() = None;
84    });
85}
86
87/// Convenience for runtime press/release/cancel paths.
88pub fn cancel_track_repeat_for_touch(touch: Option<u64>) {
89    cancel_track_repeat(key_for_touch(touch));
90}
91
92/// Fire due repeats. Call once per tick next to the other gesture polls.
93/// Returns true when a page ran (and a frame was requested).
94pub fn poll_track_repeat() -> bool {
95    let now = Instant::now();
96    let page = ACTIVE.with(|active| {
97        let mut slot = active.borrow_mut();
98        match slot.as_mut() {
99            Some(repeat) if now >= repeat.next_fire => {
100                repeat.next_fire = now + Duration::from_millis(REPEAT_MS);
101                Some(repeat.page.clone())
102            }
103            _ => None,
104        }
105    });
106    match page {
107        Some(page) => {
108            page();
109            repose_core::request_frame();
110            true
111        }
112        None => false,
113    }
114}
115
116/// Next repeat fire time, for the host wakeup deadline.
117pub fn track_repeat_deadline() -> Option<Instant> {
118    ACTIVE.with(|active| active.borrow().as_ref().map(|repeat| repeat.next_fire))
119}