mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-03-23 05:00:59 +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:
@@ -5173,9 +5173,13 @@ impl Retina {
|
||||
|
||||
#[inline]
|
||||
fn on_mouse_event(&mut self, e: &mut MouseEvent, current: usize) {
|
||||
let evt_type = e.mask & 0x7;
|
||||
if evt_type == crate::input::MOUSE_TYPE_WHEEL {
|
||||
// x and y are always 0, +1 or -1
|
||||
let evt_type = e.mask & crate::input::MOUSE_TYPE_MASK;
|
||||
// Delta-based events do not contain absolute coordinates.
|
||||
// Avoid applying Retina coordinate scaling to them.
|
||||
if evt_type == crate::input::MOUSE_TYPE_WHEEL
|
||||
|| evt_type == crate::input::MOUSE_TYPE_TRACKPAD
|
||||
|| evt_type == crate::input::MOUSE_TYPE_MOVE_RELATIVE
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some(d) = self.displays.get(current) else {
|
||||
@@ -5421,6 +5425,9 @@ mod raii {
|
||||
.unwrap()
|
||||
.on_connection_close(self.0);
|
||||
}
|
||||
// Clear per-connection state to avoid stale behavior if conn ids are reused.
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
clear_relative_mouse_active(self.0);
|
||||
AUTHED_CONNS.lock().unwrap().retain(|c| c.conn_id != self.0);
|
||||
let remote_count = AUTHED_CONNS
|
||||
.lock()
|
||||
|
||||
@@ -26,6 +26,7 @@ use std::{
|
||||
thread,
|
||||
time::{self, Duration, Instant},
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
use winapi::um::winuser::WHEEL_DELTA;
|
||||
|
||||
@@ -447,7 +448,36 @@ lazy_static::lazy_static! {
|
||||
static ref KEYS_DOWN: Arc<Mutex<HashMap<KeysDown, Instant>>> = Default::default();
|
||||
static ref LATEST_PEER_INPUT_CURSOR: Arc<Mutex<Input>> = Default::default();
|
||||
static ref LATEST_SYS_CURSOR_POS: Arc<Mutex<(Option<Instant>, (i32, i32))>> = Arc::new(Mutex::new((None, (INVALID_CURSOR_POS, INVALID_CURSOR_POS))));
|
||||
// Track connections that are currently using relative mouse movement.
|
||||
// Used to disable whiteboard/cursor display for all events while in relative mode.
|
||||
static ref RELATIVE_MOUSE_CONNS: Arc<Mutex<std::collections::HashSet<i32>>> = Default::default();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_relative_mouse_active(conn: i32, active: bool) {
|
||||
let mut lock = RELATIVE_MOUSE_CONNS.lock().unwrap();
|
||||
if active {
|
||||
lock.insert(conn);
|
||||
} else {
|
||||
lock.remove(&conn);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_relative_mouse_active(conn: i32) -> bool {
|
||||
RELATIVE_MOUSE_CONNS.lock().unwrap().contains(&conn)
|
||||
}
|
||||
|
||||
/// Clears the relative mouse mode state for a connection.
|
||||
///
|
||||
/// This must be called when an authenticated connection is dropped (during connection teardown)
|
||||
/// to avoid leaking the connection id in `RELATIVE_MOUSE_CONNS` (a `Mutex<HashSet<i32>>`).
|
||||
/// Callers are responsible for invoking this on disconnect.
|
||||
#[inline]
|
||||
pub(crate) fn clear_relative_mouse_active(conn: i32) {
|
||||
set_relative_mouse_active(conn, false);
|
||||
}
|
||||
|
||||
static EXITING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
const MOUSE_MOVE_PROTECTION_TIMEOUT: Duration = Duration::from_millis(1_000);
|
||||
@@ -644,8 +674,8 @@ async fn set_uinput_resolution(minx: i32, maxx: i32, miny: i32, maxy: i32) -> Re
|
||||
|
||||
pub fn is_left_up(evt: &MouseEvent) -> bool {
|
||||
let buttons = evt.mask >> 3;
|
||||
let evt_type = evt.mask & 0x7;
|
||||
return buttons == 1 && evt_type == 2;
|
||||
let evt_type = evt.mask & MOUSE_TYPE_MASK;
|
||||
buttons == MOUSE_BUTTON_LEFT && evt_type == MOUSE_TYPE_UP
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -1003,8 +1033,16 @@ pub fn handle_mouse_(
|
||||
handle_mouse_simulation_(evt, conn);
|
||||
}
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
if _show_cursor {
|
||||
handle_mouse_show_cursor_(evt, conn, _username, _argb);
|
||||
{
|
||||
let evt_type = evt.mask & MOUSE_TYPE_MASK;
|
||||
// Relative (delta) mouse events do not include absolute coordinates, so
|
||||
// whiteboard/cursor rendering must be disabled during relative mode to prevent
|
||||
// incorrect cursor/whiteboard updates. We check both is_relative_mouse_active(conn)
|
||||
// (connection already in relative mode from prior events) and evt_type (current
|
||||
// event is relative) to guard against the first relative event before the flag is set.
|
||||
if _show_cursor && !is_relative_mouse_active(conn) && evt_type != MOUSE_TYPE_MOVE_RELATIVE {
|
||||
handle_mouse_show_cursor_(evt, conn, _username, _argb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1020,7 +1058,7 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) {
|
||||
#[cfg(windows)]
|
||||
crate::platform::windows::try_change_desktop();
|
||||
let buttons = evt.mask >> 3;
|
||||
let evt_type = evt.mask & 0x7;
|
||||
let evt_type = evt.mask & MOUSE_TYPE_MASK;
|
||||
let mut en = ENIGO.lock().unwrap();
|
||||
#[cfg(target_os = "macos")]
|
||||
en.set_ignore_flags(enigo_ignore_flags());
|
||||
@@ -1048,6 +1086,8 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) {
|
||||
}
|
||||
match evt_type {
|
||||
MOUSE_TYPE_MOVE => {
|
||||
// Switching back to absolute movement implicitly disables relative mouse mode.
|
||||
set_relative_mouse_active(conn, false);
|
||||
en.mouse_move_to(evt.x, evt.y);
|
||||
*LATEST_PEER_INPUT_CURSOR.lock().unwrap() = Input {
|
||||
conn,
|
||||
@@ -1056,6 +1096,28 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) {
|
||||
y: evt.y,
|
||||
};
|
||||
}
|
||||
// MOUSE_TYPE_MOVE_RELATIVE: Relative mouse movement for gaming/3D applications.
|
||||
// Each client independently decides whether to use relative mode.
|
||||
// Multiple clients can mix absolute and relative movements without conflict,
|
||||
// as the server simply applies the delta to the current cursor position.
|
||||
MOUSE_TYPE_MOVE_RELATIVE => {
|
||||
set_relative_mouse_active(conn, true);
|
||||
// Clamp delta to prevent extreme/malicious values from reaching OS APIs.
|
||||
// This matches the Flutter client's kMaxRelativeMouseDelta constant.
|
||||
const MAX_RELATIVE_MOUSE_DELTA: i32 = 10000;
|
||||
let dx = evt.x.clamp(-MAX_RELATIVE_MOUSE_DELTA, MAX_RELATIVE_MOUSE_DELTA);
|
||||
let dy = evt.y.clamp(-MAX_RELATIVE_MOUSE_DELTA, MAX_RELATIVE_MOUSE_DELTA);
|
||||
en.mouse_move_relative(dx, dy);
|
||||
// Get actual cursor position after relative movement for tracking
|
||||
if let Some((x, y)) = crate::get_cursor_pos() {
|
||||
*LATEST_PEER_INPUT_CURSOR.lock().unwrap() = Input {
|
||||
conn,
|
||||
time: get_time(),
|
||||
x,
|
||||
y,
|
||||
};
|
||||
}
|
||||
}
|
||||
MOUSE_TYPE_DOWN => match buttons {
|
||||
MOUSE_BUTTON_LEFT => {
|
||||
allow_err!(en.mouse_down(MouseButton::Left));
|
||||
@@ -1154,7 +1216,7 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
pub fn handle_mouse_show_cursor_(evt: &MouseEvent, conn: i32, username: String, argb: u32) {
|
||||
let buttons = evt.mask >> 3;
|
||||
let evt_type = evt.mask & 0x7;
|
||||
let evt_type = evt.mask & MOUSE_TYPE_MASK;
|
||||
match evt_type {
|
||||
MOUSE_TYPE_MOVE => {
|
||||
whiteboard::update_whiteboard(
|
||||
@@ -1170,11 +1232,22 @@ pub fn handle_mouse_show_cursor_(evt: &MouseEvent, conn: i32, username: String,
|
||||
}
|
||||
MOUSE_TYPE_UP => {
|
||||
if buttons == MOUSE_BUTTON_LEFT {
|
||||
// Some clients intentionally send button events without coordinates.
|
||||
// Fall back to the last known cursor position to avoid jumping to (0, 0).
|
||||
// TODO(protocol): (0, 0) is a valid screen coordinate. Consider using a dedicated
|
||||
// sentinel value (e.g. INVALID_CURSOR_POS) or a protocol-level flag to distinguish
|
||||
// "coordinates not provided" from "coordinates are (0, 0)". Impact is minor since
|
||||
// this only affects whiteboard rendering and clicking exactly at (0, 0) is rare.
|
||||
let (x, y) = if evt.x == 0 && evt.y == 0 {
|
||||
get_last_input_cursor_pos()
|
||||
} else {
|
||||
(evt.x, evt.y)
|
||||
};
|
||||
whiteboard::update_whiteboard(
|
||||
whiteboard::get_key_cursor(conn),
|
||||
whiteboard::CustomEvent::Cursor(whiteboard::Cursor {
|
||||
x: evt.x as _,
|
||||
y: evt.y as _,
|
||||
x: x as _,
|
||||
y: y as _,
|
||||
argb,
|
||||
btns: buttons,
|
||||
text: username,
|
||||
|
||||
Reference in New Issue
Block a user