use super::types::Point;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GesturePhase {
Idle,
Pressed {
origin: Point,
pointer_id: i32,
},
Dragging {
origin: Point,
pointer_id: i32,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum GestureEvent {
Down { at: Point, pointer_id: i32 },
Move { at: Point, pointer_id: i32 },
Up { at: Point, pointer_id: i32 },
Hold { pointer_id: i32 },
Cancel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Promotion {
#[default]
Distance,
HoldOrSideways,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GestureEffect {
None,
Begin { origin: Point, at: Point },
Track { at: Point },
Drop { at: Point },
Tap,
Abort,
}
pub fn transition(
phase: GesturePhase,
event: GestureEvent,
threshold: f64,
) -> (GesturePhase, GestureEffect) {
transition_with(phase, event, threshold, Promotion::Distance)
}
pub fn transition_with(
phase: GesturePhase,
event: GestureEvent,
threshold: f64,
promotion: Promotion,
) -> (GesturePhase, GestureEffect) {
use GestureEffect as Fx;
use GesturePhase as P;
match (phase, event) {
(P::Idle, GestureEvent::Down { at, pointer_id }) => (
P::Pressed {
origin: at,
pointer_id,
},
Fx::None,
),
(p @ (P::Pressed { .. } | P::Dragging { .. }), GestureEvent::Down { .. }) => (p, Fx::None),
(
P::Pressed { origin, pointer_id },
GestureEvent::Move {
at,
pointer_id: pid,
},
) if pid == pointer_id => {
let d = at - origin;
if (d.x * d.x + d.y * d.y).sqrt() >= threshold {
match promotion {
Promotion::Distance => {
(P::Dragging { origin, pointer_id }, Fx::Begin { origin, at })
}
Promotion::HoldOrSideways if d.x.abs() > d.y.abs() => {
(P::Dragging { origin, pointer_id }, Fx::Begin { origin, at })
}
Promotion::HoldOrSideways => (P::Idle, Fx::None),
}
} else {
(P::Pressed { origin, pointer_id }, Fx::None)
}
}
(P::Pressed { origin, pointer_id }, GestureEvent::Hold { pointer_id: pid })
if pid == pointer_id =>
{
(
P::Dragging { origin, pointer_id },
Fx::Begin { origin, at: origin },
)
}
(
P::Pressed { pointer_id, .. },
GestureEvent::Up {
pointer_id: pid, ..
},
) if pid == pointer_id => (P::Idle, Fx::Tap),
(
P::Dragging { origin, pointer_id },
GestureEvent::Move {
at,
pointer_id: pid,
},
) if pid == pointer_id => (P::Dragging { origin, pointer_id }, Fx::Track { at }),
(
P::Dragging { pointer_id, .. },
GestureEvent::Up {
at,
pointer_id: pid,
},
) if pid == pointer_id => (P::Idle, Fx::Drop { at }),
(P::Dragging { .. }, GestureEvent::Cancel) => (P::Idle, Fx::Abort),
(P::Pressed { .. }, GestureEvent::Cancel) => (P::Idle, Fx::None),
(P::Idle, GestureEvent::Cancel) => (P::Idle, Fx::None),
(p, _) => (p, Fx::None),
}
}
#[cfg(test)]
mod tests {
use super::*;
const T: f64 = 8.0;
fn down(x: f64, y: f64, pid: i32) -> GestureEvent {
GestureEvent::Down {
at: Point::new(x, y),
pointer_id: pid,
}
}
fn mv(x: f64, y: f64, pid: i32) -> GestureEvent {
GestureEvent::Move {
at: Point::new(x, y),
pointer_id: pid,
}
}
fn up(x: f64, y: f64, pid: i32) -> GestureEvent {
GestureEvent::Up {
at: Point::new(x, y),
pointer_id: pid,
}
}
#[test]
fn full_drag_lifecycle() {
let (p, fx) = transition(GesturePhase::Idle, down(0.0, 0.0, 1), T);
assert_eq!(fx, GestureEffect::None);
let (p, fx) = transition(p, mv(3.0, 3.0, 1), T);
assert!(matches!(p, GesturePhase::Pressed { .. }));
assert_eq!(fx, GestureEffect::None);
let (p, fx) = transition(p, mv(10.0, 0.0, 1), T);
assert_eq!(
fx,
GestureEffect::Begin {
origin: Point::new(0.0, 0.0),
at: Point::new(10.0, 0.0)
}
);
let (p, fx) = transition(p, mv(20.0, 5.0, 1), T);
assert_eq!(
fx,
GestureEffect::Track {
at: Point::new(20.0, 5.0)
}
);
let (p, fx) = transition(p, up(20.0, 5.0, 1), T);
assert_eq!(p, GesturePhase::Idle);
assert_eq!(
fx,
GestureEffect::Drop {
at: Point::new(20.0, 5.0)
}
);
}
#[test]
fn release_before_threshold_is_a_tap() {
let (p, _) = transition(GesturePhase::Idle, down(0.0, 0.0, 1), T);
let (p, fx) = transition(p, up(2.0, 2.0, 1), T);
assert_eq!(p, GesturePhase::Idle);
assert_eq!(fx, GestureEffect::Tap);
}
#[test]
fn foreign_pointer_ids_are_ignored() {
let (p, _) = transition(GesturePhase::Idle, down(0.0, 0.0, 1), T);
let (p, fx) = transition(p, mv(100.0, 100.0, 2), T);
assert!(matches!(p, GesturePhase::Pressed { .. }));
assert_eq!(fx, GestureEffect::None);
let (p, fx) = transition(p, up(100.0, 100.0, 2), T);
assert!(matches!(p, GesturePhase::Pressed { .. }));
assert_eq!(fx, GestureEffect::None);
let (p, fx) = transition(p, down(50.0, 50.0, 2), T);
assert!(matches!(p, GesturePhase::Pressed { pointer_id: 1, .. }));
assert_eq!(fx, GestureEffect::None);
}
#[test]
fn cancel_paths() {
let (p, _) = transition(GesturePhase::Idle, down(0.0, 0.0, 1), T);
let (p, _) = transition(p, mv(20.0, 0.0, 1), T);
let (p, fx) = transition(p, GestureEvent::Cancel, T);
assert_eq!((p, fx), (GesturePhase::Idle, GestureEffect::Abort));
let (p, _) = transition(GesturePhase::Idle, down(0.0, 0.0, 1), T);
let (p, fx) = transition(p, GestureEvent::Cancel, T);
assert_eq!((p, fx), (GesturePhase::Idle, GestureEffect::None));
}
#[test]
fn stray_events_while_idle_are_inert() {
for ev in [mv(9.0, 9.0, 1), up(9.0, 9.0, 1)] {
let (p, fx) = transition(GesturePhase::Idle, ev, T);
assert_eq!((p, fx), (GesturePhase::Idle, GestureEffect::None));
}
}
#[test]
fn exact_threshold_promotes() {
let (p, _) = transition(GesturePhase::Idle, down(0.0, 0.0, 1), T);
let (_, fx) = transition(p, mv(8.0, 0.0, 1), T);
assert!(matches!(fx, GestureEffect::Begin { .. }));
}
fn hold(pid: i32) -> GestureEvent {
GestureEvent::Hold { pointer_id: pid }
}
fn step_auto(p: GesturePhase, ev: GestureEvent) -> (GesturePhase, GestureEffect) {
transition_with(p, ev, T, Promotion::HoldOrSideways)
}
#[test]
fn hold_promotes_at_the_press_origin() {
let (p, _) = step_auto(GesturePhase::Idle, down(5.0, 5.0, 1));
let (p, fx) = step_auto(p, hold(1));
assert!(matches!(p, GesturePhase::Dragging { .. }));
assert_eq!(
fx,
GestureEffect::Begin {
origin: Point::new(5.0, 5.0),
at: Point::new(5.0, 5.0)
}
);
let (_, fx) = step_auto(p, mv(5.0, 40.0, 1));
assert_eq!(
fx,
GestureEffect::Track {
at: Point::new(5.0, 40.0)
}
);
}
#[test]
fn stale_or_foreign_hold_is_inert() {
let (p, _) = step_auto(GesturePhase::Idle, down(0.0, 0.0, 1));
let (p2, fx) = step_auto(p, hold(2));
assert_eq!((p2, fx), (p, GestureEffect::None));
let (p, fx) = step_auto(GesturePhase::Idle, hold(1));
assert_eq!((p, fx), (GesturePhase::Idle, GestureEffect::None));
let (p, _) = step_auto(GesturePhase::Idle, down(0.0, 0.0, 1));
let (p, _) = step_auto(p, hold(1));
let (p2, fx) = step_auto(p, hold(1));
assert_eq!((p2, fx), (p, GestureEffect::None));
let (p, _) = transition(GesturePhase::Idle, down(0.0, 0.0, 1), T);
let (_, fx) = transition(p, hold(1), T);
assert!(matches!(fx, GestureEffect::Begin { .. }));
}
#[test]
fn sideways_pull_promotes_under_auto() {
let (p, _) = step_auto(GesturePhase::Idle, down(0.0, 0.0, 1));
let (_, fx) = step_auto(p, mv(9.0, 4.0, 1));
assert_eq!(
fx,
GestureEffect::Begin {
origin: Point::new(0.0, 0.0),
at: Point::new(9.0, 4.0)
}
);
}
#[test]
fn vertical_pull_yields_to_scroll_under_auto() {
let (p, _) = step_auto(GesturePhase::Idle, down(0.0, 0.0, 1));
let (p, fx) = step_auto(p, mv(1.0, 5.0, 1));
assert!(matches!(p, GesturePhase::Pressed { .. }));
assert_eq!(fx, GestureEffect::None);
let (p, fx) = step_auto(p, mv(2.0, 12.0, 1));
assert_eq!((p, fx), (GesturePhase::Idle, GestureEffect::None));
let (p, _) = step_auto(GesturePhase::Idle, down(0.0, 0.0, 1));
let (p, fx) = step_auto(p, mv(10.0, 10.0, 1));
assert_eq!((p, fx), (GesturePhase::Idle, GestureEffect::None));
}
#[test]
fn distance_policy_promotes_vertical_pulls_unchanged() {
let (p, _) = transition(GesturePhase::Idle, down(0.0, 0.0, 1), T);
let (_, fx) = transition(p, mv(0.0, 12.0, 1), T);
assert!(matches!(fx, GestureEffect::Begin { .. }));
}
}