use ratatui::crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
use std::time::{Duration, Instant};
const WINDOW: Duration = Duration::from_millis(400);
#[derive(Debug, Default)]
pub struct ClickRun {
last: Option<(u16, u16, Instant)>,
}
impl ClickRun {
pub fn observe(&mut self, event: &MouseEvent, in_editor: bool, now: Instant) -> bool {
match event.kind {
MouseEventKind::Down(MouseButton::Left) if in_editor => {
self.completes_double(event.column, event.row, now)
}
MouseEventKind::Up(_) | MouseEventKind::Moved => false,
_ => {
self.end();
false
}
}
}
fn completes_double(&mut self, column: u16, row: u16, now: Instant) -> bool {
let doubled = match self.last {
Some((c, r, at)) => c == column && r == row && now.duration_since(at) < WINDOW,
None => false,
};
self.last = if doubled {
None
} else {
Some((column, row, now))
};
doubled
}
pub fn end(&mut self) {
self.last = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn at(millis: u64) -> Instant {
static ORIGIN: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
*ORIGIN.get_or_init(Instant::now) + Duration::from_millis(millis)
}
#[test]
fn one_press_is_not_a_double() {
let mut run = ClickRun::default();
assert!(!run.completes_double(4, 2, at(0)));
}
#[test]
fn two_quick_presses_on_a_cell_are_a_double() {
let mut run = ClickRun::default();
run.completes_double(4, 2, at(0));
assert!(run.completes_double(4, 2, at(120)));
}
#[test]
fn a_slow_second_press_is_a_fresh_first() {
let mut run = ClickRun::default();
run.completes_double(4, 2, at(0));
assert!(
!run.completes_double(4, 2, at(WINDOW.as_millis() as u64)),
"a gap of exactly the window is already too long"
);
assert!(
run.completes_double(4, 2, at(WINDOW.as_millis() as u64 + 100)),
"and it counts as the first of the next pair"
);
}
#[test]
fn a_neighbouring_cell_is_a_different_target() {
let mut run = ClickRun::default();
run.completes_double(4, 2, at(0));
assert!(
!run.completes_double(5, 2, at(50)),
"one column over is a second cursor placement, not a double-click"
);
run.completes_double(4, 2, at(100));
assert!(!run.completes_double(4, 3, at(150)), "nor is one row down");
}
#[test]
fn a_third_press_does_not_pair_with_the_second() {
let mut run = ClickRun::default();
run.completes_double(4, 2, at(0));
assert!(run.completes_double(4, 2, at(100)));
assert!(
!run.completes_double(4, 2, at(200)),
"the double already fired and opened something else under the pointer"
);
}
fn ev(kind: MouseEventKind) -> MouseEvent {
MouseEvent {
kind,
column: 4,
row: 2,
modifiers: ratatui::crossterm::event::KeyModifiers::NONE,
}
}
fn press() -> MouseEvent {
ev(MouseEventKind::Down(MouseButton::Left))
}
#[test]
fn a_release_does_not_end_the_run() {
let mut run = ClickRun::default();
assert!(!run.observe(&press(), true, at(0)));
assert!(!run.observe(&ev(MouseEventKind::Up(MouseButton::Left)), true, at(10)));
assert!(
run.observe(&press(), true, at(120)),
"the release between the two presses is part of the gesture, not an interruption"
);
}
#[test]
fn motion_does_not_end_the_run() {
let mut run = ClickRun::default();
run.observe(&press(), true, at(0));
run.observe(&ev(MouseEventKind::Moved), true, at(30));
assert!(run.observe(&press(), true, at(120)));
}
#[test]
fn a_scroll_ends_the_run() {
let mut run = ClickRun::default();
run.observe(&press(), true, at(0));
run.observe(&ev(MouseEventKind::ScrollDown), true, at(30));
assert!(!run.observe(&press(), true, at(120)));
}
#[test]
fn a_drag_ends_the_run() {
let mut run = ClickRun::default();
run.observe(&press(), true, at(0));
run.observe(&ev(MouseEventKind::Drag(MouseButton::Left)), true, at(30));
assert!(!run.observe(&press(), true, at(120)));
}
#[test]
fn a_press_outside_the_editor_ends_the_run() {
let mut run = ClickRun::default();
run.observe(&press(), true, at(0));
run.observe(&press(), false, at(30));
assert!(!run.observe(&press(), true, at(120)));
}
#[test]
fn another_button_ends_the_run() {
let mut run = ClickRun::default();
run.observe(&press(), true, at(0));
run.observe(&ev(MouseEventKind::Down(MouseButton::Right)), true, at(30));
assert!(!run.observe(&press(), true, at(120)));
}
#[test]
fn anything_else_ends_the_run() {
let mut run = ClickRun::default();
run.completes_double(4, 2, at(0));
run.end();
assert!(
!run.completes_double(4, 2, at(50)),
"a key, a drag or a scroll between the two presses separates them"
);
}
}