autopilot/
mouse.rs

1// Copyright 2018, 2019, 2020 Michael Sanders
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// https://apache.org/licenses/LICENSE-2.0> or the MIT License <LICENSE-MIT or
5// https://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7//
8//! This module contains functions for getting the current state of and
9//! controlling the mouse cursor.
10//!
11//! Unless otherwise stated, coordinates are those of a screen coordinate
12//! system, where the origin is at the top left.
13
14use geometry::Point;
15use screen;
16use std::fmt;
17
18#[cfg(target_os = "macos")]
19use core_graphics::event::{
20    CGEvent, CGEventTapLocation, CGEventType, CGMouseButton, ScrollEventUnit,
21};
22#[cfg(target_os = "macos")]
23use core_graphics::event_source::CGEventSource;
24#[cfg(target_os = "macos")]
25use core_graphics::event_source::CGEventSourceStateID::HIDSystemState;
26#[cfg(target_os = "macos")]
27use core_graphics::geometry::CGPoint;
28#[cfg(windows)]
29use winapi::shared::minwindef::DWORD;
30
31#[cfg(target_os = "linux")]
32use internal;
33
34#[derive(Copy, Clone, Debug, PartialEq)]
35pub enum Button {
36    Left,
37    Middle,
38    Right,
39}
40
41#[derive(Copy, Clone, Debug, PartialEq)]
42pub enum ScrollDirection {
43    Up,
44    Down,
45}
46
47#[derive(Debug)]
48pub enum MouseError {
49    OutOfBounds,
50}
51
52impl fmt::Display for MouseError {
53    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
54        match self {
55            MouseError::OutOfBounds => write!(f, "Out of bounds"),
56        }
57    }
58}
59
60impl std::error::Error for MouseError {}
61
62/// Gradually moves the mouse to a coordinate in a straight line in the given
63/// time frame (in seconds). If no duration is given a 1 millisecond delay is
64/// defaulted to between mouse movements.
65///
66/// Returns `MouseError` if coordinate is outside the screen boundaries.
67pub fn smooth_move(destination: Point, duration: Option<f64>) -> Result<(), MouseError> {
68    if !screen::is_point_visible(destination) {
69        return Err(MouseError::OutOfBounds);
70    }
71
72    let start_position = location();
73    let distance = (start_position.x - destination.x).hypot(start_position.y - destination.y);
74    let step_count = distance.ceil() as i64;
75    let interval: u64 = duration
76        .map(|d| (d * 1000.0) / distance)
77        .unwrap_or(1.0)
78        .round() as u64;
79
80    for step in 1..=step_count {
81        let position = Point::new(
82            (destination.x - start_position.x) * (step as f64 / step_count as f64)
83                + start_position.x,
84            (destination.y - start_position.y) * (step as f64 / step_count as f64)
85                + start_position.y,
86        );
87
88        move_to(position)?;
89        std::thread::sleep(std::time::Duration::from_millis(interval));
90    }
91
92    Ok(())
93}
94
95/// A convenience wrapper around `toggle()` that holds down and then releases
96/// the given mouse button. Delay between pressing and releasing the key can be
97/// controlled using the `delay_ms` parameter. If `delay` is not given, the
98/// value defaults to 100 ms.
99pub fn click(button: Button, delay_ms: Option<u64>) {
100    toggle(button, true);
101    std::thread::sleep(std::time::Duration::from_millis(delay_ms.unwrap_or(100)));
102    toggle(button, false);
103}
104
105/// Immediately moves the mouse to the given coordinate.
106///
107/// Returns `MouseError` if coordinate is outside the screen boundaries.
108pub fn move_to(point: Point) -> Result<(), MouseError> {
109    if !screen::is_point_visible(point) {
110        Err(MouseError::OutOfBounds)
111    } else {
112        system_move_to(point);
113        Ok(())
114    }
115}
116
117/// Returns the current position of the mouse cursor.
118pub fn location() -> Point {
119    system_location()
120}
121
122/// Holds down or releases a mouse button in the current position.
123pub fn toggle(button: Button, down: bool) {
124    system_toggle(button, down);
125}
126
127/// Performs a scroll event in a direction a given number of times.
128pub fn scroll(direction: ScrollDirection, clicks: u32) {
129    system_scroll(direction, clicks);
130}
131
132#[cfg(target_os = "macos")]
133impl Button {
134    fn event_type(self, down: bool) -> CGEventType {
135        use core_graphics::event::CGEventType::*;
136        match (self, down) {
137            (Button::Left, true) => LeftMouseDown,
138            (Button::Left, false) => LeftMouseUp,
139            (Button::Right, true) => RightMouseDown,
140            (Button::Right, false) => RightMouseUp,
141            (Button::Middle, true) => OtherMouseDown,
142            (Button::Middle, false) => OtherMouseUp,
143        }
144    }
145}
146
147#[cfg(target_os = "macos")]
148impl From<Button> for CGMouseButton {
149    fn from(button: Button) -> CGMouseButton {
150        use core_graphics::event::CGMouseButton::*;
151        match button {
152            Button::Left => Left,
153            Button::Middle => Center,
154            Button::Right => Right,
155        }
156    }
157}
158
159#[cfg(target_os = "macos")]
160fn system_move_to(point: Point) {
161    let point = CGPoint::from(point);
162    let source = CGEventSource::new(HIDSystemState).unwrap();
163    let event =
164        CGEvent::new_mouse_event(source, CGEventType::MouseMoved, point, CGMouseButton::Left);
165    event.unwrap().post(CGEventTapLocation::HID);
166}
167
168#[cfg(target_os = "macos")]
169fn system_location() -> Point {
170    let source = CGEventSource::new(HIDSystemState).unwrap();
171    let event = CGEvent::new(source).unwrap();
172    Point::from(event.location())
173}
174
175#[cfg(target_os = "macos")]
176fn system_toggle(button: Button, down: bool) {
177    let point = CGPoint::from(location());
178    let source = CGEventSource::new(HIDSystemState).unwrap();
179    let event_type = button.event_type(down);
180    let event = CGEvent::new_mouse_event(source, event_type, point, CGMouseButton::from(button));
181    event.unwrap().post(CGEventTapLocation::HID);
182}
183
184#[cfg(target_os = "macos")]
185fn system_scroll(direction: ScrollDirection, clicks: u32) {
186    for _ in 0..clicks {
187        let wheel_count = if direction == ScrollDirection::Up {
188            10
189        } else {
190            -10
191        };
192        let source = CGEventSource::new(HIDSystemState).unwrap();
193        let event = CGEvent::new_scroll_event(source, ScrollEventUnit::LINE, 1, wheel_count, 0, 0);
194        event.unwrap().post(CGEventTapLocation::HID);
195    }
196}
197
198#[cfg(windows)]
199fn mouse_event_for_button(button: Button, down: bool) -> DWORD {
200    use winapi::um::winuser::{
201        MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP,
202        MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP,
203    };
204    match (button, down) {
205        (Button::Left, true) => MOUSEEVENTF_LEFTDOWN,
206        (Button::Left, false) => MOUSEEVENTF_LEFTUP,
207        (Button::Right, true) => MOUSEEVENTF_RIGHTDOWN,
208        (Button::Right, false) => MOUSEEVENTF_RIGHTUP,
209        (Button::Middle, true) => MOUSEEVENTF_MIDDLEDOWN,
210        (Button::Middle, false) => MOUSEEVENTF_MIDDLEUP,
211    }
212}
213
214#[cfg(windows)]
215fn system_move_to(point: Point) {
216    use winapi::ctypes::c_int;
217    use winapi::um::winuser::SetCursorPos;
218    let scaled_point = point.scaled(screen::scale()).round();
219    unsafe {
220        SetCursorPos(scaled_point.x as c_int, scaled_point.y as c_int);
221    };
222}
223
224#[cfg(windows)]
225fn system_location() -> Point {
226    use winapi::shared::windef::POINT;
227    use winapi::um::winuser::GetCursorPos;
228    let mut point: POINT = POINT { x: 0, y: 0 };
229    unsafe {
230        GetCursorPos(&mut point);
231    }
232    Point::from_pixel(f64::from(point.x), f64::from(point.y), screen::scale())
233}
234
235#[cfg(windows)]
236fn system_toggle(button: Button, down: bool) {
237    use winapi::um::winuser::mouse_event;
238    unsafe {
239        mouse_event(mouse_event_for_button(button, down), 0, 0, 0, 0);
240    };
241}
242
243#[cfg(windows)]
244fn system_scroll(direction: ScrollDirection, clicks: u32) {
245    use winapi::um::winuser::{mouse_event, MOUSEEVENTF_WHEEL, WHEEL_DELTA};
246    let distance: DWORD = WHEEL_DELTA as DWORD * clicks as DWORD;
247    let units: DWORD = if direction == ScrollDirection::Up {
248        distance
249    } else {
250        u32::MAX - (distance - 1)
251    };
252    unsafe {
253        mouse_event(MOUSEEVENTF_WHEEL, 0, 0, units, 0);
254    };
255}
256
257#[cfg(target_os = "linux")]
258impl From<Button> for XButton {
259    fn from(button: Button) -> XButton {
260        match button {
261            Button::Left => X_BUTTON_LEFT,
262            Button::Middle => X_BUTTON_MIDDLE,
263            Button::Right => X_BUTTON_RIGHT,
264        }
265    }
266}
267
268#[cfg(target_os = "linux")]
269impl From<ScrollDirection> for XButton {
270    fn from(direction: ScrollDirection) -> XButton {
271        match direction {
272            ScrollDirection::Up => X_BUTTON_SCROLL_UP,
273            ScrollDirection::Down => X_BUTTON_SCROLL_DOWN,
274        }
275    }
276}
277
278#[cfg(target_os = "linux")]
279fn system_move_to(point: Point) {
280    use scopeguard::guard;
281    internal::X_MAIN_DISPLAY.with(|display| unsafe {
282        let scaled_point = point.scaled(screen::scale()).round();
283        let root_window = guard(x11::xlib::XDefaultRootWindow(display.as_ptr()), |w| {
284            x11::xlib::XDestroyWindow(display.as_ptr(), w);
285        });
286        x11::xlib::XWarpPointer(
287            display.as_ptr(),
288            0,
289            *root_window,
290            0,
291            0,
292            0,
293            0,
294            scaled_point.x as i32,
295            scaled_point.y as i32,
296        );
297        x11::xlib::XFlush(display.as_ptr());
298    });
299}
300
301#[cfg(target_os = "linux")]
302fn system_location() -> Point {
303    internal::X_MAIN_DISPLAY.with(|display| unsafe {
304        let root_window = x11::xlib::XDefaultRootWindow(display.as_ptr());
305        let mut x: i32 = 0;
306        let mut y: i32 = 0;
307        let mut unused_a: x11::xlib::Window = 0;
308        let mut unused_b: x11::xlib::Window = 0;
309        let mut unused_c: i32 = 0;
310        let mut unused_d: i32 = 0;
311        let mut unused_e: u32 = 0;
312        x11::xlib::XQueryPointer(
313            display.as_ptr(),
314            root_window,
315            &mut unused_a,
316            &mut unused_b,
317            &mut x,
318            &mut y,
319            &mut unused_c,
320            &mut unused_d,
321            &mut unused_e,
322        );
323        Point::from_pixel(f64::from(x), f64::from(y), screen::scale())
324    })
325}
326
327#[cfg(target_os = "linux")]
328fn send_button_event(display: *mut x11::xlib::Display, button: XButton, down: bool) {
329    unsafe {
330        XTestFakeButtonEvent(display, button, down as i32, x11::xlib::CurrentTime);
331        x11::xlib::XFlush(display);
332    };
333}
334
335#[cfg(target_os = "linux")]
336fn system_toggle(button: Button, down: bool) {
337    internal::X_MAIN_DISPLAY.with(|display| {
338        send_button_event(display.as_ptr(), XButton::from(button), down);
339    });
340}
341
342#[cfg(target_os = "linux")]
343fn system_scroll(direction: ScrollDirection, clicks: u32) {
344    internal::X_MAIN_DISPLAY.with(|display| {
345        for _ in 0..clicks {
346            send_button_event(display.as_ptr(), XButton::from(direction), true);
347            send_button_event(display.as_ptr(), XButton::from(direction), false);
348        }
349    });
350}
351
352#[cfg(target_os = "linux")]
353type XButton = u32;
354
355#[cfg(target_os = "linux")]
356const X_BUTTON_LEFT: XButton = 1;
357#[cfg(target_os = "linux")]
358const X_BUTTON_MIDDLE: XButton = 2;
359#[cfg(target_os = "linux")]
360const X_BUTTON_RIGHT: XButton = 3;
361#[cfg(target_os = "linux")]
362const X_BUTTON_SCROLL_UP: XButton = 4;
363#[cfg(target_os = "linux")]
364const X_BUTTON_SCROLL_DOWN: XButton = 5;
365
366#[cfg(target_os = "linux")]
367extern "C" {
368    fn XTestFakeButtonEvent(
369        display: *mut x11::xlib::Display,
370        button: u32,
371        is_press: i32,
372        delay: x11::xlib::Time,
373    ) -> i32;
374}
375
376#[cfg(test)]
377mod tests {
378    use geometry::Point;
379    use mouse;
380    use rand::{thread_rng, Rng};
381    use screen;
382
383    #[test]
384    fn test_move_to() {
385        let size = screen::size();
386        let scale = screen::scale();
387        let mut rng = thread_rng();
388        for _ in 0..100 {
389            let x: f64 = rng.gen_range(0.0, size.width - 1.0);
390            let y: f64 = rng.gen_range(0.0, size.height - 1.0);
391            let target = round_pt_nearest_hundredth(Point::new(x, y));
392            mouse::move_to(target).expect("mouse::move_to call failed");
393            std::thread::sleep(std::time::Duration::from_millis(10));
394            let result = mouse::location();
395            assert_eq!(
396                target.scaled(scale).round(),
397                result.scaled(scale).round(),
398                "{} does not map to same pixel as {} for scale {} at size {}",
399                target,
400                result,
401                scale,
402                size
403            );
404        }
405    }
406
407    fn round_nearest_hundredth(x: f64) -> f64 {
408        (x * 100.0).round() / 100.0
409    }
410
411    fn round_pt_nearest_hundredth(pt: Point) -> Point {
412        Point::new(round_nearest_hundredth(pt.x), round_nearest_hundredth(pt.y))
413    }
414}