use std::collections::BTreeSet;
use crate::event::MouseButton;
use crate::touch_state::{TouchDeviceId, TouchId};
pub const TOUCH_SCROLL_THRESHOLD: f64 = 8.0;
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum EmuCmd {
MouseMove(f64, f64),
MouseDown(f64, f64, MouseButton),
MouseUp(f64, f64, MouseButton),
MouseLeave,
}
#[derive(Default)]
pub struct TouchMouseEmu {
active: BTreeSet<(TouchDeviceId, TouchId)>,
primary: Option<(TouchDeviceId, TouchId)>,
start_pos: (f64, f64),
last_pos: (f64, f64),
scrolling: bool,
suppressed: bool,
}
impl TouchMouseEmu {
pub fn new() -> Self {
Self::default()
}
pub fn on_start(&mut self, device: TouchDeviceId, id: TouchId, x: f64, y: f64) -> Vec<EmuCmd> {
self.active.insert((device, id));
if self.primary.is_none() && self.active.len() == 1 {
self.primary = Some((device, id));
self.start_pos = (x, y);
self.last_pos = (x, y);
self.scrolling = false;
self.suppressed = false;
return vec![EmuCmd::MouseMove(x, y)];
}
if self.primary.is_some() && !self.suppressed {
self.suppressed = true;
if self.scrolling {
self.scrolling = false;
return vec![EmuCmd::MouseUp(self.last_pos.0, self.last_pos.1, MouseButton::Middle)];
}
}
Vec::new()
}
pub fn on_move(&mut self, device: TouchDeviceId, id: TouchId, x: f64, y: f64) -> Vec<EmuCmd> {
if self.primary != Some((device, id)) || self.suppressed {
return Vec::new();
}
let mut cmds = Vec::new();
if !self.scrolling {
let dx = x - self.start_pos.0;
let dy = y - self.start_pos.1;
if (dx * dx + dy * dy).sqrt() >= TOUCH_SCROLL_THRESHOLD {
self.scrolling = true;
cmds.push(EmuCmd::MouseDown(
self.start_pos.0,
self.start_pos.1,
MouseButton::Middle,
));
}
}
cmds.push(EmuCmd::MouseMove(x, y));
self.last_pos = (x, y);
cmds
}
pub fn on_end(&mut self, device: TouchDeviceId, id: TouchId) -> Vec<EmuCmd> {
self.finish(device, id, true)
}
pub fn on_cancel(&mut self, device: TouchDeviceId, id: TouchId) -> Vec<EmuCmd> {
self.finish(device, id, false)
}
fn finish(&mut self, device: TouchDeviceId, id: TouchId, allow_tap: bool) -> Vec<EmuCmd> {
self.active.remove(&(device, id));
let mut cmds = Vec::new();
if self.primary == Some((device, id)) {
self.primary = None;
let (x, y) = self.last_pos;
if self.suppressed {
cmds.push(EmuCmd::MouseLeave);
} else if self.scrolling {
self.scrolling = false;
cmds.push(EmuCmd::MouseUp(x, y, MouseButton::Middle));
cmds.push(EmuCmd::MouseLeave);
} else if allow_tap {
cmds.push(EmuCmd::MouseDown(x, y, MouseButton::Left));
cmds.push(EmuCmd::MouseUp(x, y, MouseButton::Left));
cmds.push(EmuCmd::MouseLeave);
} else {
cmds.push(EmuCmd::MouseLeave);
}
}
if self.active.is_empty() {
self.primary = None;
self.scrolling = false;
self.suppressed = false;
}
cmds
}
}
#[cfg(test)]
mod tests {
use super::*;
const DEV: TouchDeviceId = TouchDeviceId(0);
const F1: TouchId = TouchId(1);
const F2: TouchId = TouchId(2);
const F3: TouchId = TouchId(3);
#[test]
fn tap_is_left_click_at_release_point() {
let mut emu = TouchMouseEmu::new();
assert_eq!(emu.on_start(DEV, F1, 100.0, 100.0), vec![EmuCmd::MouseMove(100.0, 100.0)]);
assert_eq!(emu.on_move(DEV, F1, 103.0, 102.0), vec![EmuCmd::MouseMove(103.0, 102.0)]);
assert_eq!(
emu.on_end(DEV, F1),
vec![
EmuCmd::MouseDown(103.0, 102.0, MouseButton::Left),
EmuCmd::MouseUp(103.0, 102.0, MouseButton::Left),
EmuCmd::MouseLeave,
]
);
}
#[test]
fn drag_past_threshold_becomes_middle_drag_from_start_point() {
let mut emu = TouchMouseEmu::new();
emu.on_start(DEV, F1, 50.0, 50.0);
assert_eq!(
emu.on_move(DEV, F1, 62.0, 50.0),
vec![
EmuCmd::MouseDown(50.0, 50.0, MouseButton::Middle),
EmuCmd::MouseMove(62.0, 50.0),
]
);
assert_eq!(emu.on_move(DEV, F1, 70.0, 55.0), vec![EmuCmd::MouseMove(70.0, 55.0)]);
assert_eq!(
emu.on_end(DEV, F1),
vec![EmuCmd::MouseUp(70.0, 55.0, MouseButton::Middle), EmuCmd::MouseLeave]
);
}
#[test]
fn second_finger_releases_middle_drag_and_suppresses() {
let mut emu = TouchMouseEmu::new();
emu.on_start(DEV, F1, 0.0, 0.0);
emu.on_move(DEV, F1, 20.0, 0.0); assert_eq!(
emu.on_start(DEV, F2, 100.0, 100.0),
vec![EmuCmd::MouseUp(20.0, 0.0, MouseButton::Middle)]
);
assert_eq!(emu.on_move(DEV, F1, 40.0, 0.0), Vec::<EmuCmd>::new());
assert_eq!(emu.on_end(DEV, F1), vec![EmuCmd::MouseLeave]);
assert_eq!(emu.on_end(DEV, F2), Vec::<EmuCmd>::new());
assert_eq!(emu.on_start(DEV, F3, 5.0, 5.0), vec![EmuCmd::MouseMove(5.0, 5.0)]);
}
#[test]
fn pinch_with_still_primary_fires_no_phantom_click() {
let mut emu = TouchMouseEmu::new();
let mut all = Vec::new();
all.extend(emu.on_start(DEV, F1, 100.0, 100.0));
all.extend(emu.on_start(DEV, F2, 200.0, 100.0));
all.extend(emu.on_move(DEV, F1, 98.0, 100.0)); all.extend(emu.on_move(DEV, F2, 205.0, 100.0));
all.extend(emu.on_end(DEV, F2));
all.extend(emu.on_end(DEV, F1));
assert!(
!all.iter().any(|c| matches!(c, EmuCmd::MouseDown(_, _, MouseButton::Left))),
"a pinch must never synthesize a left click, got: {all:?}"
);
}
#[test]
fn cancel_during_drag_releases_middle_without_click() {
let mut emu = TouchMouseEmu::new();
emu.on_start(DEV, F1, 0.0, 0.0);
emu.on_move(DEV, F1, 30.0, 0.0);
assert_eq!(
emu.on_cancel(DEV, F1),
vec![EmuCmd::MouseUp(30.0, 0.0, MouseButton::Middle), EmuCmd::MouseLeave]
);
}
#[test]
fn cancel_before_threshold_fires_no_click() {
let mut emu = TouchMouseEmu::new();
emu.on_start(DEV, F1, 0.0, 0.0);
assert_eq!(emu.on_cancel(DEV, F1), vec![EmuCmd::MouseLeave]);
}
#[test]
fn non_primary_fingers_never_emit() {
let mut emu = TouchMouseEmu::new();
emu.on_start(DEV, F1, 0.0, 0.0);
emu.on_start(DEV, F2, 50.0, 50.0);
assert_eq!(emu.on_move(DEV, F2, 80.0, 80.0), Vec::<EmuCmd>::new());
assert_eq!(emu.on_end(DEV, F2), Vec::<EmuCmd>::new());
assert_eq!(emu.on_end(DEV, F1), vec![EmuCmd::MouseLeave]);
}
}