use teksilo_canvas::{Point, Vec2};
use crate::event::{Modifiers, PointerButton};
use crate::pointer::{CancelReason, EventTime, PointerInfo};
mod arena;
mod arena_set;
mod config;
mod drag;
mod long_press;
mod multi_tap;
mod palm;
mod pan;
mod pinch;
mod sequence;
mod swipe;
mod tap;
pub use arena::GestureArena;
pub use arena_set::{GestureArenaSet, GestureProto};
pub use config::{MultiContact, RecognizerContext, TapStreak, default_profile};
pub use drag::DragRecognizer;
pub use long_press::LongPressRecognizer;
pub use multi_tap::{DoubleTapRecognizer, TripleTapRecognizer};
pub use palm::{PALM_CONTACT_THRESHOLD, PalmWatch};
pub use pan::PanRecognizer;
pub use pinch::TouchPinchRecognizer;
pub use sequence::{MemberRole, MemberState, PointerSequence, SequenceMember, TapBoundary};
pub use swipe::SwipeRecognizer;
pub use tap::TapRecognizer;
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct TapEvent {
pub position: Point,
pub button: PointerButton,
pub modifiers: Modifiers,
pub pointer: PointerInfo,
}
impl TapEvent {
pub fn new(position: Point, button: PointerButton, modifiers: Modifiers) -> Self {
Self {
position,
button,
modifiers,
pointer: PointerInfo::mouse(EventTime::ZERO),
}
}
pub fn with_pointer(mut self, pointer: PointerInfo) -> Self {
self.pointer = pointer;
self
}
}
#[derive(Debug, Clone, Copy)]
pub enum RawPointerEvent {
Down {
position: Point,
button: PointerButton,
modifiers: Modifiers,
pointer: PointerInfo,
time: EventTime,
},
Move {
position: Point,
pointer: PointerInfo,
time: EventTime,
},
Up {
position: Point,
button: PointerButton,
modifiers: Modifiers,
pointer: PointerInfo,
time: EventTime,
},
Cancel {
position: Point,
pointer: PointerInfo,
reason: CancelReason,
time: EventTime,
},
}
impl RawPointerEvent {
pub fn pointer(&self) -> PointerInfo {
match self {
Self::Down { pointer, .. }
| Self::Move { pointer, .. }
| Self::Up { pointer, .. }
| Self::Cancel { pointer, .. } => *pointer,
}
}
pub fn time(&self) -> EventTime {
match self {
Self::Down { time, .. }
| Self::Move { time, .. }
| Self::Up { time, .. }
| Self::Cancel { time, .. } => *time,
}
}
pub fn position(&self) -> Point {
match self {
Self::Down { position, .. }
| Self::Move { position, .. }
| Self::Up { position, .. }
| Self::Cancel { position, .. } => *position,
}
}
}
#[derive(Debug, Clone)]
pub enum GestureResult {
Pending,
Recognized(GestureEvent),
Failed,
}
#[derive(Debug, Clone, Copy)]
pub enum GestureEvent {
Tap(TapEvent),
DoubleTap(TapEvent),
TripleTap(TapEvent),
LongPress(TapEvent),
DragStarted {
position: Point,
button: PointerButton,
pointer: PointerInfo,
},
DragMoved {
position: Point,
delta: Vec2,
pointer: PointerInfo,
},
DragEnded {
position: Point,
pointer: PointerInfo,
},
DragCancelled {
position: Point,
pointer: PointerInfo,
reason: CancelReason,
},
PinchStarted {
center: Point,
},
PinchChanged {
center: Point,
scale: f32,
rotation: f32,
},
PinchEnded,
PinchCancelled {
reason: CancelReason,
},
Swipe {
direction: SwipeDirection,
velocity: f32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SwipeDirection {
Left,
Right,
Up,
Down,
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum DragPhase {
Started {
position: Point,
button: PointerButton,
pointer: PointerInfo,
},
Moved {
position: Point,
delta: Vec2,
pointer: PointerInfo,
},
Ended {
position: Point,
pointer: PointerInfo,
},
Cancelled {
position: Point,
pointer: PointerInfo,
reason: CancelReason,
},
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum PinchPhase {
Started {
center: Point,
pointer: PointerInfo,
},
Changed {
center: Point,
scale: f32,
rotation: f32,
pointer: PointerInfo,
},
Ended {
pointer: PointerInfo,
},
Cancelled {
pointer: PointerInfo,
reason: CancelReason,
},
}
pub trait GestureRecognizer {
fn process(&mut self, event: &RawPointerEvent, cx: &RecognizerContext) -> GestureResult;
fn tick(&mut self, _cx: &RecognizerContext) -> GestureResult {
GestureResult::Pending
}
fn next_deadline(&self) -> Option<EventTime> {
None
}
fn cancel(&mut self) {
self.reset();
}
fn reset(&mut self);
fn priority(&self) -> u32;
fn resets_on_peer_recognition(&self) -> bool {
true
}
fn tap_family(&self) -> bool {
false
}
fn competes_for_sequence(&self) -> bool {
false
}
fn wants_all_pointers(&self) -> bool {
false
}
}
pub(crate) fn distance(a: Point, b: Point) -> f32 {
let dx = a.x - b.x;
let dy = a.y - b.y;
(dx * dx + dy * dy).sqrt()
}
#[cfg(test)]
pub(crate) mod test_helpers {
use super::{EventTime, Modifiers, Point, PointerButton, PointerInfo, RawPointerEvent};
use crate::pointer::CancelReason;
pub fn mouse_pointer() -> PointerInfo {
PointerInfo::mouse(EventTime::ZERO)
}
pub fn down(pos: Point) -> RawPointerEvent {
down_btn(pos, PointerButton::Primary)
}
pub fn down_btn(pos: Point, button: PointerButton) -> RawPointerEvent {
down_full(pos, button, Modifiers::NONE)
}
pub fn down_full(pos: Point, button: PointerButton, modifiers: Modifiers) -> RawPointerEvent {
RawPointerEvent::Down {
position: pos,
button,
modifiers,
pointer: mouse_pointer(),
time: EventTime::ZERO,
}
}
pub fn up(pos: Point) -> RawPointerEvent {
up_btn(pos, PointerButton::Primary)
}
pub fn up_btn(pos: Point, button: PointerButton) -> RawPointerEvent {
up_full(pos, button, Modifiers::NONE)
}
pub fn up_full(pos: Point, button: PointerButton, modifiers: Modifiers) -> RawPointerEvent {
RawPointerEvent::Up {
position: pos,
button,
modifiers,
pointer: mouse_pointer(),
time: EventTime::ZERO,
}
}
pub fn move_to(pos: Point) -> RawPointerEvent {
RawPointerEvent::Move {
position: pos,
pointer: mouse_pointer(),
time: EventTime::ZERO,
}
}
pub fn cancel_at(pos: Point) -> RawPointerEvent {
RawPointerEvent::Cancel {
position: pos,
pointer: mouse_pointer(),
reason: CancelReason::Platform,
time: EventTime::ZERO,
}
}
pub fn retimed(
event: RawPointerEvent,
pointer: PointerInfo,
time: EventTime,
) -> RawPointerEvent {
match event {
RawPointerEvent::Down {
position,
button,
modifiers,
..
} => RawPointerEvent::Down {
position,
button,
modifiers,
pointer,
time,
},
RawPointerEvent::Move { position, .. } => RawPointerEvent::Move {
position,
pointer,
time,
},
RawPointerEvent::Up {
position,
button,
modifiers,
..
} => RawPointerEvent::Up {
position,
button,
modifiers,
pointer,
time,
},
RawPointerEvent::Cancel {
position, reason, ..
} => RawPointerEvent::Cancel {
position,
pointer,
reason,
time,
},
}
}
}
#[cfg(test)]
mod source_scan_tests {
fn gesture_sources() -> Vec<(String, String)> {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/gesture");
let mut out = vec![(
"gesture.rs".to_string(),
std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/gesture.rs"),
)
.expect("gesture.rs is readable"),
)];
for entry in std::fs::read_dir(&root).expect("src/gesture is readable") {
let path = entry.expect("readable dir entry").path();
if path.extension().and_then(|e| e.to_str()) == Some("rs") {
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("?")
.to_string();
out.push((name, std::fs::read_to_string(&path).expect("readable")));
}
}
out
}
fn production_only(source: &str) -> String {
const MARKER: &[u8] = b"#[cfg(test)]";
let bytes = source.as_bytes();
let mut out = String::with_capacity(source.len());
let mut kept_from = 0usize;
let mut i = 0usize;
while i < bytes.len() {
if !bytes[i..].starts_with(MARKER) {
i += 1;
continue;
}
let after = i + MARKER.len();
let head_end = bytes[after..]
.iter()
.position(|c| *c == b'{')
.map(|off| after + off);
let Some(head_end) = head_end else { break };
let head = source[after..head_end].trim_start();
if !(head.starts_with("mod ") || head.starts_with("impl ")) {
i = after;
continue;
}
let mut depth = 0usize;
let mut j = head_end;
while j < bytes.len() {
match bytes[j] {
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
j += 1;
break;
}
}
_ => {}
}
j += 1;
}
out.push_str(&source[kept_from..i]);
kept_from = j;
i = j;
}
out.push_str(&source[kept_from..]);
out
}
fn code_only(source: &str) -> String {
source
.lines()
.map(|line| match line.find("//") {
Some(idx) => &line[..idx],
None => line,
})
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn no_wall_clock_in_gestures() {
for (name, source) in gesture_sources() {
let production = code_only(&production_only(&source));
assert!(
!production.contains("Instant::now()"),
"{name} reads the wall clock outside its test blocks; gesture \
recognizers must take their time from `RecognizerContext::now`"
);
}
}
#[test]
fn the_scan_keeps_the_production_half_of_each_file() {
for (name, source) in gesture_sources() {
let production = production_only(&source);
assert!(
production.contains("// SPDX-License-Identifier"),
"{name}: the test-block stripper ate the file header"
);
assert!(
code_only(&production).contains("use "),
"{name}: the comment stripper ate the code"
);
if source.contains("pub struct") {
assert!(
production.contains("pub struct"),
"{name}: the test-block stripper ate the production types"
);
}
}
}
#[test]
fn the_source_scan_sees_every_recognizer() {
let names: Vec<String> = gesture_sources().into_iter().map(|(n, _)| n).collect();
for expected in [
"gesture.rs",
"arena.rs",
"arena_set.rs",
"config.rs",
"drag.rs",
"long_press.rs",
"multi_tap.rs",
"palm.rs",
"pan.rs",
"pinch.rs",
"swipe.rs",
"tap.rs",
] {
assert!(
names.iter().any(|n| n == expected),
"the wall-clock scan missed {expected}; it saw {names:?}"
);
}
}
}