use crate::core::Point;
use crate::event::{Event, TouchId};
use super::{
distance, point_delta, point_delta_abs, GestureRecognizer, LONG_PRESS_MAX_MOVE,
LONG_PRESS_MIN_MS, PAN_MIN_DISTANCE,
};
#[derive(Debug, Clone)]
pub struct LongPressGesture {
start_pos: Option<Point>,
start_time: Option<u64>,
touch_id: Option<TouchId>,
fired: bool,
}
impl LongPressGesture {
pub fn new() -> Self {
Self { start_pos: None, start_time: None, touch_id: None, fired: false }
}
}
impl GestureRecognizer for LongPressGesture {
fn process(&mut self, event: &Event, now_ms: u64) -> Option<Event> {
if self.fired {
if let Event::TouchEnd { touch_id, .. } = event {
if self.touch_id == Some(*touch_id) {
self.reset();
}
}
return None;
}
match event {
Event::TouchBegin { pos, touch_id } if self.touch_id.is_none() => {
self.start_pos = Some(*pos);
self.start_time = Some(now_ms);
self.touch_id = Some(*touch_id);
None
}
Event::TouchEnd { touch_id, .. } => {
if self.touch_id == Some(*touch_id) {
self.reset();
}
None
}
Event::TouchMove { pos, touch_id } => {
if self.touch_id == Some(*touch_id) {
if let Some(start) = self.start_pos {
if distance(start, *pos) > LONG_PRESS_MAX_MOVE {
self.reset();
}
}
}
None
}
_ => {
if let Some(start_time) = self.start_time {
let elapsed = now_ms.saturating_sub(start_time);
if elapsed >= LONG_PRESS_MIN_MS {
if let Some(pos) = self.start_pos {
self.fired = true;
return Some(Event::LongPress { pos });
}
}
}
None
}
}
}
fn reset(&mut self) {
self.start_pos = None;
self.start_time = None;
self.touch_id = None;
self.fired = false;
}
}
crate::impl_default_via_new!(LongPressGesture);
#[derive(Debug, Clone)]
pub struct PanGesture {
active: bool,
touch_id: Option<TouchId>,
last_pos: Option<Point>,
start_pos: Option<Point>,
dragging: bool,
}
impl PanGesture {
pub fn new() -> Self {
Self { active: false, touch_id: None, last_pos: None, start_pos: None, dragging: false }
}
}
impl GestureRecognizer for PanGesture {
fn process(&mut self, event: &Event, _now_ms: u64) -> Option<Event> {
match event {
Event::TouchBegin { pos, touch_id } if !self.active => {
self.active = true;
self.touch_id = Some(*touch_id);
self.last_pos = Some(*pos);
self.start_pos = Some(*pos);
self.dragging = false;
None
}
Event::TouchMove { pos, touch_id }
if self.active && Some(*touch_id) == self.touch_id =>
{
if !self.dragging {
let travelled = super::distance(self.start_pos?, *pos);
if travelled < PAN_MIN_DISTANCE {
return None;
}
self.dragging = true;
}
let delta = if let Some(last) = self.last_pos {
let (dx, dy) = point_delta(last, *pos);
Point::new(dx, dy)
} else {
Point::new(0, 0)
};
self.last_pos = Some(*pos);
Some(Event::Drag { pos: *pos, touch_id: *touch_id, delta })
}
Event::TouchEnd { pos: _, touch_id } if Some(*touch_id) == self.touch_id => {
self.reset();
None
}
_ => None,
}
}
fn reset(&mut self) {
self.active = false;
self.touch_id = None;
self.last_pos = None;
self.start_pos = None;
self.dragging = false;
}
}
crate::impl_default_via_new!(PanGesture);
#[derive(Debug, Clone)]
pub struct LongPressDragGesture {
start_pos: Option<Point>,
start_time: Option<u64>,
touch_id: Option<TouchId>,
long_press_fired: bool,
dragging: bool,
last_pos: Option<Point>,
}
impl LongPressDragGesture {
pub fn new() -> Self {
Self {
start_pos: None,
start_time: None,
touch_id: None,
long_press_fired: false,
dragging: false,
last_pos: None,
}
}
}
impl GestureRecognizer for LongPressDragGesture {
fn process(&mut self, event: &Event, now_ms: u64) -> Option<Event> {
match event {
Event::TouchBegin { pos, touch_id } if self.start_pos.is_none() => {
self.start_pos = Some(*pos);
self.start_time = Some(now_ms);
self.touch_id = Some(*touch_id);
self.long_press_fired = false;
self.dragging = false;
self.last_pos = Some(*pos);
None
}
Event::TouchMove { pos, touch_id } if self.touch_id == Some(*touch_id) => {
if self.dragging {
let delta = if let Some(last) = self.last_pos {
let (dx, dy) = point_delta(last, *pos);
Point::new(dx, dy)
} else {
Point::new(0, 0)
};
self.last_pos = Some(*pos);
return Some(Event::Drag { pos: *pos, touch_id: *touch_id, delta });
}
if self.long_press_fired {
self.dragging = true;
self.last_pos = Some(*pos);
let delta = if let Some(start) = self.start_pos {
let (dx, dy) = point_delta(start, *pos);
Point::new(dx, dy)
} else {
Point::new(0, 0)
};
return Some(Event::Drag { pos: *pos, touch_id: *touch_id, delta });
}
if let Some(start) = self.start_pos {
let (dx, dy) = point_delta_abs(start, *pos);
if (dx as f32) > LONG_PRESS_MAX_MOVE || (dy as f32) > LONG_PRESS_MAX_MOVE {
self.reset();
}
}
None
}
Event::TouchEnd { pos: _, touch_id } if self.touch_id == Some(*touch_id) => {
self.reset();
None
}
_ => {
if !self.long_press_fired && !self.dragging {
if let (Some(start_pos), Some(start)) = (self.start_pos, self.start_time) {
if now_ms >= start
&& now_ms - start >= LONG_PRESS_MIN_MS
&& matches!(event, Event::Timer { .. })
{
self.long_press_fired = true;
return Some(Event::LongPress { pos: start_pos });
}
}
}
None
}
}
}
fn reset(&mut self) {
self.start_pos = None;
self.start_time = None;
self.touch_id = None;
self.long_press_fired = false;
self.dragging = false;
self.last_pos = None;
}
}
crate::impl_default_via_new!(LongPressDragGesture);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_drag_delivers_accumulated_displacement() {
let mut pan = PanGesture::new();
assert!(pan
.process(&Event::TouchBegin { pos: Point::new(0, 0), touch_id: 1 }, 0)
.is_none());
assert!(pan
.process(&Event::TouchMove { pos: Point::new(7, 0), touch_id: 1 }, 10)
.is_none());
let first = pan.process(&Event::TouchMove { pos: Point::new(9, 0), touch_id: 1 }, 20);
match first {
Some(Event::Drag { delta, .. }) => assert_eq!(
delta,
Point::new(9, 0),
"the first Drag must cover the whole travel since touch-down"
),
other => panic!("crossing the threshold must emit a Drag, got {other:?}"),
}
let second = pan.process(&Event::TouchMove { pos: Point::new(11, 0), touch_id: 1 }, 30);
match second {
Some(Event::Drag { delta, .. }) => assert_eq!(delta, Point::new(2, 0)),
other => panic!("a subsequent move must emit a step delta, got {other:?}"),
}
}
#[test]
fn pan_ignores_an_unrelated_finger_end() {
let mut pan = PanGesture::new();
assert!(pan
.process(&Event::TouchBegin { pos: Point::new(0, 0), touch_id: 1 }, 0)
.is_none());
assert!(pan
.process(&Event::TouchEnd { pos: Point::new(99, 99), touch_id: 2 }, 10)
.is_none());
let drag = pan.process(&Event::TouchMove { pos: Point::new(50, 0), touch_id: 1 }, 20);
assert!(
matches!(drag, Some(Event::Drag { .. })),
"the tracked finger must still drag after an unrelated end, got {drag:?}"
);
}
#[test]
fn unrelated_finger_end_does_not_cancel_a_holding_long_press() {
let mut gesture = LongPressGesture::new();
assert!(gesture
.process(&Event::TouchBegin { pos: Point::new(10, 20), touch_id: 1 }, 0)
.is_none());
assert!(gesture
.process(&Event::TouchEnd { pos: Point::new(99, 99), touch_id: 2 }, 100)
.is_none());
let produced = gesture.process(&Event::Timer { id: 0 }, super::LONG_PRESS_MIN_MS + 1);
assert!(
matches!(produced, Some(Event::LongPress { pos }) if pos == Point::new(10, 20)),
"the tracked finger must still fire LongPress after an unrelated end, got {produced:?}"
);
}
#[test]
fn unrelated_finger_end_does_not_reset_a_fired_long_press() {
let mut gesture = LongPressGesture::new();
assert!(gesture
.process(&Event::TouchBegin { pos: Point::new(10, 20), touch_id: 1 }, 0)
.is_none());
let produced = gesture.process(&Event::Timer { id: 0 }, super::LONG_PRESS_MIN_MS + 1);
assert!(matches!(produced, Some(Event::LongPress { .. })));
assert!(gesture
.process(&Event::TouchEnd { pos: Point::new(99, 99), touch_id: 2 }, 700)
.is_none());
assert!(gesture
.process(&Event::TouchBegin { pos: Point::new(5, 5), touch_id: 3 }, 750)
.is_none());
assert!(gesture.process(&Event::Timer { id: 0 }, 1400).is_none());
}
#[test]
fn second_finger_begin_is_ignored_while_holding() {
let mut gesture = LongPressGesture::new();
assert!(gesture
.process(&Event::TouchBegin { pos: Point::new(10, 20), touch_id: 1 }, 0)
.is_none());
assert!(gesture
.process(&Event::TouchBegin { pos: Point::new(50, 60), touch_id: 2 }, 100)
.is_none());
let produced = gesture.process(&Event::Timer { id: 0 }, super::LONG_PRESS_MIN_MS + 1);
assert!(
matches!(produced, Some(Event::LongPress { pos }) if pos == Point::new(10, 20)),
"a second finger must not reassign the hold, got {produced:?}"
);
}
}