use core::ptr::NonNull;
use log::{error, info};
use objc2::rc::Retained;
use objc2_app_kit::{NSEvent, NSEventType, NSTouch, NSTouchPhase};
use objc2_core_foundation::{CFMachPort, CFRetained, CFRunLoop, kCFRunLoopCommonModes};
use objc2_core_graphics::{
CGEvent, CGEventField, CGEventFlags, CGEventTapLocation, CGEventTapOptions,
CGEventTapPlacement, CGEventTapProxy, CGEventType,
};
use objc2_foundation::NSSet;
use scopeguard::ScopeGuard;
use std::ffi::c_void;
use std::marker::PhantomPinned;
use std::pin::Pin;
use std::ptr::null_mut;
use stdext::function_name;
use crate::config::Config;
use crate::errors::{Error, Result};
use crate::events::{Event, EventSender};
use crate::platform::Modifiers;
pub(super) struct InputHandler {
events: Option<EventSender>,
config: Config,
finger_position: Option<Retained<NSSet<NSTouch>>>,
tap_port: Option<CFRetained<CFMachPort>>,
_pin: PhantomPinned,
}
pub type PinnedInputHandler =
ScopeGuard<Pin<Box<InputHandler>>, Box<dyn FnOnce(Pin<Box<InputHandler>>)>>;
impl InputHandler {
pub(super) fn new(events: EventSender, config: Config) -> Self {
InputHandler {
events: Some(events),
config,
finger_position: None,
tap_port: None,
_pin: PhantomPinned,
}
}
pub(super) fn start(self) -> Result<PinnedInputHandler> {
let mouse_event_mask = (1 << CGEventType::MouseMoved.0)
| (1 << CGEventType::LeftMouseDown.0)
| (1 << CGEventType::LeftMouseUp.0)
| (1 << CGEventType::LeftMouseDragged.0)
| (1 << CGEventType::RightMouseDown.0)
| (1 << CGEventType::RightMouseUp.0)
| (1 << CGEventType::RightMouseDragged.0)
| (1 << NSEventType::Gesture.0)
| (1 << CGEventType::KeyDown.0);
let mut pinned = Box::pin(self);
let this = unsafe { NonNull::new_unchecked(pinned.as_mut().get_unchecked_mut()) }.as_ptr();
unsafe {
(*this).tap_port = CGEvent::tap_create(
CGEventTapLocation::HIDEventTap,
CGEventTapPlacement::HeadInsertEventTap,
CGEventTapOptions::Default,
mouse_event_mask,
Some(Self::callback),
this.cast(),
);
};
if pinned.tap_port.is_none() {
return Err(Error::PermissionDenied(format!(
"{}: Can not create EventTap.",
function_name!()
)));
}
let (run_loop_source, main_loop) =
CFMachPort::new_run_loop_source(None, pinned.tap_port.as_deref(), 0)
.zip(CFRunLoop::main())
.ok_or(Error::PermissionDenied(format!(
"{}: Unable to create run loop source",
function_name!()
)))?;
let loop_mode = unsafe { kCFRunLoopCommonModes };
CFRunLoop::add_source(&main_loop, Some(&run_loop_source), loop_mode);
let port = pinned
.tap_port
.clone()
.ok_or(Error::PermissionDenied(format!(
"{}: invalid tap port.",
function_name!()
)))?;
Ok(scopeguard::guard(
pinned,
Box::new(move |_: Pin<Box<Self>>| {
info!("{}: Unregistering event_handler", function_name!());
CFRunLoop::remove_source(&main_loop, Some(&run_loop_source), loop_mode);
CFMachPort::invalidate(&port);
CGEvent::tap_enable(&port, false);
}),
))
}
extern "C-unwind" fn callback(
_: CGEventTapProxy,
event_type: CGEventType,
mut event_ref: NonNull<CGEvent>,
this: *mut c_void,
) -> *mut CGEvent {
match NonNull::new(this).map(|this| unsafe { this.cast::<InputHandler>().as_mut() }) {
Some(this) => {
let intercept = this.input_handler(event_type, unsafe { event_ref.as_ref() });
if intercept {
return null_mut();
}
}
_ => error!("Zero passed to Event Handler."),
}
unsafe { event_ref.as_mut() }
}
fn input_handler(&mut self, event_type: CGEventType, event: &CGEvent) -> bool {
let Some(events) = &self.events else {
return false;
};
let result = match event_type {
CGEventType::TapDisabledByTimeout | CGEventType::TapDisabledByUserInput => {
info!("{}: Tap Disabled", function_name!());
if let Some(port) = &self.tap_port {
CGEvent::tap_enable(port, true);
}
Ok(())
}
CGEventType::LeftMouseDown | CGEventType::RightMouseDown => {
let point = CGEvent::location(Some(event));
events.send(Event::MouseDown { point })
}
CGEventType::LeftMouseUp | CGEventType::RightMouseUp => {
let point = CGEvent::location(Some(event));
events.send(Event::MouseUp { point })
}
CGEventType::LeftMouseDragged | CGEventType::RightMouseDragged => {
let point = CGEvent::location(Some(event));
events.send(Event::MouseDragged { point })
}
CGEventType::MouseMoved => {
let point = CGEvent::location(Some(event));
events.send(Event::MouseMoved { point })
}
CGEventType::KeyDown => {
let keycode =
CGEvent::integer_value_field(Some(event), CGEventField::KeyboardEventKeycode);
let eventflags = CGEvent::flags(Some(event));
return self.handle_keypress(keycode, eventflags);
}
_ => self.handle_swipe(event),
};
if let Err(err) = result {
error!("{}: error sending event: {err}", function_name!());
self.events = None;
}
false
}
fn handle_swipe(&mut self, event: &CGEvent) -> Result<()> {
const SWIPE_THRESHOLD: f64 = 0.001;
const GESTURE_MINIMAL_FINGERS: usize = 3;
let Some(ns_event) = NSEvent::eventWithCGEvent(event) else {
return Err(Error::InvalidInput(format!(
"{}: Unable to convert {event:?} to NSEvent.",
function_name!()
)));
};
if ns_event.r#type() != NSEventType::Gesture {
return Ok(());
}
let fingers = ns_event.allTouches();
if fingers.len() < GESTURE_MINIMAL_FINGERS {
return Ok(());
}
if fingers.iter().all(|f| f.phase() != NSTouchPhase::Began)
&& let Some(prev) = &self.finger_position
{
let deltas = prev
.iter()
.zip(&fingers)
.map(|(p, c)| p.normalizedPosition().x - c.normalizedPosition().x)
.collect::<Vec<_>>();
if deltas.iter().all(|p| p.abs() > SWIPE_THRESHOLD)
&& let Some(events) = &self.events
{
_ = events.send(Event::Swipe { deltas });
}
}
self.finger_position = Some(fingers);
Ok(())
}
fn handle_keypress(&self, keycode: i64, eventflags: CGEventFlags) -> bool {
const MODIFIER_MASKS: [(Modifiers, [u64; 3]); 4] = [
(Modifiers::ALT, [0x0008_0000, 0x0000_0020, 0x0000_0040]),
(Modifiers::SHIFT, [0x0002_0000, 0x0000_0002, 0x0000_0004]),
(Modifiers::CMD, [0x0010_0000, 0x0000_0008, 0x0000_0010]),
(Modifiers::CTRL, [0x0004_0000, 0x0000_0001, 0x0000_2000]),
];
let Some(events) = &self.events else {
return false;
};
let mut mask = Modifiers::empty();
for (modifier, masks) in MODIFIER_MASKS {
#[allow(clippy::manual_contains)]
if masks.iter().any(|&m| m == (eventflags.0 & m)) {
mask |= modifier;
}
}
let keycode = keycode.try_into().ok();
keycode
.and_then(|keycode| self.config.find_keybind(keycode, &mask))
.and_then(|command| {
events
.send(Event::Command { command })
.inspect_err(|err| error!("{}: Error sending command: {err}", function_name!()))
.ok()
})
.is_some()
}
}