use std::time::{Duration, Instant};
use crate::{Axis, IsZero, Pixels, Point, TouchPhase, px};
const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
#[derive(Clone, Copy, Debug, Default)]
pub struct OngoingScroll {
last_event: Option<Instant>,
axis: Option<Axis>,
}
impl OngoingScroll {
pub fn filter(&mut self, delta: &mut Point<Pixels>, touch_phase: TouchPhase) {
self.filter_at(delta, touch_phase, Instant::now())
}
fn filter_at(&mut self, delta: &mut Point<Pixels>, touch_phase: TouchPhase, now: Instant) {
const UNLOCK_PERCENT: f32 = 1.9;
const UNLOCK_LOWER_BOUND: Pixels = px(6.);
if matches!(touch_phase, TouchPhase::Ended | TouchPhase::Cancelled) {
self.last_event = None;
self.axis = None;
return;
}
let x = delta.x.abs();
let y = delta.y.abs();
if x.is_zero() && y.is_zero() {
if touch_phase == TouchPhase::Started {
self.last_event = None;
self.axis = None;
}
return;
}
let starts_new_gesture = touch_phase == TouchPhase::Started
|| self
.last_event
.is_none_or(|last_event| now.duration_since(last_event) >= SCROLL_EVENT_SEPARATION);
let mut axis = self.axis;
if starts_new_gesture {
axis = if x <= y {
Some(Axis::Vertical)
} else {
Some(Axis::Horizontal)
};
} else if x.max(y) >= UNLOCK_LOWER_BOUND {
match axis {
Some(Axis::Vertical) if x > y && x >= y * UNLOCK_PERCENT => {
axis = None;
}
Some(Axis::Horizontal) if y > x && y >= x * UNLOCK_PERCENT => {
axis = None;
}
_ => {}
}
}
self.last_event = Some(now);
self.axis = axis;
match axis {
Some(Axis::Vertical) => delta.x = Pixels::ZERO,
Some(Axis::Horizontal) => delta.y = Pixels::ZERO,
None => {}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GestureTuning {
pub touch_slop: Pixels,
pub multi_tap_interval: Duration,
pub multi_tap_slop: Pixels,
pub long_press_duration: Duration,
pub momentum_decay_per_ms: f32,
pub min_fling_velocity: f32,
}
impl Default for GestureTuning {
fn default() -> Self {
Self {
touch_slop: px(8.),
multi_tap_interval: Duration::from_millis(400),
multi_tap_slop: px(16.),
long_press_duration: Duration::from_millis(500),
momentum_decay_per_ms: 0.998,
min_fling_velocity: 50.,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct GestureKinds {
pub tap: bool,
pub long_press: bool,
pub pan: bool,
pub pinch: bool,
}
impl GestureKinds {
pub const NONE: Self = Self {
tap: false,
long_press: false,
pan: false,
pinch: false,
};
pub const ALL: Self = Self {
tap: true,
long_press: true,
pan: true,
pinch: true,
};
}
#[derive(Clone, Debug, Default)]
pub struct LongPressEvent {
pub position: Point<Pixels>,
}
pub trait PlatformGestures {
fn tuning(&self) -> GestureTuning {
GestureTuning::default()
}
fn native_recognizers(&self) -> GestureKinds {
GestureKinds::NONE
}
}
pub struct NullPlatformGestures;
impl PlatformGestures for NullPlatformGestures {}
#[cfg(test)]
mod tests {
use super::*;
use crate::point;
#[test]
fn ongoing_scroll_locks_to_dominant_axis() {
let now = Instant::now();
let mut ongoing_scroll = OngoingScroll::default();
let mut horizontal_delta = point(px(10.), px(2.));
ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now);
assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal));
assert_eq!(horizontal_delta, point(px(10.), px(0.)));
let mut continued_delta = point(px(3.), px(2.));
ongoing_scroll.filter_at(
&mut continued_delta,
TouchPhase::Moved,
now + Duration::from_millis(1),
);
assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal));
assert_eq!(continued_delta, point(px(3.), px(0.)));
}
#[test]
fn ongoing_scroll_unlocks_when_direction_changes() {
let now = Instant::now();
let mut ongoing_scroll = OngoingScroll::default();
let mut horizontal_delta = point(px(10.), px(2.));
ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now);
let mut vertical_delta = point(px(2.), px(10.));
ongoing_scroll.filter_at(
&mut vertical_delta,
TouchPhase::Moved,
now + Duration::from_millis(1),
);
assert_eq!(ongoing_scroll.axis, None);
assert_eq!(vertical_delta, point(px(2.), px(10.)));
}
#[test]
fn ongoing_scroll_starts_new_gesture_at_timeout_boundary() {
let now = Instant::now();
let mut ongoing_scroll = OngoingScroll::default();
let mut horizontal_delta = point(px(10.), px(2.));
ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Moved, now);
let mut vertical_delta = point(px(2.), px(10.));
ongoing_scroll.filter_at(
&mut vertical_delta,
TouchPhase::Moved,
now + SCROLL_EVENT_SEPARATION,
);
assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical));
assert_eq!(vertical_delta, point(px(0.), px(10.)));
}
#[test]
fn ongoing_scroll_ignores_zero_delta_and_resets_when_ended() {
let now = Instant::now();
let mut ongoing_scroll = OngoingScroll::default();
let mut horizontal_delta = point(px(10.), px(2.));
ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now);
let mut zero_delta = Point::default();
ongoing_scroll.filter_at(
&mut zero_delta,
TouchPhase::Ended,
now + Duration::from_millis(1),
);
assert_eq!(ongoing_scroll.axis, None);
let mut vertical_delta = point(px(2.), px(3.));
ongoing_scroll.filter_at(
&mut vertical_delta,
TouchPhase::Moved,
now + Duration::from_millis(2),
);
assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical));
assert_eq!(vertical_delta, point(px(0.), px(3.)));
}
#[test]
fn ongoing_scroll_ignores_zero_delta_movement() {
let now = Instant::now();
let mut ongoing_scroll = OngoingScroll::default();
let mut horizontal_delta = point(px(10.), px(2.));
ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Started, now);
let mut zero_delta = Point::default();
ongoing_scroll.filter_at(
&mut zero_delta,
TouchPhase::Moved,
now + SCROLL_EVENT_SEPARATION,
);
let mut vertical_delta = point(px(2.), px(10.));
ongoing_scroll.filter_at(
&mut vertical_delta,
TouchPhase::Moved,
now + SCROLL_EVENT_SEPARATION,
);
assert_eq!(ongoing_scroll.axis, Some(Axis::Vertical));
assert_eq!(vertical_delta, point(px(0.), px(10.)));
}
#[test]
fn ongoing_scroll_supports_moved_only_platforms() {
let now = Instant::now();
let mut ongoing_scroll = OngoingScroll::default();
let mut horizontal_delta = point(px(10.), px(2.));
ongoing_scroll.filter_at(&mut horizontal_delta, TouchPhase::Moved, now);
assert_eq!(ongoing_scroll.axis, Some(Axis::Horizontal));
assert_eq!(horizontal_delta, point(px(10.), px(0.)));
}
}