Skip to main content

ferroui_base/input/gesture_recognizers/
scroll_gesture_recognizer.rs

1use super::velocity_tracker::VelocityTracker;
2use super::{GestureRecognizer, GestureRecognizerImpl};
3use crate::input::{
4    IPointer, PointerEventArgs, PointerPressedEventArgs, PointerReleasedEventArgs, PointerType,
5    ScrollGestureEndedEventArgs, ScrollGestureEventArgs, ScrollGestureInertiaStartingEventArgs,
6};
7use crate::media::MediaContext;
8use crate::platform::IPlatformSettings;
9use crate::threading::{Dispatcher, DispatcherPriority};
10use crate::{
11    ferro_class, ferro_impl_classes, ferro_property, instantiate, DirectProperty, DirectPropertyMetadata,
12    FerroLocator, FerroObjectImpl, FerroProperty, LocatorExtensions, Point, Ref, Size, StyledElementImpl, Vector,
13};
14use std::cell::{Cell, RefCell};
15use std::rc::Rc;
16use std::time::{Duration, Instant};
17
18fn default_scroll_start_distance() -> i32 {
19    let height = FerroLocator::current()
20        .get_service::<dyn IPlatformSettings>()
21        .map_or(10.0, |settings| settings.get_tap_size(PointerType::Touch).height);
22    (height / 2.0) as i32
23}
24
25/// Recognizes the scroll gesture: a touch or pen drag, optionally followed
26/// by inertia.
27#[repr(C)]
28pub struct ScrollGestureRecognizer {
29    base: GestureRecognizer,
30    can_horizontally_scroll: Cell<bool>,
31    can_vertically_scroll: Cell<bool>,
32    is_scroll_inertia_enabled: Cell<bool>,
33    offset: Cell<Option<Vector>>,
34    viewport: Cell<Option<Size>>,
35    extent: Cell<Option<Size>>,
36    scroll_start_distance: Cell<i32>,
37
38    scrolling: Cell<bool>,
39    tracked_root_point: Cell<Point>,
40    tracking: RefCell<Option<Rc<dyn IPointer>>>,
41    /// The clock of the inertia (upstream's `_stopWatch`): started when the
42    /// pointer is released, present while the inertia runs.
43    stop_watch: Cell<Option<Instant>>,
44    gesture_id: Cell<i32>,
45    pointer_pressed_point: Cell<Point>,
46    velocity_tracker: RefCell<Option<VelocityTracker>>,
47
48    /// Movement per second.
49    inertia: Cell<Option<Vector>>,
50    last_move_timestamp: Cell<Option<u64>>,
51    last_time: Cell<Duration>,
52    inertia_start_time: Cell<Duration>,
53    current_inertia_gesture_id: Cell<i32>,
54    delta: Cell<Point>,
55}
56
57ferro_class!(ScrollGestureRecognizer: GestureRecognizer);
58crate::ferro_class_info!(ScrollGestureRecognizer { new: ScrollGestureRecognizer::new });
59ferro_impl_classes!(ScrollGestureRecognizer: FerroObjectImpl, StyledElementImpl);
60
61impl GestureRecognizerImpl for ScrollGestureRecognizer {
62    fn pointer_pressed(this: &Self, e: &PointerPressedEventArgs) {
63        let point = e.get_current_point(None);
64
65        if matches!(e.pointer().type_(), PointerType::Touch | PointerType::Pen)
66            && point.properties.is_left_button_pressed
67        {
68            this.end_gesture();
69            drop(this.tracking.replace(Some(e.pointer().clone())));
70            this.inertia.set(None);
71            this.gesture_id.set(ScrollGestureEventArgs::get_next_free_id());
72            this.tracked_root_point.set(point.position);
73            this.pointer_pressed_point.set(point.position);
74
75            let mut tracker = VelocityTracker::new();
76            tracker.add_position(Duration::from_millis(e.timestamp()), Vector::default());
77            *this.velocity_tracker.borrow_mut() = Some(tracker);
78        }
79    }
80
81    fn pointer_moved(this: &Self, e: &PointerEventArgs) {
82        if !this.is_tracking(e.pointer()) {
83            return;
84        }
85
86        let root_point = e.get_position(None);
87        let start_distance = this.scroll_start_distance() as f64;
88
89        if !this.scrolling.get() {
90            let tracked = this.tracked_root_point.get();
91            let (offset, viewport, extent) = (this.offset(), this.viewport(), this.extent());
92
93            // At the end of the scrollable range in the direction of the
94            // drag: `offset + viewport - extent == 0`, when all are known.
95            let at_end = |offset: Option<f64>, viewport: Option<f64>, extent: Option<f64>| match (offset, viewport, extent)
96            {
97                (Some(offset), Some(viewport), Some(extent)) => offset + viewport - extent == 0.0,
98                _ => false,
99            };
100
101            if this.can_vertically_scroll() {
102                let delta = tracked.y - root_point.y;
103
104                if offset.map(|o| o.y) == Some(0.0) && delta < 0.0 {
105                    return;
106                }
107
108                if at_end(offset.map(|o| o.y), viewport.map(|v| v.height), extent.map(|x| x.height)) && delta > 0.0 {
109                    return;
110                }
111
112                if delta.abs() > start_distance {
113                    this.scrolling.set(true);
114                }
115            }
116
117            if this.can_horizontally_scroll() {
118                let delta = tracked.x - root_point.x;
119
120                if offset.map(|o| o.x) == Some(0.0) && delta < 0.0 {
121                    return;
122                }
123
124                if at_end(offset.map(|o| o.x), viewport.map(|v| v.width), extent.map(|x| x.width)) && delta > 0.0 {
125                    return;
126                }
127
128                if delta.abs() > start_distance {
129                    this.scrolling.set(true);
130                }
131            }
132
133            if this.scrolling.get() {
134                // Correct the tracked point with the start distance, so
135                // scrolling does not start with a skip of the start
136                // distance.
137                this.tracked_root_point.set(Point::new(
138                    tracked.x - if tracked.x >= root_point.x { start_distance } else { -start_distance },
139                    tracked.y - if tracked.y >= root_point.y { start_distance } else { -start_distance },
140                ));
141            }
142        }
143
144        if this.scrolling.get() {
145            let vector = Vector::from(this.tracked_root_point.get() - root_point);
146
147            let old_delta = this.delta.get();
148            let pressed = this.pointer_pressed_point.get();
149            let delta = Point::new(pressed.x - root_point.x, pressed.y - root_point.y);
150            this.delta.set(delta);
151
152            if old_delta == delta {
153                return;
154            }
155
156            if let Some(tracker) = this.velocity_tracker.borrow_mut().as_mut() {
157                tracker.add_position(Duration::from_millis(e.timestamp()), Vector::new(delta.x, delta.y));
158            }
159
160            this.last_move_timestamp.set(Some(e.timestamp()));
161
162            let scroll_event_args = ScrollGestureEventArgs::new(this.gesture_id.get(), vector);
163            if let Some(target) = this.target() {
164                target.raise_event(&scroll_event_args);
165            }
166
167            this.tracked_root_point.set(root_point);
168
169            e.set_handled(scroll_event_args.handled());
170
171            if e.handled() {
172                this.capture(e.pointer());
173            }
174        }
175    }
176
177    fn pointer_capture_lost(this: &Self, pointer: &Rc<dyn IPointer>) {
178        if this.is_tracking(pointer) {
179            this.end_gesture();
180        }
181    }
182
183    fn pointer_released(this: &Self, e: &PointerReleasedEventArgs) {
184        if !(this.is_tracking(e.pointer()) && this.scrolling.get()) {
185            return;
186        }
187
188        let inertia = this
189            .velocity_tracker
190            .borrow()
191            .as_ref()
192            .map_or(Vector::default(), |tracker| {
193                tracker.get_fling_velocity(Duration::from_millis(e.timestamp())).pixels_per_second
194            });
195        this.inertia.set(Some(inertia));
196
197        e.set_handled(true);
198
199        let last_move = this.last_move_timestamp.get();
200        let too_late = last_move.is_some_and(|last| e.timestamp().wrapping_sub(last) > 200);
201
202        if inertia == Vector::default()
203            || e.timestamp() == 0
204            || last_move == Some(0)
205            || too_late
206            || !this.is_scroll_inertia_enabled()
207        {
208            this.end_gesture();
209        } else {
210            drop(this.tracking.replace(None));
211            // The first frame of the inertia scrolls by the time since the
212            // release, as upstream: an inertia clock that started with the
213            // first frame made that frame scroll by nothing, and a scroll
214            // content presenter that chains scrolling (its default) leaves
215            // such an event unhandled, which ended the gesture at once.
216            let stop_watch = Instant::now();
217            this.stop_watch.set(Some(stop_watch));
218            this.last_time.set(stop_watch.elapsed());
219            this.inertia_start_time.set(this.last_time.get());
220            this.current_inertia_gesture_id.set(this.gesture_id.get());
221
222            if let Some(target) = this.target() {
223                target.raise_event(&ScrollGestureInertiaStartingEventArgs::new(this.gesture_id.get(), inertia));
224            }
225
226            this.request_animation_frame();
227        }
228    }
229}
230
231impl ScrollGestureRecognizer {
232    /// Pixels per second speed that is considered to be the stop of
233    /// inertial scroll.
234    pub const INERTIAL_SCROLL_SPEED_END: f64 = 5.0;
235
236    /// The resistance applied to inertial scrolling.
237    pub const INERTIAL_RESISTANCE: f64 = 0.15;
238
239}
240
241crate::ferro_properties! { impl ScrollGestureRecognizer {
242    ferro_property!(
243        /// Defines the `CanHorizontallyScroll` property.
244        pub fn can_horizontally_scroll_property() -> DirectProperty<ScrollGestureRecognizer, bool> {
245            FerroProperty::register_direct::<ScrollGestureRecognizer, _>(
246                "CanHorizontallyScroll",
247                |o| o.can_horizontally_scroll(),
248                Some(|o, v| o.set_can_horizontally_scroll(v)),
249                false,
250            )
251        }
252    );
253
254    ferro_property!(
255        /// Defines the `CanVerticallyScroll` property.
256        pub fn can_vertically_scroll_property() -> DirectProperty<ScrollGestureRecognizer, bool> {
257            FerroProperty::register_direct::<ScrollGestureRecognizer, _>(
258                "CanVerticallyScroll",
259                |o| o.can_vertically_scroll(),
260                Some(|o, v| o.set_can_vertically_scroll(v)),
261                false,
262            )
263        }
264    );
265
266    ferro_property!(
267        /// Defines the `IsScrollInertiaEnabled` property.
268        pub fn is_scroll_inertia_enabled_property() -> DirectProperty<ScrollGestureRecognizer, bool> {
269            FerroProperty::register_direct::<ScrollGestureRecognizer, _>(
270                "IsScrollInertiaEnabled",
271                |o| o.is_scroll_inertia_enabled(),
272                Some(|o, v| o.set_is_scroll_inertia_enabled(v)),
273                false,
274            )
275        }
276    );
277
278    ferro_property!(
279        /// Defines the `ScrollStartDistance` property.
280        pub fn scroll_start_distance_property() -> DirectProperty<ScrollGestureRecognizer, i32> {
281            FerroProperty::register_direct_with::<ScrollGestureRecognizer, _>(
282                "ScrollStartDistance",
283                |o| o.scroll_start_distance(),
284                Some(|o, v| o.set_scroll_start_distance(v)),
285                DirectPropertyMetadata::new(Some(default_scroll_start_distance())),
286            )
287        }
288    );
289
290    ferro_property!(
291        /// Defines the `Offset` property.
292        pub fn offset_property() -> DirectProperty<ScrollGestureRecognizer, Option<Vector>> {
293            FerroProperty::register_direct::<ScrollGestureRecognizer, _>(
294                "Offset",
295                |o| o.offset(),
296                Some(|o, v| o.set_offset(v)),
297                None,
298            )
299        }
300    );
301
302    ferro_property!(
303        /// Defines the `Extent` property.
304        pub fn extent_property() -> DirectProperty<ScrollGestureRecognizer, Option<Size>> {
305            FerroProperty::register_direct::<ScrollGestureRecognizer, _>(
306                "Extent",
307                |o| o.extent(),
308                Some(|o, v| o.set_extent(v)),
309                None,
310            )
311        }
312    );
313
314    ferro_property!(
315        /// Defines the `Viewport` property.
316        pub fn viewport_property() -> DirectProperty<ScrollGestureRecognizer, Option<Size>> {
317            FerroProperty::register_direct::<ScrollGestureRecognizer, _>(
318                "Viewport",
319                |o| o.viewport(),
320                Some(|o, v| o.set_viewport(v)),
321                None,
322            )
323        }
324    );
325} }
326
327impl ScrollGestureRecognizer {
328    pub fn new() -> Ref<Self> {
329        instantiate(Self {
330            base: GestureRecognizer::construct(),
331            can_horizontally_scroll: Cell::new(false),
332            can_vertically_scroll: Cell::new(false),
333            is_scroll_inertia_enabled: Cell::new(false),
334            offset: Cell::new(None),
335            viewport: Cell::new(None),
336            extent: Cell::new(None),
337            scroll_start_distance: Cell::new(default_scroll_start_distance()),
338            scrolling: Cell::new(false),
339            tracked_root_point: Cell::new(Point::default()),
340            tracking: RefCell::new(None),
341            stop_watch: Cell::new(None),
342            gesture_id: Cell::new(0),
343            pointer_pressed_point: Cell::new(Point::default()),
344            velocity_tracker: RefCell::new(None),
345            inertia: Cell::new(None),
346            last_move_timestamp: Cell::new(None),
347            last_time: Cell::new(Duration::ZERO),
348            inertia_start_time: Cell::new(Duration::ZERO),
349            current_inertia_gesture_id: Cell::new(0),
350            delta: Cell::new(Point::default()),
351        })
352    }
353
354    /// Whether the content can be scrolled horizontally.
355    pub fn can_horizontally_scroll(&self) -> bool {
356        self.can_horizontally_scroll.get()
357    }
358
359    pub fn set_can_horizontally_scroll(&self, value: bool) {
360        self.set_and_raise_cell(Self::can_horizontally_scroll_property(), &self.can_horizontally_scroll, value);
361    }
362
363    /// Whether the content can be scrolled vertically.
364    pub fn can_vertically_scroll(&self) -> bool {
365        self.can_vertically_scroll.get()
366    }
367
368    pub fn set_can_vertically_scroll(&self, value: bool) {
369        self.set_and_raise_cell(Self::can_vertically_scroll_property(), &self.can_vertically_scroll, value);
370    }
371
372    /// Whether scroll inertia is enabled.
373    pub fn is_scroll_inertia_enabled(&self) -> bool {
374        self.is_scroll_inertia_enabled.get()
375    }
376
377    pub fn set_is_scroll_inertia_enabled(&self, value: bool) {
378        self.set_and_raise_cell(Self::is_scroll_inertia_enabled_property(), &self.is_scroll_inertia_enabled, value);
379    }
380
381    /// The minimum distance the pointer must move before scrolling starts.
382    pub fn scroll_start_distance(&self) -> i32 {
383        self.scroll_start_distance.get()
384    }
385
386    pub fn set_scroll_start_distance(&self, value: i32) {
387        self.set_and_raise_cell(Self::scroll_start_distance_property(), &self.scroll_start_distance, value);
388    }
389
390    /// The extent of the scrollable content, if known.
391    pub fn extent(&self) -> Option<Size> {
392        self.extent.get()
393    }
394
395    pub fn set_extent(&self, value: Option<Size>) {
396        self.set_and_raise_cell(Self::extent_property(), &self.extent, value);
397    }
398
399    /// The current scroll offset, if known.
400    pub fn offset(&self) -> Option<Vector> {
401        self.offset.get()
402    }
403
404    pub fn set_offset(&self, value: Option<Vector>) {
405        self.set_and_raise_cell(Self::offset_property(), &self.offset, value);
406    }
407
408    /// The size of the viewport, if known.
409    pub fn viewport(&self) -> Option<Size> {
410        self.viewport.get()
411    }
412
413    pub fn set_viewport(&self, value: Option<Size>) {
414        self.set_and_raise_cell(Self::viewport_property(), &self.viewport, value);
415    }
416
417    fn is_tracking(&self, pointer: &Rc<dyn IPointer>) -> bool {
418        self.tracking.borrow().as_ref().is_some_and(|t| std::ptr::addr_eq(Rc::as_ptr(t), Rc::as_ptr(pointer)))
419    }
420
421    fn end_gesture(&self) {
422        drop(self.tracking.replace(None));
423        if self.scrolling.get() {
424            self.stop_watch.set(None);
425            self.inertia.set(Some(Vector::default()));
426            self.delta.set(Point::default());
427            self.scrolling.set(false);
428            drop(self.velocity_tracker.replace(None));
429            if let Some(target) = self.target() {
430                target.raise_event(&ScrollGestureEndedEventArgs::new(self.gesture_id.get()));
431            }
432            self.gesture_id.set(0);
433            self.last_move_timestamp.set(None);
434        }
435    }
436
437    fn request_animation_frame(&self) {
438        // The animation frames of the top level are not available in the
439        // base library, so the global media context is used.
440        let weak = self.to_ref().downgrade();
441        MediaContext::instance().request_animation_frame(move |_| {
442            if let Some(this) = weak.upgrade() {
443                this.on_animation_requested();
444            }
445        });
446    }
447
448    fn on_animation_requested(&self) {
449        // Calculate the current speed and dispatch the next inertia event.
450        // This is done asynchronously so the events run with input
451        // priority.
452        let weak = self.to_ref().downgrade();
453        drop(Dispatcher::current_dispatcher().invoke_async_local_with_priority(
454            move || {
455                if let Some(this) = weak.upgrade() {
456                    this.inertia_tick();
457                }
458            },
459            DispatcherPriority::INPUT,
460        ));
461    }
462
463    fn inertia_tick(&self) {
464        // Another gesture has started, finish the current one
465        let Some(inertia) = self.inertia.get() else { return };
466        let Some(stop_watch) = self.stop_watch.get() else { return };
467        if self.gesture_id.get() != self.current_inertia_gesture_id.get() {
468            return;
469        }
470
471        let time_span = stop_watch.elapsed();
472        let elapsed_since_last_tick = time_span.saturating_sub(self.last_time.get());
473        self.last_time.set(time_span);
474
475        let speed = inertia
476            * Self::INERTIAL_RESISTANCE.powf(time_span.saturating_sub(self.inertia_start_time.get()).as_secs_f64());
477        let distance = speed * elapsed_since_last_tick.as_secs_f64();
478        let scroll_gesture_event_args = ScrollGestureEventArgs::new(self.gesture_id.get(), distance);
479        if let Some(target) = self.target() {
480            target.raise_event(&scroll_gesture_event_args);
481        }
482
483        if !scroll_gesture_event_args.handled() || scroll_gesture_event_args.should_end_scroll_gesture() {
484            self.end_gesture();
485            return;
486        }
487
488        // End the gesture using the inertial scroll speed end only in the
489        // direction of scrolling
490        let (vertical, horizontal) = (self.can_vertically_scroll(), self.can_horizontally_scroll());
491        if vertical
492            && horizontal
493            && speed.x.abs() < Self::INERTIAL_SCROLL_SPEED_END
494            && speed.y.abs() <= Self::INERTIAL_SCROLL_SPEED_END
495        {
496            // NO-OP
497        } else if vertical && speed.y.abs() <= Self::INERTIAL_SCROLL_SPEED_END {
498            self.end_gesture();
499            return;
500        } else if horizontal && speed.x.abs() < Self::INERTIAL_SCROLL_SPEED_END {
501            self.end_gesture();
502            return;
503        }
504
505        // Reschedule on the next animation frame.
506        self.request_animation_frame();
507    }
508}