#![forbid(unsafe_code)]
use web_time::{Duration, Instant};
use crate::event::{Event, KeyCode, KeyEventKind, Modifiers, MouseButton, MouseEventKind};
use crate::semantic_event::{ChordKey, Position, SemanticEvent};
#[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,
}
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, &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,
});
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,
});
self.mouse_down = None;
return;
}
self.mouse_down = None;
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, 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,
});
return;
};
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 delta = (
pos.x as i16 - drag.last_pos.x as i16,
pos.y as i16 - drag.last_pos.y as i16,
);
out.push(SemanticEvent::DragMove {
start: drag.start_pos,
current: pos,
delta,
});
}
drag.last_pos = pos;
}
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(), 1);
assert!(matches!(
events[0],
SemanticEvent::DragEnd {
start: Position { x: 5, y: 5 },
end: Position { x: 12, y: 5 },
}
));
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_eq!(events.len(), 1);
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_eq!(events.len(), 1);
assert!(matches!(events[0], SemanticEvent::Click { .. }));
}
#[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 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());
}
}