mirror of
https://github.com/feschber/lan-mouse.git
synced 2026-03-07 20:09:59 +03:00
fix inconsistent mouse capture on macos
This commit is contained in:
@@ -1,31 +1,39 @@
|
|||||||
use super::{error::MacosCaptureCreationError, Capture, CaptureError, CaptureEvent, Position};
|
use super::{error::MacosCaptureCreationError, Capture, CaptureError, CaptureEvent, Position};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use core_foundation::base::{kCFAllocatorDefault, CFRelease};
|
use core_foundation::{
|
||||||
use core_foundation::date::CFTimeInterval;
|
base::{kCFAllocatorDefault, CFRelease},
|
||||||
use core_foundation::number::{kCFBooleanTrue, CFBooleanRef};
|
date::CFTimeInterval,
|
||||||
use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop, CFRunLoopSource};
|
number::{kCFBooleanTrue, CFBooleanRef},
|
||||||
use core_foundation::string::{kCFStringEncodingUTF8, CFStringCreateWithCString, CFStringRef};
|
runloop::{kCFRunLoopCommonModes, CFRunLoop, CFRunLoopSource},
|
||||||
use core_graphics::base::{kCGErrorSuccess, CGError};
|
string::{kCFStringEncodingUTF8, CFStringCreateWithCString, CFStringRef},
|
||||||
use core_graphics::display::{CGDisplay, CGPoint};
|
};
|
||||||
use core_graphics::event::{
|
use core_graphics::{
|
||||||
CGEvent, CGEventFlags, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement,
|
base::{kCGErrorSuccess, CGError},
|
||||||
CGEventTapProxy, CGEventType, CallbackResult, EventField,
|
display::{CGDisplay, CGPoint},
|
||||||
|
event::{
|
||||||
|
CGEvent, CGEventFlags, CGEventTap, CGEventTapLocation, CGEventTapOptions,
|
||||||
|
CGEventTapPlacement, CGEventTapProxy, CGEventType, CallbackResult, EventField,
|
||||||
|
},
|
||||||
|
event_source::{CGEventSource, CGEventSourceStateID},
|
||||||
};
|
};
|
||||||
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
|
|
||||||
use futures_core::Stream;
|
use futures_core::Stream;
|
||||||
use input_event::{Event, KeyboardEvent, PointerEvent, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT};
|
use input_event::{Event, KeyboardEvent, PointerEvent, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT};
|
||||||
use keycode::{KeyMap, KeyMapping};
|
use keycode::{KeyMap, KeyMapping};
|
||||||
use libc::c_void;
|
use libc::c_void;
|
||||||
use once_cell::unsync::Lazy;
|
use once_cell::unsync::Lazy;
|
||||||
use std::collections::HashSet;
|
use std::{
|
||||||
use std::ffi::{c_char, CString};
|
collections::HashSet,
|
||||||
use std::pin::Pin;
|
ffi::{c_char, CString},
|
||||||
use std::sync::Arc;
|
pin::Pin,
|
||||||
use std::task::{ready, Context, Poll};
|
sync::Arc,
|
||||||
use std::thread::{self};
|
task::{ready, Context, Poll},
|
||||||
use tokio::sync::mpsc::{self, Receiver, Sender};
|
thread::{self},
|
||||||
use tokio::sync::{oneshot, Mutex};
|
};
|
||||||
|
use tokio::sync::{
|
||||||
|
mpsc::{self, Receiver, Sender},
|
||||||
|
oneshot, Mutex,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
struct Bounds {
|
struct Bounds {
|
||||||
@@ -37,9 +45,15 @@ struct Bounds {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct InputCaptureState {
|
struct InputCaptureState {
|
||||||
|
/// active capture positions
|
||||||
active_clients: Lazy<HashSet<Position>>,
|
active_clients: Lazy<HashSet<Position>>,
|
||||||
|
/// the currently entered capture position, if any
|
||||||
current_pos: Option<Position>,
|
current_pos: Option<Position>,
|
||||||
|
/// position where the cursor was captured
|
||||||
|
enter_position: Option<CGPoint>,
|
||||||
|
/// bounds of the input capture area
|
||||||
bounds: Bounds,
|
bounds: Bounds,
|
||||||
|
/// current state of modifier keys
|
||||||
modifier_state: XMods,
|
modifier_state: XMods,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +71,7 @@ impl InputCaptureState {
|
|||||||
let mut res = Self {
|
let mut res = Self {
|
||||||
active_clients: Lazy::new(HashSet::new),
|
active_clients: Lazy::new(HashSet::new),
|
||||||
current_pos: None,
|
current_pos: None,
|
||||||
|
enter_position: None,
|
||||||
bounds: Bounds::default(),
|
bounds: Bounds::default(),
|
||||||
modifier_state: Default::default(),
|
modifier_state: Default::default(),
|
||||||
};
|
};
|
||||||
@@ -98,45 +113,34 @@ impl InputCaptureState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// We can't disable mouse movement when in a client so we need to reset the cursor position
|
/// start the input capture by
|
||||||
// to the edge of the screen, the cursor will be hidden but we dont want it to appear in a
|
fn start_capture(&mut self, event: &CGEvent, position: Position) -> Result<(), CaptureError> {
|
||||||
// random location when we exit the client
|
let mut location = event.location();
|
||||||
fn reset_mouse_position(&self, event: &CGEvent) -> Result<(), CaptureError> {
|
let edge_offset = 1.0;
|
||||||
if let Some(pos) = self.current_pos {
|
// move cursor location to display bounds
|
||||||
let location = event.location();
|
match position {
|
||||||
let edge_offset = 1.0;
|
Position::Left => location.x = self.bounds.xmin + edge_offset,
|
||||||
|
Position::Right => location.x = self.bounds.xmax - edge_offset,
|
||||||
|
Position::Top => location.y = self.bounds.ymin + edge_offset,
|
||||||
|
Position::Bottom => location.y = self.bounds.ymax - edge_offset,
|
||||||
|
};
|
||||||
|
self.enter_position = Some(location);
|
||||||
|
self.reset_cursor()
|
||||||
|
}
|
||||||
|
|
||||||
// After the cursor is warped no event is produced but the next event
|
/// resets the cursor to the position, where the capture started
|
||||||
// will carry the delta from the warp so only half the delta is needed to move the cursor
|
fn reset_cursor(&mut self) -> Result<(), CaptureError> {
|
||||||
let delta_y = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_Y) / 2.0;
|
let pos = self.enter_position.expect("capture active");
|
||||||
let delta_x = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_X) / 2.0;
|
log::trace!("Resetting cursor position to: {}, {}", pos.x, pos.y);
|
||||||
|
CGDisplay::warp_mouse_cursor_position(pos).map_err(CaptureError::WarpCursor)
|
||||||
|
}
|
||||||
|
|
||||||
let mut new_x = location.x + delta_x;
|
fn hide_cursor(&self) -> Result<(), CaptureError> {
|
||||||
let mut new_y = location.y + delta_y;
|
CGDisplay::hide_cursor(&CGDisplay::main()).map_err(CaptureError::CoreGraphics)
|
||||||
|
}
|
||||||
|
|
||||||
match pos {
|
fn show_cursor(&self) -> Result<(), CaptureError> {
|
||||||
Position::Left => {
|
CGDisplay::show_cursor(&CGDisplay::main()).map_err(CaptureError::CoreGraphics)
|
||||||
new_x = self.bounds.xmin + edge_offset;
|
|
||||||
}
|
|
||||||
Position::Right => {
|
|
||||||
new_x = self.bounds.xmax - edge_offset;
|
|
||||||
}
|
|
||||||
Position::Top => {
|
|
||||||
new_y = self.bounds.ymin + edge_offset;
|
|
||||||
}
|
|
||||||
Position::Bottom => {
|
|
||||||
new_y = self.bounds.ymax - edge_offset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let new_pos = CGPoint::new(new_x, new_y);
|
|
||||||
|
|
||||||
log::trace!("Resetting cursor position to: {new_x}, {new_y}");
|
|
||||||
|
|
||||||
return CGDisplay::warp_mouse_cursor_position(new_pos)
|
|
||||||
.map_err(CaptureError::WarpCursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(CaptureError::ResetMouseWithoutClient)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_producer_event(
|
async fn handle_producer_event(
|
||||||
@@ -147,15 +151,13 @@ impl InputCaptureState {
|
|||||||
match producer_event {
|
match producer_event {
|
||||||
ProducerEvent::Release => {
|
ProducerEvent::Release => {
|
||||||
if self.current_pos.is_some() {
|
if self.current_pos.is_some() {
|
||||||
CGDisplay::show_cursor(&CGDisplay::main())
|
self.show_cursor()?;
|
||||||
.map_err(CaptureError::CoreGraphics)?;
|
|
||||||
self.current_pos = None;
|
self.current_pos = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ProducerEvent::Grab(pos) => {
|
ProducerEvent::Grab(pos) => {
|
||||||
if self.current_pos.is_none() {
|
if self.current_pos.is_none() {
|
||||||
CGDisplay::hide_cursor(&CGDisplay::main())
|
self.hide_cursor()?;
|
||||||
.map_err(CaptureError::CoreGraphics)?;
|
|
||||||
self.current_pos = Some(pos);
|
self.current_pos = Some(pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,8 +167,7 @@ impl InputCaptureState {
|
|||||||
ProducerEvent::Destroy(p) => {
|
ProducerEvent::Destroy(p) => {
|
||||||
if let Some(current) = self.current_pos {
|
if let Some(current) = self.current_pos {
|
||||||
if current == p {
|
if current == p {
|
||||||
CGDisplay::show_cursor(&CGDisplay::main())
|
self.show_cursor()?;
|
||||||
.map_err(CaptureError::CoreGraphics)?;
|
|
||||||
self.current_pos = None;
|
self.current_pos = None;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -364,7 +365,7 @@ fn create_event_tap<'a>(
|
|||||||
move |_proxy: CGEventTapProxy, event_type: CGEventType, cg_ev: &CGEvent| {
|
move |_proxy: CGEventTapProxy, event_type: CGEventType, cg_ev: &CGEvent| {
|
||||||
log::trace!("Got event from tap: {event_type:?}");
|
log::trace!("Got event from tap: {event_type:?}");
|
||||||
let mut state = client_state.blocking_lock();
|
let mut state = client_state.blocking_lock();
|
||||||
let mut pos = None;
|
let mut capture_position = None;
|
||||||
let mut res_events = vec![];
|
let mut res_events = vec![];
|
||||||
|
|
||||||
if matches!(
|
if matches!(
|
||||||
@@ -381,7 +382,7 @@ fn create_event_tap<'a>(
|
|||||||
|
|
||||||
// Are we in a client?
|
// Are we in a client?
|
||||||
if let Some(current_pos) = state.current_pos {
|
if let Some(current_pos) = state.current_pos {
|
||||||
pos = Some(current_pos);
|
capture_position = Some(current_pos);
|
||||||
get_events(
|
get_events(
|
||||||
&event_type,
|
&event_type,
|
||||||
cg_ev,
|
cg_ev,
|
||||||
@@ -393,16 +394,22 @@ fn create_event_tap<'a>(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Keep (hidden) cursor at the edge of the screen
|
// Keep (hidden) cursor at the edge of the screen
|
||||||
if matches!(event_type, CGEventType::MouseMoved) {
|
if matches!(
|
||||||
state.reset_mouse_position(cg_ev).unwrap_or_else(|e| {
|
event_type,
|
||||||
log::error!("Failed to reset mouse position: {e}");
|
CGEventType::MouseMoved
|
||||||
})
|
| CGEventType::LeftMouseDragged
|
||||||
|
| CGEventType::RightMouseDragged
|
||||||
|
| CGEventType::OtherMouseDragged
|
||||||
|
) {
|
||||||
|
state.reset_cursor().unwrap_or_else(|e| log::warn!("{e}"));
|
||||||
}
|
}
|
||||||
}
|
} else if matches!(event_type, CGEventType::MouseMoved) {
|
||||||
// Did we cross a barrier?
|
// Did we cross a barrier?
|
||||||
else if matches!(event_type, CGEventType::MouseMoved) {
|
|
||||||
if let Some(new_pos) = state.crossed(cg_ev) {
|
if let Some(new_pos) = state.crossed(cg_ev) {
|
||||||
pos = Some(new_pos);
|
capture_position = Some(new_pos);
|
||||||
|
state
|
||||||
|
.start_capture(cg_ev, new_pos)
|
||||||
|
.unwrap_or_else(|e| log::warn!("{e}"));
|
||||||
res_events.push(CaptureEvent::Begin);
|
res_events.push(CaptureEvent::Begin);
|
||||||
notify_tx
|
notify_tx
|
||||||
.blocking_send(ProducerEvent::Grab(new_pos))
|
.blocking_send(ProducerEvent::Grab(new_pos))
|
||||||
@@ -410,7 +417,7 @@ fn create_event_tap<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(pos) = pos {
|
if let Some(pos) = capture_position {
|
||||||
res_events.iter().for_each(|e| {
|
res_events.iter().for_each(|e| {
|
||||||
// error must be ignored, since the event channel
|
// error must be ignored, since the event channel
|
||||||
// may already be closed when the InputCapture instance is dropped.
|
// may already be closed when the InputCapture instance is dropped.
|
||||||
@@ -515,10 +522,7 @@ impl MacOSInputCapture {
|
|||||||
log::error!("Failed to handle producer event: {e}");
|
log::error!("Failed to handle producer event: {e}");
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
_ = &mut tap_exit_rx => break,
|
||||||
_ = &mut tap_exit_rx => {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// show cursor
|
// show cursor
|
||||||
|
|||||||
Reference in New Issue
Block a user