mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-03-07 20:30:08 +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:
@@ -208,42 +208,56 @@ impl MouseControllable for Enigo {
|
||||
}
|
||||
|
||||
fn mouse_move_to(&mut self, x: i32, y: i32) {
|
||||
let pressed = Self::pressed_buttons();
|
||||
|
||||
let event_type = if pressed & 1 > 0 {
|
||||
CGEventType::LeftMouseDragged
|
||||
} else if pressed & 2 > 0 {
|
||||
CGEventType::RightMouseDragged
|
||||
} else {
|
||||
CGEventType::MouseMoved
|
||||
};
|
||||
|
||||
let dest = CGPoint::new(x as f64, y as f64);
|
||||
if let Some(src) = self.event_source.as_ref() {
|
||||
if let Ok(event) =
|
||||
CGEvent::new_mouse_event(src.clone(), event_type, dest, CGMouseButton::Left)
|
||||
{
|
||||
self.post(event, None);
|
||||
}
|
||||
}
|
||||
// For absolute movement, we don't set delta values
|
||||
// This maintains backward compatibility
|
||||
self.mouse_move_to_impl(x, y, None);
|
||||
}
|
||||
|
||||
fn mouse_move_relative(&mut self, x: i32, y: i32) {
|
||||
let (display_width, display_height) = Self::main_display_size();
|
||||
let (current_x, y_inv) = Self::mouse_location_raw_coords();
|
||||
let current_y = (display_height as i32) - y_inv;
|
||||
let new_x = current_x + x;
|
||||
let new_y = current_y + y;
|
||||
// Use saturating arithmetic to prevent overflow/wraparound
|
||||
let mut new_x = current_x.saturating_add(x);
|
||||
let mut new_y = current_y.saturating_add(y);
|
||||
|
||||
if new_x < 0
|
||||
|| new_x as usize > display_width
|
||||
|| new_y < 0
|
||||
|| new_y as usize > display_height
|
||||
{
|
||||
return;
|
||||
// Define screen center and edge margins for cursor reset
|
||||
let center_x = (display_width / 2) as i32;
|
||||
let center_y = (display_height / 2) as i32;
|
||||
// Margin calculation: 5% of the smaller screen dimension with a minimum of 50px.
|
||||
// This provides a comfortable buffer zone to detect when the cursor is approaching
|
||||
// screen edges, allowing us to reset it to center before it hits the boundary.
|
||||
// This ensures continuous relative mouse movement without getting stuck at edges.
|
||||
let margin = (display_width.min(display_height) / 20).max(50) as i32;
|
||||
|
||||
// Check if cursor is approaching screen boundaries
|
||||
// Use saturating_sub to prevent negative thresholds on very small displays
|
||||
let right = (display_width as i32).saturating_sub(margin);
|
||||
let bottom = (display_height as i32).saturating_sub(margin);
|
||||
let near_edge = new_x < margin
|
||||
|| new_x > right
|
||||
|| new_y < margin
|
||||
|| new_y > bottom;
|
||||
|
||||
if near_edge {
|
||||
// Reset cursor to screen center to allow continuous movement
|
||||
// The delta values are still passed correctly for games/apps
|
||||
new_x = center_x;
|
||||
new_y = center_y;
|
||||
}
|
||||
|
||||
self.mouse_move_to(new_x, new_y);
|
||||
// Clamp to screen bounds as a safety measure.
|
||||
// Use saturating_sub(1) to ensure coordinates don't exceed the last valid pixel.
|
||||
let max_x = (display_width as i32).saturating_sub(1).max(0);
|
||||
let max_y = (display_height as i32).saturating_sub(1).max(0);
|
||||
new_x = new_x.clamp(0, max_x);
|
||||
new_y = new_y.clamp(0, max_y);
|
||||
|
||||
// Pass delta values for relative movement
|
||||
// This is critical for browser Pointer Lock API support
|
||||
// The delta fields (MOUSE_EVENT_DELTA_X/Y) are used by browsers
|
||||
// to calculate movementX/Y in Pointer Lock mode
|
||||
self.mouse_move_to_impl(new_x, new_y, Some((x, y)));
|
||||
}
|
||||
|
||||
fn mouse_down(&mut self, button: MouseButton) -> crate::ResultType {
|
||||
@@ -473,6 +487,43 @@ impl Enigo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal implementation for mouse movement with optional delta values.
|
||||
///
|
||||
/// The `delta` parameter is crucial for browser Pointer Lock API support.
|
||||
/// When a browser enters Pointer Lock mode, it reads mouse delta values
|
||||
/// (MOUSE_EVENT_DELTA_X/Y) directly from CGEvent to calculate movementX/Y.
|
||||
/// Without setting these fields, the browser sees zero movement.
|
||||
fn mouse_move_to_impl(&mut self, x: i32, y: i32, delta: Option<(i32, i32)>) {
|
||||
let pressed = Self::pressed_buttons();
|
||||
|
||||
// Determine event type and corresponding mouse button based on pressed buttons.
|
||||
// The CGMouseButton must match the event type for drag events.
|
||||
let (event_type, button) = if pressed & 1 > 0 {
|
||||
(CGEventType::LeftMouseDragged, CGMouseButton::Left)
|
||||
} else if pressed & 2 > 0 {
|
||||
(CGEventType::RightMouseDragged, CGMouseButton::Right)
|
||||
} else if pressed & 4 > 0 {
|
||||
(CGEventType::OtherMouseDragged, CGMouseButton::Center)
|
||||
} else {
|
||||
(CGEventType::MouseMoved, CGMouseButton::Left) // Button doesn't matter for MouseMoved
|
||||
};
|
||||
|
||||
let dest = CGPoint::new(x as f64, y as f64);
|
||||
if let Some(src) = self.event_source.as_ref() {
|
||||
if let Ok(event) =
|
||||
CGEvent::new_mouse_event(src.clone(), event_type, dest, button)
|
||||
{
|
||||
// Set delta fields for relative mouse movement
|
||||
// This is essential for Pointer Lock API in browsers
|
||||
if let Some((dx, dy)) = delta {
|
||||
event.set_integer_value_field(EventField::MOUSE_EVENT_DELTA_X, dx as i64);
|
||||
event.set_integer_value_field(EventField::MOUSE_EVENT_DELTA_Y, dy as i64);
|
||||
}
|
||||
self.post(event, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches the `(width, height)` in pixels of the main display
|
||||
pub fn main_display_size() -> (usize, usize) {
|
||||
let display_id = unsafe { CGMainDisplayID() };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.4.4"
|
||||
version = "1.4.5"
|
||||
edition = "2021"
|
||||
description = "RustDesk Remote Desktop"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user