mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-03-27 23:21:06 +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:
@@ -1215,6 +1215,66 @@ pub fn main_set_input_source(session_id: SessionID, value: String) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set cursor position (for pointer lock re-centering).
|
||||
///
|
||||
/// # Returns
|
||||
/// - `true`: cursor position was successfully set
|
||||
/// - `false`: operation failed or not supported
|
||||
///
|
||||
/// # Platform behavior
|
||||
/// - Windows/macOS/Linux: attempts to move the cursor to (x, y)
|
||||
/// - Android/iOS: no-op, always returns `false`
|
||||
pub fn main_set_cursor_position(x: i32, y: i32) -> SyncReturn<bool> {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
SyncReturn(crate::set_cursor_pos(x, y))
|
||||
}
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
{
|
||||
let _ = (x, y);
|
||||
SyncReturn(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Clip cursor to a rectangle (for pointer lock).
|
||||
///
|
||||
/// When `enable` is true, the cursor is clipped to the rectangle defined by
|
||||
/// `left`, `top`, `right`, `bottom`. When `enable` is false, the rectangle
|
||||
/// values are ignored and the cursor is unclipped.
|
||||
///
|
||||
/// # Returns
|
||||
/// - `true`: operation succeeded or no-op completed
|
||||
/// - `false`: operation failed
|
||||
///
|
||||
/// # Platform behavior
|
||||
/// - Windows: uses ClipCursor API to confine cursor to the specified rectangle
|
||||
/// - macOS: uses CGAssociateMouseAndMouseCursorPosition for pointer lock effect;
|
||||
/// the rect coordinates are ignored (only Some/None matters)
|
||||
/// - Linux: no-op, always returns `true`; use pointer warping for similar effect
|
||||
/// - Android/iOS: no-op, always returns `false`
|
||||
pub fn main_clip_cursor(
|
||||
left: i32,
|
||||
top: i32,
|
||||
right: i32,
|
||||
bottom: i32,
|
||||
enable: bool,
|
||||
) -> SyncReturn<bool> {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
let rect = if enable {
|
||||
Some((left, top, right, bottom))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
SyncReturn(crate::clip_cursor(rect))
|
||||
}
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
{
|
||||
let _ = (left, top, right, bottom, enable);
|
||||
SyncReturn(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main_get_my_id() -> String {
|
||||
get_id()
|
||||
}
|
||||
@@ -1748,8 +1808,99 @@ pub fn session_send_pointer(session_id: SessionID, msg: String) {
|
||||
super::flutter::session_send_pointer(session_id, msg);
|
||||
}
|
||||
|
||||
/// Send mouse event from Flutter to the remote peer.
|
||||
///
|
||||
/// # Relative Mouse Mode Message Contract
|
||||
///
|
||||
/// When the message contains a `relative_mouse_mode` field, this function validates
|
||||
/// and filters activation/deactivation markers.
|
||||
///
|
||||
/// **Mode Authority:**
|
||||
/// The Flutter InputModel is authoritative for relative mouse mode activation/deactivation.
|
||||
/// The server (via `input_service.rs`) only consumes forwarded delta movements and tracks
|
||||
/// relative movement processing state, but does NOT control mode activation/deactivation.
|
||||
///
|
||||
/// **Deactivation Markers are Local-Only:**
|
||||
/// Deactivation markers (`relative_mouse_mode: "0"`) are NEVER forwarded to the server.
|
||||
/// They are handled entirely on the client side to reset local UI state (cursor visibility,
|
||||
/// pointer lock, etc.). The server does not rely on deactivation markers and should not
|
||||
/// expect to receive them.
|
||||
///
|
||||
/// **Contract (Flutter side MUST adhere to):**
|
||||
/// 1. `relative_mouse_mode` field is ONLY present on activation/deactivation marker messages,
|
||||
/// NEVER on normal pointer events (move, button, scroll).
|
||||
/// 2. Deactivation marker: `{"relative_mouse_mode": "0"}` - local-only, never forwarded.
|
||||
/// 3. Activation marker: `{"relative_mouse_mode": "1", "type": "move_relative", "x": "0", "y": "0"}`
|
||||
/// - MUST use `type="move_relative"` with `x="0"` and `y="0"` (safe no-op).
|
||||
/// - Any other combination is dropped to prevent accidental cursor movement.
|
||||
///
|
||||
/// If these assumptions are violated (e.g., `relative_mouse_mode` is added to normal events),
|
||||
/// legitimate mouse events may be silently dropped by the early-return logic below.
|
||||
pub fn session_send_mouse(session_id: SessionID, msg: String) {
|
||||
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(&msg) {
|
||||
// Relative mouse mode marker validation (Flutter-only).
|
||||
// This only validates and filters markers; the server tracks per-connection
|
||||
// relative-movement processing state but not mode activation/deactivation.
|
||||
// See doc comment above for the message contract.
|
||||
if let Some(v) = m.get("relative_mouse_mode") {
|
||||
let active = matches!(v.as_str(), "1" | "Y" | "on");
|
||||
|
||||
// Disable marker: local-only, never forwarded to the server.
|
||||
// The server does not track mode deactivation; it simply stops receiving
|
||||
// relative move events when the client exits relative mouse mode.
|
||||
if !active {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
crate::keyboard::set_relative_mouse_mode_state(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable marker: validate BEFORE setting state to avoid desync.
|
||||
// This ensures we only mark as active if the marker will actually be forwarded.
|
||||
|
||||
// Enable marker is allowed to go through only if it's a safe no-op relative move.
|
||||
// This avoids accidentally moving the remote cursor (e.g. if type/x/y are missing).
|
||||
let msg_type = m.get("type").map(|t| t.as_str());
|
||||
if msg_type != Some("move_relative") {
|
||||
log::warn!(
|
||||
"relative_mouse_mode activation marker has invalid type: {:?}, expected 'move_relative'. Dropping.",
|
||||
msg_type
|
||||
);
|
||||
return;
|
||||
}
|
||||
let x_marker = m
|
||||
.get("x")
|
||||
.map(|x| x.parse::<i32>().unwrap_or(0))
|
||||
.unwrap_or(0);
|
||||
let y_marker = m
|
||||
.get("y")
|
||||
.map(|y| y.parse::<i32>().unwrap_or(0))
|
||||
.unwrap_or(0);
|
||||
if x_marker != 0 || y_marker != 0 {
|
||||
log::warn!(
|
||||
"relative_mouse_mode activation marker has non-zero coordinates: x={}, y={}. Dropping.",
|
||||
x_marker, y_marker
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard against unexpected fields that could turn this no-op into a real event.
|
||||
if m.contains_key("buttons")
|
||||
|| m.contains_key("alt")
|
||||
|| m.contains_key("ctrl")
|
||||
|| m.contains_key("shift")
|
||||
|| m.contains_key("command")
|
||||
{
|
||||
log::warn!(
|
||||
"relative_mouse_mode activation marker contains unexpected fields (buttons/alt/ctrl/shift/command). Dropping."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// All validation passed - marker will be forwarded as a no-op relative move.
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
crate::keyboard::set_relative_mouse_mode_state(true);
|
||||
}
|
||||
|
||||
let alt = m.get("alt").is_some();
|
||||
let ctrl = m.get("ctrl").is_some();
|
||||
let shift = m.get("shift").is_some();
|
||||
@@ -1769,6 +1920,7 @@ pub fn session_send_mouse(session_id: SessionID, msg: String) {
|
||||
"up" => MOUSE_TYPE_UP,
|
||||
"wheel" => MOUSE_TYPE_WHEEL,
|
||||
"trackpad" => MOUSE_TYPE_TRACKPAD,
|
||||
"move_relative" => MOUSE_TYPE_MOVE_RELATIVE,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user