fix(natives): prevented macos input suppression
Configured Quartz event sources to permit local hardware events in both suppression states and replaced Enigo-backed global posting with the configured source. Added a macOS regression test for the event-source suppression settings. Fixes #7872
This commit is contained in:
@@ -60,7 +60,7 @@ impl KeyName {
|
||||
matches!(self, Self::Ctrl | Self::Alt | Self::Shift | Self::Meta)
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) const fn to_enigo(self) -> enigo::Key {
|
||||
use enigo::Key;
|
||||
match self {
|
||||
@@ -74,10 +74,7 @@ impl KeyName {
|
||||
Self::Space => Key::Space,
|
||||
Self::Backspace => Key::Backspace,
|
||||
Self::Delete => Key::Delete,
|
||||
#[cfg(target_os = "windows")]
|
||||
Self::Insert => Key::Insert,
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::Insert => Key::Other(0x72),
|
||||
Self::Home => Key::Home,
|
||||
Self::End => Key::End,
|
||||
Self::PageUp => Key::PageUp,
|
||||
@@ -87,14 +84,8 @@ impl KeyName {
|
||||
Self::Left => Key::LeftArrow,
|
||||
Self::Right => Key::RightArrow,
|
||||
Self::CapsLock => Key::CapsLock,
|
||||
#[cfg(target_os = "windows")]
|
||||
Self::NumLock => Key::Numlock,
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::NumLock => Key::Other(0x47),
|
||||
#[cfg(target_os = "windows")]
|
||||
Self::PrintScreen => Key::PrintScr,
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::PrintScreen => Key::Other(0x69),
|
||||
Self::F1 => Key::F1,
|
||||
Self::F2 => Key::F2,
|
||||
Self::F3 => Key::F3,
|
||||
@@ -115,22 +106,10 @@ impl KeyName {
|
||||
Self::F18 => Key::F18,
|
||||
Self::F19 => Key::F19,
|
||||
Self::F20 => Key::F20,
|
||||
#[cfg(target_os = "windows")]
|
||||
Self::F21 => Key::F21,
|
||||
#[cfg(target_os = "windows")]
|
||||
Self::F22 => Key::F22,
|
||||
#[cfg(target_os = "windows")]
|
||||
Self::F23 => Key::F23,
|
||||
#[cfg(target_os = "windows")]
|
||||
Self::F24 => Key::F24,
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::F21 => Key::Other(0x6e),
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::F22 => Key::Other(0x6f),
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::F23 => Key::Other(0x70),
|
||||
#[cfg(target_os = "macos")]
|
||||
Self::F24 => Key::Other(0x71),
|
||||
Self::Char(character) => Key::Unicode(character),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,15 @@ use std::{
|
||||
};
|
||||
|
||||
use core_graphics::{
|
||||
event::{CGEvent, CGEventFlags, CGEventType, CGMouseButton, ScrollEventUnit},
|
||||
event::{
|
||||
CGEvent, CGEventFlags, CGEventTapLocation, CGEventType, CGMouseButton, EventField,
|
||||
ScrollEventUnit,
|
||||
},
|
||||
event_source::{CGEventSource, CGEventSourceStateID},
|
||||
geometry::CGPoint,
|
||||
sys::CGEventSourceRef,
|
||||
};
|
||||
use enigo::{Axis, Button, Coordinate, Direction, Enigo, Keyboard, Mouse, Settings};
|
||||
use foreign_types::ForeignType;
|
||||
|
||||
use super::{
|
||||
super::{
|
||||
@@ -23,22 +27,12 @@ use super::{
|
||||
};
|
||||
|
||||
pub(super) struct MacInput {
|
||||
global: Enigo,
|
||||
source: CGEventSource,
|
||||
}
|
||||
|
||||
impl MacInput {
|
||||
pub(super) fn new() -> CoreResult<Self> {
|
||||
let settings = Settings { open_prompt_to_get_permissions: false, ..Settings::default() };
|
||||
let global = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| Enigo::new(&settings)))
|
||||
.map_err(|_| {
|
||||
DesktopError::input_failed("Quartz native input initialization failed unexpectedly")
|
||||
})?
|
||||
.map_err(|error| {
|
||||
DesktopError::input_failed(format!(
|
||||
"Quartz native input initialization failed: {error}"
|
||||
))
|
||||
})?;
|
||||
Ok(Self { global })
|
||||
Ok(Self { source: source()? })
|
||||
}
|
||||
|
||||
pub(super) fn pointer(
|
||||
@@ -49,7 +43,7 @@ impl MacInput {
|
||||
capture: &MacCapture,
|
||||
) -> CoreResult<()> {
|
||||
match target {
|
||||
Target::Desktop => global_pointer(&mut self.global, event),
|
||||
Target::Desktop => global_pointer(&self.source, event),
|
||||
Target::Window(id) => {
|
||||
let window = capture.window(id)?;
|
||||
let (pid, wid) = window_identity(&window)?;
|
||||
@@ -59,10 +53,10 @@ impl MacInput {
|
||||
if !window.focused {
|
||||
skylight::activate_without_raise(pid, wid)?;
|
||||
}
|
||||
background_pointer(pid, wid, &window, event)
|
||||
background_pointer(&self.source, pid, wid, &window, event)
|
||||
},
|
||||
DeliveryMode::Foreground => {
|
||||
skylight::with_foreground(pid, wid, || global_pointer(&mut self.global, event))
|
||||
skylight::with_foreground(pid, wid, || global_pointer(&self.source, event))
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -77,7 +71,7 @@ impl MacInput {
|
||||
capture: &MacCapture,
|
||||
) -> CoreResult<()> {
|
||||
match target {
|
||||
Target::Desktop => self.global.text(text).map_err(input_error),
|
||||
Target::Desktop => global_type(&self.source, text),
|
||||
Target::Window(id) => {
|
||||
let window = capture.window(id)?;
|
||||
let (pid, wid) = window_identity(&window)?;
|
||||
@@ -85,11 +79,11 @@ impl MacInput {
|
||||
DeliveryMode::Background => {
|
||||
background_guard(&window, "keyboard", None)?;
|
||||
prepare_background_keys(&window, pid, wid, capture)?;
|
||||
background_type(pid, text)
|
||||
background_type(&self.source, pid, text)
|
||||
},
|
||||
DeliveryMode::Foreground => skylight::with_foreground(pid, wid, || {
|
||||
let _ = ax::prepare_foreground_input(&window);
|
||||
self.global.text(text).map_err(input_error)
|
||||
global_type(&self.source, text)
|
||||
}),
|
||||
}
|
||||
},
|
||||
@@ -104,7 +98,7 @@ impl MacInput {
|
||||
capture: &MacCapture,
|
||||
) -> CoreResult<()> {
|
||||
match target {
|
||||
Target::Desktop => global_chord(&mut self.global, keys),
|
||||
Target::Desktop => global_chord(&self.source, keys),
|
||||
Target::Window(id) => {
|
||||
let window = capture.window(id)?;
|
||||
let (pid, wid) = window_identity(&window)?;
|
||||
@@ -112,11 +106,11 @@ impl MacInput {
|
||||
DeliveryMode::Background => {
|
||||
background_guard(&window, "keyboard", None)?;
|
||||
prepare_background_keys(&window, pid, wid, capture)?;
|
||||
background_chord(pid, keys)
|
||||
background_chord(&self.source, pid, keys)
|
||||
},
|
||||
DeliveryMode::Foreground => skylight::with_foreground(pid, wid, || {
|
||||
let _ = ax::prepare_foreground_input(&window);
|
||||
global_chord(&mut self.global, keys)
|
||||
global_chord(&self.source, keys)
|
||||
}),
|
||||
}
|
||||
},
|
||||
@@ -218,9 +212,48 @@ fn background_guard(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const LOCAL_EVENT_FILTER: u32 = 0x01 | 0x02 | 0x04;
|
||||
const SUPPRESSION_INTERVAL: u32 = 0;
|
||||
const REMOTE_MOUSE_DRAG: u32 = 1;
|
||||
|
||||
#[link(name = "CoreGraphics", kind = "framework")]
|
||||
unsafe extern "C" {
|
||||
#[link_name = "CGEventSourceSetLocalEventsSuppressionInterval"]
|
||||
fn set_local_events_suppression_interval(source: CGEventSourceRef, seconds: f64);
|
||||
#[link_name = "CGEventSourceSetLocalEventsFilterDuringSuppressionState"]
|
||||
fn set_local_events_filter_during_suppression_state(
|
||||
source: CGEventSourceRef,
|
||||
filter: u32,
|
||||
state: u32,
|
||||
);
|
||||
#[cfg(test)]
|
||||
#[link_name = "CGEventSourceGetLocalEventsSuppressionInterval"]
|
||||
fn get_local_events_suppression_interval(source: CGEventSourceRef) -> f64;
|
||||
#[cfg(test)]
|
||||
#[link_name = "CGEventSourceGetLocalEventsFilterDuringSuppressionState"]
|
||||
fn get_local_events_filter_during_suppression_state(source: CGEventSourceRef, state: u32)
|
||||
-> u32;
|
||||
}
|
||||
|
||||
fn source() -> CoreResult<CGEventSource> {
|
||||
CGEventSource::new(CGEventSourceStateID::HIDSystemState)
|
||||
.map_err(|()| DesktopError::input_failed("failed to create a Quartz input event source"))
|
||||
let source = CGEventSource::new(CGEventSourceStateID::HIDSystemState)
|
||||
.map_err(|()| DesktopError::input_failed("failed to create a Quartz input event source"))?;
|
||||
// SAFETY: `source` is a live CGEventSource and both setters accept these
|
||||
// documented masks/states.
|
||||
unsafe {
|
||||
set_local_events_suppression_interval(source.as_ptr(), 0.0);
|
||||
set_local_events_filter_during_suppression_state(
|
||||
source.as_ptr(),
|
||||
LOCAL_EVENT_FILTER,
|
||||
SUPPRESSION_INTERVAL,
|
||||
);
|
||||
set_local_events_filter_during_suppression_state(
|
||||
source.as_ptr(),
|
||||
LOCAL_EVENT_FILTER,
|
||||
REMOTE_MOUSE_DRAG,
|
||||
);
|
||||
}
|
||||
Ok(source)
|
||||
}
|
||||
|
||||
fn modifier_flags(modifiers: Modifiers) -> CGEventFlags {
|
||||
@@ -269,6 +302,7 @@ const fn button_types(
|
||||
}
|
||||
|
||||
fn background_pointer(
|
||||
source: &CGEventSource,
|
||||
pid: libc::pid_t,
|
||||
wid: u32,
|
||||
window: &DesktopWindow,
|
||||
@@ -276,7 +310,7 @@ fn background_pointer(
|
||||
) -> CoreResult<()> {
|
||||
match event {
|
||||
PointerEvent::Click { x, y, button, count, modifiers } => {
|
||||
background_click(pid, wid, window, x, y, button, count, modifiers)
|
||||
background_click(source, pid, wid, window, x, y, button, count, modifiers)
|
||||
},
|
||||
PointerEvent::Move { x, y } => {
|
||||
let group = click_group_id();
|
||||
@@ -284,7 +318,7 @@ fn background_pointer(
|
||||
pid,
|
||||
wid,
|
||||
window,
|
||||
source()?,
|
||||
source.clone(),
|
||||
CGEventType::MouseMoved,
|
||||
CGMouseButton::Left,
|
||||
x,
|
||||
@@ -297,13 +331,16 @@ fn background_pointer(
|
||||
)
|
||||
},
|
||||
PointerEvent::Drag { path, button, modifiers } => {
|
||||
background_drag(pid, wid, window, &path, button, modifiers)
|
||||
background_drag(source, pid, wid, window, &path, button, modifiers)
|
||||
},
|
||||
PointerEvent::Scroll { x, y, dx, dy } => {
|
||||
background_scroll(source, pid, wid, window, x, y, dx, dy)
|
||||
},
|
||||
PointerEvent::Scroll { x, y, dx, dy } => background_scroll(pid, wid, window, x, y, dx, dy),
|
||||
}
|
||||
}
|
||||
|
||||
fn background_click(
|
||||
source: &CGEventSource,
|
||||
pid: libc::pid_t,
|
||||
wid: u32,
|
||||
window: &DesktopWindow,
|
||||
@@ -313,11 +350,10 @@ fn background_click(
|
||||
count: u32,
|
||||
modifiers: Modifiers,
|
||||
) -> CoreResult<()> {
|
||||
let source = source()?;
|
||||
let group = click_group_id();
|
||||
let (cg_button, down, up, _, number) = button_types(button);
|
||||
let flags = modifier_flags(modifiers);
|
||||
pointer_prologue(pid, wid, window, &source, x, y, group, flags)?;
|
||||
pointer_prologue(pid, wid, window, source, x, y, group, flags)?;
|
||||
for click_state in 1..=count.max(1) {
|
||||
post_mouse(
|
||||
pid,
|
||||
@@ -419,6 +455,7 @@ fn pointer_prologue(
|
||||
}
|
||||
|
||||
fn background_drag(
|
||||
source: &CGEventSource,
|
||||
pid: libc::pid_t,
|
||||
wid: u32,
|
||||
window: &DesktopWindow,
|
||||
@@ -432,11 +469,10 @@ fn background_drag(
|
||||
if path.len() < 2 {
|
||||
return Err(DesktopError::input_failed("drag path must contain at least two points"));
|
||||
}
|
||||
let source = source()?;
|
||||
let group = click_group_id();
|
||||
let (cg_button, down, up, dragged, number) = button_types(button);
|
||||
let flags = modifier_flags(modifiers);
|
||||
pointer_prologue(pid, wid, window, &source, start_x, start_y, group, flags)?;
|
||||
pointer_prologue(pid, wid, window, source, start_x, start_y, group, flags)?;
|
||||
post_mouse(
|
||||
pid,
|
||||
wid,
|
||||
@@ -474,7 +510,21 @@ fn background_drag(
|
||||
let &(end_x, end_y) = path
|
||||
.last()
|
||||
.ok_or_else(|| DesktopError::input_failed("drag path is empty"))?;
|
||||
post_mouse(pid, wid, window, source, up, cg_button, end_x, end_y, 3, 1, number, group, flags)?;
|
||||
post_mouse(
|
||||
pid,
|
||||
wid,
|
||||
window,
|
||||
source.clone(),
|
||||
up,
|
||||
cg_button,
|
||||
end_x,
|
||||
end_y,
|
||||
3,
|
||||
1,
|
||||
number,
|
||||
group,
|
||||
flags,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -512,6 +562,7 @@ fn post_mouse(
|
||||
}
|
||||
|
||||
fn background_scroll(
|
||||
source: &CGEventSource,
|
||||
pid: libc::pid_t,
|
||||
wid: u32,
|
||||
window: &DesktopWindow,
|
||||
@@ -521,7 +572,6 @@ fn background_scroll(
|
||||
dy: f64,
|
||||
) -> CoreResult<()> {
|
||||
let group = click_group_id();
|
||||
let source = source()?;
|
||||
post_mouse(
|
||||
pid,
|
||||
wid,
|
||||
@@ -540,8 +590,9 @@ fn background_scroll(
|
||||
thread::sleep(Duration::from_millis(15));
|
||||
let wheel_x = finite_i32(dx, "horizontal scroll delta")?;
|
||||
let wheel_y = finite_i32(dy, "vertical scroll delta")?;
|
||||
let event = CGEvent::new_scroll_event(source, ScrollEventUnit::PIXEL, 2, wheel_y, wheel_x, 0)
|
||||
.map_err(|()| DesktopError::input_failed("failed to create a Quartz scroll event"))?;
|
||||
let event =
|
||||
CGEvent::new_scroll_event(source.clone(), ScrollEventUnit::PIXEL, 2, wheel_y, wheel_x, 0)
|
||||
.map_err(|()| DesktopError::input_failed("failed to create a Quartz scroll event"))?;
|
||||
event.set_location(CGPoint::new(x, y));
|
||||
skylight::stamp_event(
|
||||
&event,
|
||||
@@ -564,8 +615,19 @@ fn click_group_id() -> i64 {
|
||||
.into()
|
||||
}
|
||||
|
||||
fn background_type(pid: libc::pid_t, text: &str) -> CoreResult<()> {
|
||||
let source = source()?;
|
||||
fn background_type(source: &CGEventSource, pid: libc::pid_t, text: &str) -> CoreResult<()> {
|
||||
type_text(source, text, |event| skylight::post_keyboard(pid, event))
|
||||
}
|
||||
|
||||
fn global_type(source: &CGEventSource, text: &str) -> CoreResult<()> {
|
||||
type_text(source, text, post_global)
|
||||
}
|
||||
|
||||
fn type_text(
|
||||
source: &CGEventSource,
|
||||
text: &str,
|
||||
mut post: impl FnMut(&CGEvent) -> CoreResult<()>,
|
||||
) -> CoreResult<()> {
|
||||
for character in text.chars() {
|
||||
let value = character.to_string();
|
||||
for down in [true, false] {
|
||||
@@ -573,28 +635,39 @@ fn background_type(pid: libc::pid_t, text: &str) -> CoreResult<()> {
|
||||
.map_err(|()| DesktopError::input_failed("failed to create a Quartz keyboard event"))?;
|
||||
event.set_string(&value);
|
||||
event.set_flags(CGEventFlags::CGEventFlagNull);
|
||||
skylight::post_keyboard(pid, &event)?;
|
||||
post(&event)?;
|
||||
thread::sleep(Duration::from_millis(8));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn background_chord(pid: libc::pid_t, keys: &[KeyName]) -> CoreResult<()> {
|
||||
fn background_chord(source: &CGEventSource, pid: libc::pid_t, keys: &[KeyName]) -> CoreResult<()> {
|
||||
key_chord(source, keys, |event| skylight::post_keyboard(pid, event))
|
||||
}
|
||||
|
||||
fn global_chord(source: &CGEventSource, keys: &[KeyName]) -> CoreResult<()> {
|
||||
key_chord(source, keys, post_global)
|
||||
}
|
||||
|
||||
fn key_chord(
|
||||
source: &CGEventSource,
|
||||
keys: &[KeyName],
|
||||
mut post: impl FnMut(&CGEvent) -> CoreResult<()>,
|
||||
) -> CoreResult<()> {
|
||||
if keys.is_empty() {
|
||||
return Err(DesktopError::invalid_key("key chord must not be empty"));
|
||||
}
|
||||
let source = source()?;
|
||||
let mut active = Modifiers::default();
|
||||
for &key in keys {
|
||||
update_modifier(&mut active, key, true);
|
||||
post_key(pid, &source, key, true, modifier_flags(active))?;
|
||||
post_key(source, key, true, modifier_flags(active), &mut post)?;
|
||||
thread::sleep(Duration::from_millis(8));
|
||||
}
|
||||
let mut first_error = None;
|
||||
for &key in keys.iter().rev() {
|
||||
update_modifier(&mut active, key, false);
|
||||
if let Err(error) = post_key(pid, &source, key, false, modifier_flags(active))
|
||||
if let Err(error) = post_key(source, key, false, modifier_flags(active), &mut post)
|
||||
&& first_error.is_none()
|
||||
{
|
||||
first_error = Some(error);
|
||||
@@ -605,17 +678,17 @@ fn background_chord(pid: libc::pid_t, keys: &[KeyName]) -> CoreResult<()> {
|
||||
}
|
||||
|
||||
fn post_key(
|
||||
pid: libc::pid_t,
|
||||
source: &CGEventSource,
|
||||
key: KeyName,
|
||||
down: bool,
|
||||
flags: CGEventFlags,
|
||||
post: &mut impl FnMut(&CGEvent) -> CoreResult<()>,
|
||||
) -> CoreResult<()> {
|
||||
let code = key_code(key)?;
|
||||
let event = CGEvent::new_keyboard_event(source.clone(), code, down)
|
||||
.map_err(|()| DesktopError::input_failed("failed to create a Quartz keyboard event"))?;
|
||||
event.set_flags(flags);
|
||||
skylight::post_keyboard(pid, &event)
|
||||
post(&event)
|
||||
}
|
||||
|
||||
const fn update_modifier(modifiers: &mut Modifiers, key: KeyName, down: bool) {
|
||||
@@ -740,128 +813,142 @@ fn char_key_code(character: char) -> CoreResult<u16> {
|
||||
Ok(code)
|
||||
}
|
||||
|
||||
fn global_pointer(input: &mut Enigo, event: PointerEvent) -> CoreResult<()> {
|
||||
fn global_pointer(source: &CGEventSource, event: PointerEvent) -> CoreResult<()> {
|
||||
match event {
|
||||
PointerEvent::Click { x, y, button, count, modifiers } => {
|
||||
move_global(input, x, y)?;
|
||||
with_global_modifiers(input, modifiers, |input| {
|
||||
for _ in 0..count.max(1) {
|
||||
input
|
||||
.button(enigo_button(button), Direction::Click)
|
||||
.map_err(input_error)?;
|
||||
post_global_mouse(
|
||||
source,
|
||||
CGEventType::MouseMoved,
|
||||
CGMouseButton::Left,
|
||||
x,
|
||||
y,
|
||||
0,
|
||||
0,
|
||||
CGEventFlags::CGEventFlagNull,
|
||||
)?;
|
||||
let (cg_button, down, up, _, number) = button_types(button);
|
||||
let flags = modifier_flags(modifiers);
|
||||
for click_state in 1..=count.max(1) {
|
||||
post_global_mouse(
|
||||
source,
|
||||
down,
|
||||
cg_button,
|
||||
x,
|
||||
y,
|
||||
i64::from(click_state),
|
||||
number,
|
||||
flags,
|
||||
)?;
|
||||
thread::sleep(Duration::from_millis(1));
|
||||
post_global_mouse(source, up, cg_button, x, y, i64::from(click_state), number, flags)?;
|
||||
if click_state < count.max(1) {
|
||||
thread::sleep(Duration::from_millis(80));
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
PointerEvent::Move { x, y } => move_global(input, x, y),
|
||||
PointerEvent::Move { x, y } => post_global_mouse(
|
||||
source,
|
||||
CGEventType::MouseMoved,
|
||||
CGMouseButton::Left,
|
||||
x,
|
||||
y,
|
||||
0,
|
||||
0,
|
||||
CGEventFlags::CGEventFlagNull,
|
||||
),
|
||||
PointerEvent::Drag { path, button, modifiers } => {
|
||||
let Some(&(x, y)) = path.first() else {
|
||||
let Some(&(start_x, start_y)) = path.first() else {
|
||||
return Err(DesktopError::input_failed("drag path must contain at least two points"));
|
||||
};
|
||||
if path.len() < 2 {
|
||||
return Err(DesktopError::input_failed("drag path must contain at least two points"));
|
||||
}
|
||||
move_global(input, x, y)?;
|
||||
with_global_modifiers(input, modifiers, |input| {
|
||||
input
|
||||
.button(enigo_button(button), Direction::Press)
|
||||
.map_err(input_error)?;
|
||||
let drag = path[1..]
|
||||
.iter()
|
||||
.try_for_each(|&(x, y)| move_global(input, x, y));
|
||||
let release = input
|
||||
.button(enigo_button(button), Direction::Release)
|
||||
.map_err(input_error);
|
||||
drag.and(release)
|
||||
})
|
||||
let (cg_button, down, up, dragged, number) = button_types(button);
|
||||
let flags = modifier_flags(modifiers);
|
||||
post_global_mouse(
|
||||
source,
|
||||
CGEventType::MouseMoved,
|
||||
CGMouseButton::Left,
|
||||
start_x,
|
||||
start_y,
|
||||
0,
|
||||
0,
|
||||
CGEventFlags::CGEventFlagNull,
|
||||
)?;
|
||||
post_global_mouse(source, down, cg_button, start_x, start_y, 1, number, flags)?;
|
||||
for &(x, y) in &path[1..] {
|
||||
thread::sleep(Duration::from_millis(16));
|
||||
post_global_mouse(source, dragged, cg_button, x, y, 1, number, flags)?;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
let &(end_x, end_y) = path
|
||||
.last()
|
||||
.ok_or_else(|| DesktopError::input_failed("drag path is empty"))?;
|
||||
post_global_mouse(source, up, cg_button, end_x, end_y, 1, number, flags)
|
||||
},
|
||||
PointerEvent::Scroll { x, y, dx, dy } => {
|
||||
move_global(input, x, y)?;
|
||||
let horizontal = finite_i32(dx, "horizontal scroll delta")?;
|
||||
let vertical = finite_i32(dy, "vertical scroll delta")?;
|
||||
if horizontal != 0 {
|
||||
input
|
||||
.scroll(horizontal, Axis::Horizontal)
|
||||
.map_err(input_error)?;
|
||||
}
|
||||
if vertical != 0 {
|
||||
input
|
||||
.scroll(vertical, Axis::Vertical)
|
||||
.map_err(input_error)?;
|
||||
}
|
||||
Ok(())
|
||||
post_global_mouse(
|
||||
source,
|
||||
CGEventType::MouseMoved,
|
||||
CGMouseButton::Left,
|
||||
x,
|
||||
y,
|
||||
0,
|
||||
0,
|
||||
CGEventFlags::CGEventFlagNull,
|
||||
)?;
|
||||
let wheel_x = finite_i32(dx, "horizontal scroll delta")?;
|
||||
let wheel_y = finite_i32(dy, "vertical scroll delta")?;
|
||||
let event = CGEvent::new_scroll_event(
|
||||
source.clone(),
|
||||
ScrollEventUnit::PIXEL,
|
||||
2,
|
||||
wheel_y,
|
||||
wheel_x,
|
||||
0,
|
||||
)
|
||||
.map_err(|()| DesktopError::input_failed("failed to create a Quartz scroll event"))?;
|
||||
event.set_location(point(x, y)?);
|
||||
post_global(&event)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn global_chord(input: &mut Enigo, keys: &[KeyName]) -> CoreResult<()> {
|
||||
if keys.is_empty() {
|
||||
return Err(DesktopError::invalid_key("key chord must not be empty"));
|
||||
}
|
||||
if keys.len() == 1 {
|
||||
return input
|
||||
.key(keys[0].to_enigo(), Direction::Click)
|
||||
.map_err(input_error);
|
||||
}
|
||||
let mut pressed = Vec::with_capacity(keys.len());
|
||||
for &key in keys {
|
||||
if let Err(error) = input.key(key.to_enigo(), Direction::Press) {
|
||||
for &held in pressed.iter().rev() {
|
||||
let _ = input.key(held, Direction::Release);
|
||||
}
|
||||
return Err(input_error(error));
|
||||
}
|
||||
pressed.push(key.to_enigo());
|
||||
}
|
||||
let mut first_error = None;
|
||||
for key in pressed.into_iter().rev() {
|
||||
if let Err(error) = input.key(key, Direction::Release)
|
||||
&& first_error.is_none()
|
||||
{
|
||||
first_error = Some(input_error(error));
|
||||
}
|
||||
}
|
||||
first_error.map_or(Ok(()), Err)
|
||||
}
|
||||
|
||||
fn with_global_modifiers(
|
||||
input: &mut Enigo,
|
||||
modifiers: Modifiers,
|
||||
action: impl FnOnce(&mut Enigo) -> CoreResult<()>,
|
||||
#[allow(
|
||||
clippy::too_many_arguments,
|
||||
reason = "the parameters are the native CGEvent fields stamped together"
|
||||
)]
|
||||
fn post_global_mouse(
|
||||
source: &CGEventSource,
|
||||
event_type: CGEventType,
|
||||
button: CGMouseButton,
|
||||
x: f64,
|
||||
y: f64,
|
||||
click_state: i64,
|
||||
button_number: i64,
|
||||
flags: CGEventFlags,
|
||||
) -> CoreResult<()> {
|
||||
let requested = [
|
||||
(modifiers.ctrl, KeyName::Ctrl),
|
||||
(modifiers.alt, KeyName::Alt),
|
||||
(modifiers.shift, KeyName::Shift),
|
||||
(modifiers.meta, KeyName::Meta),
|
||||
];
|
||||
let mut pressed = Vec::new();
|
||||
for (_, key) in requested.into_iter().filter(|(enabled, _)| *enabled) {
|
||||
let key = key.to_enigo();
|
||||
if let Err(error) = input.key(key, Direction::Press) {
|
||||
for &held in pressed.iter().rev() {
|
||||
let _ = input.key(held, Direction::Release);
|
||||
}
|
||||
return Err(input_error(error));
|
||||
}
|
||||
pressed.push(key);
|
||||
let event = CGEvent::new_mouse_event(source.clone(), event_type, point(x, y)?, button)
|
||||
.map_err(|()| DesktopError::input_failed("failed to create a Quartz pointer event"))?;
|
||||
event.set_integer_value_field(EventField::MOUSE_EVENT_CLICK_STATE, click_state);
|
||||
if button_number != 0 {
|
||||
event.set_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER, button_number);
|
||||
}
|
||||
let result = action(input);
|
||||
let mut release = Ok(());
|
||||
for key in pressed.into_iter().rev() {
|
||||
if let Err(error) = input.key(key, Direction::Release)
|
||||
&& release.is_ok()
|
||||
{
|
||||
release = Err(input_error(error));
|
||||
}
|
||||
}
|
||||
result.and(release)
|
||||
event.set_flags(flags);
|
||||
post_global(&event)
|
||||
}
|
||||
|
||||
fn move_global(input: &mut Enigo, x: f64, y: f64) -> CoreResult<()> {
|
||||
input
|
||||
.move_mouse(finite_i32(x, "x coordinate")?, finite_i32(y, "y coordinate")?, Coordinate::Abs)
|
||||
.map_err(input_error)
|
||||
fn post_global(event: &CGEvent) -> CoreResult<()> {
|
||||
event.post(CGEventTapLocation::HID);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn point(x: f64, y: f64) -> CoreResult<CGPoint> {
|
||||
Ok(CGPoint::new(
|
||||
f64::from(finite_i32(x, "x coordinate")?),
|
||||
f64::from(finite_i32(y, "y coordinate")?),
|
||||
))
|
||||
}
|
||||
|
||||
fn finite_i32(value: f64, name: &str) -> CoreResult<i32> {
|
||||
@@ -873,14 +960,24 @@ fn finite_i32(value: f64, name: &str) -> CoreResult<i32> {
|
||||
Ok(value.round() as i32)
|
||||
}
|
||||
|
||||
const fn enigo_button(button: MouseButton) -> Button {
|
||||
match button {
|
||||
MouseButton::Left => Button::Left,
|
||||
MouseButton::Right => Button::Right,
|
||||
MouseButton::Middle => Button::Middle,
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn event_source_never_suppresses_local_input() {
|
||||
let source = source().expect("Quartz event source");
|
||||
// SAFETY: `source` remains live for both CoreGraphics getter calls.
|
||||
unsafe {
|
||||
assert_eq!(get_local_events_suppression_interval(source.as_ptr()), 0.0);
|
||||
assert_eq!(
|
||||
get_local_events_filter_during_suppression_state(source.as_ptr(), SUPPRESSION_INTERVAL,),
|
||||
LOCAL_EVENT_FILTER,
|
||||
);
|
||||
assert_eq!(
|
||||
get_local_events_filter_during_suppression_state(source.as_ptr(), REMOTE_MOUSE_DRAG,),
|
||||
LOCAL_EVENT_FILTER,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn input_error(error: impl std::fmt::Display) -> DesktopError {
|
||||
DesktopError::input_failed(format!("native input failed: {error}"))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed synthesized macOS keyboard and pointer events suppressing the user's physical input.
|
||||
|
||||
## [17.2.10] - 2026-08-06
|
||||
|
||||
### Fixed
|
||||
|
||||
Reference in New Issue
Block a user