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;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GestureKind {
Tap,
DoubleTap,
LongPress,
LongPressDrag,
Pan,
Swipe,
Fling,
TwoFingerTap,
TwoFingerSwipe,
Pinch,
Rotate,
Custom(&'static str),
}
impl GestureKind {
pub const fn name(self) -> &'static str {
match self {
Self::Tap => "tap",
Self::DoubleTap => "double_tap",
Self::LongPress => "long_press",
Self::LongPressDrag => "long_press_drag",
Self::Pan => "pan",
Self::Swipe => "swipe",
Self::Fling => "fling",
Self::TwoFingerTap => "two_finger_tap",
Self::TwoFingerSwipe => "two_finger_swipe",
Self::Pinch => "pinch",
Self::Rotate => "rotate",
Self::Custom(name) => name,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GestureState {
#[default]
Maybe,
Triggered,
Finished,
Canceled,
}
#[derive(Debug, Clone)]
pub struct GestureUpdate {
pub state: GestureState,
pub event: Option<Event>,
pub consume: bool,
}
impl GestureUpdate {
pub fn none(state: GestureState) -> Self {
Self { state, event: None, consume: false }
}
pub fn triggered(event: Event) -> Self {
Self { state: GestureState::Triggered, event: Some(event), consume: true }
}
pub fn preview(event: Event) -> Self {
Self { state: GestureState::Maybe, event: Some(event), consume: false }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GestureTuning {
pub swipe_min_velocity: f32,
pub swipe_min_distance: f32,
pub fling_min_velocity: f32,
pub fling_min_distance: f32,
pub double_tap_timeout_ms: u64,
pub long_press_min_ms: u64,
pub tap_timeout_ms: u64,
pub pan_min_distance: f32,
pub max_stationary_distance: f32,
pub long_press_max_move: f32,
}
impl Default for GestureTuning {
fn default() -> Self {
Self {
swipe_min_velocity: SWIPE_MIN_VELOCITY,
swipe_min_distance: SWIPE_MIN_DISTANCE,
fling_min_velocity: 300.0,
fling_min_distance: 15.0,
double_tap_timeout_ms: DOUBLE_TAP_TIMEOUT_MS,
long_press_min_ms: LONG_PRESS_MIN_MS,
tap_timeout_ms: TAP_TIMEOUT_MS,
pan_min_distance: PAN_MIN_DISTANCE,
max_stationary_distance: MAX_STATIONARY_DISTANCE,
long_press_max_move: LONG_PRESS_MAX_MOVE,
}
}
}
thread_local! {
static ACTIVE_TUNING: core::cell::Cell<Option<GestureTuning>> =
const { core::cell::Cell::new(None) };
}
pub fn tuning() -> GestureTuning {
ACTIVE_TUNING.try_with(|cell| cell.get()).ok().flatten().unwrap_or_default()
}
pub fn set_tuning(value: Option<GestureTuning>) -> Option<GestureTuning> {
ACTIVE_TUNING.try_with(|cell| cell.replace(value)).ok().flatten()
}
pub trait GestureRecognizer: std::fmt::Debug + Send {
fn process(&mut self, event: &Event, now_ms: u64) -> Option<Event>;
fn reset(&mut self);
fn kind(&self) -> GestureKind {
GestureKind::Custom("gesture")
}
fn status(&self) -> GestureState {
GestureState::Maybe
}
fn partial(&self) -> Option<Event> {
None
}
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
enum StandardRecognizer {
Tap(TapGesture),
DoubleTap(DoubleTapGesture),
LongPress(LongPressGesture),
LongPressDrag(LongPressDragGesture),
Pan(PanGesture),
Swipe(SwipeGesture),
Fling(FlingGesture),
TwoFingerTap(TwoFingerTapGesture),
TwoFingerSwipe(TwoFingerSwipeGesture),
Pinch(PinchGesture),
Rotate(RotateGesture),
}
impl GestureRecognizer for StandardRecognizer {
fn process(&mut self, event: &Event, now_ms: u64) -> Option<Event> {
match self {
Self::Tap(recognizer) => recognizer.process(event, now_ms),
Self::DoubleTap(recognizer) => recognizer.process(event, now_ms),
Self::LongPress(recognizer) => recognizer.process(event, now_ms),
Self::LongPressDrag(recognizer) => recognizer.process(event, now_ms),
Self::Pan(recognizer) => recognizer.process(event, now_ms),
Self::Swipe(recognizer) => recognizer.process(event, now_ms),
Self::Fling(recognizer) => recognizer.process(event, now_ms),
Self::TwoFingerTap(recognizer) => recognizer.process(event, now_ms),
Self::TwoFingerSwipe(recognizer) => recognizer.process(event, now_ms),
Self::Pinch(recognizer) => recognizer.process(event, now_ms),
Self::Rotate(recognizer) => recognizer.process(event, now_ms),
}
}
fn reset(&mut self) {
match self {
Self::Tap(recognizer) => recognizer.reset(),
Self::DoubleTap(recognizer) => recognizer.reset(),
Self::LongPress(recognizer) => recognizer.reset(),
Self::LongPressDrag(recognizer) => recognizer.reset(),
Self::Pan(recognizer) => recognizer.reset(),
Self::Swipe(recognizer) => recognizer.reset(),
Self::Fling(recognizer) => recognizer.reset(),
Self::TwoFingerTap(recognizer) => recognizer.reset(),
Self::TwoFingerSwipe(recognizer) => recognizer.reset(),
Self::Pinch(recognizer) => recognizer.reset(),
Self::Rotate(recognizer) => recognizer.reset(),
}
}
fn kind(&self) -> GestureKind {
match self {
Self::Tap(recognizer) => recognizer.kind(),
Self::DoubleTap(recognizer) => recognizer.kind(),
Self::LongPress(recognizer) => recognizer.kind(),
Self::LongPressDrag(recognizer) => recognizer.kind(),
Self::Pan(recognizer) => recognizer.kind(),
Self::Swipe(recognizer) => recognizer.kind(),
Self::Fling(recognizer) => recognizer.kind(),
Self::TwoFingerTap(recognizer) => recognizer.kind(),
Self::TwoFingerSwipe(recognizer) => recognizer.kind(),
Self::Pinch(recognizer) => recognizer.kind(),
Self::Rotate(recognizer) => recognizer.kind(),
}
}
fn status(&self) -> GestureState {
match self {
Self::Tap(recognizer) => recognizer.status(),
Self::DoubleTap(recognizer) => recognizer.status(),
Self::LongPress(recognizer) => recognizer.status(),
Self::LongPressDrag(recognizer) => recognizer.status(),
Self::Pan(recognizer) => recognizer.status(),
Self::Swipe(recognizer) => recognizer.status(),
Self::Fling(recognizer) => recognizer.status(),
Self::TwoFingerTap(recognizer) => recognizer.status(),
Self::TwoFingerSwipe(recognizer) => recognizer.status(),
Self::Pinch(recognizer) => recognizer.status(),
Self::Rotate(recognizer) => recognizer.status(),
}
}
fn partial(&self) -> Option<Event> {
match self {
Self::Tap(recognizer) => recognizer.partial(),
Self::DoubleTap(recognizer) => recognizer.partial(),
Self::LongPress(recognizer) => recognizer.partial(),
Self::LongPressDrag(recognizer) => recognizer.partial(),
Self::Pan(recognizer) => recognizer.partial(),
Self::Swipe(recognizer) => recognizer.partial(),
Self::Fling(recognizer) => recognizer.partial(),
Self::TwoFingerTap(recognizer) => recognizer.partial(),
Self::TwoFingerSwipe(recognizer) => recognizer.partial(),
Self::Pinch(recognizer) => recognizer.partial(),
Self::Rotate(recognizer) => recognizer.partial(),
}
}
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
enum RecognizerSlot {
Standard(StandardRecognizer),
Custom(Box<dyn GestureRecognizer>),
}
impl RecognizerSlot {
fn as_recognizer(&mut self) -> &mut dyn GestureRecognizer {
match self {
Self::Standard(recognizer) => recognizer,
Self::Custom(recognizer) => recognizer.as_mut(),
}
}
fn as_recognizer_ref(&self) -> &dyn GestureRecognizer {
match self {
Self::Standard(recognizer) => recognizer,
Self::Custom(recognizer) => recognizer.as_ref(),
}
}
}
#[derive(Debug)]
pub struct GestureEngine {
recognizers: Vec<RecognizerSlot>,
last_timestamp_ms: u64,
conflict: GestureConflictPolicy,
priorities: crate::compat::HashMap<GestureKind, i32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GestureConflictPolicy {
#[default]
Specificity,
Priority,
Exclusive,
}
impl GestureEngine {
pub fn new() -> Self {
let recognizers: Vec<RecognizerSlot> = vec![
RecognizerSlot::Standard(StandardRecognizer::Tap(TapGesture::new())),
RecognizerSlot::Standard(StandardRecognizer::DoubleTap(DoubleTapGesture::new())),
RecognizerSlot::Standard(StandardRecognizer::LongPress(LongPressGesture::new())),
RecognizerSlot::Standard(
StandardRecognizer::LongPressDrag(LongPressDragGesture::new()),
),
RecognizerSlot::Standard(StandardRecognizer::Pan(PanGesture::new())),
RecognizerSlot::Standard(StandardRecognizer::Swipe(SwipeGesture::new())),
RecognizerSlot::Standard(StandardRecognizer::Fling(FlingGesture::new())),
RecognizerSlot::Standard(StandardRecognizer::TwoFingerTap(TwoFingerTapGesture::new())),
RecognizerSlot::Standard(StandardRecognizer::TwoFingerSwipe(
TwoFingerSwipeGesture::new(),
)),
RecognizerSlot::Standard(StandardRecognizer::Pinch(PinchGesture::new())),
RecognizerSlot::Standard(StandardRecognizer::Rotate(RotateGesture::new())),
];
Self {
recognizers,
last_timestamp_ms: 0,
conflict: GestureConflictPolicy::default(),
priorities: crate::compat::HashMap::new(),
}
}
pub fn from_recognizers(recognizers: Vec<Box<dyn GestureRecognizer>>) -> Self {
Self {
recognizers: recognizers.into_iter().map(RecognizerSlot::Custom).collect(),
last_timestamp_ms: 0,
conflict: GestureConflictPolicy::default(),
priorities: crate::compat::HashMap::new(),
}
}
pub fn process(&mut self, event: &Event, now_ms: u64) -> Option<Event> {
self.last_timestamp_ms = now_ms;
let mut produced: Option<(Event, i32, usize)> = None;
let mut derived: Option<Event> = None;
let conflict = self.conflict;
let priorities = &self.priorities;
for (index, slot) in self.recognizers.iter_mut().enumerate() {
let r = slot.as_recognizer();
if !can_rearm_recognizer(event) && r.status() == GestureState::Finished {
continue;
}
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 {
let priority = priorities.get(&r.kind()).copied().unwrap_or(0);
let better = match (conflict, produced.as_ref()) {
(_, None) => true,
(GestureConflictPolicy::Specificity, Some((best, _, _))) => {
Self::is_more_specific(&gesture_event, best)
}
(GestureConflictPolicy::Priority, Some((best, best_priority, _))) => {
priority > *best_priority
|| (priority == *best_priority
&& Self::is_more_specific(&gesture_event, best))
}
(GestureConflictPolicy::Exclusive, Some((_, best_priority, _))) => {
priority > *best_priority
}
};
if better {
produced = Some((gesture_event.clone(), priority, index));
}
derived = Some(gesture_event);
}
}
if conflict == GestureConflictPolicy::Exclusive {
if let Some((_, _, winner)) = produced.as_ref() {
for (index, recognizer) in self.recognizers.iter_mut().enumerate() {
if index != *winner {
recognizer.as_recognizer().reset();
}
}
}
}
produced.map(|(event, _, _)| event)
}
pub fn set_conflict_policy(&mut self, policy: GestureConflictPolicy) {
self.conflict = policy;
}
pub fn conflict_policy(&self) -> GestureConflictPolicy {
self.conflict
}
pub fn set_priority(&mut self, kind: GestureKind, priority: i32) {
self.priorities.insert(kind, priority);
}
pub fn priority_of(&self, kind: GestureKind) -> i32 {
self.priorities.get(&kind).copied().unwrap_or(0)
}
pub fn process_update(&mut self, event: &Event, now_ms: u64) -> GestureUpdate {
match self.process(event, now_ms) {
Some(event) => GestureUpdate::triggered(event),
None => GestureUpdate::none(GestureState::Maybe),
}
}
pub fn previews(&self) -> Vec<Event> {
self.recognizers
.iter()
.filter_map(|recognizer| recognizer.as_recognizer_ref().partial())
.collect()
}
pub fn cancel(&mut self) -> GestureUpdate {
self.reset_all();
GestureUpdate::none(GestureState::Canceled)
}
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.as_recognizer().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 is_double_tap_pair(first: (Point, u64), second: (Point, u64)) -> bool {
let tuning = tuning();
crate::event::double_click::is_double_click_pair(
first,
second,
tuning.double_tap_timeout_ms,
tuning.max_stationary_distance,
)
}
fn can_rearm_recognizer(event: &Event) -> bool {
matches!(
event,
Event::TouchBegin { .. }
| Event::TouchEnd { .. }
| Event::TouchMove { .. }
| Event::MousePress { .. }
| Event::MouseRelease { .. }
| Event::Tap { .. }
| Event::DoubleTap { .. }
| Event::Timer { .. }
)
}
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 the_standard_chain_is_inline() {
let engine = GestureEngine::new();
assert!(engine.recognizers.iter().all(|slot| matches!(slot, RecognizerSlot::Standard(_))));
assert_eq!(engine.previews().len(), 0, "an idle chain has no previews");
}
#[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");
}
}
#[cfg(test)]
mod engine_policy_tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
#[derive(Debug)]
struct Emits {
event: Event,
kind: GestureKind,
resets: Arc<AtomicUsize>,
}
impl GestureRecognizer for Emits {
fn process(&mut self, _event: &Event, _now_ms: u64) -> Option<Event> {
Some(self.event.clone())
}
fn reset(&mut self) {
self.resets.fetch_add(1, Ordering::SeqCst);
}
fn kind(&self) -> GestureKind {
self.kind
}
}
fn emits(event: Event, kind: GestureKind) -> (Box<dyn GestureRecognizer>, Arc<AtomicUsize>) {
let resets = Arc::new(AtomicUsize::new(0));
let recognizer = Emits { event, kind, resets: Arc::clone(&resets) };
(Box::new(recognizer), resets)
}
#[test]
fn the_priority_policy_picks_the_higher_configured_priority() {
let (low, _) = emits(
Event::LongPress { pos: crate::core::Point::new(0, 0) },
GestureKind::Custom("low"),
);
let (high, _) =
emits(Event::Tap { pos: crate::core::Point::new(0, 0) }, GestureKind::Custom("high"));
let mut engine = GestureEngine::from_recognizers(vec![low, high]);
engine.set_conflict_policy(GestureConflictPolicy::Priority);
engine.set_priority(GestureKind::Custom("low"), 0);
engine.set_priority(GestureKind::Custom("high"), 10);
let produced = engine.process(&Event::Paint, 0);
assert!(matches!(produced, Some(Event::Tap { .. })), "got {produced:?}");
}
#[test]
fn the_exclusive_policy_resets_the_losers() {
let (first, first_resets) =
emits(Event::Tap { pos: crate::core::Point::new(0, 0) }, GestureKind::Custom("first"));
let (second, _) = emits(
Event::DoubleTap { pos: crate::core::Point::new(0, 0) },
GestureKind::Custom("second"),
);
let mut engine = GestureEngine::from_recognizers(vec![first, second]);
engine.set_conflict_policy(GestureConflictPolicy::Exclusive);
engine.set_priority(GestureKind::Custom("second"), 5);
let produced = engine.process(&Event::Paint, 0);
assert!(matches!(produced, Some(Event::DoubleTap { .. })), "got {produced:?}");
assert_eq!(
first_resets.load(Ordering::SeqCst),
1,
"the loser must be reset so it cannot interleave"
);
}
#[test]
fn the_default_policy_is_specificity() {
let engine = GestureEngine::new();
assert_eq!(engine.conflict_policy(), GestureConflictPolicy::Specificity);
assert_eq!(engine.priority_of(GestureKind::Pan), 0);
}
#[test]
fn cancel_reports_canceled_and_resets_the_chain() {
let (recognizer, resets) =
emits(Event::Tap { pos: crate::core::Point::new(0, 0) }, GestureKind::Tap);
let mut engine = GestureEngine::from_recognizers(vec![recognizer]);
let update = engine.cancel();
assert_eq!(update.state, GestureState::Canceled);
assert!(update.event.is_none());
assert_eq!(resets.load(Ordering::SeqCst), 1);
}
#[test]
fn tuning_tokens_override_the_compiled_defaults() {
assert_eq!(tuning(), GestureTuning::default());
let custom = GestureTuning { swipe_min_velocity: 900.0, ..GestureTuning::default() };
let previous = set_tuning(Some(custom));
assert_eq!(tuning().swipe_min_velocity, 900.0);
set_tuning(previous);
assert_eq!(tuning(), GestureTuning::default());
}
}