use crate::core::ObjectId;
use crate::core::Point;
#[cfg(not(feature = "touch"))]
const DEFAULT_DOUBLE_CLICK_TIMEOUT_MS: u64 = 400;
#[cfg(not(feature = "touch"))]
const DEFAULT_MAX_STATIONARY_DISTANCE: f32 = 15.0;
pub(crate) fn is_double_click_pair(
first: (Point, u64),
second: (Point, u64),
timeout_ms: u64,
max_stationary_distance: f32,
) -> bool {
let elapsed = second.1.saturating_sub(first.1);
let dx = (second.0.x as i64 - first.0.x as i64) as f32;
let dy = (second.0.y as i64 - first.0.y as i64) as f32;
elapsed <= timeout_ms && (dx * dx + dy * dy).sqrt() < max_stationary_distance * 2.0
}
fn pair_tuning() -> (u64, f32) {
#[cfg(feature = "touch")]
{
let tuning = crate::gesture::tuning();
(tuning.double_tap_timeout_ms, tuning.max_stationary_distance)
}
#[cfg(not(feature = "touch"))]
{
(DEFAULT_DOUBLE_CLICK_TIMEOUT_MS, DEFAULT_MAX_STATIONARY_DISTANCE)
}
}
fn pair_matches(first: (Point, u64), second: (Point, u64)) -> bool {
let (timeout_ms, max_move) = pair_tuning();
is_double_click_pair(first, second, timeout_ms, max_move)
}
#[derive(Debug, Clone, Copy)]
struct LastPress {
target: ObjectId,
pos: Point,
at_ms: u64,
button: u32,
modifiers: u32,
}
#[derive(Debug, Default)]
pub struct DoubleClickTracker {
last: Option<LastPress>,
}
impl DoubleClickTracker {
pub fn new() -> Self {
Self { last: None }
}
pub fn observe_press(
&mut self,
target: ObjectId,
pos: Point,
button: u32,
modifiers: u32,
now_ms: u64,
) -> bool {
const PRIMARY: u32 = crate::event::mouse_button::PRIMARY;
if button != PRIMARY {
self.last = Some(LastPress { target, pos, at_ms: now_ms, button, modifiers });
return false;
}
let double = matches!(
self.last,
Some(last) if last.target == target
&& last.button == button
&& pair_matches((last.pos, last.at_ms), (pos, now_ms))
);
self.last = if double {
None
} else {
Some(LastPress { target, pos, at_ms: now_ms, button, modifiers })
};
double
}
pub fn accept_platform_double_click(
&mut self,
target: ObjectId,
pos: Point,
button: u32,
now_ms: u64,
) -> bool {
const PRIMARY: u32 = crate::event::mouse_button::PRIMARY;
if button != PRIMARY {
return true;
}
match self.last {
None => true,
Some(last) => {
let agrees = last.target == target
&& last.button == button
&& pair_matches((last.pos, last.at_ms), (pos, now_ms));
if agrees {
self.last = None;
}
agrees
}
}
}
pub fn last_press_modifiers(&self) -> u32 {
self.last.map_or(0, |last| last.modifiers)
}
pub fn forget(&mut self, target: ObjectId) {
if matches!(self.last, Some(last) if last.target == target) {
self.last = None;
}
}
pub fn reset(&mut self) {
self.last = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "touch")]
use crate::gesture::{set_tuning, GestureTuning};
fn with_timeout<T>(timeout_ms: u64, f: impl FnOnce() -> T) -> T {
#[cfg(feature = "touch")]
{
let previous = set_tuning(Some(GestureTuning {
double_tap_timeout_ms: timeout_ms,
..GestureTuning::default()
}));
let result = f();
set_tuning(previous);
result
}
#[cfg(not(feature = "touch"))]
{
let _ = timeout_ms;
f()
}
}
#[test]
fn a_second_press_inside_the_gesture_interval_is_a_double_click() {
with_timeout(400, || {
let mut tracker = DoubleClickTracker::new();
let p = Point::new(10, 10);
assert!(!tracker.observe_press(1, p, 1, 0, 0));
assert!(tracker.observe_press(1, p, 1, 0, 200));
});
}
#[test]
fn a_second_press_after_the_interval_is_a_new_single_click() {
with_timeout(400, || {
let mut tracker = DoubleClickTracker::new();
let p = Point::new(10, 10);
assert!(!tracker.observe_press(1, p, 1, 0, 0));
assert!(!tracker.observe_press(1, p, 1, 0, 401));
assert!(tracker.observe_press(1, p, 1, 0, 500));
});
}
#[test]
fn a_distant_second_press_is_not_a_double_click() {
with_timeout(400, || {
let mut tracker = DoubleClickTracker::new();
assert!(!tracker.observe_press(1, Point::new(0, 0), 1, 0, 0));
assert!(!tracker.observe_press(1, Point::new(500, 500), 1, 0, 100));
});
}
#[test]
fn a_press_on_another_target_restarts_the_run() {
with_timeout(400, || {
let mut tracker = DoubleClickTracker::new();
let p = Point::new(10, 10);
assert!(!tracker.observe_press(1, p, 1, 0, 0));
assert!(!tracker.observe_press(2, p, 1, 0, 100));
assert!(!tracker.observe_press(1, p, 1, 0, 200));
});
}
#[test]
fn a_non_primary_button_never_forms_a_double_click() {
with_timeout(400, || {
let mut tracker = DoubleClickTracker::new();
let p = Point::new(10, 10);
assert!(!tracker.observe_press(1, p, 2, 0, 0));
assert!(!tracker.observe_press(1, p, 2, 0, 100));
});
}
#[test]
fn a_platform_double_click_inside_the_interval_is_accepted() {
with_timeout(400, || {
let mut tracker = DoubleClickTracker::new();
let p = Point::new(10, 10);
assert!(!tracker.observe_press(1, p, 1, 0, 0));
assert!(tracker.accept_platform_double_click(1, p, 1, 200));
});
}
#[test]
fn a_platform_double_click_outside_the_interval_is_rejected() {
with_timeout(400, || {
let mut tracker = DoubleClickTracker::new();
let p = Point::new(10, 10);
assert!(!tracker.observe_press(1, p, 1, 0, 0));
assert!(!tracker.accept_platform_double_click(1, p, 1, 900));
});
}
#[test]
fn an_uncontradicted_platform_double_click_is_accepted() {
let mut tracker = DoubleClickTracker::new();
assert!(tracker.accept_platform_double_click(1, Point::new(0, 0), 1, 0));
}
#[test]
fn a_third_click_starts_a_fresh_run() {
with_timeout(400, || {
let mut tracker = DoubleClickTracker::new();
let p = Point::new(10, 10);
assert!(!tracker.observe_press(1, p, 1, 0, 0));
assert!(tracker.observe_press(1, p, 1, 0, 100));
assert!(!tracker.observe_press(1, p, 1, 0, 200));
});
}
}