use crate::core::Point;
use crate::event::{Event, TouchId};
use super::{point_delta_abs, GestureRecognizer, DOUBLE_TAP_TIMEOUT_MS, MAX_STATIONARY_DISTANCE};
#[derive(Debug, Clone)]
pub struct TapGesture {
start_pos: Option<Point>,
start_time: Option<u64>,
touch_id: Option<TouchId>,
}
impl TapGesture {
pub fn new() -> Self {
Self { start_pos: None, start_time: None, touch_id: None }
}
}
impl GestureRecognizer for TapGesture {
fn process(&mut self, event: &Event, now_ms: u64) -> Option<Event> {
match event {
Event::TouchBegin { pos, touch_id } => {
self.start_pos = Some(*pos);
self.start_time = Some(now_ms);
self.touch_id = Some(*touch_id);
None
}
Event::TouchEnd { pos, touch_id } => {
if self.touch_id != Some(*touch_id) {
return None;
}
let start = self.start_pos?;
let start_time = self.start_time?;
let dt = now_ms.saturating_sub(start_time);
if dt < super::TAP_TIMEOUT_MS
&& super::distance(start, *pos) < MAX_STATIONARY_DISTANCE
{
let result = Event::Tap { pos: *pos };
self.reset();
return Some(result);
}
self.reset();
None
}
Event::TouchMove { pos, touch_id } => {
if self.touch_id == Some(*touch_id) {
if let Some(start) = self.start_pos {
if super::distance(start, *pos) >= MAX_STATIONARY_DISTANCE {
self.reset();
}
}
}
None
}
_ => None,
}
}
fn reset(&mut self) {
self.start_pos = None;
self.start_time = None;
self.touch_id = None;
}
}
crate::impl_default_via_new!(TapGesture);
#[derive(Debug, Clone)]
pub struct DoubleTapGesture {
first_tap_pos: Option<Point>,
first_tap_time: Option<u64>,
waiting_for_second: bool,
}
impl DoubleTapGesture {
pub fn new() -> Self {
Self { first_tap_pos: None, first_tap_time: None, waiting_for_second: false }
}
}
impl GestureRecognizer for DoubleTapGesture {
fn process(&mut self, event: &Event, now_ms: u64) -> Option<Event> {
match event {
Event::Tap { pos } => {
if self.waiting_for_second {
if let (Some(first_pos), Some(first_time)) =
(self.first_tap_pos, self.first_tap_time)
{
let dt = now_ms.saturating_sub(first_time);
if dt <= DOUBLE_TAP_TIMEOUT_MS
&& super::distance(first_pos, *pos) < MAX_STATIONARY_DISTANCE * 2.0
{
let result = Event::DoubleTap { pos: *pos };
self.reset();
return Some(result);
}
}
self.first_tap_pos = Some(*pos);
self.first_tap_time = Some(now_ms);
} else {
self.first_tap_pos = Some(*pos);
self.first_tap_time = Some(now_ms);
self.waiting_for_second = true;
}
None
}
_ => None,
}
}
fn reset(&mut self) {
self.first_tap_pos = None;
self.first_tap_time = None;
self.waiting_for_second = false;
}
}
crate::impl_default_via_new!(DoubleTapGesture);
const TWO_FINGER_TAP_TIMEOUT_MS: u64 = 150;
const TWO_FINGER_TAP_DURATION_MS: u64 = 300;
const TWO_FINGER_COUNT: usize = 2;
#[derive(Debug)]
pub struct TwoFingerTapGesture {
touches: Vec<(Point, Point, TouchId, u64)>,
completed: Vec<(u64, u64, Point)>,
}
impl TwoFingerTapGesture {
pub fn new() -> Self {
Self { touches: Vec::new(), completed: Vec::new() }
}
}
impl GestureRecognizer for TwoFingerTapGesture {
fn process(&mut self, event: &Event, now_ms: u64) -> Option<Event> {
match event {
Event::TouchBegin { pos, touch_id } => {
if self.touches.len() >= 2 {
return None;
}
self.touches.push((*pos, *pos, *touch_id, now_ms));
None
}
Event::TouchMove { pos, touch_id } => {
if let Some(t) = self.touches.iter_mut().find(|(_, _, id, _)| *id == *touch_id) {
let (dx, dy) = point_delta_abs(t.0, *pos);
if (dx as f32) > MAX_STATIONARY_DISTANCE
|| (dy as f32) > MAX_STATIONARY_DISTANCE
{
self.reset(); } else {
t.1 = *pos; }
}
None
}
Event::TouchEnd { pos, touch_id } => {
if let Some(idx) = self.touches.iter().position(|(_, _, id, _)| *id == *touch_id) {
let start_time = self.touches[idx].3;
let touchdown = self.touches[idx].0;
self.touches.remove(idx);
if super::distance(touchdown, *pos) >= MAX_STATIONARY_DISTANCE {
self.reset();
return None;
}
self.completed.push((start_time, now_ms, *pos));
if self.touches.is_empty() && self.completed.len() == TWO_FINGER_COUNT {
let (s0, e0, p0) = self.completed[0];
let (s1, e1, p1) = self.completed[1];
let landing_gap = s0.abs_diff(s1);
let dur0 = e0.saturating_sub(s0);
let dur1 = e1.saturating_sub(s1);
if landing_gap <= TWO_FINGER_TAP_TIMEOUT_MS
&& dur0 <= TWO_FINGER_TAP_DURATION_MS
&& dur1 <= TWO_FINGER_TAP_DURATION_MS
{
let centroid = Point::new(
((p0.x as i64 + p1.x as i64) / 2) as i32,
((p0.y as i64 + p1.y as i64) / 2) as i32,
);
self.reset();
return Some(Event::TwoFingerTap { pos: centroid });
}
self.reset();
} else if self.touches.is_empty() {
self.reset();
}
}
None
}
_ => {
if let Some(first) = self.touches.first() {
if now_ms.saturating_sub(first.3) > TWO_FINGER_TAP_TIMEOUT_MS * 2 {
self.reset();
}
}
None
}
}
}
fn reset(&mut self) {
self.touches.clear();
self.completed.clear();
}
}
crate::impl_default_via_new!(TwoFingerTapGesture);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn two_finger_tap_rejects_slow_inter_landing() {
let mut gesture = TwoFingerTapGesture::new();
assert!(gesture
.process(&Event::TouchBegin { pos: Point::new(10, 10), touch_id: 1 }, 0)
.is_none());
assert!(gesture
.process(&Event::TouchBegin { pos: Point::new(30, 10), touch_id: 2 }, 500)
.is_none());
assert!(gesture
.process(&Event::TouchEnd { pos: Point::new(10, 10), touch_id: 1 }, 520)
.is_none());
let produced =
gesture.process(&Event::TouchEnd { pos: Point::new(30, 10), touch_id: 2 }, 530);
assert!(
produced.is_none(),
"a 500ms inter-landing gap must not be a TwoFingerTap, got {produced:?}"
);
}
#[test]
fn two_finger_tap_rejects_long_first_finger() {
let mut gesture = TwoFingerTapGesture::new();
assert!(gesture
.process(&Event::TouchBegin { pos: Point::new(10, 10), touch_id: 1 }, 0)
.is_none());
assert!(gesture
.process(&Event::TouchBegin { pos: Point::new(30, 10), touch_id: 2 }, 10)
.is_none());
assert!(gesture
.process(&Event::TouchEnd { pos: Point::new(10, 10), touch_id: 1 }, 400)
.is_none());
let produced =
gesture.process(&Event::TouchEnd { pos: Point::new(30, 10), touch_id: 2 }, 60);
assert!(
produced.is_none(),
"an over-long first finger must invalidate the tap, got {produced:?}"
);
}
#[test]
fn two_finger_tap_uses_release_positions_for_centroid() {
let mut gesture = TwoFingerTapGesture::new();
assert!(gesture
.process(&Event::TouchBegin { pos: Point::new(10, 10), touch_id: 1 }, 0)
.is_none());
assert!(gesture
.process(&Event::TouchBegin { pos: Point::new(30, 10), touch_id: 2 }, 10)
.is_none());
assert!(gesture
.process(&Event::TouchEnd { pos: Point::new(10, 20), touch_id: 1 }, 20)
.is_none());
let produced =
gesture.process(&Event::TouchEnd { pos: Point::new(30, 20), touch_id: 2 }, 30);
match produced {
Some(Event::TwoFingerTap { pos }) => assert_eq!(pos, Point::new(20, 20)),
other => panic!("expected TwoFingerTap, got {other:?}"),
}
}
#[test]
fn two_finger_tap_reversed_release_order() {
let mut gesture = TwoFingerTapGesture::new();
assert!(gesture
.process(&Event::TouchBegin { pos: Point::new(10, 10), touch_id: 1 }, 0)
.is_none());
assert!(gesture
.process(&Event::TouchBegin { pos: Point::new(30, 10), touch_id: 2 }, 10)
.is_none());
assert!(gesture
.process(&Event::TouchEnd { pos: Point::new(30, 20), touch_id: 2 }, 20)
.is_none());
let produced =
gesture.process(&Event::TouchEnd { pos: Point::new(10, 20), touch_id: 1 }, 30);
match produced {
Some(Event::TwoFingerTap { pos }) => assert_eq!(pos, Point::new(20, 20)),
other => panic!("reversed release order must still be a tap, got {other:?}"),
}
}
#[test]
fn two_finger_tap_accepts_boundaries() {
let mut gesture = TwoFingerTapGesture::new();
assert!(gesture
.process(&Event::TouchBegin { pos: Point::new(10, 10), touch_id: 1 }, 0)
.is_none());
assert!(gesture
.process(&Event::TouchBegin { pos: Point::new(30, 10), touch_id: 2 }, 150)
.is_none());
assert!(gesture
.process(&Event::TouchEnd { pos: Point::new(10, 10), touch_id: 1 }, 300)
.is_none());
let produced =
gesture.process(&Event::TouchEnd { pos: Point::new(30, 10), touch_id: 2 }, 450);
match produced {
Some(Event::TwoFingerTap { pos }) => assert_eq!(pos, Point::new(20, 10)),
other => panic!("boundary timings must still be a tap, got {other:?}"),
}
}
#[test]
fn two_finger_tap_rejects_a_far_away_release_without_a_move() {
let mut gesture = TwoFingerTapGesture::new();
assert!(gesture
.process(&Event::TouchBegin { pos: Point::new(0, 0), touch_id: 1 }, 0)
.is_none());
assert!(gesture
.process(&Event::TouchBegin { pos: Point::new(10, 0), touch_id: 2 }, 0)
.is_none());
assert!(gesture
.process(&Event::TouchEnd { pos: Point::new(1000, 0), touch_id: 1 }, 100)
.is_none());
let produced =
gesture.process(&Event::TouchEnd { pos: Point::new(1010, 0), touch_id: 2 }, 100);
assert!(produced.is_none(), "a far release must not be a TwoFingerTap, got {produced:?}");
}
}