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 as i64 - b.x as i64) as f32;
let dy = (a.y as i64 - b.y as i64) as f32;
(dx * dx + dy * dy).sqrt()
}
pub(crate) fn point_delta(from: Point, to: Point) -> (i32, i32) {
let dx = (to.x as i64 - from.x as i64).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
let dy = (to.y as i64 - from.y as i64).clamp(i32::MIN as i64, i32::MAX as i64) as i32;
(dx, dy)
}
pub(crate) fn point_delta_abs(from: Point, to: Point) -> (u32, u32) {
let dx = (to.x as i64 - from.x as i64).unsigned_abs().min(u32::MAX as u64) as u32;
let dy = (to.y as i64 - from.y as i64).unsigned_abs().min(u32::MAX as u64) as u32;
(dx, dy)
}
#[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, modifiers: 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:?}"),
}
}
#[test]
fn distance_is_total_across_the_i32_domain() {
let a = Point::new(i32::MIN, i32::MIN);
let b = Point::new(i32::MAX, i32::MAX);
let d = distance(a, b);
assert!(d.is_finite() && d > 1.0e9, "distance overflowed to {d}");
assert_eq!(distance(Point::new(5, 7), Point::new(5, 7)), 0.0);
assert!((distance(Point::new(0, 0), Point::new(3, 4)) - 5.0).abs() < 1.0e-6);
}
#[test]
fn point_delta_helpers_saturate_instead_of_wrapping() {
let (dx, dy) = point_delta(Point::new(i32::MIN, 0), Point::new(i32::MAX, 0));
assert_eq!(dx, i32::MAX, "the difference must saturate, not wrap to -1");
assert_eq!(dy, 0);
let (ax, _) = point_delta_abs(Point::new(0, 0), Point::new(i32::MIN, 0));
assert_eq!(ax, 1u32 << 31, "`|-2^31|` is representable as a `u32`");
let (bx, _) = point_delta_abs(Point::new(i32::MAX, 0), Point::new(i32::MIN, 0));
assert_eq!(bx, u32::MAX, "the full-range span saturates at u32::MAX");
}
}