1use std::collections::BTreeSet;
34
35use crate::event::MouseButton;
36use crate::touch_state::{TouchDeviceId, TouchId};
37
38pub const TOUCH_SCROLL_THRESHOLD: f64 = 8.0;
41
42#[derive(Copy, Clone, Debug, PartialEq)]
45pub enum EmuCmd {
46 MouseMove(f64, f64),
47 MouseDown(f64, f64, MouseButton),
48 MouseUp(f64, f64, MouseButton),
49 MouseLeave,
50}
51
52#[derive(Default)]
56pub struct TouchMouseEmu {
57 active: BTreeSet<(TouchDeviceId, TouchId)>,
59 primary: Option<(TouchDeviceId, TouchId)>,
61 start_pos: (f64, f64),
64 last_pos: (f64, f64),
67 scrolling: bool,
70 suppressed: bool,
73}
74
75impl TouchMouseEmu {
76 pub fn new() -> Self {
77 Self::default()
78 }
79
80 pub fn on_start(&mut self, device: TouchDeviceId, id: TouchId, x: f64, y: f64) -> Vec<EmuCmd> {
81 self.active.insert((device, id));
82 if self.primary.is_none() && self.active.len() == 1 {
83 self.primary = Some((device, id));
87 self.start_pos = (x, y);
88 self.last_pos = (x, y);
89 self.scrolling = false;
90 self.suppressed = false;
91 return vec![EmuCmd::MouseMove(x, y)];
92 }
93 if self.primary.is_some() && !self.suppressed {
97 self.suppressed = true;
98 if self.scrolling {
99 self.scrolling = false;
100 return vec![EmuCmd::MouseUp(self.last_pos.0, self.last_pos.1, MouseButton::Middle)];
101 }
102 }
103 Vec::new()
104 }
105
106 pub fn on_move(&mut self, device: TouchDeviceId, id: TouchId, x: f64, y: f64) -> Vec<EmuCmd> {
107 if self.primary != Some((device, id)) || self.suppressed {
108 return Vec::new();
109 }
110 let mut cmds = Vec::new();
111 if !self.scrolling {
112 let dx = x - self.start_pos.0;
113 let dy = y - self.start_pos.1;
114 if (dx * dx + dy * dy).sqrt() >= TOUCH_SCROLL_THRESHOLD {
115 self.scrolling = true;
119 cmds.push(EmuCmd::MouseDown(
120 self.start_pos.0,
121 self.start_pos.1,
122 MouseButton::Middle,
123 ));
124 }
125 }
126 cmds.push(EmuCmd::MouseMove(x, y));
129 self.last_pos = (x, y);
130 cmds
131 }
132
133 pub fn on_end(&mut self, device: TouchDeviceId, id: TouchId) -> Vec<EmuCmd> {
134 self.finish(device, id, true)
135 }
136
137 pub fn on_cancel(&mut self, device: TouchDeviceId, id: TouchId) -> Vec<EmuCmd> {
140 self.finish(device, id, false)
141 }
142
143 fn finish(&mut self, device: TouchDeviceId, id: TouchId, allow_tap: bool) -> Vec<EmuCmd> {
144 self.active.remove(&(device, id));
145 let mut cmds = Vec::new();
146 if self.primary == Some((device, id)) {
147 self.primary = None;
148 let (x, y) = self.last_pos;
149 if self.suppressed {
150 cmds.push(EmuCmd::MouseLeave);
155 } else if self.scrolling {
156 self.scrolling = false;
157 cmds.push(EmuCmd::MouseUp(x, y, MouseButton::Middle));
158 cmds.push(EmuCmd::MouseLeave);
159 } else if allow_tap {
160 cmds.push(EmuCmd::MouseDown(x, y, MouseButton::Left));
161 cmds.push(EmuCmd::MouseUp(x, y, MouseButton::Left));
162 cmds.push(EmuCmd::MouseLeave);
163 } else {
164 cmds.push(EmuCmd::MouseLeave);
165 }
166 }
167 if self.active.is_empty() {
168 self.primary = None;
171 self.scrolling = false;
172 self.suppressed = false;
173 }
174 cmds
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 const DEV: TouchDeviceId = TouchDeviceId(0);
183 const F1: TouchId = TouchId(1);
184 const F2: TouchId = TouchId(2);
185 const F3: TouchId = TouchId(3);
186
187 #[test]
188 fn tap_is_left_click_at_release_point() {
189 let mut emu = TouchMouseEmu::new();
190 assert_eq!(emu.on_start(DEV, F1, 100.0, 100.0), vec![EmuCmd::MouseMove(100.0, 100.0)]);
191 assert_eq!(emu.on_move(DEV, F1, 103.0, 102.0), vec![EmuCmd::MouseMove(103.0, 102.0)]);
193 assert_eq!(
194 emu.on_end(DEV, F1),
195 vec![
196 EmuCmd::MouseDown(103.0, 102.0, MouseButton::Left),
197 EmuCmd::MouseUp(103.0, 102.0, MouseButton::Left),
198 EmuCmd::MouseLeave,
199 ]
200 );
201 }
202
203 #[test]
204 fn drag_past_threshold_becomes_middle_drag_from_start_point() {
205 let mut emu = TouchMouseEmu::new();
206 emu.on_start(DEV, F1, 50.0, 50.0);
207 assert_eq!(
209 emu.on_move(DEV, F1, 62.0, 50.0),
210 vec![
211 EmuCmd::MouseDown(50.0, 50.0, MouseButton::Middle),
212 EmuCmd::MouseMove(62.0, 50.0),
213 ]
214 );
215 assert_eq!(emu.on_move(DEV, F1, 70.0, 55.0), vec![EmuCmd::MouseMove(70.0, 55.0)]);
217 assert_eq!(
218 emu.on_end(DEV, F1),
219 vec![EmuCmd::MouseUp(70.0, 55.0, MouseButton::Middle), EmuCmd::MouseLeave]
220 );
221 }
222
223 #[test]
224 fn second_finger_releases_middle_drag_and_suppresses() {
225 let mut emu = TouchMouseEmu::new();
226 emu.on_start(DEV, F1, 0.0, 0.0);
227 emu.on_move(DEV, F1, 20.0, 0.0); assert_eq!(
230 emu.on_start(DEV, F2, 100.0, 100.0),
231 vec![EmuCmd::MouseUp(20.0, 0.0, MouseButton::Middle)]
232 );
233 assert_eq!(emu.on_move(DEV, F1, 40.0, 0.0), Vec::<EmuCmd>::new());
235 assert_eq!(emu.on_end(DEV, F1), vec![EmuCmd::MouseLeave]);
237 assert_eq!(emu.on_end(DEV, F2), Vec::<EmuCmd>::new());
238 assert_eq!(emu.on_start(DEV, F3, 5.0, 5.0), vec![EmuCmd::MouseMove(5.0, 5.0)]);
240 }
241
242 #[test]
243 fn pinch_with_still_primary_fires_no_phantom_click() {
244 let mut emu = TouchMouseEmu::new();
245 let mut all = Vec::new();
246 all.extend(emu.on_start(DEV, F1, 100.0, 100.0));
247 all.extend(emu.on_start(DEV, F2, 200.0, 100.0));
248 all.extend(emu.on_move(DEV, F1, 98.0, 100.0)); all.extend(emu.on_move(DEV, F2, 205.0, 100.0));
250 all.extend(emu.on_end(DEV, F2));
251 all.extend(emu.on_end(DEV, F1));
252 assert!(
253 !all.iter().any(|c| matches!(c, EmuCmd::MouseDown(_, _, MouseButton::Left))),
254 "a pinch must never synthesize a left click, got: {all:?}"
255 );
256 }
257
258 #[test]
259 fn cancel_during_drag_releases_middle_without_click() {
260 let mut emu = TouchMouseEmu::new();
261 emu.on_start(DEV, F1, 0.0, 0.0);
262 emu.on_move(DEV, F1, 30.0, 0.0);
263 assert_eq!(
264 emu.on_cancel(DEV, F1),
265 vec![EmuCmd::MouseUp(30.0, 0.0, MouseButton::Middle), EmuCmd::MouseLeave]
266 );
267 }
268
269 #[test]
270 fn cancel_before_threshold_fires_no_click() {
271 let mut emu = TouchMouseEmu::new();
272 emu.on_start(DEV, F1, 0.0, 0.0);
273 assert_eq!(emu.on_cancel(DEV, F1), vec![EmuCmd::MouseLeave]);
274 }
275
276 #[test]
277 fn non_primary_fingers_never_emit() {
278 let mut emu = TouchMouseEmu::new();
279 emu.on_start(DEV, F1, 0.0, 0.0);
280 emu.on_start(DEV, F2, 50.0, 50.0);
281 assert_eq!(emu.on_move(DEV, F2, 80.0, 80.0), Vec::<EmuCmd>::new());
282 assert_eq!(emu.on_end(DEV, F2), Vec::<EmuCmd>::new());
283 assert_eq!(emu.on_end(DEV, F1), vec![EmuCmd::MouseLeave]);
285 }
286}