use crate::core::Point;
use crate::event::{Event, TouchId};
use super::{distance, GestureRecognizer};
#[derive(Debug, Clone)]
pub struct PinchTouch {
pub pos: Point,
pub id: TouchId,
}
#[derive(Debug, Clone)]
pub struct PinchGesture {
touches: Vec<PinchTouch>,
initial_distance: Option<f32>,
}
impl PinchGesture {
pub fn new() -> Self {
Self { touches: Vec::with_capacity(2), initial_distance: None }
}
}
impl GestureRecognizer for PinchGesture {
fn process(&mut self, event: &Event, _now_ms: u64) -> Option<Event> {
match event {
Event::TouchBegin { pos, touch_id } => {
if self.touches.len() < 2 {
self.touches.push(PinchTouch { pos: *pos, id: *touch_id });
if self.touches.len() == 2 {
self.initial_distance =
Some(distance(self.touches[0].pos, self.touches[1].pos));
}
}
None
}
Event::TouchMove { pos, touch_id } => {
if let Some(t) = self.touches.iter_mut().find(|t| t.id == *touch_id) {
t.pos = *pos;
}
if self.touches.len() == 2 {
let current = distance(self.touches[0].pos, self.touches[1].pos);
if !current.is_finite() {
self.initial_distance = None;
return None;
}
match self.initial_distance {
None => {
self.initial_distance = Some(current);
}
Some(initial) => {
if !initial.is_finite() || initial <= 0.0 {
if current > 0.0 {
self.initial_distance = Some(current);
}
return None;
}
if current <= 0.0 {
return Some(Event::Pinch { scale: 0.0 });
}
let scale = current / initial;
if !scale.is_finite() {
self.initial_distance = Some(current);
return None;
}
if (scale - 1.0).abs() > 0.05 {
self.initial_distance = Some(current); return Some(Event::Pinch { scale });
}
}
}
}
None
}
Event::TouchEnd { touch_id, .. } => {
self.touches.retain(|t| t.id != *touch_id);
if self.touches.len() < 2 {
self.initial_distance = None;
}
None
}
_ => None,
}
}
fn reset(&mut self) {
self.touches.clear();
self.initial_distance = None;
}
}
crate::impl_default_via_new!(PinchGesture);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn coincident_start_recovers_after_separation() {
let mut gesture = PinchGesture::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(0, 0), touch_id: 2 }, 0)
.is_none());
assert!(gesture
.process(&Event::TouchMove { pos: Point::new(10, 0), touch_id: 2 }, 10)
.is_none());
let produced =
gesture.process(&Event::TouchMove { pos: Point::new(20, 0), touch_id: 2 }, 20);
match produced {
Some(Event::Pinch { scale }) => assert!(
scale > 1.0,
"separating after a coincident start must zoom in, got scale {scale}"
),
other => panic!("a coincident start must recover into a pinch, got {other:?}"),
}
}
#[test]
fn mid_gesture_collapse_then_separate_recovers() {
let mut gesture = PinchGesture::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());
let collapsed =
gesture.process(&Event::TouchMove { pos: Point::new(0, 0), touch_id: 2 }, 10);
match collapsed {
Some(Event::Pinch { scale }) => assert_eq!(scale, 0.0),
other => panic!("collapsing fingers must emit a zero scale, got {other:?}"),
}
let produced =
gesture.process(&Event::TouchMove { pos: Point::new(20, 0), touch_id: 2 }, 20);
match produced {
Some(Event::Pinch { scale }) => {
assert!(scale > 1.0, "separating after a collapse must zoom in, got scale {scale}")
}
other => panic!("a collapse must not poison later separation, got {other:?}"),
}
}
}