1use crate::layout::Point;
9use rand::RngExt;
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::time::Duration;
12
13#[derive(Debug, Clone)]
15pub struct SmartMouseConfig {
16 pub steps: usize,
21 pub overshoot: f64,
23 pub jitter: f64,
25 pub step_delay_ms: u64,
30 pub easing: bool,
32 pub auto_size: bool,
39 pub min_duration_ms: u64,
41 pub max_duration_ms: u64,
43 pub pre_click_dwell_ms: Option<(u64, u64)>,
51}
52
53impl Default for SmartMouseConfig {
54 fn default() -> Self {
55 Self {
56 steps: 25,
57 overshoot: 0.15,
58 jitter: 1.5,
59 step_delay_ms: 8,
60 easing: true,
61 auto_size: true,
62 min_duration_ms: 100,
63 max_duration_ms: 800,
64 pre_click_dwell_ms: Some((40, 120)),
65 }
66 }
67}
68
69const MIN_AUTO_STEPS: usize = 6;
73const MAX_AUTO_STEPS: usize = 40;
77
78fn fitts_total_ms(distance: f64, config: &SmartMouseConfig) -> f64 {
83 const A_MS: f64 = 80.0;
84 const B_MS: f64 = 110.0;
85 const W_PX: f64 = 40.0;
86 let raw = A_MS + B_MS * (distance / W_PX + 1.0).log2();
87 let lo = config.min_duration_ms as f64;
88 let hi = (config.max_duration_ms as f64).max(lo);
89 raw.clamp(lo, hi)
90}
91
92#[derive(Debug, Clone)]
94pub struct MovementStep {
95 pub point: Point,
97 pub delay: Duration,
99}
100
101fn cubic_bezier(p0: Point, p1: Point, p2: Point, p3: Point, t: f64) -> Point {
106 let inv = 1.0 - t;
107 let inv2 = inv * inv;
108 let inv3 = inv2 * inv;
109 let t2 = t * t;
110 let t3 = t2 * t;
111
112 Point {
113 x: inv3 * p0.x + 3.0 * inv2 * t * p1.x + 3.0 * inv * t2 * p2.x + t3 * p3.x,
114 y: inv3 * p0.y + 3.0 * inv2 * t * p1.y + 3.0 * inv * t2 * p2.y + t3 * p3.y,
115 }
116}
117
118fn ease_in_out(t: f64) -> f64 {
120 if t < 0.5 {
121 4.0 * t * t * t
122 } else {
123 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
124 }
125}
126
127pub fn generate_path(from: Point, to: Point, config: &SmartMouseConfig) -> Vec<MovementStep> {
132 let mut rng = rand::rng();
133
134 let dx = to.x - from.x;
135 let dy = to.y - from.y;
136 let distance = (dx * dx + dy * dy).sqrt();
137
138 if distance < 2.0 {
140 return vec![MovementStep {
141 point: to,
142 delay: Duration::from_millis(config.step_delay_ms),
143 }];
144 }
145
146 let (steps, step_delay_ms) = if config.auto_size {
154 let total_ms = fitts_total_ms(distance, config);
155 let target = (config.step_delay_ms as f64).max(1.0);
156 let raw = (total_ms / target).round() as usize;
157 let s = raw.clamp(MIN_AUTO_STEPS, MAX_AUTO_STEPS);
158 (s, total_ms / s as f64)
159 } else {
160 (config.steps.max(2), config.step_delay_ms as f64)
161 };
162
163 let (perp_x, perp_y) = if distance > 0.001 {
165 (-dy / distance, dx / distance)
166 } else {
167 (0.0, 1.0)
168 };
169
170 let spread = distance * 0.3;
172 let offset1: f64 = rng.random_range(-spread..spread);
173 let offset2: f64 = rng.random_range(-spread..spread);
174
175 let cp1 = Point {
176 x: from.x + dx * 0.25 + perp_x * offset1,
177 y: from.y + dy * 0.25 + perp_y * offset1,
178 };
179 let cp2 = Point {
180 x: from.x + dx * 0.75 + perp_x * offset2,
181 y: from.y + dy * 0.75 + perp_y * offset2,
182 };
183
184 let should_overshoot = config.overshoot > 0.0 && distance > 200.0;
188
189 let overshoot_target = if should_overshoot {
190 let overshoot_amount = distance * config.overshoot * rng.random_range(0.5..1.5);
191 Point {
192 x: to.x + (dx / distance) * overshoot_amount,
193 y: to.y + (dy / distance) * overshoot_amount,
194 }
195 } else {
196 to
197 };
198
199 let main_steps = if should_overshoot {
200 (steps as f64 * 0.85) as usize
201 } else {
202 steps
203 };
204
205 let mut path = Vec::with_capacity(steps + 2);
206
207 let end = if should_overshoot {
209 overshoot_target
210 } else {
211 to
212 };
213
214 for i in 1..=main_steps {
215 let raw_t = i as f64 / main_steps as f64;
216 let t = if config.easing {
217 ease_in_out(raw_t)
218 } else {
219 raw_t
220 };
221
222 let mut p = cubic_bezier(from, cp1, cp2, end, t);
223
224 if config.jitter > 0.0 && i < main_steps.saturating_sub(2) {
226 p.x += rng.random_range(-config.jitter..config.jitter);
227 p.y += rng.random_range(-config.jitter..config.jitter);
228 }
229
230 let delay_variation: f64 = rng.random_range(0.7..1.3);
232 let delay = Duration::from_millis((step_delay_ms * delay_variation) as u64);
233
234 path.push(MovementStep { point: p, delay });
235 }
236
237 if should_overshoot {
239 let correction_steps = steps.saturating_sub(main_steps).max(3);
240 let last = path.last().map(|s| s.point).unwrap_or(overshoot_target);
241
242 for i in 1..=correction_steps {
243 let t = i as f64 / correction_steps as f64;
244 let t = if config.easing { ease_in_out(t) } else { t };
245
246 let p = Point {
247 x: last.x + (to.x - last.x) * t,
248 y: last.y + (to.y - last.y) * t,
249 };
250
251 let delay = Duration::from_millis((step_delay_ms * 0.6) as u64);
252 path.push(MovementStep { point: p, delay });
253 }
254 }
255
256 if let Some(last) = path.last_mut() {
258 last.point = to;
259 }
260
261 path
262}
263
264pub struct SmartMouse {
271 pos_x: AtomicU64,
272 pos_y: AtomicU64,
273 config: SmartMouseConfig,
274}
275
276impl std::fmt::Debug for SmartMouse {
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 let pos = self.position();
279 f.debug_struct("SmartMouse")
280 .field("position", &pos)
281 .field("config", &self.config)
282 .finish()
283 }
284}
285
286impl SmartMouse {
287 pub fn new() -> Self {
289 Self {
290 pos_x: AtomicU64::new(0.0_f64.to_bits()),
291 pos_y: AtomicU64::new(0.0_f64.to_bits()),
292 config: SmartMouseConfig::default(),
293 }
294 }
295
296 pub fn with_config(config: SmartMouseConfig) -> Self {
298 Self {
299 pos_x: AtomicU64::new(0.0_f64.to_bits()),
300 pos_y: AtomicU64::new(0.0_f64.to_bits()),
301 config,
302 }
303 }
304
305 pub fn position(&self) -> Point {
307 Point {
308 x: f64::from_bits(self.pos_x.load(Ordering::Relaxed)),
309 y: f64::from_bits(self.pos_y.load(Ordering::Relaxed)),
310 }
311 }
312
313 pub fn set_position(&self, point: Point) {
315 self.pos_x.store(point.x.to_bits(), Ordering::Relaxed);
316 self.pos_y.store(point.y.to_bits(), Ordering::Relaxed);
317 }
318
319 pub fn config(&self) -> &SmartMouseConfig {
321 &self.config
322 }
323
324 pub fn path_to(&self, target: Point) -> Vec<MovementStep> {
329 let from = self.position();
330 self.set_position(target);
331 generate_path(from, target, &self.config)
332 }
333
334 pub fn pre_click_dwell(&self) -> Option<Duration> {
343 let (lo, hi) = self.config.pre_click_dwell_ms?;
344 if hi == 0 {
345 return None;
346 }
347 let lo = lo.min(hi);
348 let ms = if lo == hi {
349 lo
350 } else {
351 let mut rng = rand::rng();
352 rng.random_range(lo..=hi)
353 };
354 Some(Duration::from_millis(ms))
355 }
356}
357
358impl Default for SmartMouse {
359 fn default() -> Self {
360 Self::new()
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 #[test]
369 fn test_cubic_bezier_endpoints() {
370 let p0 = Point::new(0.0, 0.0);
371 let p1 = Point::new(25.0, 50.0);
372 let p2 = Point::new(75.0, 50.0);
373 let p3 = Point::new(100.0, 100.0);
374
375 let start = cubic_bezier(p0, p1, p2, p3, 0.0);
376 assert!((start.x - p0.x).abs() < 1e-10);
377 assert!((start.y - p0.y).abs() < 1e-10);
378
379 let end = cubic_bezier(p0, p1, p2, p3, 1.0);
380 assert!((end.x - p3.x).abs() < 1e-10);
381 assert!((end.y - p3.y).abs() < 1e-10);
382 }
383
384 #[test]
385 fn test_cubic_bezier_midpoint() {
386 let p0 = Point::new(0.0, 0.0);
388 let p1 = Point::new(33.3, 33.3);
389 let p2 = Point::new(66.6, 66.6);
390 let p3 = Point::new(100.0, 100.0);
391
392 let mid = cubic_bezier(p0, p1, p2, p3, 0.5);
393 assert!((mid.x - 50.0).abs() < 1.0);
395 assert!((mid.y - 50.0).abs() < 1.0);
396 }
397
398 #[test]
399 fn test_ease_in_out_boundaries() {
400 assert!((ease_in_out(0.0)).abs() < 1e-10);
401 assert!((ease_in_out(1.0) - 1.0).abs() < 1e-10);
402 }
403
404 #[test]
405 fn test_ease_in_out_midpoint() {
406 let mid = ease_in_out(0.5);
407 assert!((mid - 0.5).abs() < 1e-10);
408 }
409
410 #[test]
411 fn test_ease_in_out_monotonic() {
412 let mut prev = 0.0;
413 for i in 1..=100 {
414 let t = i as f64 / 100.0;
415 let val = ease_in_out(t);
416 assert!(
417 val >= prev,
418 "ease_in_out should be monotonically increasing"
419 );
420 prev = val;
421 }
422 }
423
424 #[test]
425 fn test_generate_path_ends_at_target() {
426 let from = Point::new(10.0, 20.0);
427 let to = Point::new(500.0, 300.0);
428 let config = SmartMouseConfig::default();
429
430 let path = generate_path(from, to, &config);
431
432 assert!(!path.is_empty());
433 let last = &path.last().unwrap().point;
434 assert!(
435 (last.x - to.x).abs() < 1e-10 && (last.y - to.y).abs() < 1e-10,
436 "path must end exactly at target, got ({}, {})",
437 last.x,
438 last.y
439 );
440 }
441
442 #[test]
443 fn test_generate_path_short_distance() {
444 let from = Point::new(100.0, 100.0);
445 let to = Point::new(100.5, 100.5);
446 let config = SmartMouseConfig::default();
447
448 let path = generate_path(from, to, &config);
449
450 assert_eq!(
451 path.len(),
452 1,
453 "very short moves should produce a single step"
454 );
455 assert!((path[0].point.x - to.x).abs() < 1e-10);
456 assert!((path[0].point.y - to.y).abs() < 1e-10);
457 }
458
459 #[test]
460 fn test_generate_path_no_overshoot() {
461 let from = Point::new(0.0, 0.0);
462 let to = Point::new(200.0, 200.0);
463 let config = SmartMouseConfig {
464 overshoot: 0.0,
465 auto_size: false,
466 ..Default::default()
467 };
468
469 let path = generate_path(from, to, &config);
470 assert_eq!(path.len(), config.steps);
471 }
472
473 #[test]
474 fn test_generate_path_no_jitter() {
475 let from = Point::new(0.0, 0.0);
476 let to = Point::new(200.0, 200.0);
477 let config = SmartMouseConfig {
478 jitter: 0.0,
479 overshoot: 0.0,
480 easing: false,
481 ..Default::default()
482 };
483
484 let path = generate_path(from, to, &config);
488 assert!(!path.is_empty());
489 let last = &path.last().unwrap().point;
490 assert!((last.x - to.x).abs() < 1e-10);
491 assert!((last.y - to.y).abs() < 1e-10);
492 }
493
494 #[test]
495 fn test_generate_path_step_count_with_overshoot() {
496 let from = Point::new(0.0, 0.0);
497 let to = Point::new(500.0, 500.0);
498 let config = SmartMouseConfig {
499 steps: 30,
500 overshoot: 0.2,
501 auto_size: false,
502 ..Default::default()
503 };
504
505 let path = generate_path(from, to, &config);
506 assert!(path.len() >= config.steps);
508 }
509
510 #[test]
511 fn test_generate_path_no_huge_jumps() {
512 let from = Point::new(0.0, 0.0);
513 let to = Point::new(300.0, 300.0);
514 let config = SmartMouseConfig {
515 steps: 50,
516 overshoot: 0.0,
517 jitter: 0.0,
518 ..Default::default()
519 };
520
521 let path = generate_path(from, to, &config);
522
523 let mut prev = from;
524 let max_distance = (300.0_f64 * 300.0 + 300.0 * 300.0).sqrt(); for step in &path {
527 let dx = step.point.x - prev.x;
528 let dy = step.point.y - prev.y;
529 let step_dist = (dx * dx + dy * dy).sqrt();
530 assert!(
532 step_dist < max_distance * 0.6,
533 "step jumped {} pixels (max total: {})",
534 step_dist,
535 max_distance
536 );
537 prev = step.point;
538 }
539 }
540
541 #[test]
542 fn test_smart_mouse_position_tracking() {
543 let mouse = SmartMouse::new();
544
545 assert_eq!(mouse.position(), Point::new(0.0, 0.0));
546
547 mouse.set_position(Point::new(100.0, 200.0));
548 assert_eq!(mouse.position(), Point::new(100.0, 200.0));
549 }
550
551 #[test]
552 fn test_smart_mouse_path_to_updates_position() {
553 let mouse = SmartMouse::new();
554 let target = Point::new(500.0, 300.0);
555
556 let path = mouse.path_to(target);
557 assert!(!path.is_empty());
558
559 assert_eq!(mouse.position(), target);
561 }
562
563 #[test]
564 fn test_smart_mouse_consecutive_paths() {
565 let mouse = SmartMouse::with_config(SmartMouseConfig {
566 overshoot: 0.0,
567 jitter: 0.0,
568 ..Default::default()
569 });
570
571 let target1 = Point::new(100.0, 100.0);
572 let path1 = mouse.path_to(target1);
573 assert!(!path1.is_empty());
574 assert_eq!(mouse.position(), target1);
575
576 let target2 = Point::new(400.0, 300.0);
577 let _path2 = mouse.path_to(target2);
578 assert_eq!(mouse.position(), target2);
579 }
580
581 #[test]
582 fn test_smart_mouse_same_position_no_move() {
583 let mouse = SmartMouse::new();
584 mouse.set_position(Point::new(100.0, 100.0));
585
586 let path = mouse.path_to(Point::new(100.0, 100.0));
587 assert_eq!(path.len(), 1);
589 }
590
591 #[test]
592 fn test_smart_mouse_custom_config() {
593 let config = SmartMouseConfig {
594 steps: 10,
595 overshoot: 0.0,
596 jitter: 0.0,
597 step_delay_ms: 16,
598 easing: false,
599 auto_size: false,
600 ..Default::default()
601 };
602
603 let mouse = SmartMouse::with_config(config.clone());
604 let path = mouse.path_to(Point::new(200.0, 200.0));
605
606 assert_eq!(path.len(), config.steps);
607 }
608
609 #[test]
610 fn test_movement_delays_are_reasonable() {
611 let config = SmartMouseConfig {
612 step_delay_ms: 10,
613 ..Default::default()
614 };
615
616 let path = generate_path(Point::new(0.0, 0.0), Point::new(500.0, 500.0), &config);
617
618 for step in &path {
619 assert!(
621 step.delay.as_millis() <= 30,
622 "delay too large: {:?}",
623 step.delay
624 );
625 }
626 }
627
628 #[test]
629 fn test_default_config() {
630 let config = SmartMouseConfig::default();
631 assert_eq!(config.steps, 25);
632 assert!((config.overshoot - 0.15).abs() < 1e-10);
633 assert!((config.jitter - 1.5).abs() < 1e-10);
634 assert_eq!(config.step_delay_ms, 8);
635 assert!(config.easing);
636 assert!(config.auto_size);
637 assert_eq!(config.min_duration_ms, 100);
638 assert_eq!(config.max_duration_ms, 800);
639 assert_eq!(config.pre_click_dwell_ms, Some((40, 120)));
640 }
641
642 #[test]
643 fn test_auto_size_scales_with_distance() {
644 let config = SmartMouseConfig {
647 overshoot: 0.0,
648 jitter: 0.0,
649 ..Default::default()
650 };
651
652 let short = generate_path(Point::new(0.0, 0.0), Point::new(60.0, 0.0), &config);
653 let long = generate_path(Point::new(0.0, 0.0), Point::new(1500.0, 0.0), &config);
654
655 assert!(
656 long.len() > short.len(),
657 "auto_size should give longer moves more steps: short={}, long={}",
658 short.len(),
659 long.len()
660 );
661 }
662
663 #[test]
664 fn test_auto_size_clamps_step_count() {
665 let config = SmartMouseConfig {
666 overshoot: 0.0,
667 jitter: 0.0,
668 ..Default::default()
669 };
670
671 let tiny = generate_path(Point::new(0.0, 0.0), Point::new(8.0, 0.0), &config);
673 assert!(
674 tiny.len() >= MIN_AUTO_STEPS,
675 "tiny move should hit min step floor, got {}",
676 tiny.len()
677 );
678
679 let huge = generate_path(Point::new(0.0, 0.0), Point::new(5000.0, 5000.0), &config);
681 assert!(
682 huge.len() <= MAX_AUTO_STEPS,
683 "huge move should hit max step ceiling, got {}",
684 huge.len()
685 );
686 }
687
688 #[test]
689 fn test_auto_size_total_duration_within_bounds() {
690 let config = SmartMouseConfig {
691 overshoot: 0.0,
692 jitter: 0.0,
693 ..Default::default()
694 };
695
696 let path = generate_path(Point::new(0.0, 0.0), Point::new(800.0, 600.0), &config);
697 let total_ms: u128 = path.iter().map(|s| s.delay.as_millis()).sum();
698
699 assert!(
703 (total_ms as u64) <= config.max_duration_ms + 300,
704 "auto-sized total {} ms should stay near max_duration_ms ({})",
705 total_ms,
706 config.max_duration_ms
707 );
708 }
709
710 #[test]
711 fn test_overshoot_skipped_for_short_moves() {
712 let config = SmartMouseConfig {
717 steps: 10,
718 overshoot: 0.5,
719 jitter: 0.0,
720 easing: false,
721 auto_size: false,
722 ..Default::default()
723 };
724
725 let path = generate_path(Point::new(0.0, 0.0), Point::new(150.0, 0.0), &config);
726 assert_eq!(path.len(), config.steps);
727 }
728
729 #[test]
730 fn test_overshoot_engages_for_long_moves() {
731 let config = SmartMouseConfig {
737 steps: 20,
738 overshoot: 0.5,
739 jitter: 0.0,
740 easing: false,
741 auto_size: false,
742 ..Default::default()
743 };
744
745 let target_x = 800.0;
746 let path = generate_path(Point::new(0.0, 0.0), Point::new(target_x, 0.0), &config);
747
748 let passed_target = path.iter().any(|s| s.point.x > target_x + 1.0);
749 assert!(
750 passed_target,
751 "long move with overshoot should pass the target before correcting back"
752 );
753
754 let last = path.last().expect("non-empty path");
756 assert!((last.point.x - target_x).abs() < 1e-9);
757 }
758
759 #[test]
760 fn test_pre_click_dwell_in_range() {
761 let mouse = SmartMouse::with_config(SmartMouseConfig {
762 pre_click_dwell_ms: Some((50, 100)),
763 ..Default::default()
764 });
765
766 for _ in 0..100 {
767 let dwell = mouse.pre_click_dwell().expect("dwell enabled");
768 let ms = dwell.as_millis() as u64;
769 assert!(
770 (50..=100).contains(&ms),
771 "dwell out of [50,100] range: {} ms",
772 ms
773 );
774 }
775 }
776
777 #[test]
778 fn test_pre_click_dwell_disabled() {
779 let mouse = SmartMouse::with_config(SmartMouseConfig {
780 pre_click_dwell_ms: None,
781 ..Default::default()
782 });
783 assert!(mouse.pre_click_dwell().is_none());
784
785 let mouse = SmartMouse::with_config(SmartMouseConfig {
786 pre_click_dwell_ms: Some((0, 0)),
787 ..Default::default()
788 });
789 assert!(mouse.pre_click_dwell().is_none());
790 }
791
792 #[test]
793 fn test_pre_click_dwell_fixed_when_min_eq_max() {
794 let mouse = SmartMouse::with_config(SmartMouseConfig {
795 pre_click_dwell_ms: Some((75, 75)),
796 ..Default::default()
797 });
798 let dwell = mouse.pre_click_dwell().expect("dwell enabled");
799 assert_eq!(dwell.as_millis(), 75);
800 }
801}