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:
fufesou
2026-01-09 10:03:14 +08:00
committed by GitHub
parent 3a9084006f
commit 998b75856d
90 changed files with 3089 additions and 165 deletions

View File

@@ -71,6 +71,19 @@ pub mod input {
pub const MOUSE_TYPE_UP: i32 = 2;
pub const MOUSE_TYPE_WHEEL: i32 = 3;
pub const MOUSE_TYPE_TRACKPAD: i32 = 4;
/// Relative mouse movement type for gaming/3D applications.
/// This type sends delta (dx, dy) values instead of absolute coordinates.
/// NOTE: This is only supported by the Flutter client. The Sciter client (deprecated)
/// does not support relative mouse mode due to:
/// 1. Fixed send_mouse() function signature that doesn't allow type differentiation
/// 2. Lack of pointer lock API in Sciter/TIS
/// 3. No OS cursor control (hide/show/clip) FFI bindings in Sciter UI
pub const MOUSE_TYPE_MOVE_RELATIVE: i32 = 5;
/// Mask to extract the mouse event type from the mask field.
/// The lower 3 bits contain the event type (MOUSE_TYPE_*), giving a valid range of 0-7.
/// Currently defined types use values 0-5; values 6 and 7 are reserved for future use.
pub const MOUSE_TYPE_MASK: i32 = 0x7;
pub const MOUSE_BUTTON_LEFT: i32 = 0x01;
pub const MOUSE_BUTTON_RIGHT: i32 = 0x02;
@@ -175,6 +188,20 @@ pub fn is_support_file_transfer_resume_num(ver: i64) -> bool {
ver >= hbb_common::get_version_number("1.4.2")
}
/// Minimum server version required for relative mouse mode support.
/// This constant must mirror Flutter's `kMinVersionForRelativeMouseMode` in `consts.dart`.
const MIN_VERSION_RELATIVE_MOUSE_MODE: &str = "1.4.5";
#[inline]
pub fn is_support_relative_mouse_mode(ver: &str) -> bool {
is_support_relative_mouse_mode_num(hbb_common::get_version_number(ver))
}
#[inline]
pub fn is_support_relative_mouse_mode_num(ver: i64) -> bool {
ver >= hbb_common::get_version_number(MIN_VERSION_RELATIVE_MOUSE_MODE)
}
// is server process, with "--server" args
#[inline]
pub fn is_server() -> bool {
@@ -2462,4 +2489,36 @@ mod tests {
assert!(!is_public("https://rustdesk.computer.com"));
assert!(!is_public("rustdesk.comhello.com"));
}
#[test]
fn test_mouse_event_constants_and_mask_layout() {
use super::input::*;
// Verify MOUSE_TYPE constants are unique and within the mask range.
let types = [
MOUSE_TYPE_MOVE,
MOUSE_TYPE_DOWN,
MOUSE_TYPE_UP,
MOUSE_TYPE_WHEEL,
MOUSE_TYPE_TRACKPAD,
MOUSE_TYPE_MOVE_RELATIVE,
];
let mut seen = std::collections::HashSet::new();
for t in types.iter() {
assert!(seen.insert(*t), "Duplicate mouse type: {}", t);
assert_eq!(
*t & MOUSE_TYPE_MASK,
*t,
"Mouse type {} exceeds mask {}",
t,
MOUSE_TYPE_MASK
);
}
// The mask layout is: lower 3 bits for type, upper bits for buttons (shifted by 3).
let combined_mask = MOUSE_TYPE_DOWN | ((MOUSE_BUTTON_LEFT | MOUSE_BUTTON_RIGHT) << 3);
assert_eq!(combined_mask & MOUSE_TYPE_MASK, MOUSE_TYPE_DOWN);
assert_eq!(combined_mask >> 3, MOUSE_BUTTON_LEFT | MOUSE_BUTTON_RIGHT);
}
}