#![forbid(unsafe_code)]
use web_time::{Duration, Instant};
use crate::event::{Event, KeyCode, KeyEventKind, Modifiers, MouseButton, MouseEventKind};
use crate::semantic_event::{ChordKey, Position, SemanticEvent, SwipeDirection};
#[derive(Debug, Clone)]
pub struct GestureConfig {
pub multi_click_timeout: Duration,
pub long_press_threshold: Duration,
pub drag_threshold: u16,
pub chord_timeout: Duration,
pub swipe_velocity_threshold: f32,
pub click_tolerance: u16,
}
impl Default for GestureConfig {
fn default() -> Self {
Self {
multi_click_timeout: Duration::from_millis(300),
long_press_threshold: Duration::from_millis(500),
drag_threshold: 3,
chord_timeout: Duration::from_millis(1000),
swipe_velocity_threshold: 50.0,
click_tolerance: 1,
}
}
}
#[derive(Debug, Clone)]
struct ClickState {
pos: Position,
button: MouseButton,
time: Instant,
count: u8,
}
#[derive(Debug, Clone)]
struct DragTracker {
start_pos: Position,
button: MouseButton,
last_pos: Position,
started: bool,
first_move: Option<Instant>,
}
pub struct GestureRecognizer {
config: GestureConfig,
last_click: Option<ClickState>,
mouse_down: Option<(Position, MouseButton, Instant)>,
drag: Option<DragTracker>,
long_press_pos: Option<(Position, Instant)>,
long_press_fired: bool,
chord_buffer: Vec<ChordKey>,
chord_start: Option<Instant>,
}
impl std::fmt::Debug for GestureRecognizer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GestureRecognizer")
.field("dragging", &self.is_dragging())
.field("chord_len", &self.chord_buffer.len())
.finish()
}
}
impl GestureRecognizer {
#[must_use]
pub fn new(config: GestureConfig) -> Self {
Self {
config,
last_click: None,
mouse_down: None,
drag: None,
long_press_pos: None,
long_press_fired: false,
chord_buffer: Vec::with_capacity(4),
chord_start: None,
}
}
#[inline]
pub fn process(&mut self, event: &Event, now: Instant) -> Vec<SemanticEvent> {
let mut out = Vec::with_capacity(2);
self.expire_chord(now);
match event {
Event::Mouse(mouse) => {
let pos = Position::new(mouse.x, mouse.y);
match mouse.kind {
MouseEventKind::Down(button) => {
self.on_mouse_down(pos, button, now, &mut out);
}
MouseEventKind::Up(button) => {
self.on_mouse_up(pos, button, now, &mut out);
}
MouseEventKind::Drag(button) => {
self.on_mouse_drag(pos, button, now, &mut out);
}
MouseEventKind::Moved => {
self.long_press_pos = None;
}
_ => {}
}
}
Event::Key(key) => {
if key.kind != KeyEventKind::Press {
return out;
}
if key.code == KeyCode::Escape {
if let Some(drag) = self.drag.take()
&& drag.started
{
out.push(SemanticEvent::DragCancel);
}
self.mouse_down = None;
self.long_press_pos = None;
self.chord_buffer.clear();
self.chord_start = None;
return out;
}
let has_modifier = key
.modifiers
.intersects(Modifiers::CTRL | Modifiers::ALT | Modifiers::SUPER);
if has_modifier {
let chord_key = ChordKey::new(key.code, key.modifiers);
if self.chord_buffer.is_empty() {
self.chord_start = Some(now);
}
self.chord_buffer.push(chord_key);
if self.chord_buffer.len() >= 2 {
out.push(SemanticEvent::Chord {
sequence: self.chord_buffer.clone(),
});
self.chord_buffer.clear();
self.chord_start = None;
}
} else {
self.chord_buffer.clear();
self.chord_start = None;
}
}
Event::Focus(false) => {
if let Some(drag) = self.drag.take()
&& drag.started
{
out.push(SemanticEvent::DragCancel);
}
self.mouse_down = None;
self.long_press_pos = None;
self.long_press_fired = false;
}
_ => {}
}
out
}
#[inline]
pub fn check_long_press(&mut self, now: Instant) -> Option<SemanticEvent> {
if self.long_press_fired {
return None;
}
if let Some((pos, down_time)) = self.long_press_pos {
let elapsed = now.saturating_duration_since(down_time);
if elapsed >= self.config.long_press_threshold {
self.long_press_fired = true;
return Some(SemanticEvent::LongPress {
pos,
duration: elapsed,
});
}
}
None
}
#[inline]
#[must_use]
pub fn is_dragging(&self) -> bool {
self.drag.as_ref().is_some_and(|d| d.started)
}
pub fn reset(&mut self) {
self.last_click = None;
self.mouse_down = None;
self.drag = None;
self.long_press_pos = None;
self.long_press_fired = false;
self.chord_buffer.clear();
self.chord_start = None;
}
#[inline]
#[must_use]
pub fn config(&self) -> &GestureConfig {
&self.config
}
pub fn set_config(&mut self, config: GestureConfig) {
self.config = config;
}
}
impl GestureRecognizer {
fn on_mouse_down(
&mut self,
pos: Position,
button: MouseButton,
now: Instant,
_out: &mut Vec<SemanticEvent>,
) {
self.mouse_down = Some((pos, button, now));
self.drag = Some(DragTracker {
start_pos: pos,
button,
last_pos: pos,
started: false,
first_move: None,
});
self.long_press_pos = Some((pos, now));
self.long_press_fired = false;
}
fn on_mouse_up(
&mut self,
pos: Position,
button: MouseButton,
now: Instant,
out: &mut Vec<SemanticEvent>,
) {
self.long_press_pos = None;
self.long_press_fired = false;
if let Some(drag) = self.drag.take()
&& drag.started
{
out.push(SemanticEvent::DragEnd {
start: drag.start_pos,
end: pos,
});
if let Some(swipe) = self.classify_swipe(&drag, pos, now) {
out.push(swipe);
}
self.mouse_down = None;
return;
}
let pressed = self.mouse_down.take();
if !matches!(pressed, Some((_, down_button, _)) if down_button == button) {
return;
}
let click_count = if let Some(ref last) = self.last_click {
if last.button == button
&& last.pos.manhattan_distance(pos) <= u32::from(self.config.click_tolerance)
&& now.saturating_duration_since(last.time) <= self.config.multi_click_timeout
&& last.count < 3
{
last.count + 1
} else {
1
}
} else {
1
};
self.last_click = Some(ClickState {
pos,
button,
time: now,
count: click_count,
});
match click_count {
1 => out.push(SemanticEvent::Click { pos, button }),
2 => out.push(SemanticEvent::DoubleClick { pos, button }),
3 => out.push(SemanticEvent::TripleClick { pos, button }),
_ => out.push(SemanticEvent::Click { pos, button }),
}
}
fn on_mouse_drag(
&mut self,
pos: Position,
button: MouseButton,
now: Instant,
out: &mut Vec<SemanticEvent>,
) {
self.long_press_pos = None;
let Some(ref mut drag) = self.drag else {
self.drag = Some(DragTracker {
start_pos: pos,
button,
last_pos: pos,
started: false,
first_move: Some(now),
});
return;
};
drag.first_move.get_or_insert(now);
if !drag.started {
let distance = drag.start_pos.manhattan_distance(pos);
if distance >= u32::from(self.config.drag_threshold) {
drag.started = true;
out.push(SemanticEvent::DragStart {
pos: drag.start_pos,
button: drag.button,
});
}
}
if drag.started {
let axis = |now: u16, before: u16| {
let d = i32::from(now) - i32::from(before);
i16::try_from(d).unwrap_or(if d < 0 { i16::MIN } else { i16::MAX })
};
let delta = (axis(pos.x, drag.last_pos.x), axis(pos.y, drag.last_pos.y));
out.push(SemanticEvent::DragMove {
start: drag.start_pos,
current: pos,
delta,
});
}
drag.last_pos = pos;
}
fn classify_swipe(
&self,
drag: &DragTracker,
end: Position,
now: Instant,
) -> Option<SemanticEvent> {
let dx = i32::from(end.x) - i32::from(drag.start_pos.x);
let dy = i32::from(end.y) - i32::from(drag.start_pos.y);
let direction = match dx.abs().cmp(&dy.abs()) {
std::cmp::Ordering::Greater if dx > 0 => SwipeDirection::Right,
std::cmp::Ordering::Greater => SwipeDirection::Left,
std::cmp::Ordering::Less if dy > 0 => SwipeDirection::Down,
std::cmp::Ordering::Less => SwipeDirection::Up,
std::cmp::Ordering::Equal => return None,
};
let distance = u16::try_from(dx.abs().max(dy.abs())).unwrap_or(u16::MAX);
if distance < self.config.drag_threshold {
return None;
}
let elapsed = now
.saturating_duration_since(drag.first_move?)
.max(Duration::from_millis(1));
let velocity = f32::from(distance) / elapsed.as_secs_f32();
(velocity >= self.config.swipe_velocity_threshold).then_some(SemanticEvent::Swipe {
direction,
distance,
velocity,
})
}
fn expire_chord(&mut self, now: Instant) {
if let Some(start) = self.chord_start
&& now.saturating_duration_since(start) > self.config.chord_timeout
{
self.chord_buffer.clear();
self.chord_start = None;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{KeyEvent, MouseEvent};
fn now() -> Instant {
Instant::now()
}
fn mouse_down(x: u16, y: u16, button: MouseButton) -> Event {
Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(button),
x,
y,
modifiers: Modifiers::NONE,
})
}
fn mouse_up(x: u16, y: u16, button: MouseButton) -> Event {
Event::Mouse(MouseEvent {
kind: MouseEventKind::Up(button),
x,
y,
modifiers: Modifiers::NONE,
})
}
fn mouse_drag(x: u16, y: u16, button: MouseButton) -> Event {
Event::Mouse(MouseEvent {
kind: MouseEventKind::Drag(button),
x,
y,
modifiers: Modifiers::NONE,
})
}
fn key_press(code: KeyCode, modifiers: Modifiers) -> Event {
Event::Key(KeyEvent {
code,
modifiers,
kind: KeyEventKind::Press,
})
}
fn esc() -> Event {
key_press(KeyCode::Escape, Modifiers::NONE)
}
const MS_50: Duration = Duration::from_millis(50);
const MS_100: Duration = Duration::from_millis(100);
const MS_200: Duration = Duration::from_millis(200);
const MS_500: Duration = Duration::from_millis(500);
const MS_600: Duration = Duration::from_millis(600);
#[test]
fn single_click() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
let events = gr.process(&mouse_down(5, 5, MouseButton::Left), t);
assert!(events.is_empty());
let events = gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
assert_eq!(events.len(), 1);
assert!(matches!(
events[0],
SemanticEvent::Click {
pos: Position { x: 5, y: 5 },
button: MouseButton::Left,
}
));
}
#[test]
fn double_click() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
gr.process(&mouse_down(5, 5, MouseButton::Left), t + MS_100);
let events = gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_200);
assert_eq!(events.len(), 1);
assert!(matches!(events[0], SemanticEvent::DoubleClick { .. }));
}
#[test]
fn triple_click() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
gr.process(&mouse_down(5, 5, MouseButton::Left), t + MS_100);
gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_200);
gr.process(
&mouse_down(5, 5, MouseButton::Left),
t + Duration::from_millis(250),
);
let events = gr.process(
&mouse_up(5, 5, MouseButton::Left),
t + Duration::from_millis(280),
);
assert_eq!(events.len(), 1);
assert!(matches!(events[0], SemanticEvent::TripleClick { .. }));
}
#[test]
fn double_click_timeout_resets_to_single() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
gr.process(&mouse_down(5, 5, MouseButton::Left), t + MS_500);
let events = gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_600);
assert_eq!(events.len(), 1);
assert!(matches!(events[0], SemanticEvent::Click { .. }));
}
#[test]
fn different_position_resets_click_count() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
gr.process(&mouse_down(20, 20, MouseButton::Left), t + MS_100);
let events = gr.process(&mouse_up(20, 20, MouseButton::Left), t + MS_200);
assert_eq!(events.len(), 1);
assert!(matches!(events[0], SemanticEvent::Click { .. }));
}
#[test]
fn different_button_resets_click_count() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
gr.process(&mouse_down(5, 5, MouseButton::Right), t + MS_100);
let events = gr.process(&mouse_up(5, 5, MouseButton::Right), t + MS_200);
assert_eq!(events.len(), 1);
assert!(matches!(
events[0],
SemanticEvent::Click {
button: MouseButton::Right,
..
}
));
}
#[test]
fn click_position_tolerance() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
gr.process(&mouse_down(6, 5, MouseButton::Left), t + MS_100);
let events = gr.process(&mouse_up(6, 5, MouseButton::Left), t + MS_200);
assert_eq!(events.len(), 1);
assert!(matches!(events[0], SemanticEvent::DoubleClick { .. }));
}
#[test]
fn drag_starts_after_threshold() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
let events = gr.process(&mouse_drag(6, 5, MouseButton::Left), t + MS_50);
assert!(events.is_empty());
assert!(!gr.is_dragging());
let events = gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_100);
assert!(!events.is_empty());
assert!(
events
.iter()
.any(|e| matches!(e, SemanticEvent::DragStart { .. }))
);
assert!(gr.is_dragging());
}
#[test]
fn drag_move_has_correct_delta() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
let events = gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
let drag_move = events
.iter()
.find(|e| matches!(e, SemanticEvent::DragMove { .. }));
assert!(drag_move.is_some());
if let Some(SemanticEvent::DragMove {
start,
current,
delta,
}) = drag_move
{
assert_eq!(*start, Position::new(5, 5));
assert_eq!(*current, Position::new(10, 5));
assert_eq!(*delta, (5, 0));
}
}
#[test]
fn drag_end_on_mouse_up() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
let events = gr.process(&mouse_up(12, 5, MouseButton::Left), t + MS_100);
assert_eq!(events.len(), 2);
assert!(matches!(
events[0],
SemanticEvent::DragEnd {
start: Position { x: 5, y: 5 },
end: Position { x: 12, y: 5 },
}
));
match events[1] {
SemanticEvent::Swipe {
direction,
distance,
velocity,
} => {
assert_eq!(direction, SwipeDirection::Right);
assert_eq!(distance, 7);
assert!((velocity - 140.0).abs() < 1e-3, "velocity {velocity}");
}
ref other => panic!("expected Swipe, got {other:?}"),
}
assert!(!gr.is_dragging());
}
#[test]
fn drag_cancel_on_escape() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
assert!(gr.is_dragging());
let events = gr.process(&esc(), t + MS_100);
assert_eq!(events.len(), 1);
assert!(matches!(events[0], SemanticEvent::DragCancel));
assert!(!gr.is_dragging());
}
#[test]
fn drag_prevents_click() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
let events = gr.process(&mouse_up(10, 5, MouseButton::Left), t + MS_100);
assert!(matches!(events[0], SemanticEvent::DragEnd { .. }));
assert!(
!events
.iter()
.any(|e| matches!(e, SemanticEvent::Click { .. }))
);
}
#[test]
fn long_press_fires_after_threshold() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
assert!(gr.check_long_press(t + MS_200).is_none());
let lp = gr.check_long_press(t + MS_600);
assert!(lp.is_some());
if let Some(SemanticEvent::LongPress { pos, duration }) = lp {
assert_eq!(pos, Position::new(5, 5));
assert!(duration >= Duration::from_millis(500));
}
}
#[test]
fn long_press_not_repeated() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
assert!(gr.check_long_press(t + MS_600).is_some());
assert!(
gr.check_long_press(t + Duration::from_millis(700))
.is_none()
);
}
#[test]
fn drag_cancels_long_press() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_drag(6, 5, MouseButton::Left), t + MS_100);
assert!(gr.check_long_press(t + MS_600).is_none());
}
#[test]
fn mouse_up_cancels_long_press() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_100);
assert!(gr.check_long_press(t + MS_600).is_none());
}
#[test]
fn two_key_chord() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
let events1 = gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
assert!(events1.is_empty());
let events2 = gr.process(&key_press(KeyCode::Char('c'), Modifiers::CTRL), t + MS_100);
assert_eq!(events2.len(), 1);
if let SemanticEvent::Chord { sequence } = &events2[0] {
assert_eq!(sequence.len(), 2);
assert_eq!(sequence[0].code, KeyCode::Char('k'));
assert_eq!(sequence[1].code, KeyCode::Char('c'));
} else {
panic!("Expected Chord event");
}
}
#[test]
fn chord_timeout_clears_buffer() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
let events = gr.process(
&key_press(KeyCode::Char('c'), Modifiers::CTRL),
t + Duration::from_millis(1100),
);
assert!(events.is_empty());
}
#[test]
fn non_modifier_key_clears_chord() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
gr.process(&key_press(KeyCode::Char('x'), Modifiers::NONE), t + MS_50);
let events = gr.process(&key_press(KeyCode::Char('c'), Modifiers::CTRL), t + MS_100);
assert!(events.is_empty()); }
#[test]
fn escape_clears_chord() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
gr.process(&esc(), t + MS_50);
let events = gr.process(&key_press(KeyCode::Char('c'), Modifiers::CTRL), t + MS_100);
assert!(events.is_empty());
}
#[test]
fn focus_loss_cancels_drag() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
assert!(gr.is_dragging());
let events = gr.process(&Event::Focus(false), t + MS_100);
assert_eq!(events.len(), 1);
assert!(matches!(events[0], SemanticEvent::DragCancel));
assert!(!gr.is_dragging());
}
#[test]
fn focus_loss_without_drag_is_silent() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
let events = gr.process(&Event::Focus(false), t);
assert!(events.is_empty());
}
#[test]
fn reset_clears_all_state() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t + MS_100);
assert!(gr.is_dragging());
gr.reset();
assert!(!gr.is_dragging());
assert!(gr.last_click.is_none());
assert!(gr.mouse_down.is_none());
assert!(gr.drag.is_none());
assert!(gr.chord_buffer.is_empty());
assert!(gr.chord_start.is_none());
}
#[test]
fn quadruple_click_wraps_to_single() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
for i in 0..3u32 {
let offset = Duration::from_millis(i as u64 * 80);
gr.process(&mouse_down(5, 5, MouseButton::Left), t + offset);
gr.process(&mouse_up(5, 5, MouseButton::Left), t + offset + MS_50);
}
gr.process(
&mouse_down(5, 5, MouseButton::Left),
t + Duration::from_millis(260),
);
let events = gr.process(
&mouse_up(5, 5, MouseButton::Left),
t + Duration::from_millis(280),
);
assert_eq!(events.len(), 1);
assert!(matches!(events[0], SemanticEvent::Click { .. }));
}
#[test]
fn key_release_ignored() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
let events = gr.process(
&Event::Key(KeyEvent {
code: KeyCode::Char('k'),
modifiers: Modifiers::CTRL,
kind: KeyEventKind::Release,
}),
t,
);
assert!(events.is_empty());
}
#[test]
fn debug_format() {
let gr = GestureRecognizer::new(GestureConfig::default());
let dbg = format!("{:?}", gr);
assert!(dbg.contains("GestureRecognizer"));
}
#[test]
fn right_click() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(10, 10, MouseButton::Right), t);
let events = gr.process(&mouse_up(10, 10, MouseButton::Right), t + MS_50);
assert_eq!(events.len(), 1);
assert!(matches!(
events[0],
SemanticEvent::Click {
button: MouseButton::Right,
..
}
));
}
#[test]
fn middle_click() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(10, 10, MouseButton::Middle), t);
let events = gr.process(&mouse_up(10, 10, MouseButton::Middle), t + MS_50);
assert_eq!(events.len(), 1);
assert!(matches!(
events[0],
SemanticEvent::Click {
button: MouseButton::Middle,
..
}
));
}
#[test]
fn click_at_origin() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(0, 0, MouseButton::Left), t);
let events = gr.process(&mouse_up(0, 0, MouseButton::Left), t + MS_50);
assert_eq!(events.len(), 1);
if let SemanticEvent::Click { pos, .. } = &events[0] {
assert_eq!(pos.x, 0);
assert_eq!(pos.y, 0);
}
}
#[test]
fn click_at_max_position() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(u16::MAX, u16::MAX, MouseButton::Left), t);
let events = gr.process(&mouse_up(u16::MAX, u16::MAX, MouseButton::Left), t + MS_50);
assert_eq!(events.len(), 1);
if let SemanticEvent::Click { pos, .. } = &events[0] {
assert_eq!(pos.x, u16::MAX);
assert_eq!(pos.y, u16::MAX);
}
}
#[test]
fn double_click_right_button() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Right), t);
gr.process(&mouse_up(5, 5, MouseButton::Right), t + MS_50);
gr.process(&mouse_down(5, 5, MouseButton::Right), t + MS_100);
let events = gr.process(&mouse_up(5, 5, MouseButton::Right), t + MS_200);
assert_eq!(events.len(), 1);
assert!(matches!(
events[0],
SemanticEvent::DoubleClick {
button: MouseButton::Right,
..
}
));
}
#[test]
fn click_position_beyond_tolerance() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
gr.process(&mouse_down(8, 5, MouseButton::Left), t + MS_100);
let events = gr.process(&mouse_up(8, 5, MouseButton::Left), t + MS_200);
assert_eq!(events.len(), 1);
assert!(matches!(events[0], SemanticEvent::Click { .. }));
}
#[test]
fn drag_with_right_button() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Right), t);
let events = gr.process(&mouse_drag(10, 5, MouseButton::Right), t + MS_50);
assert!(events.iter().any(|e| matches!(
e,
SemanticEvent::DragStart {
button: MouseButton::Right,
..
}
)));
}
#[test]
fn drag_vertical() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
let events = gr.process(&mouse_drag(5, 10, MouseButton::Left), t + MS_50);
assert!(
events
.iter()
.any(|e| matches!(e, SemanticEvent::DragStart { .. }))
);
let drag_move = events
.iter()
.find(|e| matches!(e, SemanticEvent::DragMove { .. }));
if let Some(SemanticEvent::DragMove { delta, .. }) = drag_move {
assert_eq!(delta.0, 0); assert_eq!(delta.1, 5); }
}
#[test]
fn drag_multiple_moves() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
let events = gr.process(&mouse_drag(15, 5, MouseButton::Left), t + MS_100);
let drag_move = events
.iter()
.find(|e| matches!(e, SemanticEvent::DragMove { .. }));
if let Some(SemanticEvent::DragMove {
start,
current,
delta,
}) = drag_move
{
assert_eq!(*start, Position::new(5, 5));
assert_eq!(*current, Position::new(15, 5));
assert_eq!(*delta, (5, 0)); }
}
#[test]
fn drag_threshold_exactly_met() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
let events = gr.process(&mouse_drag(8, 5, MouseButton::Left), t + MS_50);
assert!(
events
.iter()
.any(|e| matches!(e, SemanticEvent::DragStart { .. }))
);
}
#[test]
fn drag_threshold_one_below() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
let events = gr.process(&mouse_drag(7, 5, MouseButton::Left), t + MS_50);
assert!(
!events
.iter()
.any(|e| matches!(e, SemanticEvent::DragStart { .. }))
);
assert!(!gr.is_dragging());
}
#[test]
fn drag_state_reset_after_end() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
gr.process(&mouse_up(10, 5, MouseButton::Left), t + MS_100);
assert!(!gr.is_dragging());
gr.process(&mouse_down(20, 20, MouseButton::Left), t + MS_200);
let events = gr.process(
&mouse_up(20, 20, MouseButton::Left),
t + Duration::from_millis(250),
);
assert_eq!(events.len(), 1);
assert!(matches!(events[0], SemanticEvent::Click { .. }));
}
#[test]
fn no_click_after_drag_cancel() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
gr.process(&esc(), t + MS_100);
let events = gr.process(&mouse_up(10, 5, MouseButton::Left), t + MS_200);
assert!(
!events
.iter()
.any(|e| matches!(e, SemanticEvent::DragEnd { .. }))
);
}
#[test]
fn long_press_correct_position() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(42, 17, MouseButton::Left), t);
let lp = gr.check_long_press(t + MS_600);
if let Some(SemanticEvent::LongPress { pos, .. }) = lp {
assert_eq!(pos, Position::new(42, 17));
} else {
panic!("Expected LongPress");
}
}
#[test]
fn long_press_with_custom_threshold() {
let config = GestureConfig {
long_press_threshold: Duration::from_millis(200),
..Default::default()
};
let mut gr = GestureRecognizer::new(config);
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
assert!(
gr.check_long_press(t + Duration::from_millis(150))
.is_none()
);
assert!(
gr.check_long_press(t + Duration::from_millis(250))
.is_some()
);
}
#[test]
fn long_press_resets_on_new_mouse_down() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_100);
gr.process(&mouse_down(10, 10, MouseButton::Left), t + MS_200);
assert!(gr.check_long_press(t + MS_600).is_none());
assert!(
gr.check_long_press(t + Duration::from_millis(750))
.is_some()
);
}
#[test]
fn alt_key_chord() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&key_press(KeyCode::Char('x'), Modifiers::ALT), t);
let events = gr.process(&key_press(KeyCode::Char('y'), Modifiers::ALT), t + MS_100);
assert_eq!(events.len(), 1);
if let SemanticEvent::Chord { sequence } = &events[0] {
assert_eq!(sequence.len(), 2);
assert!(sequence[0].modifiers.contains(Modifiers::ALT));
}
}
#[test]
fn mixed_modifier_chord() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
let events = gr.process(&key_press(KeyCode::Char('d'), Modifiers::ALT), t + MS_100);
assert_eq!(events.len(), 1);
if let SemanticEvent::Chord { sequence } = &events[0] {
assert_eq!(sequence[0].modifiers, Modifiers::CTRL);
assert_eq!(sequence[1].modifiers, Modifiers::ALT);
}
}
#[test]
fn chord_with_function_key() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
let events = gr.process(&key_press(KeyCode::F(1), Modifiers::CTRL), t + MS_100);
assert_eq!(events.len(), 1);
if let SemanticEvent::Chord { sequence } = &events[0] {
assert_eq!(sequence[1].code, KeyCode::F(1));
}
}
#[test]
fn single_modifier_key_no_chord_emitted() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
let events = gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
assert!(events.is_empty());
}
#[test]
fn custom_click_tolerance() {
let config = GestureConfig {
click_tolerance: 5,
..Default::default()
};
let mut gr = GestureRecognizer::new(config);
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
gr.process(&mouse_down(10, 5, MouseButton::Left), t + MS_100);
let events = gr.process(&mouse_up(10, 5, MouseButton::Left), t + MS_200);
assert!(matches!(events[0], SemanticEvent::DoubleClick { .. }));
}
#[test]
fn custom_drag_threshold() {
let config = GestureConfig {
drag_threshold: 10,
..Default::default()
};
let mut gr = GestureRecognizer::new(config);
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
let events = gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
assert!(!gr.is_dragging());
assert!(events.is_empty());
let events = gr.process(&mouse_drag(15, 5, MouseButton::Left), t + MS_100);
assert!(gr.is_dragging());
assert!(
events
.iter()
.any(|e| matches!(e, SemanticEvent::DragStart { .. }))
);
}
#[test]
fn custom_multi_click_timeout() {
let config = GestureConfig {
multi_click_timeout: Duration::from_millis(100),
..Default::default()
};
let mut gr = GestureRecognizer::new(config);
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
gr.process(
&mouse_down(5, 5, MouseButton::Left),
t + Duration::from_millis(150),
);
let events = gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_200);
assert!(matches!(events[0], SemanticEvent::Click { .. }));
}
#[test]
fn config_getter_and_setter() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
assert_eq!(gr.config().drag_threshold, 3);
let new_config = GestureConfig {
drag_threshold: 10,
..Default::default()
};
gr.set_config(new_config);
assert_eq!(gr.config().drag_threshold, 10);
}
#[test]
fn click_then_drag_are_independent() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
let click_events = gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
assert!(matches!(click_events[0], SemanticEvent::Click { .. }));
gr.process(&mouse_down(5, 5, MouseButton::Left), t + MS_200);
let drag_events = gr.process(
&mouse_drag(10, 5, MouseButton::Left),
t + Duration::from_millis(250),
);
assert!(
drag_events
.iter()
.any(|e| matches!(e, SemanticEvent::DragStart { .. }))
);
let end_events = gr.process(
&mouse_up(10, 5, MouseButton::Left),
t + Duration::from_millis(300),
);
assert!(matches!(end_events[0], SemanticEvent::DragEnd { .. }));
}
#[test]
fn interleaved_mouse_and_keyboard() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
gr.process(&mouse_down(5, 5, MouseButton::Left), t + MS_50);
gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_100);
let events = gr.process(&key_press(KeyCode::Char('c'), Modifiers::CTRL), t + MS_200);
assert_eq!(events.len(), 1);
assert!(matches!(events[0], SemanticEvent::Chord { .. }));
}
#[test]
fn rapid_clicks_produce_correct_sequence() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
let mut results = Vec::new();
for i in 0..5u32 {
let offset = Duration::from_millis(i as u64 * 60);
gr.process(&mouse_down(5, 5, MouseButton::Left), t + offset);
let events = gr.process(
&mouse_up(5, 5, MouseButton::Left),
t + offset + Duration::from_millis(30),
);
results.extend(events);
}
assert!(results.len() == 5);
assert!(matches!(results[0], SemanticEvent::Click { .. }));
assert!(matches!(results[1], SemanticEvent::DoubleClick { .. }));
assert!(matches!(results[2], SemanticEvent::TripleClick { .. }));
assert!(matches!(results[3], SemanticEvent::Click { .. }));
assert!(matches!(results[4], SemanticEvent::DoubleClick { .. }));
}
#[test]
fn tick_event_ignored() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
let events = gr.process(&Event::Tick, t);
assert!(events.is_empty());
}
#[test]
fn resize_event_ignored() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
let events = gr.process(
&Event::Resize {
width: 80,
height: 24,
},
t,
);
assert!(events.is_empty());
}
#[test]
fn focus_gain_ignored() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
let events = gr.process(&Event::Focus(true), t);
assert!(events.is_empty());
}
#[test]
fn default_config_values() {
let config = GestureConfig::default();
assert_eq!(config.multi_click_timeout, Duration::from_millis(300));
assert_eq!(config.long_press_threshold, Duration::from_millis(500));
assert_eq!(config.drag_threshold, 3);
assert_eq!(config.chord_timeout, Duration::from_millis(1000));
assert_eq!(config.click_tolerance, 1);
}
fn mouse_moved(x: u16, y: u16) -> Event {
Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
x,
y,
modifiers: Modifiers::NONE,
})
}
fn mouse_scroll_up(x: u16, y: u16) -> Event {
Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollUp,
x,
y,
modifiers: Modifiers::NONE,
})
}
fn mouse_scroll_down(x: u16, y: u16) -> Event {
Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollDown,
x,
y,
modifiers: Modifiers::NONE,
})
}
#[test]
fn drag_without_prior_mouse_down() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
let events = gr.process(&mouse_drag(10, 10, MouseButton::Left), t);
assert!(events.is_empty()); assert!(!gr.is_dragging());
let events = gr.process(&mouse_drag(20, 10, MouseButton::Left), t + MS_50);
assert!(
events
.iter()
.any(|e| matches!(e, SemanticEvent::DragStart { .. }))
);
assert!(gr.is_dragging());
}
#[test]
fn mouse_moved_cancels_long_press() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_moved(6, 5), t + MS_100);
assert!(gr.check_long_press(t + MS_600).is_none());
}
#[test]
fn mouse_moved_does_not_affect_drag_tracker() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
let events = gr.process(&mouse_moved(20, 20), t + MS_50);
assert!(events.is_empty());
let events = gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_100);
assert!(
events
.iter()
.any(|e| matches!(e, SemanticEvent::DragStart { .. }))
);
}
#[test]
fn escape_during_unstarted_drag_no_cancel() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_drag(6, 5, MouseButton::Left), t + MS_50);
assert!(!gr.is_dragging());
let events = gr.process(&esc(), t + MS_100);
assert!(events.is_empty()); }
#[test]
fn focus_loss_during_unstarted_drag_no_cancel() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_drag(6, 5, MouseButton::Left), t + MS_50);
assert!(!gr.is_dragging());
let events = gr.process(&Event::Focus(false), t + MS_100);
assert!(events.is_empty()); }
#[test]
fn chord_with_super_modifier() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&key_press(KeyCode::Char('a'), Modifiers::SUPER), t);
let events = gr.process(&key_press(KeyCode::Char('b'), Modifiers::SUPER), t + MS_100);
assert_eq!(events.len(), 1);
if let SemanticEvent::Chord { sequence } = &events[0] {
assert_eq!(sequence.len(), 2);
assert!(sequence[0].modifiers.contains(Modifiers::SUPER));
assert!(sequence[1].modifiers.contains(Modifiers::SUPER));
} else {
panic!("Expected Chord event");
}
}
#[test]
fn long_press_exactly_at_threshold() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
let lp = gr.check_long_press(t + Duration::from_millis(500));
assert!(lp.is_some());
if let Some(SemanticEvent::LongPress { duration, .. }) = lp {
assert_eq!(duration, Duration::from_millis(500));
}
}
#[test]
fn long_press_one_ms_before_threshold() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
assert!(
gr.check_long_press(t + Duration::from_millis(499))
.is_none()
);
}
#[test]
fn drag_negative_direction() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(20, 10, MouseButton::Left), t);
let events = gr.process(&mouse_drag(15, 10, MouseButton::Left), t + MS_50);
let drag_move = events
.iter()
.find(|e| matches!(e, SemanticEvent::DragMove { .. }));
if let Some(SemanticEvent::DragMove { delta, .. }) = drag_move {
assert_eq!(delta.0, -5); assert_eq!(delta.1, 0);
} else {
panic!("Expected DragMove");
}
}
#[test]
fn drag_negative_y_direction() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 20, MouseButton::Left), t);
let events = gr.process(&mouse_drag(5, 15, MouseButton::Left), t + MS_50);
let drag_move = events
.iter()
.find(|e| matches!(e, SemanticEvent::DragMove { .. }));
if let Some(SemanticEvent::DragMove { delta, .. }) = drag_move {
assert_eq!(delta.0, 0);
assert_eq!(delta.1, -5); } else {
panic!("Expected DragMove");
}
}
#[test]
fn multiple_sequential_drags() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
gr.process(&mouse_up(10, 5, MouseButton::Left), t + MS_100);
assert!(!gr.is_dragging());
gr.process(&mouse_down(20, 20, MouseButton::Left), t + MS_200);
let events = gr.process(
&mouse_drag(30, 20, MouseButton::Left),
t + Duration::from_millis(250),
);
assert!(events.iter().any(|e| matches!(
e,
SemanticEvent::DragStart {
pos: Position { x: 20, y: 20 },
..
}
)));
assert!(gr.is_dragging());
let events = gr.process(
&mouse_up(30, 20, MouseButton::Left),
t + Duration::from_millis(300),
);
assert!(matches!(
events[0],
SemanticEvent::DragEnd {
start: Position { x: 20, y: 20 },
end: Position { x: 30, y: 20 },
}
));
}
#[test]
fn check_long_press_no_mouse_down() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
assert!(gr.check_long_press(t).is_none());
assert!(gr.check_long_press(t + Duration::from_secs(10)).is_none());
}
#[test]
fn zero_drag_threshold() {
let config = GestureConfig {
drag_threshold: 0,
..Default::default()
};
let mut gr = GestureRecognizer::new(config);
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
let events = gr.process(&mouse_drag(5, 5, MouseButton::Left), t + MS_50);
assert!(
events
.iter()
.any(|e| matches!(e, SemanticEvent::DragStart { .. }))
);
assert!(gr.is_dragging());
}
#[test]
fn zero_click_tolerance() {
let config = GestureConfig {
click_tolerance: 0,
..Default::default()
};
let mut gr = GestureRecognizer::new(config);
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
gr.process(&mouse_down(6, 5, MouseButton::Left), t + MS_100);
let events = gr.process(&mouse_up(6, 5, MouseButton::Left), t + MS_200);
assert!(matches!(events[0], SemanticEvent::Click { .. }));
gr.process(
&mouse_down(6, 5, MouseButton::Left),
t + Duration::from_millis(250),
);
let events = gr.process(
&mouse_up(6, 5, MouseButton::Left),
t + Duration::from_millis(280),
);
assert!(matches!(events[0], SemanticEvent::DoubleClick { .. }));
}
#[test]
fn scroll_events_ignored() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
let events = gr.process(&mouse_scroll_up(5, 5), t);
assert!(events.is_empty());
let events = gr.process(&mouse_scroll_down(5, 5), t + MS_50);
assert!(events.is_empty());
}
#[test]
fn key_repeat_ignored() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
let events = gr.process(
&Event::Key(KeyEvent {
code: KeyCode::Char('k'),
modifiers: Modifiers::CTRL,
kind: KeyEventKind::Repeat,
}),
t,
);
assert!(events.is_empty());
}
#[test]
fn double_click_at_exact_timeout_boundary() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
gr.process(
&mouse_down(5, 5, MouseButton::Left),
t + Duration::from_millis(350),
);
let events = gr.process(
&mouse_up(5, 5, MouseButton::Left),
t + Duration::from_millis(350),
);
assert!(matches!(events[0], SemanticEvent::DoubleClick { .. }));
}
#[test]
fn mouse_up_position_becomes_click_position() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
let events = gr.process(&mouse_up(6, 6, MouseButton::Left), t + MS_50);
assert_eq!(events.len(), 1);
if let SemanticEvent::Click { pos, .. } = &events[0] {
assert_eq!(*pos, Position::new(6, 6)); } else {
panic!("Expected Click");
}
}
#[test]
fn multiple_mouse_downs_overwrite_state() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_down(20, 20, MouseButton::Right), t + MS_50);
let events = gr.process(&mouse_drag(30, 20, MouseButton::Right), t + MS_100);
if let Some(SemanticEvent::DragStart { pos, button }) = events
.iter()
.find(|e| matches!(e, SemanticEvent::DragStart { .. }))
{
assert_eq!(*pos, Position::new(20, 20));
assert_eq!(*button, MouseButton::Right);
} else {
panic!("Expected DragStart from second mouse-down position");
}
}
#[test]
fn triple_click_then_timeout_then_single() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
for i in 0..3u32 {
let offset = Duration::from_millis(i as u64 * 80);
gr.process(&mouse_down(5, 5, MouseButton::Left), t + offset);
gr.process(&mouse_up(5, 5, MouseButton::Left), t + offset + MS_50);
}
let late = t + Duration::from_secs(2);
gr.process(&mouse_down(5, 5, MouseButton::Left), late);
let events = gr.process(&mouse_up(5, 5, MouseButton::Left), late + MS_50);
assert_eq!(events.len(), 1);
assert!(matches!(events[0], SemanticEvent::Click { .. }));
}
#[test]
fn chord_ctrl_alt_combined_modifier() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
let mods = Modifiers::CTRL | Modifiers::ALT;
gr.process(&key_press(KeyCode::Char('a'), mods), t);
let events = gr.process(&key_press(KeyCode::Char('b'), Modifiers::CTRL), t + MS_100);
assert_eq!(events.len(), 1);
if let SemanticEvent::Chord { sequence } = &events[0] {
assert_eq!(sequence[0].modifiers, Modifiers::CTRL | Modifiers::ALT);
assert_eq!(sequence[1].modifiers, Modifiers::CTRL);
}
}
#[test]
fn chord_timeout_then_new_chord() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&key_press(KeyCode::Char('k'), Modifiers::CTRL), t);
gr.process(
&key_press(KeyCode::Char('a'), Modifiers::CTRL),
t + Duration::from_millis(1100),
);
let events = gr.process(
&key_press(KeyCode::Char('b'), Modifiers::CTRL),
t + Duration::from_millis(1200),
);
assert_eq!(events.len(), 1);
if let SemanticEvent::Chord { sequence } = &events[0] {
assert_eq!(sequence.len(), 2);
assert_eq!(sequence[0].code, KeyCode::Char('a'));
assert_eq!(sequence[1].code, KeyCode::Char('b'));
}
}
#[test]
fn drag_start_and_move_emitted_together() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
let events = gr.process(&mouse_drag(10, 5, MouseButton::Left), t + MS_50);
assert_eq!(events.len(), 2);
assert!(matches!(events[0], SemanticEvent::DragStart { .. }));
assert!(matches!(events[1], SemanticEvent::DragMove { .. }));
}
#[test]
fn config_clone() {
let config = GestureConfig {
drag_threshold: 42,
click_tolerance: 7,
..Default::default()
};
let cloned = config.clone();
assert_eq!(cloned.drag_threshold, 42);
assert_eq!(cloned.click_tolerance, 7);
assert_eq!(cloned.multi_click_timeout, config.multi_click_timeout);
}
#[test]
fn scroll_does_not_affect_click_state() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50);
gr.process(&mouse_scroll_up(5, 5), t + MS_100);
gr.process(&mouse_down(5, 5, MouseButton::Left), t + MS_200);
let events = gr.process(
&mouse_up(5, 5, MouseButton::Left),
t + Duration::from_millis(250),
);
assert!(matches!(events[0], SemanticEvent::DoubleClick { .. }));
}
#[test]
fn escape_clears_mouse_down_state() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&esc(), t + MS_50);
assert!(gr.check_long_press(t + MS_600).is_none());
let events = gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_100);
assert!(events.is_empty(), "{events:?}");
gr.process(&mouse_down(5, 5, MouseButton::Left), t + MS_200);
let events = gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_200 + MS_50);
assert!(
matches!(events[..], [SemanticEvent::Click { .. }]),
"{events:?}"
);
}
#[test]
fn focus_loss_clears_long_press_fired_flag() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
assert!(gr.check_long_press(t + MS_600).is_some());
assert!(
gr.check_long_press(t + Duration::from_millis(700))
.is_none()
);
gr.process(&Event::Focus(false), t + Duration::from_millis(800));
gr.process(
&mouse_down(10, 10, MouseButton::Left),
t + Duration::from_secs(1),
);
assert!(
gr.check_long_press(t + Duration::from_millis(1600))
.is_some()
);
}
#[test]
fn drag_at_u16_max_coordinates() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(u16::MAX - 5, u16::MAX, MouseButton::Left), t);
let events = gr.process(
&mouse_drag(u16::MAX, u16::MAX, MouseButton::Left),
t + MS_50,
);
assert!(
events
.iter()
.any(|e| matches!(e, SemanticEvent::DragStart { .. }))
);
if let Some(SemanticEvent::DragMove { delta, .. }) = events
.iter()
.find(|e| matches!(e, SemanticEvent::DragMove { .. }))
{
assert_eq!(delta.0, 5);
assert_eq!(delta.1, 0);
}
}
#[test]
fn drag_delta_crosses_i16_max_and_saturates_huge_jumps() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
let delta_of = |events: Vec<SemanticEvent>| {
events.into_iter().find_map(|e| match e {
SemanticEvent::DragMove { delta, .. } => Some(delta),
_ => None,
})
};
gr.process(&mouse_down(32_760, 0, MouseButton::Left), t);
let started = gr.process(&mouse_drag(32_767, 0, MouseButton::Left), t + MS_50);
assert_eq!(delta_of(started), Some((7, 0)));
let crossed = gr.process(&mouse_drag(32_768, 0, MouseButton::Left), t + MS_50);
assert_eq!(delta_of(crossed), Some((1, 0)));
let back = gr.process(&mouse_drag(0, u16::MAX, MouseButton::Left), t + MS_50);
assert_eq!(delta_of(back), Some((i16::MIN, i16::MAX)));
}
#[test]
fn reset_during_long_press_pending() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.reset();
assert!(gr.check_long_press(t + MS_600).is_none());
}
#[test]
fn release_without_a_press_is_not_a_click() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
assert!(gr.process(&mouse_up(5, 5, MouseButton::Left), t).is_empty());
gr.process(&mouse_down(5, 5, MouseButton::Left), t);
gr.process(&Event::Focus(false), t + MS_50);
assert!(
gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_100)
.is_empty()
);
}
#[test]
fn release_of_another_button_is_not_a_click() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(5, 5, MouseButton::Right), t);
assert!(
gr.process(&mouse_up(5, 5, MouseButton::Left), t + MS_50)
.is_empty()
);
}
fn flick(
gr: &mut GestureRecognizer,
from: (u16, u16),
via: (u16, u16),
to: (u16, u16),
first_move: Duration,
release: Duration,
) -> Vec<SemanticEvent> {
let t = now();
gr.process(&mouse_down(from.0, from.1, MouseButton::Left), t);
gr.process(&mouse_drag(via.0, via.1, MouseButton::Left), t + first_move);
gr.process(&mouse_up(to.0, to.1, MouseButton::Left), t + release)
}
fn swipe_of(events: &[SemanticEvent]) -> Option<(SwipeDirection, u16, f32)> {
events.iter().find_map(|e| match *e {
SemanticEvent::Swipe {
direction,
distance,
velocity,
} => Some((direction, distance, velocity)),
_ => None,
})
}
#[test]
fn swipe_follows_drag_end_in_each_direction() {
let cases = [
((10, 10), (20, 10), SwipeDirection::Right),
((20, 10), (10, 10), SwipeDirection::Left),
((10, 20), (10, 10), SwipeDirection::Up),
((10, 10), (10, 20), SwipeDirection::Down),
];
for (from, to, want) in cases {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let events = flick(&mut gr, from, to, to, MS_50, MS_100);
assert!(
matches!(events[0], SemanticEvent::DragEnd { .. }),
"{events:?}"
);
let (direction, distance, velocity) = swipe_of(&events).expect("swipe");
assert_eq!((direction, distance), (want, 10));
assert!((velocity - 200.0).abs() < 1e-3, "velocity {velocity}");
assert_eq!(events.len(), 2);
}
}
#[test]
fn swipe_threshold_is_inclusive_and_read_from_config() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let at = flick(&mut gr, (0, 0), (5, 0), (5, 0), MS_50, MS_50 + MS_100);
assert_eq!(swipe_of(&at).map(|s| s.0), Some(SwipeDirection::Right));
let mut gr = GestureRecognizer::new(GestureConfig::default());
let under = flick(
&mut gr,
(0, 0),
(5, 0),
(5, 0),
MS_50,
MS_50 + MS_100 + Duration::from_millis(1),
);
assert!(
matches!(under[..], [SemanticEvent::DragEnd { .. }]),
"{under:?}"
);
let mut gr = GestureRecognizer::new(GestureConfig {
swipe_velocity_threshold: 51.0,
..GestureConfig::default()
});
let raised = flick(&mut gr, (0, 0), (5, 0), (5, 0), MS_50, MS_50 + MS_100);
assert!(swipe_of(&raised).is_none(), "{raised:?}");
}
#[test]
fn slow_drag_is_not_a_swipe() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let events = flick(
&mut gr,
(0, 0),
(5, 0),
(20, 0),
MS_50,
MS_50 + Duration::from_secs(1),
);
assert!(
matches!(events[..], [SemanticEvent::DragEnd { .. }]),
"{events:?}"
);
}
#[test]
fn swipe_velocity_is_measured_from_first_movement_not_the_press() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let hold = Duration::from_millis(400);
let events = flick(&mut gr, (0, 5), (10, 5), (10, 5), hold, hold + MS_50);
let (direction, _, velocity) = swipe_of(&events).expect("swipe");
assert_eq!(direction, SwipeDirection::Right);
assert!((velocity - 200.0).abs() < 1e-3, "velocity {velocity}");
}
#[test]
fn diagonal_and_out_and_back_drags_are_not_swipes() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let diagonal = flick(&mut gr, (0, 0), (8, 8), (8, 8), MS_50, MS_100);
assert!(swipe_of(&diagonal).is_none(), "{diagonal:?}");
let mut gr = GestureRecognizer::new(GestureConfig::default());
let back = flick(&mut gr, (10, 0), (20, 0), (11, 0), MS_50, MS_100);
assert!(matches!(back[0], SemanticEvent::DragEnd { .. }));
assert!(swipe_of(&back).is_none(), "{back:?}");
}
#[test]
fn cancelled_drag_never_swipes() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let t = now();
gr.process(&mouse_down(0, 0, MouseButton::Left), t);
gr.process(&mouse_drag(10, 0, MouseButton::Left), t + MS_50);
assert!(matches!(
gr.process(&esc(), t + MS_50)[..],
[SemanticEvent::DragCancel]
));
let up = gr.process(&mouse_up(20, 0, MouseButton::Left), t + MS_100);
assert!(swipe_of(&up).is_none(), "{up:?}");
}
#[test]
fn same_instant_release_has_finite_velocity() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let events = flick(&mut gr, (0, 0), (30, 0), (30, 0), MS_50, MS_50);
let (_, distance, velocity) = swipe_of(&events).expect("swipe");
assert_eq!(distance, 30);
assert!(
velocity.is_finite() && velocity >= 0.0,
"velocity {velocity}"
);
assert!((velocity - 30_000.0).abs() < 1e-1, "velocity {velocity}");
}
#[test]
fn swipe_distance_saturates_on_extreme_coordinates() {
let mut gr = GestureRecognizer::new(GestureConfig::default());
let events = flick(&mut gr, (0, 0), (u16::MAX, 0), (u16::MAX, 0), MS_50, MS_100);
assert_eq!(swipe_of(&events).map(|s| s.1), Some(u16::MAX));
}
}