use crate::core::{InputEvent, MouseButton, Point, px};
use std::time::{Duration, Instant};
use winit::event::TouchPhase;
const SLOP: f32 = 10.0;
pub const LONG_PRESS: Duration = Duration::from_millis(400);
const FRICTION: f32 = 2.0;
const MIN_FLING: f32 = 40.0;
const NOWHERE: Point = Point { x: px(-1.0e6), y: px(-1.0e6) };
#[derive(Clone, Copy, PartialEq)]
enum Mode {
Pending,
Pan,
Press,
}
struct Finger {
id: u64,
mode: Mode,
start: Point,
last: Point,
started_at: Instant,
moved_at: Instant,
velocity: Point,
}
struct Fling {
at: Point,
velocity: Point,
last: Instant,
}
#[derive(Default)]
pub struct TouchState {
finger: Option<Finger>,
fling: Option<Fling>,
}
fn distance(a: Point, b: Point) -> f32 {
(a.x.0 - b.x.0).hypot(a.y.0 - b.y.0)
}
impl TouchState {
pub fn touch(&mut self, phase: TouchPhase, id: u64, position: Point, now: Instant) -> Vec<InputEvent> {
let left = MouseButton::Left;
match phase {
TouchPhase::Started if self.finger.is_none() => {
self.fling = None;
self.finger = Some(Finger {
id,
mode: Mode::Pending,
start: position,
last: position,
started_at: now,
moved_at: now,
velocity: Point::new(px(0.0), px(0.0)),
});
vec![InputEvent::MouseMove { position }]
}
TouchPhase::Moved => {
let mut events = self.tick(now);
let Some(finger) = self.finger.as_mut().filter(|finger| finger.id == id) else { return events };
if finger.mode == Mode::Pending && distance(position, finger.start) > SLOP {
finger.mode = Mode::Pan;
}
match finger.mode {
Mode::Pan => {
let delta = Point::new(position.x - finger.last.x, position.y - finger.last.y);
let dt = now.saturating_duration_since(finger.moved_at).as_secs_f32().max(1.0e-3);
finger.velocity = Point::new(
px(finger.velocity.x.0 * 0.6 + delta.x.0 / dt * 0.4),
px(finger.velocity.y.0 * 0.6 + delta.y.0 / dt * 0.4),
);
events.push(InputEvent::Scroll { position: finger.start, delta });
}
Mode::Press => events.push(InputEvent::MouseMove { position }),
Mode::Pending => {}
}
finger.last = position;
finger.moved_at = now;
events
}
TouchPhase::Ended | TouchPhase::Cancelled => {
let Some(finger) = self.finger.take_if(|finger| finger.id == id) else { return Vec::new() };
let cancelled = phase == TouchPhase::Cancelled;
let mut events = match finger.mode {
Mode::Pending if !cancelled => vec![
InputEvent::MouseDown { position, button: left },
InputEvent::MouseUp { position, button: left },
],
Mode::Press => {
vec![InputEvent::MouseUp { position: if cancelled { NOWHERE } else { position }, button: left }]
}
Mode::Pan if !cancelled => {
let still = now.saturating_duration_since(finger.moved_at) > Duration::from_millis(80);
let speed = finger.velocity.x.0.hypot(finger.velocity.y.0);
if !still && speed > MIN_FLING {
self.fling = Some(Fling { at: finger.start, velocity: finger.velocity, last: now });
}
Vec::new()
}
_ => Vec::new(),
};
events.push(InputEvent::MouseMove { position: NOWHERE });
events
}
_ => Vec::new(),
}
}
pub fn tick(&mut self, now: Instant) -> Vec<InputEvent> {
match self.finger.as_mut() {
Some(finger) if finger.mode == Mode::Pending && now >= finger.started_at + LONG_PRESS => {
finger.mode = Mode::Press;
vec![InputEvent::MouseDown { position: finger.start, button: MouseButton::Left }]
}
_ => Vec::new(),
}
}
pub fn next_deadline(&self) -> Option<Instant> {
self.finger.as_ref().filter(|finger| finger.mode == Mode::Pending).map(|finger| finger.started_at + LONG_PRESS)
}
pub fn is_flinging(&self) -> bool {
self.fling.is_some()
}
pub fn step_fling(&mut self, now: Instant) -> Option<InputEvent> {
let fling = self.fling.as_mut()?;
let dt = now.saturating_duration_since(fling.last).as_secs_f32().min(0.1);
fling.last = now;
let decay = (-FRICTION * dt).exp();
fling.velocity = Point::new(px(fling.velocity.x.0 * decay), px(fling.velocity.y.0 * decay));
let delta = Point::new(px(fling.velocity.x.0 * dt), px(fling.velocity.y.0 * dt));
let event = InputEvent::Scroll { position: fling.at, delta };
if fling.velocity.x.0.hypot(fling.velocity.y.0) < MIN_FLING {
self.fling = None;
}
Some(event)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn at(x: f32, y: f32) -> Point {
Point::new(px(x), px(y))
}
fn is_click(events: &[InputEvent]) -> bool {
matches!(events, [InputEvent::MouseDown { .. }, InputEvent::MouseUp { .. }, InputEvent::MouseMove { .. }])
}
#[test]
fn a_short_tap_is_a_click() {
let mut touch = TouchState::default();
let now = Instant::now();
touch.touch(TouchPhase::Started, 1, at(10.0, 10.0), now);
touch.touch(TouchPhase::Moved, 1, at(13.0, 12.0), now + Duration::from_millis(30));
assert!(is_click(&touch.touch(TouchPhase::Ended, 1, at(13.0, 12.0), now + Duration::from_millis(60))));
}
#[test]
fn swiping_scrolls_and_keeps_moving_after_release() {
let mut touch = TouchState::default();
let mut now = Instant::now();
touch.touch(TouchPhase::Started, 1, at(50.0, 300.0), now);
for step in 1..=5 {
now += Duration::from_millis(16);
let events = touch.touch(TouchPhase::Moved, 1, at(50.0, 300.0 - step as f32 * 30.0), now);
assert!(matches!(events.as_slice(), [] | [InputEvent::Scroll { .. }]));
}
assert!(!is_click(&touch.touch(TouchPhase::Ended, 1, at(50.0, 150.0), now)));
assert!(touch.is_flinging());
let Some(InputEvent::Scroll { delta, .. }) = touch.step_fling(now + Duration::from_millis(16)) else {
panic!()
};
assert!(delta.y.0 < 0.0, "sigue en la dirección del dedo");
for frame in 2..400 {
touch.step_fling(now + Duration::from_millis(16 * frame));
}
assert!(!touch.is_flinging(), "la inercia termina sola");
}
#[test]
fn holding_still_turns_into_a_drag() {
let mut touch = TouchState::default();
let now = Instant::now();
touch.touch(TouchPhase::Started, 1, at(10.0, 10.0), now);
assert_eq!(touch.next_deadline(), Some(now + LONG_PRESS));
assert!(matches!(touch.tick(now + LONG_PRESS).as_slice(), [InputEvent::MouseDown { .. }]));
let moved = touch.touch(TouchPhase::Moved, 1, at(80.0, 10.0), now + LONG_PRESS * 2);
assert!(matches!(moved.as_slice(), [InputEvent::MouseMove { .. }]));
let ended = touch.touch(TouchPhase::Ended, 1, at(80.0, 10.0), now + LONG_PRESS * 2);
assert!(matches!(ended.as_slice(), [InputEvent::MouseUp { .. }, InputEvent::MouseMove { .. }]));
}
#[test]
fn a_second_finger_is_ignored() {
let mut touch = TouchState::default();
let now = Instant::now();
touch.touch(TouchPhase::Started, 1, at(10.0, 10.0), now);
assert!(touch.touch(TouchPhase::Started, 2, at(90.0, 90.0), now).is_empty());
assert!(touch.touch(TouchPhase::Ended, 2, at(90.0, 90.0), now).is_empty());
}
}