1use 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
62pub 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
95pub 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
105pub 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
117pub fn location() -> Point {
119 system_location()
120}
121
122pub fn toggle(button: Button, down: bool) {
124 system_toggle(button, down);
125}
126
127pub 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}