mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-03-25 22:21:01 +03:00
feat: Add relative mouse mode (#13928)
* feat: Add relative mouse mode - Add "Relative Mouse Mode" toggle in desktop toolbar and bind to InputModel - Implement relative mouse movement path: Flutter pointer deltas -> `type: move_relative` -> new `MOUSE_TYPE_MOVE_RELATIVE` in Rust - In server input service, simulate relative movement via Enigo and keep latest cursor position in sync - Track pointer-lock center in Flutter (local widget + screen coordinates) and re-center OS cursor after each relative move - Update pointer-lock center on window move/resize/restore/maximize and when remote display geometry changes - Hide local cursor when relative mouse mode is active (both Flutter cursor and OS cursor), restore on leave/disable - On Windows, clip OS cursor to the window rect while in relative mode and release clip when leaving/turning off - Implement platform helpers: `get_cursor_pos`, `set_cursor_pos`, `show_cursor`, `clip_cursor` (no-op clip/hide on Linux for now) - Add keyboard shortcut Ctrl+Alt+Shift+M to toggle relative mode (enabled by default, works on all platforms) - Remove `enable-relative-mouse-shortcut` config option - shortcut is now always available when keyboard permission is granted - Handle window blur/focus/minimize events to properly release/restore cursor constraints - Add MOUSE_TYPE_MASK constant and unit tests for mouse event constants Note: Relative mouse mode state is NOT persisted to config (session-only). Note: On Linux, show_cursor and clip_cursor are no-ops; cursor hiding is handled by Flutter side. Signed-off-by: fufesou <linlong1266@gmail.com> * feat(mouse): relative mouse mode, exit hint Signed-off-by: fufesou <linlong1266@gmail.com> * refact(relative mouse): shortcut Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
@@ -97,6 +97,7 @@ extern "C" {
|
||||
y: *mut c_int,
|
||||
screen_num: *mut c_int,
|
||||
) -> c_int;
|
||||
fn xdo_move_mouse(xdo: Xdo, x: c_int, y: c_int, screen: c_int) -> c_int;
|
||||
fn xdo_new(display: *const c_char) -> Xdo;
|
||||
fn xdo_get_active_window(xdo: Xdo, window: *mut *mut c_void) -> c_int;
|
||||
fn xdo_get_window_location(
|
||||
@@ -174,6 +175,56 @@ pub fn get_cursor_pos() -> Option<(i32, i32)> {
|
||||
res
|
||||
}
|
||||
|
||||
pub fn set_cursor_pos(x: i32, y: i32) -> bool {
|
||||
let mut res = false;
|
||||
XDO.with(|xdo| {
|
||||
match xdo.try_borrow_mut() {
|
||||
Ok(xdo) => {
|
||||
if xdo.is_null() {
|
||||
log::debug!("set_cursor_pos: xdo is null");
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
let ret = xdo_move_mouse(*xdo, x, y, 0);
|
||||
if ret != 0 {
|
||||
log::debug!(
|
||||
"set_cursor_pos: xdo_move_mouse failed with code {} for coordinates ({}, {})",
|
||||
ret, x, y
|
||||
);
|
||||
}
|
||||
res = ret == 0;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
log::debug!("set_cursor_pos: failed to borrow xdo");
|
||||
}
|
||||
}
|
||||
});
|
||||
res
|
||||
}
|
||||
|
||||
/// Clip cursor - Linux implementation is a no-op.
|
||||
///
|
||||
/// On X11, there's no direct equivalent to Windows ClipCursor. XGrabPointer
|
||||
/// can confine the pointer but requires a window handle and has side effects.
|
||||
///
|
||||
/// On Wayland, pointer constraints require the zwp_pointer_constraints_v1
|
||||
/// protocol which is compositor-dependent.
|
||||
///
|
||||
/// For relative mouse mode on Linux, the Flutter side uses pointer warping
|
||||
/// (set_cursor_pos) to re-center the cursor after each movement, which achieves
|
||||
/// a similar effect without requiring cursor clipping.
|
||||
///
|
||||
/// Returns true (always succeeds as no-op).
|
||||
pub fn clip_cursor(_rect: Option<(i32, i32, i32, i32)>) -> bool {
|
||||
// Log only once per process to avoid flooding logs when called frequently.
|
||||
static LOGGED: AtomicBool = AtomicBool::new(false);
|
||||
if !LOGGED.swap(true, Ordering::Relaxed) {
|
||||
log::debug!("clip_cursor called (no-op on Linux, this message is logged only once)");
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn reset_input_cache() {}
|
||||
|
||||
pub fn get_focused_display(displays: Vec<DisplayInfo>) -> Option<usize> {
|
||||
|
||||
@@ -32,8 +32,12 @@ use std::{
|
||||
os::unix::process::CommandExt,
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
// macOS boolean_t is defined as `int` in <mach/boolean.h>
|
||||
type BooleanT = hbb_common::libc::c_int;
|
||||
|
||||
static PRIVILEGES_SCRIPTS_DIR: Dir =
|
||||
include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts");
|
||||
static mut LATEST_SEED: i32 = 0;
|
||||
@@ -42,6 +46,11 @@ static mut LATEST_SEED: i32 = 0;
|
||||
// using one that includes the custom client name.
|
||||
const UPDATE_TEMP_DIR: &str = "/tmp/.rustdeskupdate";
|
||||
|
||||
/// Global mutex to serialize CoreGraphics cursor operations.
|
||||
/// This prevents race conditions between cursor visibility (hide depth tracking)
|
||||
/// and cursor positioning/clipping operations.
|
||||
static CG_CURSOR_MUTEX: Mutex<()> = Mutex::new(());
|
||||
|
||||
extern "C" {
|
||||
fn CGSCurrentCursorSeed() -> i32;
|
||||
fn CGEventCreate(r: *const c_void) -> *const c_void;
|
||||
@@ -64,6 +73,8 @@ extern "C" {
|
||||
fn majorVersion() -> u32;
|
||||
fn MacGetMode(display: u32, width: *mut u32, height: *mut u32) -> BOOL;
|
||||
fn MacSetMode(display: u32, width: u32, height: u32, tryHiDPI: bool) -> BOOL;
|
||||
fn CGWarpMouseCursorPosition(newCursorPosition: CGPoint) -> CGError;
|
||||
fn CGAssociateMouseAndMouseCursorPosition(connected: BooleanT) -> CGError;
|
||||
}
|
||||
|
||||
pub fn major_version() -> u32 {
|
||||
@@ -387,6 +398,99 @@ pub fn get_cursor_pos() -> Option<(i32, i32)> {
|
||||
*/
|
||||
}
|
||||
|
||||
/// Warp the mouse cursor to the specified screen position.
|
||||
///
|
||||
/// # Thread Safety
|
||||
/// This function affects global cursor state and acquires `CG_CURSOR_MUTEX`.
|
||||
/// Callers must ensure no nested calls occur while the mutex is held.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `x` - X coordinate in screen points (macOS uses points, not pixels)
|
||||
/// * `y` - Y coordinate in screen points
|
||||
pub fn set_cursor_pos(x: i32, y: i32) -> bool {
|
||||
// Acquire lock with deadlock detection in debug builds.
|
||||
// In debug builds, try_lock detects re-entrant calls early; on failure we return immediately.
|
||||
// In release builds, we use blocking lock() which will wait if contended.
|
||||
#[cfg(debug_assertions)]
|
||||
let _guard = match CG_CURSOR_MUTEX.try_lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(std::sync::TryLockError::WouldBlock) => {
|
||||
log::error!("[BUG] set_cursor_pos: CG_CURSOR_MUTEX is already held - potential deadlock!");
|
||||
debug_assert!(false, "Re-entrant call to set_cursor_pos detected");
|
||||
return false;
|
||||
}
|
||||
Err(std::sync::TryLockError::Poisoned(e)) => e.into_inner(),
|
||||
};
|
||||
#[cfg(not(debug_assertions))]
|
||||
let _guard = CG_CURSOR_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
|
||||
unsafe {
|
||||
let result = CGWarpMouseCursorPosition(CGPoint {
|
||||
x: x as f64,
|
||||
y: y as f64,
|
||||
});
|
||||
if result != CGError::Success {
|
||||
log::error!(
|
||||
"CGWarpMouseCursorPosition({}, {}) returned error: {:?}",
|
||||
x,
|
||||
y,
|
||||
result
|
||||
);
|
||||
}
|
||||
result == CGError::Success
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle pointer lock (dissociate/associate mouse from cursor position).
|
||||
///
|
||||
/// On macOS, cursor clipping is not supported directly like Windows ClipCursor.
|
||||
/// Instead, we use CGAssociateMouseAndMouseCursorPosition to dissociate mouse
|
||||
/// movement from cursor position, achieving a "pointer lock" effect.
|
||||
///
|
||||
/// # Thread Safety
|
||||
/// This function affects global cursor state and acquires `CG_CURSOR_MUTEX`.
|
||||
/// Callers must ensure only one owner toggles pointer lock at a time;
|
||||
/// nested Some/None transitions from different call sites may cause unexpected behavior.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `rect` - When `Some(_)`, dissociates mouse from cursor (enables pointer lock).
|
||||
/// When `None`, re-associates mouse with cursor (disables pointer lock).
|
||||
/// The rect coordinate values are ignored on macOS; only `Some`/`None` matters.
|
||||
/// The parameter signature matches Windows for API consistency.
|
||||
pub fn clip_cursor(rect: Option<(i32, i32, i32, i32)>) -> bool {
|
||||
// Acquire lock with deadlock detection in debug builds.
|
||||
// In debug builds, try_lock detects re-entrant calls early; on failure we return immediately.
|
||||
// In release builds, we use blocking lock() which will wait if contended.
|
||||
#[cfg(debug_assertions)]
|
||||
let _guard = match CG_CURSOR_MUTEX.try_lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(std::sync::TryLockError::WouldBlock) => {
|
||||
log::error!("[BUG] clip_cursor: CG_CURSOR_MUTEX is already held - potential deadlock!");
|
||||
debug_assert!(false, "Re-entrant call to clip_cursor detected");
|
||||
return false;
|
||||
}
|
||||
Err(std::sync::TryLockError::Poisoned(e)) => e.into_inner(),
|
||||
};
|
||||
#[cfg(not(debug_assertions))]
|
||||
let _guard = CG_CURSOR_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
|
||||
// CGAssociateMouseAndMouseCursorPosition takes a boolean_t:
|
||||
// 1 (true) = associate mouse with cursor position (normal mode)
|
||||
// 0 (false) = dissociate mouse from cursor position (pointer lock mode)
|
||||
// When rect is Some, we want pointer lock (dissociate), so associate = false (0).
|
||||
// When rect is None, we want normal mode (associate), so associate = true (1).
|
||||
let associate: BooleanT = if rect.is_some() { 0 } else { 1 };
|
||||
unsafe {
|
||||
let result = CGAssociateMouseAndMouseCursorPosition(associate);
|
||||
if result != CGError::Success {
|
||||
log::warn!(
|
||||
"CGAssociateMouseAndMouseCursorPosition({}) returned error: {:?}",
|
||||
associate,
|
||||
result
|
||||
);
|
||||
}
|
||||
result == CGError::Success
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_focused_display(displays: Vec<DisplayInfo>) -> Option<usize> {
|
||||
autoreleasepool(|| unsafe_get_focused_display(displays))
|
||||
}
|
||||
|
||||
@@ -26,18 +26,13 @@ pub mod linux_desktop_manager;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod gtk_sudo;
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use hbb_common::{
|
||||
message_proto::CursorData,
|
||||
sysinfo::Pid,
|
||||
ResultType,
|
||||
};
|
||||
#[cfg(all(
|
||||
not(all(target_os = "windows", not(target_pointer_width = "64"))),
|
||||
not(any(target_os = "android", target_os = "ios"))))]
|
||||
use hbb_common::{
|
||||
sysinfo::System,
|
||||
};
|
||||
not(any(target_os = "android", target_os = "ios"))
|
||||
))]
|
||||
use hbb_common::sysinfo::System;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use hbb_common::{message_proto::CursorData, sysinfo::Pid, ResultType};
|
||||
use std::sync::{Arc, Mutex};
|
||||
#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
|
||||
pub const SERVICE_INTERVAL: u64 = 300;
|
||||
|
||||
@@ -116,12 +116,51 @@ pub fn get_focused_display(displays: Vec<DisplayInfo>) -> Option<usize> {
|
||||
|
||||
pub fn get_cursor_pos() -> Option<(i32, i32)> {
|
||||
unsafe {
|
||||
#[allow(invalid_value)]
|
||||
let mut out = mem::MaybeUninit::uninit().assume_init();
|
||||
if GetCursorPos(&mut out) == FALSE {
|
||||
let mut out = mem::MaybeUninit::<POINT>::uninit();
|
||||
if GetCursorPos(out.as_mut_ptr()) == FALSE {
|
||||
return None;
|
||||
}
|
||||
return Some((out.x, out.y));
|
||||
let out = out.assume_init();
|
||||
Some((out.x, out.y))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_cursor_pos(x: i32, y: i32) -> bool {
|
||||
unsafe {
|
||||
if SetCursorPos(x, y) == FALSE {
|
||||
let err = GetLastError();
|
||||
log::warn!("SetCursorPos failed: x={}, y={}, error_code={}", x, y, err);
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Clip cursor to a rectangle. Pass None to unclip.
|
||||
pub fn clip_cursor(rect: Option<(i32, i32, i32, i32)>) -> bool {
|
||||
unsafe {
|
||||
let result = match rect {
|
||||
Some((left, top, right, bottom)) => {
|
||||
let r = RECT {
|
||||
left,
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
};
|
||||
ClipCursor(&r)
|
||||
}
|
||||
None => ClipCursor(std::ptr::null()),
|
||||
};
|
||||
if result == FALSE {
|
||||
let err = GetLastError();
|
||||
log::warn!(
|
||||
"ClipCursor failed: rect={:?}, error_code={}",
|
||||
rect,
|
||||
err
|
||||
);
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user