use geometry::Point;
use screen;
use std::fmt;
#[cfg(target_os = "macos")]
use core_graphics::event::{
CGEvent, CGEventTapLocation, CGEventType, CGMouseButton, ScrollEventUnit,
};
#[cfg(target_os = "macos")]
use core_graphics::event_source::CGEventSource;
#[cfg(target_os = "macos")]
use core_graphics::event_source::CGEventSourceStateID::HIDSystemState;
#[cfg(target_os = "macos")]
use core_graphics::geometry::CGPoint;
#[cfg(windows)]
use winapi::shared::minwindef::DWORD;
#[cfg(target_os = "linux")]
use internal;
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Button {
Left,
Middle,
Right,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ScrollDirection {
Up,
Down,
}
#[derive(Debug)]
pub enum MouseError {
OutOfBounds,
}
impl fmt::Display for MouseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MouseError::OutOfBounds => write!(f, "Out of bounds"),
}
}
}
impl std::error::Error for MouseError {}
pub fn smooth_move(destination: Point, duration: Option<f64>) -> Result<(), MouseError> {
if !screen::is_point_visible(destination) {
return Err(MouseError::OutOfBounds);
}
let start_position = location();
let distance = (start_position.x - destination.x).hypot(start_position.y - destination.y);
let step_count = distance.ceil() as i64;
let interval: u64 = duration
.map(|d| (d * 1000.0) / distance)
.unwrap_or(1.0)
.round() as u64;
for step in 1..=step_count {
let position = Point::new(
(destination.x - start_position.x) * (step as f64 / step_count as f64)
+ start_position.x,
(destination.y - start_position.y) * (step as f64 / step_count as f64)
+ start_position.y,
);
move_to(position)?;
std::thread::sleep(std::time::Duration::from_millis(interval));
}
Ok(())
}
pub fn click(button: Button, delay_ms: Option<u64>) {
toggle(button, true);
std::thread::sleep(std::time::Duration::from_millis(delay_ms.unwrap_or(100)));
toggle(button, false);
}
pub fn move_to(point: Point) -> Result<(), MouseError> {
if !screen::is_point_visible(point) {
Err(MouseError::OutOfBounds)
} else {
system_move_to(point);
Ok(())
}
}
pub fn location() -> Point {
system_location()
}
pub fn toggle(button: Button, down: bool) {
system_toggle(button, down);
}
pub fn scroll(direction: ScrollDirection, clicks: u32) {
system_scroll(direction, clicks);
}
#[cfg(target_os = "macos")]
impl Button {
fn event_type(self, down: bool) -> CGEventType {
use core_graphics::event::CGEventType::*;
match (self, down) {
(Button::Left, true) => LeftMouseDown,
(Button::Left, false) => LeftMouseUp,
(Button::Right, true) => RightMouseDown,
(Button::Right, false) => RightMouseUp,
(Button::Middle, true) => OtherMouseDown,
(Button::Middle, false) => OtherMouseUp,
}
}
}
#[cfg(target_os = "macos")]
impl From<Button> for CGMouseButton {
fn from(button: Button) -> CGMouseButton {
use core_graphics::event::CGMouseButton::*;
match button {
Button::Left => Left,
Button::Middle => Center,
Button::Right => Right,
}
}
}
#[cfg(target_os = "macos")]
fn system_move_to(point: Point) {
let point = CGPoint::from(point);
let source = CGEventSource::new(HIDSystemState).unwrap();
let event =
CGEvent::new_mouse_event(source, CGEventType::MouseMoved, point, CGMouseButton::Left);
event.unwrap().post(CGEventTapLocation::HID);
}
#[cfg(target_os = "macos")]
fn system_location() -> Point {
let source = CGEventSource::new(HIDSystemState).unwrap();
let event = CGEvent::new(source).unwrap();
Point::from(event.location())
}
#[cfg(target_os = "macos")]
fn system_toggle(button: Button, down: bool) {
let point = CGPoint::from(location());
let source = CGEventSource::new(HIDSystemState).unwrap();
let event_type = button.event_type(down);
let event = CGEvent::new_mouse_event(source, event_type, point, CGMouseButton::from(button));
event.unwrap().post(CGEventTapLocation::HID);
}
#[cfg(target_os = "macos")]
fn system_scroll(direction: ScrollDirection, clicks: u32) {
for _ in 0..clicks {
let wheel_count = if direction == ScrollDirection::Up {
10
} else {
-10
};
let source = CGEventSource::new(HIDSystemState).unwrap();
let event = CGEvent::new_scroll_event(source, ScrollEventUnit::LINE, 1, wheel_count, 0, 0);
event.unwrap().post(CGEventTapLocation::HID);
}
}
#[cfg(windows)]
fn mouse_event_for_button(button: Button, down: bool) -> DWORD {
use winapi::um::winuser::{
MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP,
MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP,
};
match (button, down) {
(Button::Left, true) => MOUSEEVENTF_LEFTDOWN,
(Button::Left, false) => MOUSEEVENTF_LEFTUP,
(Button::Right, true) => MOUSEEVENTF_RIGHTDOWN,
(Button::Right, false) => MOUSEEVENTF_RIGHTUP,
(Button::Middle, true) => MOUSEEVENTF_MIDDLEDOWN,
(Button::Middle, false) => MOUSEEVENTF_MIDDLEUP,
}
}
#[cfg(windows)]
fn system_move_to(point: Point) {
use winapi::ctypes::c_int;
use winapi::um::winuser::SetCursorPos;
let scaled_point = point.scaled(screen::scale()).round();
unsafe {
SetCursorPos(scaled_point.x as c_int, scaled_point.y as c_int);
};
}
#[cfg(windows)]
fn system_location() -> Point {
use winapi::shared::windef::POINT;
use winapi::um::winuser::GetCursorPos;
let mut point: POINT = POINT { x: 0, y: 0 };
unsafe {
GetCursorPos(&mut point);
}
Point::from_pixel(f64::from(point.x), f64::from(point.y), screen::scale())
}
#[cfg(windows)]
fn system_toggle(button: Button, down: bool) {
use winapi::um::winuser::mouse_event;
unsafe {
mouse_event(mouse_event_for_button(button, down), 0, 0, 0, 0);
};
}
#[cfg(windows)]
fn system_scroll(direction: ScrollDirection, clicks: u32) {
use winapi::um::winuser::{mouse_event, MOUSEEVENTF_WHEEL, WHEEL_DELTA};
let distance: DWORD = WHEEL_DELTA as DWORD * clicks as DWORD;
let units: DWORD = if direction == ScrollDirection::Up {
distance
} else {
u32::MAX - (distance - 1)
};
unsafe {
mouse_event(MOUSEEVENTF_WHEEL, 0, 0, units, 0);
};
}
#[cfg(target_os = "linux")]
impl From<Button> for XButton {
fn from(button: Button) -> XButton {
match button {
Button::Left => X_BUTTON_LEFT,
Button::Middle => X_BUTTON_MIDDLE,
Button::Right => X_BUTTON_RIGHT,
}
}
}
#[cfg(target_os = "linux")]
impl From<ScrollDirection> for XButton {
fn from(direction: ScrollDirection) -> XButton {
match direction {
ScrollDirection::Up => X_BUTTON_SCROLL_UP,
ScrollDirection::Down => X_BUTTON_SCROLL_DOWN,
}
}
}
#[cfg(target_os = "linux")]
fn system_move_to(point: Point) {
use scopeguard::guard;
internal::X_MAIN_DISPLAY.with(|display| unsafe {
let scaled_point = point.scaled(screen::scale()).round();
let root_window = guard(x11::xlib::XDefaultRootWindow(display.as_ptr()), |w| {
x11::xlib::XDestroyWindow(display.as_ptr(), w);
});
x11::xlib::XWarpPointer(
display.as_ptr(),
0,
*root_window,
0,
0,
0,
0,
scaled_point.x as i32,
scaled_point.y as i32,
);
x11::xlib::XFlush(display.as_ptr());
});
}
#[cfg(target_os = "linux")]
fn system_location() -> Point {
internal::X_MAIN_DISPLAY.with(|display| unsafe {
let root_window = x11::xlib::XDefaultRootWindow(display.as_ptr());
let mut x: i32 = 0;
let mut y: i32 = 0;
let mut unused_a: x11::xlib::Window = 0;
let mut unused_b: x11::xlib::Window = 0;
let mut unused_c: i32 = 0;
let mut unused_d: i32 = 0;
let mut unused_e: u32 = 0;
x11::xlib::XQueryPointer(
display.as_ptr(),
root_window,
&mut unused_a,
&mut unused_b,
&mut x,
&mut y,
&mut unused_c,
&mut unused_d,
&mut unused_e,
);
Point::from_pixel(f64::from(x), f64::from(y), screen::scale())
})
}
#[cfg(target_os = "linux")]
fn send_button_event(display: *mut x11::xlib::Display, button: XButton, down: bool) {
unsafe {
XTestFakeButtonEvent(display, button, down as i32, x11::xlib::CurrentTime);
x11::xlib::XFlush(display);
};
}
#[cfg(target_os = "linux")]
fn system_toggle(button: Button, down: bool) {
internal::X_MAIN_DISPLAY.with(|display| {
send_button_event(display.as_ptr(), XButton::from(button), down);
});
}
#[cfg(target_os = "linux")]
fn system_scroll(direction: ScrollDirection, clicks: u32) {
internal::X_MAIN_DISPLAY.with(|display| {
for _ in 0..clicks {
send_button_event(display.as_ptr(), XButton::from(direction), true);
send_button_event(display.as_ptr(), XButton::from(direction), false);
}
});
}
#[cfg(target_os = "linux")]
type XButton = u32;
#[cfg(target_os = "linux")]
const X_BUTTON_LEFT: XButton = 1;
#[cfg(target_os = "linux")]
const X_BUTTON_MIDDLE: XButton = 2;
#[cfg(target_os = "linux")]
const X_BUTTON_RIGHT: XButton = 3;
#[cfg(target_os = "linux")]
const X_BUTTON_SCROLL_UP: XButton = 4;
#[cfg(target_os = "linux")]
const X_BUTTON_SCROLL_DOWN: XButton = 5;
#[cfg(target_os = "linux")]
extern "C" {
fn XTestFakeButtonEvent(
display: *mut x11::xlib::Display,
button: u32,
is_press: i32,
delay: x11::xlib::Time,
) -> i32;
}
#[cfg(test)]
mod tests {
use geometry::Point;
use mouse;
use rand::{thread_rng, Rng};
use screen;
#[test]
fn test_move_to() {
let size = screen::size();
let scale = screen::scale();
let mut rng = thread_rng();
for _ in 0..100 {
let x: f64 = rng.gen_range(0.0, size.width - 1.0);
let y: f64 = rng.gen_range(0.0, size.height - 1.0);
let target = round_pt_nearest_hundredth(Point::new(x, y));
mouse::move_to(target).expect("mouse::move_to call failed");
std::thread::sleep(std::time::Duration::from_millis(10));
let result = mouse::location();
assert_eq!(
target.scaled(scale).round(),
result.scaled(scale).round(),
"{} does not map to same pixel as {} for scale {} at size {}",
target,
result,
scale,
size
);
}
}
fn round_nearest_hundredth(x: f64) -> f64 {
(x * 100.0).round() / 100.0
}
fn round_pt_nearest_hundredth(pt: Point) -> Point {
Point::new(round_nearest_hundredth(pt.x), round_nearest_hundredth(pt.y))
}
}