Skip to content

Commit 3483d24

Browse files
authored
fix inconsistent mouse capture on macos (#346)
1 parent 35773df commit 3483d24

File tree

1 file changed

+80
-76
lines changed

1 file changed

+80
-76
lines changed

input-capture/src/macos.rs

Lines changed: 80 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,39 @@
11
use super::{error::MacosCaptureCreationError, Capture, CaptureError, CaptureEvent, Position};
22
use async_trait::async_trait;
33
use bitflags::bitflags;
4-
use core_foundation::base::{kCFAllocatorDefault, CFRelease};
5-
use core_foundation::date::CFTimeInterval;
6-
use core_foundation::number::{kCFBooleanTrue, CFBooleanRef};
7-
use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop, CFRunLoopSource};
8-
use core_foundation::string::{kCFStringEncodingUTF8, CFStringCreateWithCString, CFStringRef};
9-
use core_graphics::base::{kCGErrorSuccess, CGError};
10-
use core_graphics::display::{CGDisplay, CGPoint};
11-
use core_graphics::event::{
12-
CGEvent, CGEventFlags, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement,
13-
CGEventTapProxy, CGEventType, CallbackResult, EventField,
4+
use core_foundation::{
5+
base::{kCFAllocatorDefault, CFRelease},
6+
date::CFTimeInterval,
7+
number::{kCFBooleanTrue, CFBooleanRef},
8+
runloop::{kCFRunLoopCommonModes, CFRunLoop, CFRunLoopSource},
9+
string::{kCFStringEncodingUTF8, CFStringCreateWithCString, CFStringRef},
10+
};
11+
use core_graphics::{
12+
base::{kCGErrorSuccess, CGError},
13+
display::{CGDisplay, CGPoint},
14+
event::{
15+
CGEvent, CGEventFlags, CGEventTap, CGEventTapLocation, CGEventTapOptions,
16+
CGEventTapPlacement, CGEventTapProxy, CGEventType, CallbackResult, EventField,
17+
},
18+
event_source::{CGEventSource, CGEventSourceStateID},
1419
};
15-
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
1620
use futures_core::Stream;
1721
use input_event::{Event, KeyboardEvent, PointerEvent, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT};
1822
use keycode::{KeyMap, KeyMapping};
1923
use libc::c_void;
2024
use once_cell::unsync::Lazy;
21-
use std::collections::HashSet;
22-
use std::ffi::{c_char, CString};
23-
use std::pin::Pin;
24-
use std::sync::Arc;
25-
use std::task::{ready, Context, Poll};
26-
use std::thread::{self};
27-
use tokio::sync::mpsc::{self, Receiver, Sender};
28-
use tokio::sync::{oneshot, Mutex};
25+
use std::{
26+
collections::HashSet,
27+
ffi::{c_char, CString},
28+
pin::Pin,
29+
sync::Arc,
30+
task::{ready, Context, Poll},
31+
thread::{self},
32+
};
33+
use tokio::sync::{
34+
mpsc::{self, Receiver, Sender},
35+
oneshot, Mutex,
36+
};
2937

3038
#[derive(Debug, Default)]
3139
struct Bounds {
@@ -37,9 +45,15 @@ struct Bounds {
3745

3846
#[derive(Debug)]
3947
struct InputCaptureState {
48+
/// active capture positions
4049
active_clients: Lazy<HashSet<Position>>,
50+
/// the currently entered capture position, if any
4151
current_pos: Option<Position>,
52+
/// position where the cursor was captured
53+
enter_position: Option<CGPoint>,
54+
/// bounds of the input capture area
4255
bounds: Bounds,
56+
/// current state of modifier keys
4357
modifier_state: XMods,
4458
}
4559

@@ -57,6 +71,7 @@ impl InputCaptureState {
5771
let mut res = Self {
5872
active_clients: Lazy::new(HashSet::new),
5973
current_pos: None,
74+
enter_position: None,
6075
bounds: Bounds::default(),
6176
modifier_state: Default::default(),
6277
};
@@ -98,45 +113,34 @@ impl InputCaptureState {
98113
Ok(())
99114
}
100115

101-
// We can't disable mouse movement when in a client so we need to reset the cursor position
102-
// to the edge of the screen, the cursor will be hidden but we dont want it to appear in a
103-
// random location when we exit the client
104-
fn reset_mouse_position(&self, event: &CGEvent) -> Result<(), CaptureError> {
105-
if let Some(pos) = self.current_pos {
106-
let location = event.location();
107-
let edge_offset = 1.0;
108-
109-
// After the cursor is warped no event is produced but the next event
110-
// will carry the delta from the warp so only half the delta is needed to move the cursor
111-
let delta_y = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_Y) / 2.0;
112-
let delta_x = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_X) / 2.0;
113-
114-
let mut new_x = location.x + delta_x;
115-
let mut new_y = location.y + delta_y;
116-
117-
match pos {
118-
Position::Left => {
119-
new_x = self.bounds.xmin + edge_offset;
120-
}
121-
Position::Right => {
122-
new_x = self.bounds.xmax - edge_offset;
123-
}
124-
Position::Top => {
125-
new_y = self.bounds.ymin + edge_offset;
126-
}
127-
Position::Bottom => {
128-
new_y = self.bounds.ymax - edge_offset;
129-
}
130-
}
131-
let new_pos = CGPoint::new(new_x, new_y);
116+
/// start the input capture by
117+
fn start_capture(&mut self, event: &CGEvent, position: Position) -> Result<(), CaptureError> {
118+
let mut location = event.location();
119+
let edge_offset = 1.0;
120+
// move cursor location to display bounds
121+
match position {
122+
Position::Left => location.x = self.bounds.xmin + edge_offset,
123+
Position::Right => location.x = self.bounds.xmax - edge_offset,
124+
Position::Top => location.y = self.bounds.ymin + edge_offset,
125+
Position::Bottom => location.y = self.bounds.ymax - edge_offset,
126+
};
127+
self.enter_position = Some(location);
128+
self.reset_cursor()
129+
}
132130

133-
log::trace!("Resetting cursor position to: {new_x}, {new_y}");
131+
/// resets the cursor to the position, where the capture started
132+
fn reset_cursor(&mut self) -> Result<(), CaptureError> {
133+
let pos = self.enter_position.expect("capture active");
134+
log::trace!("Resetting cursor position to: {}, {}", pos.x, pos.y);
135+
CGDisplay::warp_mouse_cursor_position(pos).map_err(CaptureError::WarpCursor)
136+
}
134137

135-
return CGDisplay::warp_mouse_cursor_position(new_pos)
136-
.map_err(CaptureError::WarpCursor);
137-
}
138+
fn hide_cursor(&self) -> Result<(), CaptureError> {
139+
CGDisplay::hide_cursor(&CGDisplay::main()).map_err(CaptureError::CoreGraphics)
140+
}
138141

139-
Err(CaptureError::ResetMouseWithoutClient)
142+
fn show_cursor(&self) -> Result<(), CaptureError> {
143+
CGDisplay::show_cursor(&CGDisplay::main()).map_err(CaptureError::CoreGraphics)
140144
}
141145

142146
async fn handle_producer_event(
@@ -147,15 +151,13 @@ impl InputCaptureState {
147151
match producer_event {
148152
ProducerEvent::Release => {
149153
if self.current_pos.is_some() {
150-
CGDisplay::show_cursor(&CGDisplay::main())
151-
.map_err(CaptureError::CoreGraphics)?;
154+
self.show_cursor()?;
152155
self.current_pos = None;
153156
}
154157
}
155158
ProducerEvent::Grab(pos) => {
156159
if self.current_pos.is_none() {
157-
CGDisplay::hide_cursor(&CGDisplay::main())
158-
.map_err(CaptureError::CoreGraphics)?;
160+
self.hide_cursor()?;
159161
self.current_pos = Some(pos);
160162
}
161163
}
@@ -165,8 +167,7 @@ impl InputCaptureState {
165167
ProducerEvent::Destroy(p) => {
166168
if let Some(current) = self.current_pos {
167169
if current == p {
168-
CGDisplay::show_cursor(&CGDisplay::main())
169-
.map_err(CaptureError::CoreGraphics)?;
170+
self.show_cursor()?;
170171
self.current_pos = None;
171172
};
172173
}
@@ -364,7 +365,7 @@ fn create_event_tap<'a>(
364365
move |_proxy: CGEventTapProxy, event_type: CGEventType, cg_ev: &CGEvent| {
365366
log::trace!("Got event from tap: {event_type:?}");
366367
let mut state = client_state.blocking_lock();
367-
let mut pos = None;
368+
let mut capture_position = None;
368369
let mut res_events = vec![];
369370

370371
if matches!(
@@ -381,7 +382,7 @@ fn create_event_tap<'a>(
381382

382383
// Are we in a client?
383384
if let Some(current_pos) = state.current_pos {
384-
pos = Some(current_pos);
385+
capture_position = Some(current_pos);
385386
get_events(
386387
&event_type,
387388
cg_ev,
@@ -393,24 +394,30 @@ fn create_event_tap<'a>(
393394
});
394395

395396
// Keep (hidden) cursor at the edge of the screen
396-
if matches!(event_type, CGEventType::MouseMoved) {
397-
state.reset_mouse_position(cg_ev).unwrap_or_else(|e| {
398-
log::error!("Failed to reset mouse position: {e}");
399-
})
397+
if matches!(
398+
event_type,
399+
CGEventType::MouseMoved
400+
| CGEventType::LeftMouseDragged
401+
| CGEventType::RightMouseDragged
402+
| CGEventType::OtherMouseDragged
403+
) {
404+
state.reset_cursor().unwrap_or_else(|e| log::warn!("{e}"));
400405
}
401-
}
402-
// Did we cross a barrier?
403-
else if matches!(event_type, CGEventType::MouseMoved) {
406+
} else if matches!(event_type, CGEventType::MouseMoved) {
407+
// Did we cross a barrier?
404408
if let Some(new_pos) = state.crossed(cg_ev) {
405-
pos = Some(new_pos);
409+
capture_position = Some(new_pos);
410+
state
411+
.start_capture(cg_ev, new_pos)
412+
.unwrap_or_else(|e| log::warn!("{e}"));
406413
res_events.push(CaptureEvent::Begin);
407414
notify_tx
408415
.blocking_send(ProducerEvent::Grab(new_pos))
409416
.expect("Failed to send notification");
410417
}
411418
}
412419

413-
if let Some(pos) = pos {
420+
if let Some(pos) = capture_position {
414421
res_events.iter().for_each(|e| {
415422
// error must be ignored, since the event channel
416423
// may already be closed when the InputCapture instance is dropped.
@@ -515,10 +522,7 @@ impl MacOSInputCapture {
515522
log::error!("Failed to handle producer event: {e}");
516523
})
517524
}
518-
519-
_ = &mut tap_exit_rx => {
520-
break;
521-
}
525+
_ = &mut tap_exit_rx => break,
522526
}
523527
}
524528
// show cursor

0 commit comments

Comments
 (0)