use std::time::Instant;
use teksilo_canvas::Point;
use super::{GestureEvent, GestureRecognizer, GestureResult, RawPointerEvent, SwipeDirection};
#[derive(Debug)]
pub struct SwipeRecognizer {
min_velocity: f32,
min_distance: f32,
max_cross_ratio: f32,
down_position: Option<Point>,
down_time: Option<Instant>,
}
impl SwipeRecognizer {
pub fn new() -> Self {
Self {
min_velocity: 200.0, min_distance: 30.0, max_cross_ratio: 0.5, down_position: None,
down_time: None,
}
}
pub fn min_velocity(mut self, v: f32) -> Self {
self.min_velocity = v;
self
}
pub fn min_distance(mut self, d: f32) -> Self {
self.min_distance = d;
self
}
pub fn process_at(&mut self, event: &RawPointerEvent, now: Instant) -> GestureResult {
match event {
RawPointerEvent::Down { position, .. } => {
self.down_position = Some(*position);
self.down_time = Some(now);
GestureResult::Pending
}
RawPointerEvent::Move { .. } => GestureResult::Pending,
RawPointerEvent::Up { position, .. } => {
let (Some(down), Some(time)) = (self.down_position, self.down_time) else {
return GestureResult::Failed;
};
let dx = position.x - down.x;
let dy = position.y - down.y;
let dist = (dx * dx + dy * dy).sqrt();
let elapsed = now.duration_since(time).as_secs_f32();
if dist < self.min_distance || elapsed <= 0.0 {
self.reset();
return GestureResult::Failed;
}
let velocity = dist / elapsed;
if velocity < self.min_velocity {
self.reset();
return GestureResult::Failed;
}
let abs_dx = dx.abs();
let abs_dy = dy.abs();
let (direction, cross_ratio) = if abs_dx >= abs_dy {
let dir = if dx > 0.0 {
SwipeDirection::Right
} else {
SwipeDirection::Left
};
(dir, abs_dy / abs_dx.max(0.001))
} else {
let dir = if dy > 0.0 {
SwipeDirection::Down
} else {
SwipeDirection::Up
};
(dir, abs_dx / abs_dy.max(0.001))
};
if cross_ratio > self.max_cross_ratio {
self.reset();
return GestureResult::Failed;
}
self.reset();
GestureResult::Recognized(GestureEvent::Swipe {
direction,
velocity,
})
}
}
}
}
impl Default for SwipeRecognizer {
fn default() -> Self {
Self::new()
}
}
impl GestureRecognizer for SwipeRecognizer {
fn process(&mut self, event: &RawPointerEvent) -> GestureResult {
self.process_at(event, Instant::now())
}
fn reset(&mut self) {
self.down_position = None;
self.down_time = None;
}
fn priority(&self) -> u32 {
30 }
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::gesture::test_helpers::*;
#[test]
fn swipe_right_recognized() {
let mut rec = SwipeRecognizer::new()
.min_velocity(100.0)
.min_distance(20.0);
let t0 = Instant::now();
rec.process_at(&down(Point::new(10.0, 50.0)), t0);
let result = rec.process_at(
&up(Point::new(200.0, 55.0)),
t0 + Duration::from_millis(100),
);
match result {
GestureResult::Recognized(GestureEvent::Swipe {
direction,
velocity,
}) => {
assert_eq!(direction, SwipeDirection::Right);
assert!(velocity > 100.0);
}
other => panic!("Expected Swipe, got {:?}", other),
}
}
#[test]
fn swipe_left_recognized() {
let mut rec = SwipeRecognizer::new()
.min_velocity(100.0)
.min_distance(20.0);
let t0 = Instant::now();
rec.process_at(&down(Point::new(200.0, 50.0)), t0);
let result = rec.process_at(&up(Point::new(10.0, 55.0)), t0 + Duration::from_millis(100));
assert!(matches!(
result,
GestureResult::Recognized(GestureEvent::Swipe {
direction: SwipeDirection::Left,
..
})
));
}
#[test]
fn swipe_fails_if_too_slow() {
let mut rec = SwipeRecognizer::new().min_velocity(500.0);
let t0 = Instant::now();
rec.process_at(&down(Point::new(10.0, 10.0)), t0);
let result = rec.process_at(
&up(Point::new(50.0, 10.0)),
t0 + Duration::from_secs(5), );
assert!(matches!(result, GestureResult::Failed));
}
#[test]
fn swipe_fails_if_diagonal() {
let mut rec = SwipeRecognizer::new()
.min_velocity(100.0)
.min_distance(20.0);
let t0 = Instant::now();
rec.process_at(&down(Point::new(10.0, 10.0)), t0);
let result = rec.process_at(
&up(Point::new(100.0, 100.0)),
t0 + Duration::from_millis(100),
);
assert!(matches!(result, GestureResult::Failed));
}
}