use std::time::Duration;
use teksilo_canvas::{Point, Rect};
use teksilo_tokens::{GestureProfile, InputTokens, PointerKind, TargetDensity};
use crate::event::PointerButton;
use crate::pointer::{EventTime, PointerInfo};
use super::{RawPointerEvent, distance};
const FALLBACK_INPUT: InputTokens = InputTokens::for_density(TargetDensity::Compact);
pub fn default_profile(kind: PointerKind) -> GestureProfile {
*FALLBACK_INPUT.profile(kind)
}
#[derive(Debug, Clone, Copy)]
pub struct RecognizerContext<'a> {
pub now: EventTime,
pub profile: GestureProfile,
pub local_bounds: Rect,
pub pointer: PointerInfo,
pub streak: &'a TapStreak,
}
static NO_STREAK: TapStreak = TapStreak::EMPTY;
impl<'a> RecognizerContext<'a> {
pub fn new(
now: EventTime,
profile: GestureProfile,
local_bounds: Rect,
pointer: PointerInfo,
) -> RecognizerContext<'static> {
RecognizerContext {
now,
profile,
local_bounds,
pointer,
streak: &NO_STREAK,
}
}
pub fn with_streak<'b>(&self, streak: &'b TapStreak) -> RecognizerContext<'b> {
RecognizerContext {
now: self.now,
profile: self.profile,
local_bounds: self.local_bounds,
pointer: self.pointer,
streak,
}
}
pub fn for_event(event: &RawPointerEvent) -> RecognizerContext<'static> {
let pointer = event.pointer();
Self::new(
event.time(),
default_profile(pointer.kind),
Rect::ZERO,
pointer,
)
}
}
const MAX_STREAK: u8 = 3;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TapStreak {
count: u8,
last_up: Option<EventTime>,
last_position: Point,
button: Option<PointerButton>,
gap: Duration,
travel: f32,
}
impl TapStreak {
pub const EMPTY: Self = Self {
count: 0,
last_up: None,
last_position: Point::ZERO,
button: None,
gap: Duration::ZERO,
travel: 0.0,
};
pub fn count(&self) -> u8 {
self.count
}
pub fn last_up(&self) -> Option<EventTime> {
self.last_up
}
pub fn last_position(&self) -> Point {
self.last_position
}
pub fn button(&self) -> Option<PointerButton> {
self.button
}
pub fn since_previous(&self) -> Duration {
self.gap
}
pub fn travel_from_previous(&self) -> f32 {
self.travel
}
pub fn advance(
&mut self,
now: EventTime,
profile: &GestureProfile,
position: Point,
button: PointerButton,
) -> u8 {
let gap = self.last_up.map(|last| now.saturating_since(last));
let travel = self.last_up.map(|_| distance(position, self.last_position));
let continues = match (gap, travel, self.button) {
(Some(gap), Some(travel), Some(previous)) => {
previous == button
&& gap <= profile.multi_tap_interval
&& travel <= profile.multi_tap_slop
}
_ => false,
};
if continues && self.count < MAX_STREAK {
self.count += 1;
self.gap = gap.unwrap_or(Duration::ZERO);
self.travel = travel.unwrap_or(0.0);
} else {
self.count = 1;
self.gap = Duration::ZERO;
self.travel = 0.0;
}
self.last_up = Some(now);
self.last_position = position;
self.button = Some(button);
self.count
}
pub fn reset(&mut self) {
*self = Self::EMPTY;
}
}
impl Default for TapStreak {
fn default() -> Self {
Self::EMPTY
}
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct TapContact {
press: Option<(Point, PointerButton)>,
strayed: bool,
}
impl TapContact {
pub(crate) fn observe(
&mut self,
event: &RawPointerEvent,
profile: &GestureProfile,
) -> Option<(Point, PointerButton)> {
match event {
RawPointerEvent::Down {
position, button, ..
} => {
self.press = Some((*position, *button));
self.strayed = false;
None
}
RawPointerEvent::Move { position, .. } => {
if let Some((press, _)) = self.press
&& distance(*position, press) > profile.multi_tap_slop
{
self.strayed = true;
}
None
}
RawPointerEvent::Up {
position, button, ..
} => {
let (press, pressed_button) = self.press.take()?;
if self.strayed
|| pressed_button != *button
|| distance(*position, press) > profile.multi_tap_slop
{
return None;
}
Some((press, *button))
}
RawPointerEvent::Cancel { .. } => {
self.press = None;
self.strayed = false;
None
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MultiContact {
#[default]
First,
All,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gesture::test_helpers::*;
fn mouse() -> GestureProfile {
GestureProfile::MOUSE
}
#[test]
fn a_first_tap_starts_the_streak_at_one() {
let mut streak = TapStreak::EMPTY;
assert_eq!(streak.count(), 0);
let count = streak.advance(
EventTime::from_millis(10),
&mouse(),
Point::new(4.0, 4.0),
PointerButton::Primary,
);
assert_eq!(count, 1);
assert_eq!(streak.since_previous(), Duration::ZERO);
}
#[test]
fn a_second_tap_in_the_window_continues() {
let mut streak = TapStreak::EMPTY;
let p = Point::new(4.0, 4.0);
streak.advance(
EventTime::from_millis(10),
&mouse(),
p,
PointerButton::Primary,
);
let count = streak.advance(
EventTime::from_millis(200),
&mouse(),
p,
PointerButton::Primary,
);
assert_eq!(count, 2);
assert_eq!(streak.since_previous(), Duration::from_millis(190));
}
#[test]
fn a_different_button_restarts_the_streak() {
let mut streak = TapStreak::EMPTY;
let p = Point::new(4.0, 4.0);
streak.advance(
EventTime::from_millis(10),
&mouse(),
p,
PointerButton::Primary,
);
let count = streak.advance(
EventTime::from_millis(100),
&mouse(),
p,
PointerButton::Secondary,
);
assert_eq!(count, 1, "a mixed-button pair is never a double tap");
}
#[test]
fn exceeding_the_interval_restarts_the_streak() {
let mut streak = TapStreak::EMPTY;
let p = Point::new(4.0, 4.0);
streak.advance(
EventTime::from_millis(10),
&mouse(),
p,
PointerButton::Primary,
);
let count = streak.advance(
EventTime::from_millis(10 + 301),
&mouse(),
p,
PointerButton::Primary,
);
assert_eq!(count, 1);
}
#[test]
fn exceeding_the_multi_tap_slop_restarts_the_streak() {
let mut streak = TapStreak::EMPTY;
streak.advance(
EventTime::from_millis(10),
&mouse(),
Point::new(0.0, 0.0),
PointerButton::Primary,
);
let count = streak.advance(
EventTime::from_millis(100),
&mouse(),
Point::new(11.0, 0.0),
PointerButton::Primary,
);
assert_eq!(
count, 1,
"11 dp apart exceeds the mouse 10 dp multi-tap slop"
);
}
#[test]
fn a_reset_between_taps_restarts_the_streak() {
let mut streak = TapStreak::EMPTY;
let p = Point::new(4.0, 4.0);
streak.advance(
EventTime::from_millis(10),
&mouse(),
p,
PointerButton::Primary,
);
streak.reset();
let count = streak.advance(
EventTime::from_millis(100),
&mouse(),
p,
PointerButton::Primary,
);
assert_eq!(count, 1);
}
#[test]
fn the_streak_restarts_after_three() {
let mut streak = TapStreak::EMPTY;
let p = Point::new(4.0, 4.0);
let mut counts = Vec::new();
for i in 0..4 {
counts.push(streak.advance(
EventTime::from_millis(10 + 100 * i),
&mouse(),
p,
PointerButton::Primary,
));
}
assert_eq!(counts, vec![1, 2, 3, 1]);
}
#[test]
fn a_touch_profile_widens_the_window_the_streak_accepts() {
let mut streak = TapStreak::EMPTY;
streak.advance(
EventTime::from_millis(0),
&GestureProfile::TOUCH,
Point::new(0.0, 0.0),
PointerButton::Primary,
);
let count = streak.advance(
EventTime::from_millis(100),
&GestureProfile::TOUCH,
Point::new(30.0, 0.0),
PointerButton::Primary,
);
assert_eq!(count, 2);
}
#[test]
fn a_release_that_strayed_does_not_qualify() {
let mut contact = TapContact::default();
let profile = mouse();
contact.observe(&down(Point::new(0.0, 0.0)), &profile);
contact.observe(&move_to(Point::new(40.0, 0.0)), &profile);
assert!(
contact
.observe(&up(Point::new(0.0, 0.0)), &profile)
.is_none()
);
}
#[test]
fn a_release_within_slop_qualifies_and_reports_the_press_point() {
let mut contact = TapContact::default();
let profile = mouse();
contact.observe(&down(Point::new(1.0, 2.0)), &profile);
assert_eq!(
contact.observe(&up(Point::new(4.0, 2.0)), &profile),
Some((Point::new(1.0, 2.0), PointerButton::Primary))
);
}
#[test]
fn the_default_profile_follows_the_pointer_kind() {
assert_eq!(default_profile(PointerKind::Mouse), GestureProfile::MOUSE);
assert_eq!(default_profile(PointerKind::Touch), GestureProfile::TOUCH);
}
}