1use std::time::{Duration, Instant};
14
15use crate::{Axis, IsZero, Pixels, Point, TouchPhase, px};
16
17const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
18
19#[derive(Clone, Copy, Debug, Default)]
21pub struct OngoingScroll {
22 last_event: Option<Instant>,
23 axis: Option<Axis>,
24}
25
26impl OngoingScroll {
27 pub fn filter(&mut self, delta: &mut Point<Pixels>, touch_phase: TouchPhase) {
32 self.filter_at(delta, touch_phase, Instant::now())
33 }
34
35 fn filter_at(&mut self, delta: &mut Point<Pixels>, touch_phase: TouchPhase, now: Instant) {
36 const UNLOCK_PERCENT: f32 = 1.9;
37 const UNLOCK_LOWER_BOUND: Pixels = px(6.);
38
39 if matches!(touch_phase, TouchPhase::Ended | TouchPhase::Cancelled) {
40 self.last_event = None;
41 self.axis = None;
42 return;
43 }
44
45 let x = delta.x.abs();
46 let y = delta.y.abs();
47 if x.is_zero() && y.is_zero() {
48 if touch_phase == TouchPhase::Started {
49 self.last_event = None;
50 self.axis = None;
51 }
52 return;
53 }
54
55 let starts_new_gesture = touch_phase == TouchPhase::Started
56 || self
57 .last_event
58 .is_none_or(|last_event| now.duration_since(last_event) >= SCROLL_EVENT_SEPARATION);
59 let mut axis = self.axis;
60 if starts_new_gesture {
61 axis = if x <= y {
62 Some(Axis::Vertical)
63 } else {
64 Some(Axis::Horizontal)
65 };
66 } else if x.max(y) >= UNLOCK_LOWER_BOUND {
67 match axis {
68 Some(Axis::Vertical) if x > y && x >= y * UNLOCK_PERCENT => {
69 axis = None;
70 }
71 Some(Axis::Horizontal) if y > x && y >= x * UNLOCK_PERCENT => {
72 axis = None;
73 }
74 _ => {}
75 }
76 }
77
78 self.last_event = Some(now);
79 self.axis = axis;
80 match axis {
81 Some(Axis::Vertical) => delta.x = Pixels::ZERO,
82 Some(Axis::Horizontal) => delta.y = Pixels::ZERO,
83 None => {}
84 }
85 }
86}
87
88#[derive(Clone, Copy, Debug, PartialEq)]
92pub struct GestureTuning {
93 pub touch_slop: Pixels,
96 pub multi_tap_interval: Duration,
98 pub multi_tap_slop: Pixels,
100 pub long_press_duration: Duration,
103 pub momentum_decay_per_ms: f32,
107 pub min_fling_velocity: f32,
110}
111
112impl Default for GestureTuning {
113 fn default() -> Self {
114 Self {
115 touch_slop: px(8.),
116 multi_tap_interval: Duration::from_millis(400),
117 multi_tap_slop: px(16.),
118 long_press_duration: Duration::from_millis(500),
119 momentum_decay_per_ms: 0.998,
120 min_fling_velocity: 50.,
121 }
122 }
123}
124
125#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
131pub struct GestureKinds {
132 pub tap: bool,
134 pub long_press: bool,
136 pub pan: bool,
139 pub pinch: bool,
141}
142
143impl GestureKinds {
144 pub const NONE: Self = Self {
146 tap: false,
147 long_press: false,
148 pan: false,
149 pinch: false,
150 };
151
152 pub const ALL: Self = Self {
154 tap: true,
155 long_press: true,
156 pan: true,
157 pinch: true,
158 };
159}
160
161#[derive(Clone, Debug, Default)]
169pub struct LongPressEvent {
170 pub position: Point<Pixels>,
172}
173
174pub trait PlatformGestures {
179 fn tuning(&self) -> GestureTuning {
181 GestureTuning::default()
182 }
183
184 fn native_recognizers(&self) -> GestureKinds {
186 GestureKinds::NONE
187 }
188}
189
190pub struct NullPlatformGestures;
193
194impl PlatformGestures for NullPlatformGestures {}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use crate::point;
200
201 #[test]
202 fn ongoing_scroll_locks_to_dominant_axis() {
203 let now = Instant::now();
204 let mut ongoing_scroll = OngoingScroll::default();
205 let mut horizontal_delta = point(px(10.), px(2.));
206 ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now);
207 assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal));
208 assert_eq!(horizontal_delta, point(px(10.), px(0.)));
209
210 let mut continued_delta = point(px(3.), px(2.));
211 ongoing_scroll.filter_at(
212 &mut continued_delta,
213 TouchPhase::Moved,
214 now + Duration::from_millis(1),
215 );
216 assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal));
217 assert_eq!(continued_delta, point(px(3.), px(0.)));
218 }
219
220 #[test]
221 fn ongoing_scroll_unlocks_when_direction_changes() {
222 let now = Instant::now();
223 let mut ongoing_scroll = OngoingScroll::default();
224 let mut horizontal_delta = point(px(10.), px(2.));
225 ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now);
226
227 let mut vertical_delta = point(px(2.), px(10.));
228 ongoing_scroll.filter_at(
229 &mut vertical_delta,
230 TouchPhase::Moved,
231 now + Duration::from_millis(1),
232 );
233 assert_eq!(ongoing_scroll.axis, None);
234 assert_eq!(vertical_delta, point(px(2.), px(10.)));
235 }
236
237 #[test]
238 fn ongoing_scroll_starts_new_gesture_at_timeout_boundary() {
239 let now = Instant::now();
240 let mut ongoing_scroll = OngoingScroll::default();
241 let mut horizontal_delta = point(px(10.), px(2.));
242 ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Moved, now);
243
244 let mut vertical_delta = point(px(2.), px(10.));
245 ongoing_scroll.filter_at(
246 &mut vertical_delta,
247 TouchPhase::Moved,
248 now + SCROLL_EVENT_SEPARATION,
249 );
250 assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical));
251 assert_eq!(vertical_delta, point(px(0.), px(10.)));
252 }
253
254 #[test]
255 fn ongoing_scroll_ignores_zero_delta_and_resets_when_ended() {
256 let now = Instant::now();
257 let mut ongoing_scroll = OngoingScroll::default();
258 let mut horizontal_delta = point(px(10.), px(2.));
259 ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now);
260
261 let mut zero_delta = Point::default();
262 ongoing_scroll.filter_at(
263 &mut zero_delta,
264 TouchPhase::Ended,
265 now + Duration::from_millis(1),
266 );
267 assert_eq!(ongoing_scroll.axis, None);
268
269 let mut vertical_delta = point(px(2.), px(3.));
270 ongoing_scroll.filter_at(
271 &mut vertical_delta,
272 TouchPhase::Moved,
273 now + Duration::from_millis(2),
274 );
275 assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical));
276 assert_eq!(vertical_delta, point(px(0.), px(3.)));
277 }
278
279 #[test]
280 fn ongoing_scroll_ignores_zero_delta_movement() {
281 let now = Instant::now();
282 let mut ongoing_scroll = OngoingScroll::default();
283 let mut horizontal_delta = point(px(10.), px(2.));
284 ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now);
285
286 let mut zero_delta = Point::default();
287 ongoing_scroll.filter_at(
288 &mut zero_delta,
289 TouchPhase::Moved,
290 now + SCROLL_EVENT_SEPARATION,
291 );
292
293 let mut vertical_delta = point(px(2.), px(10.));
294 ongoing_scroll.filter_at(
295 &mut vertical_delta,
296 TouchPhase::Moved,
297 now + SCROLL_EVENT_SEPARATION,
298 );
299 assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical));
300 assert_eq!(vertical_delta, point(px(0.), px(10.)));
301 }
302
303 #[test]
304 fn ongoing_scroll_supports_moved_only_platforms() {
305 let now = Instant::now();
306 let mut ongoing_scroll = OngoingScroll::default();
307 let mut horizontal_delta = point(px(10.), px(2.));
308 ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Moved, now);
309 assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal));
310 assert_eq!(horizontal_delta, point(px(10.), px(0.)));
311 }
312}