use core_graphics::event::{CGEvent, CGEventField, CGEventFlags, CGEventType, EventField};
use tracing::debug;
use super::sender::{event_sender_id, sender_device_info};
use crate::{ButtonId, KeyEvent, KeyModifiers, MouseEvent, ScrollDelta};
fn button_number_to_id(n: i64) -> Option<ButtonId> {
match n {
0 => Some(ButtonId::LeftClick),
1 => Some(ButtonId::RightClick),
2 => Some(ButtonId::MiddleClick),
3 => Some(ButtonId::Back),
4 => Some(ButtonId::Forward),
_ => None,
}
}
fn button_source(event: &CGEvent) -> Option<crate::EventDevice> {
event_sender_id(event).map(|id| sender_device_info(id).event_device)
}
fn modifiers_from_flags(flags: CGEventFlags) -> KeyModifiers {
KeyModifiers {
shift: flags.contains(CGEventFlags::CGEventFlagShift),
control: flags.contains(CGEventFlags::CGEventFlagControl),
option: flags.contains(CGEventFlags::CGEventFlagAlternate),
command: flags.contains(CGEventFlags::CGEventFlagCommand),
}
}
pub(super) fn translate_key(etype: CGEventType, event: &CGEvent) -> Option<KeyEvent> {
let pressed = match etype {
CGEventType::KeyDown => true,
CGEventType::KeyUp => false,
_ => return None,
};
let keycode = event.get_integer_value_field(EventField::KEYBOARD_EVENT_KEYCODE);
let keycode = u16::try_from(keycode).ok()?;
Some(KeyEvent {
keycode,
pressed,
modifiers: modifiers_from_flags(event.get_flags()),
})
}
pub(super) fn translate(etype: CGEventType, event: &CGEvent) -> Option<MouseEvent> {
let can_be_synthetic = matches!(
etype,
CGEventType::LeftMouseDown
| CGEventType::LeftMouseUp
| CGEventType::RightMouseDown
| CGEventType::RightMouseUp
| CGEventType::OtherMouseDown
| CGEventType::OtherMouseUp
| CGEventType::ScrollWheel
);
if can_be_synthetic
&& event.get_integer_value_field(EventField::EVENT_SOURCE_USER_DATA)
== openlogi_inject::SYNTHETIC_EVENT_USER_DATA
{
return None;
}
match etype {
CGEventType::LeftMouseDown => Some(MouseEvent::Button {
id: ButtonId::LeftClick,
pressed: true,
device: button_source(event),
}),
CGEventType::LeftMouseUp => Some(MouseEvent::Button {
id: ButtonId::LeftClick,
pressed: false,
device: button_source(event),
}),
CGEventType::RightMouseDown => Some(MouseEvent::Button {
id: ButtonId::RightClick,
pressed: true,
device: button_source(event),
}),
CGEventType::RightMouseUp => Some(MouseEvent::Button {
id: ButtonId::RightClick,
pressed: false,
device: button_source(event),
}),
CGEventType::OtherMouseDown => {
let n = event.get_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER);
button_number_to_id(n).map(|id| MouseEvent::Button {
id,
pressed: true,
device: button_source(event),
})
}
CGEventType::OtherMouseUp => {
let n = event.get_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER);
button_number_to_id(n).map(|id| MouseEvent::Button {
id,
pressed: false,
device: button_source(event),
})
}
CGEventType::ScrollWheel => {
let continuous =
event.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_IS_CONTINUOUS) != 0;
let delta = if continuous {
ScrollDelta::pixels(
precise_scroll_delta(event, HORIZONTAL),
precise_scroll_delta(event, VERTICAL),
)
} else {
non_continuous_scroll_delta(event)
};
let phase = event.get_integer_value_field(SCROLL_PHASE) != 0
|| event.get_integer_value_field(MOMENTUM_PHASE) != 0
|| event.get_integer_value_field(SCROLL_COUNT) != 0;
let sender = event_sender_id(event);
let device_info = sender.map(sender_device_info);
let from_trackpad = device_info.as_ref().map_or(phase, |info| info.is_trackpad);
Some(MouseEvent::Scroll {
delta,
from_trackpad,
device: device_info.map(|info| info.event_device),
})
}
CGEventType::MouseMoved
| CGEventType::LeftMouseDragged
| CGEventType::RightMouseDragged
| CGEventType::OtherMouseDragged => {
let dx = event.get_integer_value_field(EventField::MOUSE_EVENT_DELTA_X);
let dy = event.get_integer_value_field(EventField::MOUSE_EVENT_DELTA_Y);
#[expect(
clippy::cast_possible_truncation,
reason = "per-event pointer deltas are small integers, far within i32"
)]
Some(MouseEvent::Moved {
delta_x: dx as i32,
delta_y: dy as i32,
})
}
CGEventType::TapDisabledByTimeout | CGEventType::TapDisabledByUserInput => {
debug!("CGEventTap disabled by OS (type={etype:?}); re-enabling, cancelling any hold");
Some(MouseEvent::CaptureInterrupted)
}
_ => None,
}
}
#[derive(Clone, Copy)]
struct ScrollAxisFields {
line: CGEventField,
fixed: CGEventField,
point: CGEventField,
}
const VERTICAL: ScrollAxisFields = ScrollAxisFields {
line: EventField::SCROLL_WHEEL_EVENT_DELTA_AXIS_1,
fixed: EventField::SCROLL_WHEEL_EVENT_FIXED_POINT_DELTA_AXIS_1,
point: EventField::SCROLL_WHEEL_EVENT_POINT_DELTA_AXIS_1,
};
const HORIZONTAL: ScrollAxisFields = ScrollAxisFields {
line: EventField::SCROLL_WHEEL_EVENT_DELTA_AXIS_2,
fixed: EventField::SCROLL_WHEEL_EVENT_FIXED_POINT_DELTA_AXIS_2,
point: EventField::SCROLL_WHEEL_EVENT_POINT_DELTA_AXIS_2,
};
const SCROLL_PHASE: CGEventField = 99; const SCROLL_COUNT: CGEventField = 100; const MOMENTUM_PHASE: CGEventField = 123;
fn precise_scroll_delta(event: &CGEvent, axis: ScrollAxisFields) -> f64 {
let point = event.get_double_value_field(axis.point);
if point != 0.0 {
return point;
}
let fixed = event.get_double_value_field(axis.fixed);
if fixed != 0.0 {
return fixed;
}
0.0
}
fn non_continuous_scroll_delta(event: &CGEvent) -> ScrollDelta {
let x = fractional_line_scroll_delta(event, HORIZONTAL);
let y = fractional_line_scroll_delta(event, VERTICAL);
if x != 0.0 || y != 0.0 {
return ScrollDelta::wheel_ticks(x, y);
}
ScrollDelta::pixels(
event.get_double_value_field(HORIZONTAL.point),
event.get_double_value_field(VERTICAL.point),
)
}
fn fractional_line_scroll_delta(event: &CGEvent, axis: ScrollAxisFields) -> f64 {
let fixed = event.get_double_value_field(axis.fixed);
if fixed != 0.0 {
return fixed;
}
line_scroll_delta(event, axis)
}
#[expect(
clippy::cast_precision_loss,
reason = "physical per-event line deltas are small integers, exactly represented by f64"
)]
fn line_scroll_delta(event: &CGEvent, axis: ScrollAxisFields) -> f64 {
event.get_integer_value_field(axis.line) as f64
}