use crate::core::Point;
use crate::event::Event;
use super::{
DoubleTapGesture, FlingGesture, LongPressDragGesture, LongPressGesture, PanGesture,
PinchGesture, RotateGesture, SwipeGesture, TapGesture, TwoFingerSwipeGesture,
TwoFingerTapGesture,
};
pub(crate) const DOUBLE_TAP_TIMEOUT_MS: u64 = 400;
pub(crate) const LONG_PRESS_MIN_MS: u64 = 500;
pub(crate) const SWIPE_MIN_VELOCITY: f32 = 500.0;
pub(crate) const MAX_STATIONARY_DISTANCE: f32 = 15.0;
pub(crate) const LONG_PRESS_MAX_MOVE: f32 = 10.0;
pub(crate) const SWIPE_MIN_DISTANCE: f32 = 30.0;
pub(crate) const PAN_MIN_DISTANCE: f32 = 8.0;
pub(crate) const TAP_TIMEOUT_MS: u64 = 300;
pub trait GestureRecognizer: std::fmt::Debug + Send {
fn process(&mut self, event: &Event, now_ms: u64) -> Option<Event>;
fn reset(&mut self);
}
#[derive(Debug)]
pub struct GestureEngine {
recognizers: Vec<Box<dyn GestureRecognizer>>,
last_timestamp_ms: u64,
}
impl GestureEngine {
pub fn new() -> Self {
let recognizers: Vec<Box<dyn GestureRecognizer>> = vec![
Box::new(TapGesture::new()),
Box::new(DoubleTapGesture::new()),
Box::new(LongPressGesture::new()),
Box::new(LongPressDragGesture::new()),
Box::new(PanGesture::new()),
Box::new(SwipeGesture::new()),
Box::new(FlingGesture::new()),
Box::new(TwoFingerTapGesture::new()),
Box::new(TwoFingerSwipeGesture::new()),
Box::new(PinchGesture::new()),
Box::new(RotateGesture::new()),
];
Self { recognizers, last_timestamp_ms: 0 }
}
pub fn process(&mut self, event: &Event, now_ms: u64) -> Option<Event> {
self.last_timestamp_ms = now_ms;
let mut produced: Option<Event> = None;
let mut derived: Option<Event> = None;
for r in &mut self.recognizers {
let mut hit = r.process(event, now_ms);
if hit.is_none() {
if let Some(ref derived_event) = derived {
hit = r.process(derived_event, now_ms);
}
}
if let Some(gesture_event) = hit {
if produced.as_ref().is_none_or(|best| Self::is_more_specific(&gesture_event, best))
{
produced = Some(gesture_event.clone());
}
derived = Some(gesture_event);
}
}
produced
}
fn is_more_specific(candidate: &Event, incumbent: &Event) -> bool {
Self::gesture_rank(candidate) > Self::gesture_rank(incumbent)
}
fn gesture_rank(event: &Event) -> u8 {
match event {
Event::Drag { .. } => 0,
Event::Tap { .. } | Event::LongPress { .. } => 1,
Event::Swipe { .. } | Event::Pinch { .. } | Event::Rotate { .. } => 2,
Event::Fling { .. } => 3,
Event::DoubleTap { .. } | Event::TwoFingerTap { .. } | Event::TwoFingerSwipe { .. } => {
4
}
_ => 0,
}
}
pub fn reset_all(&mut self) {
for r in &mut self.recognizers {
r.reset();
}
}
}
crate::impl_default_via_new!(GestureEngine);
pub(crate) fn distance(a: Point, b: Point) -> f32 {
let dx = (a.x - b.x) as f32;
let dy = (a.y - b.y) as f32;
(dx * dx + dy * dy).sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::Event;
#[test]
fn gesture_engine_new() {
let engine = GestureEngine::new();
assert_eq!(engine.recognizers.len(), 11);
}
#[test]
fn gesture_engine_process_none_for_unrelated_event() {
let mut engine = GestureEngine::new();
let result =
engine.process(&Event::MousePress { pos: crate::core::Point::new(0, 0), button: 0 }, 0);
assert!(result.is_none());
}
#[test]
fn gesture_engine_reset_all_clears_state() {
let mut engine = GestureEngine::new();
engine.reset_all();
assert_eq!(engine.recognizers.len(), 11);
}
#[test]
fn every_recogniser_reports_velocity_in_pixels_per_second() {
const START: i32 = 0;
const END: i32 = 100; const ELAPSED_MS: u64 = 100; const TOUCH: u64 = 1;
const EXPECTED: std::ops::RangeInclusive<f32> = 900.0..=1100.0;
let mut swipe = SwipeGesture::new();
assert!(swipe
.process(&Event::TouchBegin { pos: Point::new(START, START), touch_id: TOUCH }, 0)
.is_none());
let swipe_event = swipe
.process(&Event::TouchEnd { pos: Point::new(END, START), touch_id: TOUCH }, ELAPSED_MS);
let swipe_velocity = match swipe_event {
Some(Event::Swipe { velocity, .. }) => velocity,
other => panic!("expected Event::Swipe over 100px in 100ms, got {other:?}"),
};
let mut fling = FlingGesture::new();
assert!(fling
.process(&Event::TouchBegin { pos: Point::new(START, START), touch_id: TOUCH }, 0)
.is_none());
for step in 1..=4u64 {
fling.process(
&Event::TouchMove {
pos: Point::new(START + (step as i32) * (END - START) / 4, START),
touch_id: TOUCH,
},
ELAPSED_MS / 4 * step,
);
}
let fling_event = fling
.process(&Event::TouchEnd { pos: Point::new(END, START), touch_id: TOUCH }, ELAPSED_MS);
let fling_velocity = match fling_event {
Some(Event::Fling { velocity, .. }) => velocity,
other => panic!("expected Event::Fling over 100px in 100ms, got {other:?}"),
};
for (name, velocity) in [("swipe", swipe_velocity), ("fling", fling_velocity.x as f32)] {
assert!(
EXPECTED.contains(&velocity),
"{name} velocity must be ~1000 px/s for 100px in 100ms, got {velocity}. \
A value near 1 means px/ms leaked through, which contradicts the \
unit documented on Event::Swipe / Event::Fling"
);
}
}
#[test]
fn second_tap_is_reported_as_double_tap() {
let mut engine = GestureEngine::new();
let p = Point::new(10, 10);
assert!(engine.process(&Event::TouchBegin { pos: p, touch_id: 1 }, 0).is_none());
let first = engine.process(&Event::TouchEnd { pos: p, touch_id: 1 }, 50);
assert!(
matches!(first, Some(Event::Tap { .. })),
"a lone tap must be reported as Tap, got {first:?}"
);
assert!(engine.process(&Event::TouchBegin { pos: p, touch_id: 1 }, 100).is_none());
let second = engine.process(&Event::TouchEnd { pos: p, touch_id: 1 }, 150);
assert!(
matches!(second, Some(Event::DoubleTap { .. })),
"the second tap must upgrade to DoubleTap, got {second:?}"
);
}
#[test]
fn one_finger_tap_is_never_two_finger_tap() {
let mut engine = GestureEngine::new();
let p = Point::new(10, 10);
engine.process(&Event::TouchBegin { pos: p, touch_id: 1 }, 0);
let released = engine.process(&Event::TouchEnd { pos: p, touch_id: 1 }, 50);
assert!(
!matches!(released, Some(Event::TwoFingerTap { .. })),
"one finger cannot make a two-finger tap, got {released:?}"
);
}
#[test]
fn two_fingers_together_are_two_finger_tap() {
let mut engine = GestureEngine::new();
engine.process(&Event::TouchBegin { pos: Point::new(10, 10), touch_id: 1 }, 0);
engine.process(&Event::TouchBegin { pos: Point::new(30, 10), touch_id: 2 }, 10);
engine.process(&Event::TouchEnd { pos: Point::new(10, 10), touch_id: 1 }, 40);
let released =
engine.process(&Event::TouchEnd { pos: Point::new(30, 10), touch_id: 2 }, 50);
assert!(
matches!(released, Some(Event::TwoFingerTap { .. })),
"two fingers down and up together must be a TwoFingerTap, got {released:?}"
);
}
#[test]
fn pan_waits_for_the_distance_threshold() {
let mut engine = GestureEngine::new();
engine.process(&Event::TouchBegin { pos: Point::new(0, 0), touch_id: 1 }, 0);
let jitter = engine.process(&Event::TouchMove { pos: Point::new(3, 0), touch_id: 1 }, 10);
assert!(jitter.is_none(), "3 px is jitter, so no Drag yet, got {jitter:?}");
let drag = engine.process(&Event::TouchMove { pos: Point::new(50, 0), touch_id: 1 }, 20);
assert!(
matches!(drag, Some(Event::Drag { .. })),
"50 px of travel is a drag, got {drag:?}"
);
}
#[test]
fn rotation_never_reports_a_full_turn_for_a_tiny_change() {
let mut engine = GestureEngine::new();
let a = Point::new(1000, 26); let b = Point::new(-1000, -26); engine.process(&Event::TouchBegin { pos: a, touch_id: 1 }, 0);
engine.process(&Event::TouchBegin { pos: b, touch_id: 2 }, 0);
let b2 = Point::new(-1000, 26); let produced = engine.process(&Event::TouchMove { pos: b2, touch_id: 2 }, 10);
if let Some(Event::Rotate { angle }) = produced {
assert!(
angle.abs() <= core::f32::consts::PI,
"a rotation must be the shortest signed turn (|angle| <= pi), got {angle}. \
A value near 2*pi means the atan2 wrap-around was not normalised"
);
}
}
#[test]
fn long_hold_produces_long_press() {
let mut engine = GestureEngine::new();
let p = Point::new(40, 60);
assert!(engine.process(&Event::TouchBegin { pos: p, touch_id: 1 }, 0).is_none());
let produced = engine.process(&Event::Timer { id: 0 }, LONG_PRESS_MIN_MS + 1);
match produced {
Some(Event::LongPress { pos }) => assert_eq!(pos, p),
other => panic!("a {LONG_PRESS_MIN_MS}ms hold must be a LongPress, got {other:?}"),
}
}
#[test]
fn fast_flick_produces_fling() {
let mut engine = GestureEngine::new();
let start = Point::new(0, 0);
let end = Point::new(200, 0);
engine.process(&Event::TouchBegin { pos: start, touch_id: 1 }, 0);
engine.process(&Event::TouchMove { pos: Point::new(100, 0), touch_id: 1 }, 25);
let produced = engine.process(&Event::TouchEnd { pos: end, touch_id: 1 }, 50);
match produced {
Some(Event::Fling { velocity, .. }) => assert!(
velocity.x > 0,
"a flick to the right must have positive x velocity, got {velocity:?}"
),
other => panic!("a 200px flick in 50ms must produce Fling, got {other:?}"),
}
}
#[test]
fn fingers_moving_apart_produce_pinch_out() {
let mut engine = GestureEngine::new();
engine.process(&Event::TouchBegin { pos: Point::new(100, 100), touch_id: 1 }, 0);
engine.process(&Event::TouchBegin { pos: Point::new(200, 100), touch_id: 2 }, 0);
let produced =
engine.process(&Event::TouchMove { pos: Point::new(300, 100), touch_id: 2 }, 10);
match produced {
Some(Event::Pinch { scale }) => {
assert!(scale > 1.0, "spreading the fingers must zoom in (scale > 1), got {scale}")
}
other => panic!("two fingers moving apart must produce Pinch, got {other:?}"),
}
}
#[test]
fn two_fingers_moving_together_produce_two_finger_swipe() {
let mut engine = GestureEngine::new();
engine.process(&Event::TouchBegin { pos: Point::new(0, 100), touch_id: 1 }, 0);
engine.process(&Event::TouchBegin { pos: Point::new(0, 120), touch_id: 2 }, 0);
engine.process(&Event::TouchMove { pos: Point::new(150, 100), touch_id: 1 }, 25);
engine.process(&Event::TouchMove { pos: Point::new(150, 120), touch_id: 2 }, 25);
engine.process(&Event::TouchEnd { pos: Point::new(150, 100), touch_id: 1 }, 50);
let produced =
engine.process(&Event::TouchEnd { pos: Point::new(150, 120), touch_id: 2 }, 50);
assert!(
matches!(produced, Some(Event::TwoFingerSwipe { .. })),
"two fingers swiping together must produce TwoFingerSwipe, got {produced:?}"
);
}
#[test]
fn two_independent_contacts_reach_pinch() {
let mut engine = GestureEngine::new();
engine.process(&Event::TouchBegin { pos: Point::new(100, 100), touch_id: 1 }, 0);
engine.process(&Event::TouchBegin { pos: Point::new(200, 100), touch_id: 2 }, 1);
engine.process(&Event::TouchMove { pos: Point::new(40, 100), touch_id: 1 }, 2);
let produced =
engine.process(&Event::TouchMove { pos: Point::new(260, 100), touch_id: 2 }, 3);
match produced {
Some(Event::Pinch { scale }) => assert!(
scale > 1.0,
"spreading two identified fingers must zoom in, got scale {scale}"
),
other => panic!(
"two independent contacts must reach Pinch; got {other:?}. \
If this fails, a backend has stopped emitting TouchBegin/Move with \
distinct touch ids"
),
}
}
#[test]
fn two_independent_contacts_reach_rotate() {
let mut engine = GestureEngine::new();
engine.process(&Event::TouchBegin { pos: Point::new(100, 100), touch_id: 1 }, 0);
engine.process(&Event::TouchBegin { pos: Point::new(200, 100), touch_id: 2 }, 1);
let produced =
engine.process(&Event::TouchMove { pos: Point::new(100, 200), touch_id: 2 }, 2);
match produced {
Some(Event::Rotate { angle }) => {
let quarter_turn = core::f32::consts::FRAC_PI_2;
assert!(
(angle - quarter_turn).abs() < 0.01,
"a quarter turn must report pi/2 (~1.5708), got {angle}"
);
}
other => panic!("two independent contacts must reach Rotate, got {other:?}"),
}
}
#[test]
fn ordinary_rotation_is_reported_with_the_correct_sign() {
let mut engine = GestureEngine::new();
engine.process(&Event::TouchBegin { pos: Point::new(100, 100), touch_id: 1 }, 0);
engine.process(&Event::TouchBegin { pos: Point::new(200, 100), touch_id: 2 }, 0);
let produced =
engine.process(&Event::TouchMove { pos: Point::new(100, 0), touch_id: 2 }, 10);
match produced {
Some(Event::Rotate { angle }) => {
let quarter_turn = core::f32::consts::FRAC_PI_2;
assert!(
(angle + quarter_turn).abs() < 0.01,
"a quarter turn to the screen-up direction must report -pi/2 (~-1.5708), \
got {angle}"
);
}
other => panic!("a quarter-turn rotation must produce Rotate, got {other:?}"),
}
}
}