use std::time::{Duration, Instant};
use teksilo_canvas::Point;
use crate::event::{ButtonMask, PointerButton};
use super::{GestureEvent, GestureRecognizer, GestureResult, RawPointerEvent, TapEvent, distance};
#[derive(Debug)]
pub struct DoubleTapRecognizer {
max_distance: f32,
max_interval: Duration,
accept: ButtonMask,
first_tap_position: Option<Point>,
first_tap_time: Option<Instant>,
first_tap_button: Option<PointerButton>,
down_position: Option<Point>,
down_button: Option<PointerButton>,
}
impl DoubleTapRecognizer {
pub fn new() -> Self {
Self {
max_distance: 10.0,
max_interval: Duration::from_millis(300),
accept: ButtonMask::PRIMARY,
first_tap_position: None,
first_tap_time: None,
first_tap_button: None,
down_position: None,
down_button: None,
}
}
pub fn max_distance(mut self, d: f32) -> Self {
self.max_distance = d;
self
}
pub fn max_interval(mut self, interval: Duration) -> Self {
self.max_interval = interval;
self
}
pub fn accept_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
self.accept = mask.into();
self
}
pub fn accept_any_button(self) -> Self {
self.accept_buttons(ButtonMask::ALL)
}
pub fn process_at(&mut self, event: &RawPointerEvent, now: Instant) -> GestureResult {
match event {
RawPointerEvent::Down {
position, button, ..
} => {
if !self.accept.contains(*button) {
return GestureResult::Pending;
}
if let Some(first_button) = self.first_tap_button
&& first_button != *button
{
self.first_tap_position = None;
self.first_tap_time = None;
self.first_tap_button = None;
}
self.down_position = Some(*position);
self.down_button = Some(*button);
GestureResult::Pending
}
RawPointerEvent::Move { position } => {
if let Some(down) = self.down_position
&& distance(*position, down) > self.max_distance
{
return GestureResult::Failed;
}
GestureResult::Pending
}
RawPointerEvent::Up {
position,
button,
modifiers,
} => {
let Some(down) = self.down_position else {
return GestureResult::Failed;
};
let Some(down_button) = self.down_button else {
return GestureResult::Failed;
};
self.down_position = None;
self.down_button = None;
if *button != down_button {
return GestureResult::Failed;
}
if distance(*position, down) > self.max_distance {
return GestureResult::Failed;
}
if let (Some(first_pos), Some(first_time), Some(first_button)) = (
self.first_tap_position,
self.first_tap_time,
self.first_tap_button,
) {
if first_button == *button
&& distance(*position, first_pos) <= self.max_distance
&& now.duration_since(first_time) <= self.max_interval
{
self.reset();
return GestureResult::Recognized(GestureEvent::DoubleTap(TapEvent {
position: *position,
button: *button,
modifiers: *modifiers,
}));
}
self.first_tap_position = Some(*position);
self.first_tap_time = Some(now);
self.first_tap_button = Some(*button);
GestureResult::Pending
} else {
self.first_tap_position = Some(*position);
self.first_tap_time = Some(now);
self.first_tap_button = Some(*button);
GestureResult::Pending
}
}
}
}
}
impl Default for DoubleTapRecognizer {
fn default() -> Self {
Self::new()
}
}
impl GestureRecognizer for DoubleTapRecognizer {
fn process(&mut self, event: &RawPointerEvent) -> GestureResult {
self.process_at(event, Instant::now())
}
fn reset(&mut self) {
self.first_tap_position = None;
self.first_tap_time = None;
self.first_tap_button = None;
self.down_position = None;
self.down_button = None;
}
fn priority(&self) -> u32 {
15 }
fn resets_on_peer_recognition(&self) -> bool {
false
}
}
#[derive(Debug)]
pub struct TripleTapRecognizer {
max_distance: f32,
max_interval: Duration,
accept: ButtonMask,
first_tap_position: Option<Point>,
first_tap_time: Option<Instant>,
first_tap_button: Option<PointerButton>,
second_tap_position: Option<Point>,
second_tap_time: Option<Instant>,
second_tap_button: Option<PointerButton>,
down_position: Option<Point>,
down_button: Option<PointerButton>,
}
impl TripleTapRecognizer {
pub fn new() -> Self {
Self {
max_distance: 10.0,
max_interval: Duration::from_millis(300),
accept: ButtonMask::PRIMARY,
first_tap_position: None,
first_tap_time: None,
first_tap_button: None,
second_tap_position: None,
second_tap_time: None,
second_tap_button: None,
down_position: None,
down_button: None,
}
}
pub fn max_distance(mut self, d: f32) -> Self {
self.max_distance = d;
self
}
pub fn max_interval(mut self, interval: Duration) -> Self {
self.max_interval = interval;
self
}
pub fn accept_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
self.accept = mask.into();
self
}
pub fn accept_any_button(self) -> Self {
self.accept_buttons(ButtonMask::ALL)
}
pub fn process_at(&mut self, event: &RawPointerEvent, now: Instant) -> GestureResult {
match event {
RawPointerEvent::Down {
position, button, ..
} => {
if !self.accept.contains(*button) {
return GestureResult::Pending;
}
let mismatch = self.first_tap_button.map(|b| b != *button).unwrap_or(false)
|| self
.second_tap_button
.map(|b| b != *button)
.unwrap_or(false);
if mismatch {
self.first_tap_position = None;
self.first_tap_time = None;
self.first_tap_button = None;
self.second_tap_position = None;
self.second_tap_time = None;
self.second_tap_button = None;
}
self.down_position = Some(*position);
self.down_button = Some(*button);
GestureResult::Pending
}
RawPointerEvent::Move { position } => {
if let Some(down) = self.down_position
&& distance(*position, down) > self.max_distance
{
return GestureResult::Failed;
}
GestureResult::Pending
}
RawPointerEvent::Up {
position,
button,
modifiers,
} => {
let Some(down) = self.down_position else {
return GestureResult::Failed;
};
let Some(down_button) = self.down_button else {
return GestureResult::Failed;
};
self.down_position = None;
self.down_button = None;
if *button != down_button {
return GestureResult::Failed;
}
if distance(*position, down) > self.max_distance {
return GestureResult::Failed;
}
if let (
Some(first_pos),
Some(first_time),
Some(first_button),
Some(second_pos),
Some(second_time),
Some(second_button),
) = (
self.first_tap_position,
self.first_tap_time,
self.first_tap_button,
self.second_tap_position,
self.second_tap_time,
self.second_tap_button,
) {
if first_button == *button
&& second_button == *button
&& distance(*position, second_pos) <= self.max_distance
&& now.duration_since(second_time) <= self.max_interval
&& distance(second_pos, first_pos) <= self.max_distance
&& second_time.duration_since(first_time) <= self.max_interval
{
self.reset();
return GestureResult::Recognized(GestureEvent::TripleTap(TapEvent {
position: *position,
button: *button,
modifiers: *modifiers,
}));
}
self.first_tap_position = Some(*position);
self.first_tap_time = Some(now);
self.first_tap_button = Some(*button);
self.second_tap_position = None;
self.second_tap_time = None;
self.second_tap_button = None;
return GestureResult::Pending;
}
if let (Some(first_pos), Some(first_time), Some(first_button)) = (
self.first_tap_position,
self.first_tap_time,
self.first_tap_button,
) {
if first_button == *button
&& distance(*position, first_pos) <= self.max_distance
&& now.duration_since(first_time) <= self.max_interval
{
self.second_tap_position = Some(*position);
self.second_tap_time = Some(now);
self.second_tap_button = Some(*button);
return GestureResult::Pending;
}
self.first_tap_position = Some(*position);
self.first_tap_time = Some(now);
self.first_tap_button = Some(*button);
self.second_tap_position = None;
self.second_tap_time = None;
self.second_tap_button = None;
return GestureResult::Pending;
}
self.first_tap_position = Some(*position);
self.first_tap_time = Some(now);
self.first_tap_button = Some(*button);
self.second_tap_position = None;
self.second_tap_time = None;
self.second_tap_button = None;
GestureResult::Pending
}
}
}
}
impl Default for TripleTapRecognizer {
fn default() -> Self {
Self::new()
}
}
impl GestureRecognizer for TripleTapRecognizer {
fn process(&mut self, event: &RawPointerEvent) -> GestureResult {
self.process_at(event, Instant::now())
}
fn reset(&mut self) {
self.first_tap_position = None;
self.first_tap_time = None;
self.first_tap_button = None;
self.second_tap_position = None;
self.second_tap_time = None;
self.second_tap_button = None;
self.down_position = None;
self.down_button = None;
}
fn priority(&self) -> u32 {
20
}
fn resets_on_peer_recognition(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::Modifiers;
use crate::gesture::test_helpers::*;
#[test]
fn double_tap_recognized_within_interval() {
let mut rec = DoubleTapRecognizer::new().max_interval(Duration::from_millis(500));
let t0 = Instant::now();
let p = Point::new(10.0, 10.0);
rec.process_at(&down(p), t0);
rec.process_at(&up(p), t0 + Duration::from_millis(50));
rec.process_at(
&down(Point::new(11.0, 10.0)),
t0 + Duration::from_millis(200),
);
let result = rec.process_at(&up(Point::new(11.0, 10.0)), t0 + Duration::from_millis(250));
assert!(matches!(
result,
GestureResult::Recognized(GestureEvent::DoubleTap(_))
));
}
#[test]
fn double_tap_fails_if_too_slow() {
let mut rec = DoubleTapRecognizer::new().max_interval(Duration::from_millis(300));
let t0 = Instant::now();
let p = Point::new(10.0, 10.0);
rec.process_at(&down(p), t0);
rec.process_at(&up(p), t0 + Duration::from_millis(50));
rec.process_at(&down(p), t0 + Duration::from_millis(400));
let result = rec.process_at(&up(p), t0 + Duration::from_millis(450));
assert!(matches!(result, GestureResult::Pending));
}
#[test]
fn double_tap_fails_if_too_far() {
let mut rec = DoubleTapRecognizer::new().max_distance(5.0);
let t0 = Instant::now();
let p = Point::new(10.0, 10.0);
rec.process_at(&down(p), t0);
rec.process_at(&up(p), t0 + Duration::from_millis(50));
rec.process_at(
&down(Point::new(50.0, 50.0)),
t0 + Duration::from_millis(100),
);
let result = rec.process_at(&up(Point::new(50.0, 50.0)), t0 + Duration::from_millis(150));
assert!(matches!(result, GestureResult::Pending));
}
#[test]
fn double_tap_button_mismatch_fails_at_second_down() {
let mut rec = DoubleTapRecognizer::new().accept_any_button();
let t0 = Instant::now();
let p = Point::new(10.0, 10.0);
rec.process_at(&down_btn(p, PointerButton::Primary), t0);
rec.process_at(
&up_btn(p, PointerButton::Primary),
t0 + Duration::from_millis(50),
);
rec.process_at(
&down_btn(p, PointerButton::Secondary),
t0 + Duration::from_millis(150),
);
let result = rec.process_at(
&up_btn(p, PointerButton::Secondary),
t0 + Duration::from_millis(200),
);
assert!(matches!(result, GestureResult::Pending));
}
#[test]
fn double_tap_carries_modifiers_from_second_up() {
let mut rec = DoubleTapRecognizer::new();
let t0 = Instant::now();
let p = Point::new(10.0, 10.0);
rec.process_at(&down(p), t0);
rec.process_at(&up(p), t0 + Duration::from_millis(50));
rec.process_at(&down(p), t0 + Duration::from_millis(150));
let result = rec.process_at(
&up_full(p, PointerButton::Primary, Modifiers::SHIFT),
t0 + Duration::from_millis(200),
);
match result {
GestureResult::Recognized(GestureEvent::DoubleTap(event)) => {
assert!(event.modifiers.shift());
}
other => panic!("expected DoubleTap with shift, got {:?}", other),
}
}
#[test]
fn triple_tap_recognized_within_intervals() {
let mut rec = TripleTapRecognizer::new();
let t0 = Instant::now();
let p = Point::new(10.0, 10.0);
for i in 0..3 {
let offset = Duration::from_millis(200 * i as u64);
rec.process_at(&down(p), t0 + offset);
let result = rec.process_at(&up(p), t0 + offset + Duration::from_millis(50));
if i < 2 {
assert!(matches!(result, GestureResult::Pending));
} else {
assert!(matches!(
result,
GestureResult::Recognized(GestureEvent::TripleTap(_))
));
}
}
}
#[test]
fn triple_tap_fails_if_third_is_too_slow() {
let mut rec = TripleTapRecognizer::new().max_interval(Duration::from_millis(300));
let t0 = Instant::now();
let stamp = |ms| t0 + Duration::from_millis(ms);
let p = Point::new(10.0, 10.0);
rec.process_at(&down(p), stamp(0));
rec.process_at(&up(p), stamp(50));
rec.process_at(&down(p), stamp(200));
rec.process_at(&up(p), stamp(250));
rec.process_at(&down(p), stamp(700));
let result = rec.process_at(&up(p), stamp(750));
assert!(matches!(result, GestureResult::Pending));
}
#[test]
fn triple_tap_fails_if_third_is_too_far() {
let mut rec = TripleTapRecognizer::new().max_distance(5.0);
let t0 = Instant::now();
let stamp = |ms| t0 + Duration::from_millis(ms);
let p = Point::new(10.0, 10.0);
rec.process_at(&down(p), stamp(0));
rec.process_at(&up(p), stamp(50));
rec.process_at(&down(p), stamp(100));
rec.process_at(&up(p), stamp(150));
rec.process_at(&down(Point::new(30.0, 10.0)), stamp(200));
let result = rec.process_at(&up(Point::new(30.0, 10.0)), stamp(250));
assert!(matches!(result, GestureResult::Pending));
}
#[test]
fn triple_tap_button_mismatch_fails_at_third_down() {
let mut rec = TripleTapRecognizer::new().accept_any_button();
let t0 = Instant::now();
let stamp = |ms| t0 + Duration::from_millis(ms);
let p = Point::new(10.0, 10.0);
rec.process_at(&down_btn(p, PointerButton::Primary), stamp(0));
rec.process_at(&up_btn(p, PointerButton::Primary), stamp(50));
rec.process_at(&down_btn(p, PointerButton::Primary), stamp(150));
rec.process_at(&up_btn(p, PointerButton::Primary), stamp(200));
rec.process_at(&down_btn(p, PointerButton::Secondary), stamp(300));
let result = rec.process_at(&up_btn(p, PointerButton::Secondary), stamp(350));
assert!(matches!(result, GestureResult::Pending));
}
}