Skip to content

Commit 96dd1a8

Browse files
committed
fix inconsistent mouse capture on macos
1 parent 35773df commit 96dd1a8

File tree

1 file changed

+81
-76
lines changed

1 file changed

+81
-76
lines changed

input-capture/src/macos.rs

Lines changed: 81 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,35 @@ 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);
134135

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

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

142147
async fn handle_producer_event(
@@ -147,15 +152,13 @@ impl InputCaptureState {
147152
match producer_event {
148153
ProducerEvent::Release => {
149154
if self.current_pos.is_some() {
150-
CGDisplay::show_cursor(&CGDisplay::main())
151-
.map_err(CaptureError::CoreGraphics)?;
155+
self.show_cursor()?;
152156
self.current_pos = None;
153157
}
154158
}
155159
ProducerEvent::Grab(pos) => {
156160
if self.current_pos.is_none() {
157-
CGDisplay::hide_cursor(&CGDisplay::main())
158-
.map_err(CaptureError::CoreGraphics)?;
161+
self.hide_cursor()?;
159162
self.current_pos = Some(pos);
160163
}
161164
}
@@ -165,8 +168,7 @@ impl InputCaptureState {
165168
ProducerEvent::Destroy(p) => {
166169
if let Some(current) = self.current_pos {
167170
if current == p {
168-
CGDisplay::show_cursor(&CGDisplay::main())
169-
.map_err(CaptureError::CoreGraphics)?;
171+
self.show_cursor()?;
170172
self.current_pos = None;
171173
};
172174
}
@@ -364,7 +366,7 @@ fn create_event_tap<'a>(
364366
move |_proxy: CGEventTapProxy, event_type: CGEventType, cg_ev: &CGEvent| {
365367
log::trace!("Got event from tap: {event_type:?}");
366368
let mut state = client_state.blocking_lock();
367-
let mut pos = None;
369+
let mut capture_position = None;
368370
let mut res_events = vec![];
369371

370372
if matches!(
@@ -381,7 +383,7 @@ fn create_event_tap<'a>(
381383

382384
// Are we in a client?
383385
if let Some(current_pos) = state.current_pos {
384-
pos = Some(current_pos);
386+
capture_position = Some(current_pos);
385387
get_events(
386388
&event_type,
387389
cg_ev,
@@ -393,24 +395,30 @@ fn create_event_tap<'a>(
393395
});
394396

395397
// 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-
})
398+
if matches!(
399+
event_type,
400+
CGEventType::MouseMoved
401+
| CGEventType::LeftMouseDragged
402+
| CGEventType::RightMouseDragged
403+
| CGEventType::OtherMouseDragged
404+
) {
405+
state.reset_cursor().unwrap_or_else(|e| log::warn!("{e}"));
400406
}
401-
}
402-
// Did we cross a barrier?
403-
else if matches!(event_type, CGEventType::MouseMoved) {
407+
} else if matches!(event_type, CGEventType::MouseMoved) {
408+
// Did we cross a barrier?
404409
if let Some(new_pos) = state.crossed(cg_ev) {
405-
pos = Some(new_pos);
410+
capture_position = Some(new_pos);
411+
state
412+
.start_capture(cg_ev, new_pos)
413+
.unwrap_or_else(|e| log::warn!("{e}"));
406414
res_events.push(CaptureEvent::Begin);
407415
notify_tx
408416
.blocking_send(ProducerEvent::Grab(new_pos))
409417
.expect("Failed to send notification");
410418
}
411419
}
412420

413-
if let Some(pos) = pos {
421+
if let Some(pos) = capture_position {
414422
res_events.iter().for_each(|e| {
415423
// error must be ignored, since the event channel
416424
// may already be closed when the InputCapture instance is dropped.
@@ -515,10 +523,7 @@ impl MacOSInputCapture {
515523
log::error!("Failed to handle producer event: {e}");
516524
})
517525
}
518-
519-
_ = &mut tap_exit_rx => {
520-
break;
521-
}
526+
_ = &mut tap_exit_rx => break,
522527
}
523528
}
524529
// show cursor

0 commit comments

Comments
 (0)