489 lines
16 KiB
Rust
489 lines
16 KiB
Rust
use core_graphics;
|
|
|
|
// TODO(dustin): use only the things i need
|
|
|
|
use self::core_graphics::display::*;
|
|
use self::core_graphics::event::*;
|
|
use self::core_graphics::event_source::*;
|
|
|
|
use crate::macos::keycodes::*;
|
|
use crate::{Key, KeyboardControllable, MouseButton, MouseControllable};
|
|
use objc::runtime::Class;
|
|
|
|
struct MyCGEvent;
|
|
|
|
#[allow(improper_ctypes)]
|
|
#[allow(non_snake_case)]
|
|
#[link(name = "ApplicationServices", kind = "framework")]
|
|
extern "C" {
|
|
fn CGEventPost(tapLocation: CGEventTapLocation, event: *mut MyCGEvent);
|
|
// not present in servo/core-graphics
|
|
fn CGEventCreateScrollWheelEvent(
|
|
source: &CGEventSourceRef,
|
|
units: ScrollUnit,
|
|
wheelCount: u32,
|
|
wheel1: i32,
|
|
...
|
|
) -> *mut MyCGEvent;
|
|
fn CGEventSourceKeyState(stateID: i32, key: u16) -> bool;
|
|
}
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy)]
|
|
struct NSPoint {
|
|
x: f64,
|
|
y: f64,
|
|
}
|
|
|
|
// not present in servo/core-graphics
|
|
#[allow(dead_code)]
|
|
#[derive(Debug)]
|
|
enum ScrollUnit {
|
|
Pixel = 0,
|
|
Line = 1,
|
|
}
|
|
// hack
|
|
|
|
/// The main struct for handling the event emitting
|
|
pub struct Enigo {
|
|
event_source: Option<CGEventSource>,
|
|
double_click_interval: u32,
|
|
last_click_time: Option<std::time::Instant>,
|
|
multiple_click: i64,
|
|
flags: CGEventFlags,
|
|
}
|
|
|
|
impl Enigo {
|
|
///
|
|
pub fn reset_flag(&mut self) {
|
|
self.flags = CGEventFlags::CGEventFlagNull;
|
|
}
|
|
|
|
///
|
|
pub fn add_flag(&mut self, key: &Key) {
|
|
let flag = match key {
|
|
&Key::CapsLock => CGEventFlags::CGEventFlagAlphaShift,
|
|
&Key::Shift => CGEventFlags::CGEventFlagShift,
|
|
&Key::Control => CGEventFlags::CGEventFlagControl,
|
|
&Key::Alt => CGEventFlags::CGEventFlagAlternate,
|
|
&Key::Meta => CGEventFlags::CGEventFlagCommand,
|
|
&Key::NumLock => CGEventFlags::CGEventFlagNumericPad,
|
|
_ => CGEventFlags::CGEventFlagNull,
|
|
};
|
|
self.flags |= flag;
|
|
}
|
|
|
|
fn post(&self, event: CGEvent) {
|
|
event.set_flags(self.flags);
|
|
event.post(CGEventTapLocation::HID);
|
|
}
|
|
}
|
|
|
|
impl Default for Enigo {
|
|
fn default() -> Self {
|
|
let mut double_click_interval = 500;
|
|
if let Some(ns_event) = Class::get("NSEvent") {
|
|
let tm: f64 = unsafe { msg_send![ns_event, doubleClickInterval] };
|
|
if tm > 0. {
|
|
double_click_interval = (tm * 1000.) as u32;
|
|
log::info!("double click interval: {}ms", double_click_interval);
|
|
}
|
|
}
|
|
Self {
|
|
// TODO(dustin): return error rather than panic here
|
|
event_source: if let Ok(src) =
|
|
CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
|
|
{
|
|
Some(src)
|
|
} else {
|
|
None
|
|
},
|
|
double_click_interval,
|
|
multiple_click: 1,
|
|
last_click_time: None,
|
|
flags: CGEventFlags::CGEventFlagNull,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl MouseControllable for Enigo {
|
|
fn mouse_move_to(&mut self, x: i32, y: i32) {
|
|
let pressed = Self::pressed_buttons();
|
|
|
|
let event_type = if pressed & 1 > 0 {
|
|
CGEventType::LeftMouseDragged
|
|
} else if pressed & 2 > 0 {
|
|
CGEventType::RightMouseDragged
|
|
} else {
|
|
CGEventType::MouseMoved
|
|
};
|
|
|
|
let dest = CGPoint::new(x as f64, y as f64);
|
|
if let Some(src) = self.event_source.as_ref() {
|
|
if let Ok(event) =
|
|
CGEvent::new_mouse_event(src.clone(), event_type, dest, CGMouseButton::Left)
|
|
{
|
|
self.post(event);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn mouse_move_relative(&mut self, x: i32, y: i32) {
|
|
let (display_width, display_height) = Self::main_display_size();
|
|
let (current_x, y_inv) = Self::mouse_location_raw_coords();
|
|
let current_y = (display_height as i32) - y_inv;
|
|
let new_x = current_x + x;
|
|
let new_y = current_y + y;
|
|
|
|
if new_x < 0
|
|
|| new_x as usize > display_width
|
|
|| new_y < 0
|
|
|| new_y as usize > display_height
|
|
{
|
|
return;
|
|
}
|
|
|
|
self.mouse_move_to(new_x, new_y);
|
|
}
|
|
|
|
fn mouse_down(&mut self, button: MouseButton) -> crate::ResultType {
|
|
let now = std::time::Instant::now();
|
|
if let Some(t) = self.last_click_time {
|
|
if t.elapsed().as_millis() as u32 <= self.double_click_interval {
|
|
self.multiple_click += 1;
|
|
} else {
|
|
self.multiple_click = 1;
|
|
}
|
|
}
|
|
self.last_click_time = Some(now);
|
|
let (current_x, current_y) = Self::mouse_location();
|
|
let (button, event_type) = match button {
|
|
MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown),
|
|
MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown),
|
|
MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown),
|
|
_ => unimplemented!(),
|
|
};
|
|
let dest = CGPoint::new(current_x as f64, current_y as f64);
|
|
if let Some(src) = self.event_source.as_ref() {
|
|
if let Ok(event) = CGEvent::new_mouse_event(src.clone(), event_type, dest, button) {
|
|
if self.multiple_click > 1 {
|
|
event.set_integer_value_field(
|
|
EventField::MOUSE_EVENT_CLICK_STATE,
|
|
self.multiple_click,
|
|
);
|
|
}
|
|
self.post(event);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn mouse_up(&mut self, button: MouseButton) {
|
|
let (current_x, current_y) = Self::mouse_location();
|
|
let (button, event_type) = match button {
|
|
MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp),
|
|
MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp),
|
|
MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseUp),
|
|
_ => unimplemented!(),
|
|
};
|
|
let dest = CGPoint::new(current_x as f64, current_y as f64);
|
|
if let Some(src) = self.event_source.as_ref() {
|
|
if let Ok(event) = CGEvent::new_mouse_event(src.clone(), event_type, dest, button) {
|
|
if self.multiple_click > 1 {
|
|
event.set_integer_value_field(
|
|
EventField::MOUSE_EVENT_CLICK_STATE,
|
|
self.multiple_click,
|
|
);
|
|
}
|
|
self.post(event);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn mouse_click(&mut self, button: MouseButton) {
|
|
self.mouse_down(button).ok();
|
|
self.mouse_up(button);
|
|
}
|
|
|
|
fn mouse_scroll_x(&mut self, length: i32) {
|
|
let mut scroll_direction = -1; // 1 left -1 right;
|
|
let mut length = length;
|
|
|
|
if length < 0 {
|
|
length *= -1;
|
|
scroll_direction *= -1;
|
|
}
|
|
|
|
if let Some(src) = self.event_source.as_ref() {
|
|
for _ in 0..length {
|
|
unsafe {
|
|
let mouse_ev = CGEventCreateScrollWheelEvent(
|
|
&src,
|
|
ScrollUnit::Line,
|
|
2, // CGWheelCount 1 = y 2 = xy 3 = xyz
|
|
0,
|
|
scroll_direction,
|
|
);
|
|
|
|
CGEventPost(CGEventTapLocation::HID, mouse_ev);
|
|
CFRelease(mouse_ev as *const std::ffi::c_void);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn mouse_scroll_y(&mut self, length: i32) {
|
|
let mut scroll_direction = -1; // 1 left -1 right;
|
|
let mut length = length;
|
|
|
|
if length < 0 {
|
|
length *= -1;
|
|
scroll_direction *= -1;
|
|
}
|
|
|
|
if let Some(src) = self.event_source.as_ref() {
|
|
for _ in 0..length {
|
|
unsafe {
|
|
let mouse_ev = CGEventCreateScrollWheelEvent(
|
|
&src,
|
|
ScrollUnit::Line,
|
|
1, // CGWheelCount 1 = y 2 = xy 3 = xyz
|
|
scroll_direction,
|
|
);
|
|
|
|
CGEventPost(CGEventTapLocation::HID, mouse_ev);
|
|
CFRelease(mouse_ev as *const std::ffi::c_void);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// https://stackoverflow.
|
|
// com/questions/1918841/how-to-convert-ascii-character-to-cgkeycode
|
|
|
|
impl KeyboardControllable for Enigo {
|
|
fn key_sequence(&mut self, sequence: &str) {
|
|
// NOTE(dustin): This is a fix for issue https://github.com/enigo-rs/enigo/issues/68
|
|
// TODO(dustin): This could be improved by aggregating 20 bytes worth of graphemes at a time
|
|
// but i am unsure what would happen for grapheme clusters greater than 20 bytes ...
|
|
use unicode_segmentation::UnicodeSegmentation;
|
|
let clusters = UnicodeSegmentation::graphemes(sequence, true).collect::<Vec<&str>>();
|
|
for cluster in clusters {
|
|
if let Some(src) = self.event_source.as_ref() {
|
|
if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), 0, true) {
|
|
event.set_string(cluster);
|
|
self.post(event);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn key_click(&mut self, key: Key) {
|
|
let keycode = self.key_to_keycode(key);
|
|
if keycode == 0 {
|
|
return;
|
|
}
|
|
|
|
if let Some(src) = self.event_source.as_ref() {
|
|
if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), keycode, true) {
|
|
self.post(event);
|
|
}
|
|
|
|
if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), keycode, false) {
|
|
self.post(event);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn key_down(&mut self, key: Key) -> crate::ResultType {
|
|
if let Some(src) = self.event_source.as_ref() {
|
|
if let Ok(event) =
|
|
CGEvent::new_keyboard_event(src.clone(), self.key_to_keycode(key), true)
|
|
{
|
|
self.post(event);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn key_up(&mut self, key: Key) {
|
|
if let Some(src) = self.event_source.as_ref() {
|
|
if let Ok(event) =
|
|
CGEvent::new_keyboard_event(src.clone(), self.key_to_keycode(key), false)
|
|
{
|
|
self.post(event);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_key_state(&mut self, key: Key) -> bool {
|
|
let keycode = self.key_to_keycode(key);
|
|
unsafe { CGEventSourceKeyState(1, keycode) }
|
|
}
|
|
}
|
|
|
|
impl Enigo {
|
|
fn pressed_buttons() -> usize {
|
|
if let Some(ns_event) = Class::get("NSEvent") {
|
|
unsafe { msg_send![ns_event, pressedMouseButtons] }
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
/// Fetches the `(width, height)` in pixels of the main display
|
|
pub fn main_display_size() -> (usize, usize) {
|
|
let display_id = unsafe { CGMainDisplayID() };
|
|
let width = unsafe { CGDisplayPixelsWide(display_id) };
|
|
let height = unsafe { CGDisplayPixelsHigh(display_id) };
|
|
(width, height)
|
|
}
|
|
|
|
/// Returns the current mouse location in Cocoa coordinates which have Y
|
|
/// inverted from the Carbon coordinates used in the rest of the API.
|
|
/// This function exists so that mouse_move_relative only has to fetch
|
|
/// the screen size once.
|
|
fn mouse_location_raw_coords() -> (i32, i32) {
|
|
if let Some(ns_event) = Class::get("NSEvent") {
|
|
let pt: NSPoint = unsafe { msg_send![ns_event, mouseLocation] };
|
|
(pt.x as i32, pt.y as i32)
|
|
} else {
|
|
(0, 0)
|
|
}
|
|
}
|
|
|
|
/// The mouse coordinates in points, only works on the main display
|
|
pub fn mouse_location() -> (i32, i32) {
|
|
let (x, y_inv) = Self::mouse_location_raw_coords();
|
|
let (_, display_height) = Self::main_display_size();
|
|
(x, (display_height as i32) - y_inv)
|
|
}
|
|
|
|
fn key_to_keycode(&mut self, key: Key) -> CGKeyCode {
|
|
#[allow(deprecated)]
|
|
// I mean duh, we still need to support deprecated keys until they're removed
|
|
match key {
|
|
Key::Alt => kVK_Option,
|
|
Key::Backspace => kVK_Delete,
|
|
Key::CapsLock => kVK_CapsLock,
|
|
Key::Control => kVK_Control,
|
|
Key::Delete => kVK_ForwardDelete,
|
|
Key::DownArrow => kVK_DownArrow,
|
|
Key::End => kVK_End,
|
|
Key::Escape => kVK_Escape,
|
|
Key::F1 => kVK_F1,
|
|
Key::F10 => kVK_F10,
|
|
Key::F11 => kVK_F11,
|
|
Key::F12 => kVK_F12,
|
|
Key::F2 => kVK_F2,
|
|
Key::F3 => kVK_F3,
|
|
Key::F4 => kVK_F4,
|
|
Key::F5 => kVK_F5,
|
|
Key::F6 => kVK_F6,
|
|
Key::F7 => kVK_F7,
|
|
Key::F8 => kVK_F8,
|
|
Key::F9 => kVK_F9,
|
|
Key::Home => kVK_Home,
|
|
Key::LeftArrow => kVK_LeftArrow,
|
|
Key::Option => kVK_Option,
|
|
Key::PageDown => kVK_PageDown,
|
|
Key::PageUp => kVK_PageUp,
|
|
Key::Return => kVK_Return,
|
|
Key::RightArrow => kVK_RightArrow,
|
|
Key::Shift => kVK_Shift,
|
|
Key::Space => kVK_Space,
|
|
Key::Tab => kVK_Tab,
|
|
Key::UpArrow => kVK_UpArrow,
|
|
Key::Numpad0 => kVK_ANSI_Keypad0,
|
|
Key::Numpad1 => kVK_ANSI_Keypad1,
|
|
Key::Numpad2 => kVK_ANSI_Keypad2,
|
|
Key::Numpad3 => kVK_ANSI_Keypad3,
|
|
Key::Numpad4 => kVK_ANSI_Keypad4,
|
|
Key::Numpad5 => kVK_ANSI_Keypad5,
|
|
Key::Numpad6 => kVK_ANSI_Keypad6,
|
|
Key::Numpad7 => kVK_ANSI_Keypad7,
|
|
Key::Numpad8 => kVK_ANSI_Keypad8,
|
|
Key::Numpad9 => kVK_ANSI_Keypad9,
|
|
Key::Mute => kVK_Mute,
|
|
Key::VolumeDown => kVK_VolumeUp,
|
|
Key::VolumeUp => kVK_VolumeDown,
|
|
Key::Help => kVK_Help,
|
|
Key::Snapshot => kVK_F13,
|
|
Key::Clear => kVK_ANSI_KeypadClear,
|
|
Key::Decimal => kVK_ANSI_KeypadDecimal,
|
|
Key::Multiply => kVK_ANSI_KeypadMultiply,
|
|
Key::Add => kVK_ANSI_KeypadPlus,
|
|
Key::Divide => kVK_ANSI_KeypadDivide,
|
|
Key::NumpadEnter => kVK_ANSI_KeypadEnter,
|
|
Key::Subtract => kVK_ANSI_KeypadMinus,
|
|
Key::Equals => kVK_ANSI_KeypadEquals,
|
|
Key::NumLock => kVK_ANSI_KeypadClear,
|
|
Key::RWin => kVK_RIGHT_COMMAND,
|
|
Key::RightShift => kVK_RightShift,
|
|
Key::RightControl => kVK_RightControl,
|
|
Key::RightAlt => kVK_RightOption,
|
|
|
|
Key::Raw(raw_keycode) => raw_keycode,
|
|
Key::Layout(c) => self.map_key_board(c),
|
|
|
|
Key::Super | Key::Command | Key::Windows | Key::Meta => kVK_Command,
|
|
_ => 0,
|
|
}
|
|
}
|
|
fn map_key_board(&self, ch: char) -> CGKeyCode {
|
|
match ch {
|
|
'a' => kVK_ANSI_A,
|
|
'b' => kVK_ANSI_B,
|
|
'c' => kVK_ANSI_C,
|
|
'd' => kVK_ANSI_D,
|
|
'e' => kVK_ANSI_E,
|
|
'f' => kVK_ANSI_F,
|
|
'g' => kVK_ANSI_G,
|
|
'h' => kVK_ANSI_H,
|
|
'i' => kVK_ANSI_I,
|
|
'j' => kVK_ANSI_J,
|
|
'k' => kVK_ANSI_K,
|
|
'l' => kVK_ANSI_L,
|
|
'm' => kVK_ANSI_M,
|
|
'n' => kVK_ANSI_N,
|
|
'o' => kVK_ANSI_O,
|
|
'p' => kVK_ANSI_P,
|
|
'q' => kVK_ANSI_Q,
|
|
'r' => kVK_ANSI_R,
|
|
's' => kVK_ANSI_S,
|
|
't' => kVK_ANSI_T,
|
|
'u' => kVK_ANSI_U,
|
|
'v' => kVK_ANSI_V,
|
|
'w' => kVK_ANSI_W,
|
|
'x' => kVK_ANSI_X,
|
|
'y' => kVK_ANSI_Y,
|
|
'z' => kVK_ANSI_Z,
|
|
'0' => kVK_ANSI_0,
|
|
'1' => kVK_ANSI_1,
|
|
'2' => kVK_ANSI_2,
|
|
'3' => kVK_ANSI_3,
|
|
'4' => kVK_ANSI_4,
|
|
'5' => kVK_ANSI_5,
|
|
'6' => kVK_ANSI_6,
|
|
'7' => kVK_ANSI_7,
|
|
'8' => kVK_ANSI_8,
|
|
'9' => kVK_ANSI_9,
|
|
'-' => kVK_ANSI_Minus,
|
|
'=' => kVK_ANSI_Equal,
|
|
'[' => kVK_ANSI_LeftBracket,
|
|
']' => kVK_ANSI_RightBracket,
|
|
'\\' => kVK_ANSI_Backslash,
|
|
';' => kVK_ANSI_Semicolon,
|
|
'\'' => kVK_ANSI_Quote,
|
|
',' => kVK_ANSI_Comma,
|
|
'.' => kVK_ANSI_Period,
|
|
'/' => kVK_ANSI_Slash,
|
|
'`' => kVK_ANSI_Grave,
|
|
_ => 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
unsafe impl Send for Enigo {}
|