use std::time::Duration;
#[cfg(test)]
use std::time::Instant;
use teksilo_canvas::Point;
use crate::event::{ButtonMask, PointerButton};
use super::{
GestureEvent, GestureRecognizer, GestureResult, RawPointerEvent, RecognizerContext, TapEvent,
distance,
};
#[derive(Debug)]
pub struct DoubleTapRecognizer {
max_distance: Option<f32>,
max_interval: Option<Duration>,
accept: ButtonMask,
down_position: Option<Point>,
down_button: Option<PointerButton>,
#[cfg(test)]
test_state: TestDriver,
}
impl DoubleTapRecognizer {
pub fn new() -> Self {
Self {
max_distance: None,
max_interval: None,
accept: ButtonMask::PRIMARY,
down_position: None,
down_button: None,
#[cfg(test)]
test_state: TestDriver::default(),
}
}
pub fn max_distance(mut self, d: f32) -> Self {
self.max_distance = Some(d);
self
}
pub fn max_interval(mut self, interval: Duration) -> Self {
self.max_interval = Some(interval);
self
}
fn slop(&self, cx: &RecognizerContext) -> f32 {
self.max_distance.unwrap_or(cx.profile.multi_tap_slop)
}
fn interval(&self, cx: &RecognizerContext) -> Duration {
self.max_interval.unwrap_or(cx.profile.multi_tap_interval)
}
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)
}
}
impl Default for DoubleTapRecognizer {
fn default() -> Self {
Self::new()
}
}
impl GestureRecognizer for DoubleTapRecognizer {
fn process(&mut self, event: &RawPointerEvent, cx: &RecognizerContext) -> GestureResult {
match event {
RawPointerEvent::Down {
position, button, ..
} => {
if !self.accept.contains(*button) {
return GestureResult::Pending;
}
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.slop(cx)
{
return GestureResult::Failed;
}
GestureResult::Pending
}
RawPointerEvent::Up {
position,
button,
modifiers,
pointer,
..
} => {
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.slop(cx) {
return GestureResult::Failed;
}
if cx.streak.count() == 2
&& cx.streak.since_previous() <= self.interval(cx)
&& cx.streak.travel_from_previous() <= self.slop(cx)
{
return GestureResult::Recognized(GestureEvent::DoubleTap(TapEvent {
position: *position,
button: *button,
modifiers: *modifiers,
pointer: *pointer,
}));
}
GestureResult::Pending
}
RawPointerEvent::Cancel { .. } => {
self.reset();
GestureResult::Failed
}
}
}
fn reset(&mut self) {
self.down_position = None;
self.down_button = None;
}
fn tap_family(&self) -> bool {
true
}
fn priority(&self) -> u32 {
15 }
fn resets_on_peer_recognition(&self) -> bool {
false
}
}
#[derive(Debug)]
pub struct TripleTapRecognizer {
max_distance: Option<f32>,
max_interval: Option<Duration>,
accept: ButtonMask,
down_position: Option<Point>,
down_button: Option<PointerButton>,
#[cfg(test)]
test_state: TestDriver,
}
impl TripleTapRecognizer {
pub fn new() -> Self {
Self {
max_distance: None,
max_interval: None,
accept: ButtonMask::PRIMARY,
down_position: None,
down_button: None,
#[cfg(test)]
test_state: TestDriver::default(),
}
}
pub fn max_distance(mut self, d: f32) -> Self {
self.max_distance = Some(d);
self
}
pub fn max_interval(mut self, interval: Duration) -> Self {
self.max_interval = Some(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)
}
fn slop(&self, cx: &RecognizerContext) -> f32 {
self.max_distance.unwrap_or(cx.profile.multi_tap_slop)
}
fn interval(&self, cx: &RecognizerContext) -> Duration {
self.max_interval.unwrap_or(cx.profile.multi_tap_interval)
}
}
impl Default for TripleTapRecognizer {
fn default() -> Self {
Self::new()
}
}
impl GestureRecognizer for TripleTapRecognizer {
fn process(&mut self, event: &RawPointerEvent, cx: &RecognizerContext) -> GestureResult {
match event {
RawPointerEvent::Down {
position, button, ..
} => {
if !self.accept.contains(*button) {
return GestureResult::Pending;
}
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.slop(cx)
{
return GestureResult::Failed;
}
GestureResult::Pending
}
RawPointerEvent::Up {
position,
button,
modifiers,
pointer,
..
} => {
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.slop(cx) {
return GestureResult::Failed;
}
if cx.streak.count() == 3
&& cx.streak.since_previous() <= self.interval(cx)
&& cx.streak.travel_from_previous() <= self.slop(cx)
{
return GestureResult::Recognized(GestureEvent::TripleTap(TapEvent {
position: *position,
button: *button,
modifiers: *modifiers,
pointer: *pointer,
}));
}
GestureResult::Pending
}
RawPointerEvent::Cancel { .. } => {
self.reset();
GestureResult::Failed
}
}
}
fn reset(&mut self) {
self.down_position = None;
self.down_button = None;
}
fn priority(&self) -> u32 {
20
}
fn tap_family(&self) -> bool {
true
}
fn resets_on_peer_recognition(&self) -> bool {
false
}
}
#[cfg(test)]
#[derive(Debug, Default)]
struct TestDriver {
epoch: Option<std::time::Instant>,
streak: super::TapStreak,
contact: super::config::TapContact,
}
#[cfg(test)]
fn drive_standalone<R: GestureRecognizer + ?Sized>(
rec: &mut R,
driver: &mut TestDriver,
event: &RawPointerEvent,
now: std::time::Instant,
) -> GestureResult {
let epoch = *driver.epoch.get_or_insert(now);
let now = crate::pointer::EventTime::from_duration(now.saturating_duration_since(epoch));
let profile = teksilo_tokens::GestureProfile::MOUSE;
if let Some((press, button)) = driver.contact.observe(event, &profile) {
driver.streak.advance(now, &profile, press, button);
}
let base = RecognizerContext::new(now, profile, teksilo_canvas::Rect::ZERO, event.pointer());
let cx = base.with_streak(&driver.streak);
rec.process(event, &cx)
}
#[cfg(test)]
impl DoubleTapRecognizer {
fn process_at(&mut self, event: &RawPointerEvent, now: std::time::Instant) -> GestureResult {
let mut driver = std::mem::take(&mut self.test_state);
let result = drive_standalone(self, &mut driver, event, now);
self.test_state = driver;
result
}
}
#[cfg(test)]
impl TripleTapRecognizer {
fn process_at(&mut self, event: &RawPointerEvent, now: std::time::Instant) -> GestureResult {
let mut driver = std::mem::take(&mut self.test_state);
let result = drive_standalone(self, &mut driver, event, now);
self.test_state = driver;
result
}
}
#[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));
}
}