use std::time::Duration;
use web_time::Instant;
use frus_widgets::Point;
pub(crate) const LONG_PRESS_DELAY: Duration = Duration::from_millis(500);
const SLOP: f32 = 18.0;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum PointerKind {
Down,
Move,
Up,
Cancel,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct PointerEvent {
pub kind: PointerKind,
pub position: Point,
pub touch: bool,
}
#[derive(Debug)]
enum State {
Idle,
Possible { origin: Point, deadline: Instant },
Fired,
Rejected,
}
pub(crate) struct PressRecognizer {
state: State,
}
impl PressRecognizer {
pub fn new() -> Self {
Self { state: State::Idle }
}
pub fn down(&mut self, at: Point, now: Instant, interested: bool) {
self.state = if interested {
State::Possible {
origin: at,
deadline: now + LONG_PRESS_DELAY,
}
} else {
State::Rejected
};
}
pub fn moved(&mut self, at: Point) {
if let State::Possible { origin, .. } = self.state {
let (dx, dy) = (at.x - origin.x, at.y - origin.y);
if dx * dx + dy * dy > SLOP * SLOP {
self.state = State::Rejected;
}
}
}
pub fn deadline(&self) -> Option<Instant> {
match self.state {
State::Possible { deadline, .. } => Some(deadline),
_ => None,
}
}
pub fn poll(&mut self, now: Instant) -> bool {
if let State::Possible { deadline, .. } = self.state {
if now >= deadline {
self.state = State::Fired;
return true;
}
}
false
}
pub fn up(&mut self) -> bool {
let swallow = matches!(self.state, State::Fired);
self.state = State::Idle;
swallow
}
pub fn cancel(&mut self) {
self.state = State::Idle;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn start() -> Instant {
Instant::now()
}
#[test]
fn long_press_fires_after_the_delay_and_swallows_the_release() {
let mut rec = PressRecognizer::new();
let t0 = start();
rec.down(Point::new(10.0, 10.0), t0, true);
assert_eq!(rec.deadline(), Some(t0 + LONG_PRESS_DELAY));
assert!(!rec.poll(t0 + Duration::from_millis(499)));
assert!(rec.poll(t0 + LONG_PRESS_DELAY));
assert!(!rec.poll(t0 + Duration::from_millis(600)));
assert!(rec.up());
assert!(rec.deadline().is_none());
}
#[test]
fn release_before_the_delay_is_a_plain_tap() {
let mut rec = PressRecognizer::new();
let t0 = start();
rec.down(Point::new(0.0, 0.0), t0, true);
assert!(!rec.up(), "a tap: the click is not swallowed");
assert!(
!rec.poll(t0 + Duration::from_secs(1)),
"nothing left to fire"
);
}
#[test]
fn movement_beyond_slop_rejects_the_long_press() {
let mut rec = PressRecognizer::new();
let t0 = start();
rec.down(Point::new(0.0, 0.0), t0, true);
rec.moved(Point::new(SLOP * 0.3, SLOP * 0.3));
assert!(rec.deadline().is_some());
rec.moved(Point::new(SLOP, SLOP));
assert!(rec.deadline().is_none());
assert!(!rec.poll(t0 + Duration::from_secs(1)));
assert!(!rec.up(), "an ordinary click is still possible");
}
#[test]
fn uninterested_target_never_arms() {
let mut rec = PressRecognizer::new();
rec.down(Point::new(0.0, 0.0), start(), false);
assert!(rec.deadline().is_none());
assert!(!rec.poll(start() + Duration::from_secs(1)));
}
#[test]
fn cancel_abandons_without_firing() {
let mut rec = PressRecognizer::new();
let t0 = start();
rec.down(Point::new(0.0, 0.0), t0, true);
rec.cancel();
assert!(!rec.poll(t0 + Duration::from_secs(1)));
assert!(!rec.up());
}
}