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);
}
}

View File

@@ -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,
};
}

View File

@@ -32,9 +32,33 @@ const OS_LOWER_MACOS: &str = "macos";
#[allow(dead_code)]
const OS_LOWER_ANDROID: &str = "android";
#[cfg(any(target_os = "windows", target_os = "macos"))]
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false);
// Track key down state for relative mouse mode exit shortcut.
// macOS: Cmd+G (track G key)
// Windows/Linux: Ctrl+Alt (track whichever modifier was pressed last)
// This prevents the exit from retriggering on OS key-repeat.
#[cfg(all(feature = "flutter", any(target_os = "windows", target_os = "macos", target_os = "linux")))]
static EXIT_SHORTCUT_KEY_DOWN: AtomicBool = AtomicBool::new(false);
// Track whether relative mouse mode is currently active.
// This is set by Flutter via set_relative_mouse_mode_state() and checked
// by the rdev grab loop to determine if exit shortcuts should be processed.
#[cfg(all(feature = "flutter", any(target_os = "windows", target_os = "macos", target_os = "linux")))]
static RELATIVE_MOUSE_MODE_ACTIVE: AtomicBool = AtomicBool::new(false);
/// Set the relative mouse mode state from Flutter.
/// This is called when entering or exiting relative mouse mode.
#[cfg(all(feature = "flutter", any(target_os = "windows", target_os = "macos", target_os = "linux")))]
pub fn set_relative_mouse_mode_state(active: bool) {
RELATIVE_MOUSE_MODE_ACTIVE.store(active, Ordering::SeqCst);
// Reset exit shortcut state when mode changes to avoid stale state
if !active {
EXIT_SHORTCUT_KEY_DOWN.store(false, Ordering::SeqCst);
}
}
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
static IS_RDEV_ENABLED: AtomicBool = AtomicBool::new(false);
@@ -82,7 +106,7 @@ pub mod client {
GrabState::Run => {
#[cfg(windows)]
update_grab_get_key_name(keyboard_mode);
#[cfg(any(target_os = "windows", target_os = "macos"))]
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
KEYBOARD_HOOKED.swap(true, Ordering::SeqCst);
#[cfg(target_os = "linux")]
@@ -94,7 +118,7 @@ pub mod client {
release_remote_keys(keyboard_mode);
#[cfg(any(target_os = "windows", target_os = "macos"))]
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
KEYBOARD_HOOKED.swap(false, Ordering::SeqCst);
#[cfg(target_os = "linux")]
@@ -266,6 +290,136 @@ fn get_keyboard_mode() -> String {
"legacy".to_string()
}
/// Check if exit shortcut for relative mouse mode is active.
/// Exit shortcuts (only exits, not toggles):
/// - macOS: Cmd+G
/// - Windows/Linux: Ctrl+Alt (triggered when both are pressed)
/// Note: This shortcut is only available in Flutter client. Sciter client does not support relative mouse mode.
#[cfg(feature = "flutter")]
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
fn is_exit_relative_mouse_shortcut(key: Key) -> bool {
let modifiers = MODIFIERS_STATE.lock().unwrap();
#[cfg(target_os = "macos")]
{
// macOS: Cmd+G to exit
if key != Key::KeyG {
return false;
}
let meta = *modifiers.get(&Key::MetaLeft).unwrap_or(&false)
|| *modifiers.get(&Key::MetaRight).unwrap_or(&false);
return meta;
}
#[cfg(not(target_os = "macos"))]
{
// Windows/Linux: Ctrl+Alt to exit
// Triggered when Ctrl is pressed while Alt is down, or Alt is pressed while Ctrl is down
let is_ctrl_key = key == Key::ControlLeft || key == Key::ControlRight;
let is_alt_key = key == Key::Alt || key == Key::AltGr;
if !is_ctrl_key && !is_alt_key {
return false;
}
let ctrl = *modifiers.get(&Key::ControlLeft).unwrap_or(&false)
|| *modifiers.get(&Key::ControlRight).unwrap_or(&false);
let alt = *modifiers.get(&Key::Alt).unwrap_or(&false)
|| *modifiers.get(&Key::AltGr).unwrap_or(&false);
// When Ctrl is pressed and Alt is already down, or vice versa
(is_ctrl_key && alt) || (is_alt_key && ctrl)
}
}
/// Notify Flutter to exit relative mouse mode.
/// Note: This is Flutter-only. Sciter client does not support relative mouse mode.
#[cfg(feature = "flutter")]
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
fn notify_exit_relative_mouse_mode() {
let session_id = flutter::get_cur_session_id();
flutter::push_session_event(&session_id, "exit_relative_mouse_mode", vec![]);
}
/// Handle relative mouse mode shortcuts in the rdev grab loop.
/// Returns true if the event should be blocked from being sent to the peer.
#[cfg(feature = "flutter")]
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
#[inline]
fn can_exit_relative_mouse_mode_from_grab_loop() -> bool {
// Only process exit shortcuts when relative mouse mode is actually active.
// This prevents blocking Ctrl+Alt (or Cmd+G) when not in relative mouse mode.
if !RELATIVE_MOUSE_MODE_ACTIVE.load(Ordering::SeqCst) {
return false;
}
let Some(session) = flutter::get_cur_session() else {
return false;
};
// Only for remote desktop sessions.
if !session.is_default() {
return false;
}
// Must have keyboard permission and not be in view-only mode.
if !*session.server_keyboard_enabled.read().unwrap() {
return false;
}
let lc = session.lc.read().unwrap();
if lc.view_only.v {
return false;
}
// Peer must support relative mouse mode.
crate::common::is_support_relative_mouse_mode_num(lc.version)
}
#[cfg(feature = "flutter")]
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
#[inline]
fn should_block_relative_mouse_shortcut(key: Key, is_press: bool) -> bool {
if !KEYBOARD_HOOKED.load(Ordering::SeqCst) {
return false;
}
// Determine which key to track for key-up blocking based on platform
#[cfg(target_os = "macos")]
let is_tracked_key = key == Key::KeyG;
#[cfg(not(target_os = "macos"))]
let is_tracked_key = key == Key::ControlLeft
|| key == Key::ControlRight
|| key == Key::Alt
|| key == Key::AltGr;
// Block key up if key down was blocked (to avoid orphan key up event on remote).
// This must be checked before clearing the flag below.
if is_tracked_key && !is_press && EXIT_SHORTCUT_KEY_DOWN.swap(false, Ordering::SeqCst) {
return true;
}
// Exit relative mouse mode shortcuts:
// - macOS: Cmd+G
// - Windows/Linux: Ctrl+Alt
// Guard it to supported/eligible sessions to avoid blocking the chord unexpectedly.
if is_exit_relative_mouse_shortcut(key) {
if !can_exit_relative_mouse_mode_from_grab_loop() {
return false;
}
if is_press {
// Only trigger exit on transition from "not pressed" to "pressed".
// This prevents retriggering on OS key-repeat.
if !EXIT_SHORTCUT_KEY_DOWN.swap(true, Ordering::SeqCst) {
notify_exit_relative_mouse_mode();
}
}
return true;
}
false
}
fn start_grab_loop() {
std::env::set_var("KEYBOARD_ONLY", "y");
#[cfg(any(target_os = "windows", target_os = "macos"))]
@@ -278,6 +432,12 @@ fn start_grab_loop() {
let _scan_code = event.position_code;
let _code = event.platform_code as KeyCode;
#[cfg(feature = "flutter")]
if should_block_relative_mouse_shortcut(key, is_press) {
return None;
}
let res = if KEYBOARD_HOOKED.load(Ordering::SeqCst) {
client::process_event(&get_keyboard_mode(), &event, None);
if is_press {
@@ -337,9 +497,14 @@ fn start_grab_loop() {
#[cfg(target_os = "linux")]
if let Err(err) = rdev::start_grab_listen(move |event: Event| match event.event_type {
EventType::KeyPress(key) | EventType::KeyRelease(key) => {
let is_press = matches!(event.event_type, EventType::KeyPress(_));
if let Key::Unknown(keycode) = key {
log::error!("rdev get unknown key, keycode is {:?}", keycode);
} else {
#[cfg(feature = "flutter")]
if should_block_relative_mouse_shortcut(key, is_press) {
return None;
}
client::process_event(&get_keyboard_mode(), &event, None);
}
None

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", "أدخل الملاحظة هنا"),
("note-at-conn-end-tip", "سيتم عرض هذه الملاحظة عند نهاية الاتصال"),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", "输入备注"),
("note-at-conn-end-tip", "在连接结束时请求备注"),
("Show terminal extra keys", "显示终端扩展键"),
("Relative mouse mode", "相对鼠标模式"),
("rel-mouse-not-supported-peer-tip", "被控端不支持相对鼠标模式"),
("rel-mouse-not-ready-tip", "相对鼠标模式尚未准备好,请稍后再试"),
("rel-mouse-lock-failed-tip", "无法锁定鼠标,相对鼠标模式已禁用"),
("rel-mouse-exit-{}-tip", "按下 {} 退出"),
("rel-mouse-permission-lost-tip", "键盘权限被撤销。相对鼠标模式已被禁用。"),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", "Hier eine Notiz eingeben"),
("note-at-conn-end-tip", "Am Ende der Verbindung um eine Notiz bitten."),
("Show terminal extra keys", "Zusätzliche Tasten des Terminals anzeigen"),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -262,5 +262,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("disable-udp-tip", "Controls whether to use TCP only.\nWhen this option enabled, RustDesk will not use UDP 21116 any more, TCP 21116 will be used instead."),
("server-oss-not-support-tip", "NOTE: RustDesk server OSS doesn't include this feature."),
("note-at-conn-end-tip", "Ask for note at end of connection"),
("rel-mouse-not-supported-peer-tip", "Relative Mouse Mode is not supported by the connected peer."),
("rel-mouse-not-ready-tip", "Relative Mouse Mode is not ready yet. Please try again."),
("rel-mouse-lock-failed-tip", "Failed to lock cursor. Relative Mouse Mode has been disabled."),
("rel-mouse-exit-{}-tip", "Press {} to exit."),
("rel-mouse-permission-lost-tip", "Keyboard permission was revoked. Relative Mouse Mode has been disabled."),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", "یادداشت را اینجا وارد کنید"),
("note-at-conn-end-tip", "در پایان اتصال، یادداشت بخواهید"),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", "saisir la note ici"),
("note-at-conn-end-tip", "Proposer de rédiger une note une fois la connexion terminée"),
("Show terminal extra keys", "Afficher les touches supplémentaires du terminal"),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", "Megjegyzés bevitele"),
("note-at-conn-end-tip", "Megjegyzés a kapcsolat végén"),
("Show terminal extra keys", "További terminálgombok megjelenítése"),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", "Inserisci nota qui"),
("note-at-conn-end-tip", "Visualizza nota alla fine della connessione"),
("Show terminal extra keys", "Visualizza tasti aggiuntivi terminale"),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", "ここにメモを入力"),
("note-at-conn-end-tip", "接続終了時にメモを要求する"),
("Show terminal extra keys", "ターミナルの追加キーを表示する"),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", "여기에 노트 입력"),
("note-at-conn-end-tip", "연결이 끝날 때 메모 요청"),
("Show terminal extra keys", "터미널 추가 키 표시"),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", "voeg hier een opmerking toe"),
("note-at-conn-end-tip", "Vraag om een opmerking aan het einde van de verbinding"),
("Show terminal extra keys", "Toon extra toetsen voor terminal"),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", "Wstaw tutaj notatkę"),
("note-at-conn-end-tip", "Poproś o notatkę po zakończeniu połączenia."),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", "введите заметку"),
("note-at-conn-end-tip", "Запрашивать заметку в конце соединения"),
("Show terminal extra keys", "Показывать дополнительные кнопки терминала"),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", "Notu buraya girin"),
("note-at-conn-end-tip", "Bağlantı bittiğinde not sorulsun"),
("Show terminal extra keys", "Terminal ek tuşlarını göster"),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", "輸入備註"),
("note-at-conn-end-tip", "在連接結束時請求備註"),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
].iter().cloned().collect();
}

View File

@@ -3,7 +3,8 @@ mod keyboard;
pub mod platform;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub use platform::{
get_cursor, get_cursor_data, get_cursor_pos, get_focused_display, start_os_service,
clip_cursor, get_cursor, get_cursor_data, get_cursor_pos, get_focused_display,
set_cursor_pos, start_os_service,
};
#[cfg(not(any(target_os = "ios")))]
/// cbindgen:ignore

View File

@@ -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> {

View File

@@ -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))
}

View File

@@ -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;

View File

@@ -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
}
}

View File

@@ -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()

View File

@@ -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,

View File

@@ -1,6 +1,9 @@
use crate::{
common::{get_supported_keyboard_modes, is_keyboard_mode_supported},
input::{MOUSE_BUTTON_LEFT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP, MOUSE_TYPE_WHEEL},
input::{
MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_MASK,
MOUSE_TYPE_TRACKPAD, MOUSE_TYPE_UP, MOUSE_TYPE_WHEEL,
},
ui_interface::use_texture_render,
};
use async_trait::async_trait;
@@ -1222,7 +1225,9 @@ impl<T: InvokeUiSession> Session<T> {
}
}
let (x, y) = if mask == MOUSE_TYPE_WHEEL || mask == MOUSE_TYPE_TRACKPAD {
// Compute event type once using MOUSE_TYPE_MASK for reuse
let event_type = mask & MOUSE_TYPE_MASK;
let (x, y) = if event_type == MOUSE_TYPE_WHEEL || event_type == MOUSE_TYPE_TRACKPAD {
self.get_scroll_xy((x, y))
} else {
(x, y)
@@ -1231,8 +1236,6 @@ impl<T: InvokeUiSession> Session<T> {
// #[cfg(not(any(target_os = "android", target_os = "ios")))]
let (alt, ctrl, shift, command) =
keyboard::client::get_modifiers_state(alt, ctrl, shift, command);
use crate::input::*;
let is_left = (mask & (MOUSE_BUTTON_LEFT << 3)) > 0;
let is_right = (mask & (MOUSE_BUTTON_RIGHT << 3)) > 0;
if is_left ^ is_right {
@@ -1252,9 +1255,8 @@ impl<T: InvokeUiSession> Session<T> {
// to-do: how about ctrl + left from win to macos
if cfg!(target_os = "macos") {
let buttons = mask >> 3;
let evt_type = mask & 0x7;
if buttons == MOUSE_BUTTON_LEFT
&& evt_type == MOUSE_TYPE_DOWN
&& event_type == MOUSE_TYPE_DOWN
&& ctrl
&& self.peer_platform() != "Mac OS"
{