use super::{error::EmulationError, Emulation, EmulationHandle}; use async_trait::async_trait; use core_graphics::base::CGFloat; use core_graphics::display::{ CGDirectDisplayID, CGDisplayBounds, CGGetDisplaysWithRect, CGPoint, CGRect, CGSize, }; use core_graphics::event::{ CGEvent, CGEventTapLocation, CGEventType, CGKeyCode, CGMouseButton, EventField, ScrollEventUnit, }; use core_graphics::event_source::{CGEventSource, CGEventSourceStateID}; use input_event::{Event, KeyboardEvent, PointerEvent}; use keycode::{KeyMap, KeyMapping}; use std::ops::{Index, IndexMut}; use std::sync::Arc; use std::time::Duration; use tokio::{sync::Notify, task::JoinHandle}; use super::error::MacOSEmulationCreationError; const DEFAULT_REPEAT_DELAY: Duration = Duration::from_millis(500); const DEFAULT_REPEAT_INTERVAL: Duration = Duration::from_millis(32); pub(crate) struct MacOSEmulation { event_source: CGEventSource, repeat_task: Option>, button_state: ButtonState, notify_repeat_task: Arc, } struct ButtonState { left: bool, right: bool, center: bool, } impl Index for ButtonState { type Output = bool; fn index(&self, index: CGMouseButton) -> &Self::Output { match index { CGMouseButton::Left => &self.left, CGMouseButton::Right => &self.right, CGMouseButton::Center => &self.center, } } } impl IndexMut for ButtonState { fn index_mut(&mut self, index: CGMouseButton) -> &mut Self::Output { match index { CGMouseButton::Left => &mut self.left, CGMouseButton::Right => &mut self.right, CGMouseButton::Center => &mut self.center, } } } unsafe impl Send for MacOSEmulation {} impl MacOSEmulation { pub(crate) fn new() -> Result { let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState) .map_err(|_| MacOSEmulationCreationError::EventSourceCreation)?; let button_state = ButtonState { left: false, right: false, center: false, }; Ok(Self { event_source, button_state, repeat_task: None, notify_repeat_task: Arc::new(Notify::new()), }) } fn get_mouse_location(&self) -> Option { let event: CGEvent = CGEvent::new(self.event_source.clone()).ok()?; Some(event.location()) } async fn spawn_repeat_task(&mut self, key: u16) { // there can only be one repeating key and it's // always the last to be pressed self.cancel_repeat_task().await; let event_source = self.event_source.clone(); let notify = self.notify_repeat_task.clone(); let repeat_task = tokio::task::spawn_local(async move { let stop = tokio::select! { _ = tokio::time::sleep(DEFAULT_REPEAT_DELAY) => false, _ = notify.notified() => true, }; if !stop { loop { key_event(event_source.clone(), key, 1); tokio::select! { _ = tokio::time::sleep(DEFAULT_REPEAT_INTERVAL) => {}, _ = notify.notified() => break, } } } // release key when cancelled key_event(event_source.clone(), key, 0); }); self.repeat_task = Some(repeat_task); } async fn cancel_repeat_task(&mut self) { if let Some(task) = self.repeat_task.take() { self.notify_repeat_task.notify_waiters(); let _ = task.await; } } } fn key_event(event_source: CGEventSource, key: u16, state: u8) { let event = match CGEvent::new_keyboard_event(event_source, key, state != 0) { Ok(e) => e, Err(_) => { log::warn!("unable to create key event"); return; } }; event.post(CGEventTapLocation::HID); } fn get_display_at_point(x: CGFloat, y: CGFloat) -> Option { let mut displays: [CGDirectDisplayID; 16] = [0; 16]; let mut display_count: u32 = 0; let rect = CGRect::new(&CGPoint::new(x, y), &CGSize::new(0.0, 0.0)); let error = unsafe { CGGetDisplaysWithRect( rect, 1, displays.as_mut_ptr(), &mut display_count as *mut u32, ) }; if error != 0 { log::warn!("error getting displays at point ({}, {}): {}", x, y, error); return Option::None; } if display_count == 0 { log::debug!("no displays found at point ({}, {})", x, y); return Option::None; } return displays.first().copied(); } fn get_display_bounds(display: CGDirectDisplayID) -> (CGFloat, CGFloat, CGFloat, CGFloat) { unsafe { let bounds = CGDisplayBounds(display); let min_x = bounds.origin.x; let max_x = bounds.origin.x + bounds.size.width; let min_y = bounds.origin.y; let max_y = bounds.origin.y + bounds.size.height; (min_x as f64, min_y as f64, max_x as f64, max_y as f64) } } fn clamp_to_screen_space( current_x: CGFloat, current_y: CGFloat, dx: CGFloat, dy: CGFloat, ) -> (CGFloat, CGFloat) { // Check which display the mouse is currently on // Determine what the location of the mouse would be after applying the move // Get the display at the new location // If the point is not on a display // Clamp the mouse to the current display // Else If the point is on a display // Clamp the mouse to the new display let current_display = match get_display_at_point(current_x, current_y) { Some(display) => display, None => { log::warn!("could not get current display!"); return (current_x, current_y); } }; let new_x = current_x + dx; let new_y = current_y + dy; let final_display = get_display_at_point(new_x, new_y).unwrap_or(current_display); let (min_x, min_y, max_x, max_y) = get_display_bounds(final_display); ( new_x.clamp(min_x, max_x - 1.), new_y.clamp(min_y, max_y - 1.), ) } #[async_trait] impl Emulation for MacOSEmulation { async fn consume( &mut self, event: Event, _handle: EmulationHandle, ) -> Result<(), EmulationError> { match event { Event::Pointer(pointer_event) => match pointer_event { PointerEvent::Motion { time: _, dx, dy } => { let mut mouse_location = match self.get_mouse_location() { Some(l) => l, None => { log::warn!("could not get mouse location!"); return Ok(()); } }; let (new_mouse_x, new_mouse_y) = clamp_to_screen_space(mouse_location.x, mouse_location.y, dx, dy); mouse_location.x = new_mouse_x; mouse_location.y = new_mouse_y; let mut event_type = CGEventType::MouseMoved; if self.button_state.left { event_type = CGEventType::LeftMouseDragged } else if self.button_state.right { event_type = CGEventType::RightMouseDragged } else if self.button_state.center { event_type = CGEventType::OtherMouseDragged }; let event = match CGEvent::new_mouse_event( self.event_source.clone(), event_type, mouse_location, CGMouseButton::Left, ) { Ok(e) => e, Err(_) => { log::warn!("mouse event creation failed!"); return Ok(()); } }; event.set_integer_value_field(EventField::MOUSE_EVENT_DELTA_X, dx as i64); event.set_integer_value_field(EventField::MOUSE_EVENT_DELTA_Y, dy as i64); event.post(CGEventTapLocation::HID); } PointerEvent::Button { time: _, button, state, } => { let (event_type, mouse_button) = match (button, state) { (b, 1) if b == input_event::BTN_LEFT => { (CGEventType::LeftMouseDown, CGMouseButton::Left) } (b, 0) if b == input_event::BTN_LEFT => { (CGEventType::LeftMouseUp, CGMouseButton::Left) } (b, 1) if b == input_event::BTN_RIGHT => { (CGEventType::RightMouseDown, CGMouseButton::Right) } (b, 0) if b == input_event::BTN_RIGHT => { (CGEventType::RightMouseUp, CGMouseButton::Right) } (b, 1) if b == input_event::BTN_MIDDLE => { (CGEventType::OtherMouseDown, CGMouseButton::Center) } (b, 0) if b == input_event::BTN_MIDDLE => { (CGEventType::OtherMouseUp, CGMouseButton::Center) } _ => { log::warn!("invalid button event: {button},{state}"); return Ok(()); } }; // store button state self.button_state[mouse_button] = state == 1; let location = self.get_mouse_location().unwrap(); let event = match CGEvent::new_mouse_event( self.event_source.clone(), event_type, location, mouse_button, ) { Ok(e) => e, Err(()) => { log::warn!("mouse event creation failed!"); return Ok(()); } }; event.post(CGEventTapLocation::HID); } PointerEvent::Axis { time: _, axis, value, } => { let value = value as i32; let (count, wheel1, wheel2, wheel3) = match axis { 0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis) 1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x) _ => { log::warn!("invalid scroll event: {axis}, {value}"); return Ok(()); } }; let event = match CGEvent::new_scroll_event( self.event_source.clone(), ScrollEventUnit::PIXEL, count, wheel1, wheel2, wheel3, ) { Ok(e) => e, Err(()) => { log::warn!("scroll event creation failed!"); return Ok(()); } }; event.post(CGEventTapLocation::HID); } PointerEvent::AxisDiscrete120 { axis, value } => { let (count, wheel1, wheel2, wheel3) = match axis { 0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis) 1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x) _ => { log::warn!("invalid scroll event: {axis}, {value}"); return Ok(()); } }; let event = match CGEvent::new_scroll_event( self.event_source.clone(), ScrollEventUnit::PIXEL, count, wheel1, wheel2, wheel3, ) { Ok(e) => e, Err(()) => { log::warn!("scroll event creation failed!"); return Ok(()); } }; event.post(CGEventTapLocation::HID); } }, Event::Keyboard(keyboard_event) => match keyboard_event { KeyboardEvent::Key { time: _, key, state, } => { let code = match KeyMap::from_key_mapping(KeyMapping::Evdev(key as u16)) { Ok(k) => k.mac as CGKeyCode, Err(_) => { log::warn!("unable to map key event"); return Ok(()); } }; match state { // pressed 1 => self.spawn_repeat_task(code).await, _ => self.cancel_repeat_task().await, } key_event(self.event_source.clone(), code, state) } KeyboardEvent::Modifiers { .. } => {} }, } // FIXME Ok(()) } async fn create(&mut self, _handle: EmulationHandle) {} async fn destroy(&mut self, _handle: EmulationHandle) {} async fn terminate(&mut self) {} }