mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-17 14:07:28 +08: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:
2
.github/workflows/flutter-build.yml
vendored
2
.github/workflows/flutter-build.yml
vendored
@@ -39,7 +39,7 @@ env:
|
|||||||
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
|
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
|
||||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||||
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
|
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
|
||||||
VERSION: "1.4.4"
|
VERSION: "1.4.5"
|
||||||
NDK_VERSION: "r27c"
|
NDK_VERSION: "r27c"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
|
|||||||
2
.github/workflows/playground.yml
vendored
2
.github/workflows/playground.yml
vendored
@@ -17,7 +17,7 @@ env:
|
|||||||
TAG_NAME: "nightly"
|
TAG_NAME: "nightly"
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||||
VERSION: "1.4.4"
|
VERSION: "1.4.5"
|
||||||
NDK_VERSION: "r26d"
|
NDK_VERSION: "r26d"
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
|
|||||||
4
.github/workflows/winget.yml
vendored
4
.github/workflows/winget.yml
vendored
@@ -10,6 +10,6 @@ jobs:
|
|||||||
- uses: vedantmgoyal9/winget-releaser@main
|
- uses: vedantmgoyal9/winget-releaser@main
|
||||||
with:
|
with:
|
||||||
identifier: RustDesk.RustDesk
|
identifier: RustDesk.RustDesk
|
||||||
version: "1.4.4"
|
version: "1.4.5"
|
||||||
release-tag: "1.4.4"
|
release-tag: "1.4.5"
|
||||||
token: ${{ secrets.WINGET_TOKEN }}
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
|
|||||||
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -7134,7 +7134,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.4.4"
|
version = "1.4.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-wakelock",
|
"android-wakelock",
|
||||||
"android_logger",
|
"android_logger",
|
||||||
@@ -7249,7 +7249,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.4.4"
|
version = "1.4.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.4.4"
|
version = "1.4.5"
|
||||||
authors = ["rustdesk <info@rustdesk.com>"]
|
authors = ["rustdesk <info@rustdesk.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build= "build.rs"
|
build= "build.rs"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.4.4
|
version: 1.4.5
|
||||||
exec: usr/share/rustdesk/rustdesk
|
exec: usr/share/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.4.4
|
version: 1.4.5
|
||||||
exec: usr/share/rustdesk/rustdesk
|
exec: usr/share/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
@@ -1011,13 +1011,15 @@ makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void showToast(String text, {Duration timeout = const Duration(seconds: 3)}) {
|
void showToast(String text,
|
||||||
|
{Duration timeout = const Duration(seconds: 3),
|
||||||
|
Alignment alignment = const Alignment(0.0, 0.8)}) {
|
||||||
final overlayState = globalKey.currentState?.overlay;
|
final overlayState = globalKey.currentState?.overlay;
|
||||||
if (overlayState == null) return;
|
if (overlayState == null) return;
|
||||||
final entry = OverlayEntry(builder: (context) {
|
final entry = OverlayEntry(builder: (context) {
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: const Alignment(0.0, 0.8),
|
alignment: alignment,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: MyTheme.color(context).toastBg,
|
color: MyTheme.color(context).toastBg,
|
||||||
@@ -4069,3 +4071,23 @@ String decode_http_response(http.Response resp) {
|
|||||||
bool peerTabShowNote(PeerTabIndex peerTabIndex) {
|
bool peerTabShowNote(PeerTabIndex peerTabIndex) {
|
||||||
return peerTabIndex == PeerTabIndex.ab || peerTabIndex == PeerTabIndex.group;
|
return peerTabIndex == PeerTabIndex.ab || peerTabIndex == PeerTabIndex.group;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: We should support individual bits combinations in the future.
|
||||||
|
// But for now, just keep it simple, because the old code only supports single button.
|
||||||
|
// No users have requested multi-button support yet.
|
||||||
|
String mouseButtonsToPeer(int buttons) {
|
||||||
|
switch (buttons) {
|
||||||
|
case kPrimaryMouseButton:
|
||||||
|
return 'left';
|
||||||
|
case kSecondaryMouseButton:
|
||||||
|
return 'right';
|
||||||
|
case kMiddleMouseButton:
|
||||||
|
return 'wheel';
|
||||||
|
case kBackMouseButton:
|
||||||
|
return 'back';
|
||||||
|
case kForwardMouseButton:
|
||||||
|
return 'forward';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -372,7 +372,10 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
await ffi.cursorModel
|
await ffi.cursorModel
|
||||||
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
||||||
}
|
}
|
||||||
|
// In relative mouse mode, skip mouse down - only send movement via sendMobileRelativeMouseMove
|
||||||
|
if (!inputModel.relativeMouseMode.value) {
|
||||||
await inputModel.sendMouse('down', MouseButtons.left);
|
await inputModel.sendMouse('down', MouseButtons.left);
|
||||||
|
}
|
||||||
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||||
} else {
|
} else {
|
||||||
final offset = ffi.cursorModel.offset;
|
final offset = ffi.cursorModel.offset;
|
||||||
@@ -397,8 +400,13 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
if (handleTouch && !_touchModePanStarted) {
|
if (handleTouch && !_touchModePanStarted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// In relative mouse mode, send delta directly without position tracking.
|
||||||
|
if (inputModel.relativeMouseMode.value) {
|
||||||
|
await inputModel.sendMobileRelativeMouseMove(d.delta.dx, d.delta.dy);
|
||||||
|
} else {
|
||||||
await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onOneFingerPanEnd(DragEndDetails d) async {
|
onOneFingerPanEnd(DragEndDetails d) async {
|
||||||
_touchModePanStarted = false;
|
_touchModePanStarted = false;
|
||||||
@@ -409,9 +417,12 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
ffi.cursorModel.clearRemoteWindowCoords();
|
ffi.cursorModel.clearRemoteWindowCoords();
|
||||||
}
|
}
|
||||||
if (handleTouch) {
|
if (handleTouch) {
|
||||||
|
// In relative mouse mode, skip mouse up - matches the skipped mouse down in onOneFingerPanStart
|
||||||
|
if (!inputModel.relativeMouseMode.value) {
|
||||||
await inputModel.sendMouse('up', MouseButtons.left);
|
await inputModel.sendMouse('up', MouseButtons.left);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// scale + pan event
|
// scale + pan event
|
||||||
onTwoFingerScaleStart(ScaleStartDetails d) {
|
onTwoFingerScaleStart(ScaleStartDetails d) {
|
||||||
|
|||||||
@@ -831,6 +831,7 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
|
|||||||
final ffiModel = ffi.ffiModel;
|
final ffiModel = ffi.ffiModel;
|
||||||
final pi = ffiModel.pi;
|
final pi = ffiModel.pi;
|
||||||
final sessionId = ffi.sessionId;
|
final sessionId = ffi.sessionId;
|
||||||
|
final isDefaultConn = ffi.connType == ConnType.defaultConn;
|
||||||
List<TToggleMenu> v = [];
|
List<TToggleMenu> v = [];
|
||||||
|
|
||||||
// swap key
|
// swap key
|
||||||
@@ -852,6 +853,34 @@ List<TToggleMenu> toolbarKeyboardToggles(FFI ffi) {
|
|||||||
child: Text(translate('Swap control-command key'))));
|
child: Text(translate('Swap control-command key'))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Relative mouse mode (gaming mode).
|
||||||
|
// Only show when server supports MOUSE_TYPE_MOVE_RELATIVE (version >= 1.4.5)
|
||||||
|
// Note: This feature is only available in Flutter client. Sciter client does not support this.
|
||||||
|
// Web client is not supported yet due to Pointer Lock API integration complexity with Flutter's input system.
|
||||||
|
// Wayland is not supported due to cursor warping limitations.
|
||||||
|
// Mobile: This option is now in GestureHelp widget, shown only when joystick is visible.
|
||||||
|
final isWayland = isDesktop && isLinux && bind.mainCurrentIsWayland();
|
||||||
|
if (isDesktop &&
|
||||||
|
isDefaultConn &&
|
||||||
|
!isWeb &&
|
||||||
|
!isWayland &&
|
||||||
|
ffiModel.keyboard &&
|
||||||
|
!ffiModel.viewOnly &&
|
||||||
|
ffi.inputModel.isRelativeMouseModeSupported) {
|
||||||
|
v.add(TToggleMenu(
|
||||||
|
value: ffi.inputModel.relativeMouseMode.value,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null) return;
|
||||||
|
final previousValue = ffi.inputModel.relativeMouseMode.value;
|
||||||
|
final success = ffi.inputModel.setRelativeMouseMode(value);
|
||||||
|
if (!success) {
|
||||||
|
// Revert the observable toggle to reflect the actual state
|
||||||
|
ffi.inputModel.relativeMouseMode.value = previousValue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(translate('Relative mouse mode'))));
|
||||||
|
}
|
||||||
|
|
||||||
// reverse mouse wheel
|
// reverse mouse wheel
|
||||||
if (ffiModel.keyboard) {
|
if (ffiModel.keyboard) {
|
||||||
var optionValue =
|
var optionValue =
|
||||||
|
|||||||
@@ -258,6 +258,33 @@ const int kMinTrackpadSpeed = 10;
|
|||||||
const int kDefaultTrackpadSpeed = 100;
|
const int kDefaultTrackpadSpeed = 100;
|
||||||
const int kMaxTrackpadSpeed = 1000;
|
const int kMaxTrackpadSpeed = 1000;
|
||||||
|
|
||||||
|
// relative mouse mode
|
||||||
|
/// Throttle duration (in milliseconds) for updating pointer lock center during
|
||||||
|
/// window move/resize events. Lower values provide more responsive updates but
|
||||||
|
/// may cause performance issues during rapid window operations.
|
||||||
|
const int kDefaultPointerLockCenterThrottleMs = 100;
|
||||||
|
|
||||||
|
/// Minimum server version required for relative mouse mode (MOUSE_TYPE_MOVE_RELATIVE).
|
||||||
|
/// Servers older than this version will ignore relative mouse events.
|
||||||
|
///
|
||||||
|
/// IMPORTANT: This value must be kept in sync with the Rust constant
|
||||||
|
/// `MIN_VERSION_RELATIVE_MOUSE_MODE` in `src/common.rs`.
|
||||||
|
const String kMinVersionForRelativeMouseMode = '1.4.5';
|
||||||
|
|
||||||
|
/// Maximum delta value for relative mouse movement.
|
||||||
|
/// Large values could cause issues with i32 overflow on server side,
|
||||||
|
/// and no reasonable mouse movement should exceed this bound.
|
||||||
|
///
|
||||||
|
/// IMPORTANT: This value must be kept in sync with the Rust constant
|
||||||
|
/// `MAX_RELATIVE_MOUSE_DELTA` in `src/server/input_service.rs`.
|
||||||
|
const int kMaxRelativeMouseDelta = 10000;
|
||||||
|
|
||||||
|
/// Debounce duration (in milliseconds) for relative mouse mode toggle.
|
||||||
|
/// This prevents double-toggle from race condition between Rust rdev grab loop
|
||||||
|
/// and Flutter keyboard handling. Value should be small enough to allow
|
||||||
|
/// intentional quick toggles but large enough to prevent accidental double-triggers.
|
||||||
|
const int kRelativeMouseModeToggleDebounceMs = 150;
|
||||||
|
|
||||||
// incomming (should be incoming) is kept, because change it will break the previous setting.
|
// incomming (should be incoming) is kept, because change it will break the previous setting.
|
||||||
const String kKeyPrinterIncomingJobAction = 'printer-incomming-job-action';
|
const String kKeyPrinterIncomingJobAction = 'printer-incomming-job-action';
|
||||||
const String kValuePrinterIncomingJobDismiss = 'dismiss';
|
const String kValuePrinterIncomingJobDismiss = 'dismiss';
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import '../../common.dart';
|
|||||||
import '../../common/widgets/dialog.dart';
|
import '../../common/widgets/dialog.dart';
|
||||||
import '../../common/widgets/toolbar.dart';
|
import '../../common/widgets/toolbar.dart';
|
||||||
import '../../models/model.dart';
|
import '../../models/model.dart';
|
||||||
|
import '../../models/input_model.dart';
|
||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
import '../../common/shared_state.dart';
|
import '../../common/shared_state.dart';
|
||||||
import '../../utils/image.dart';
|
import '../../utils/image.dart';
|
||||||
@@ -90,6 +91,10 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
|
|
||||||
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
|
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
|
||||||
|
|
||||||
|
// Debounce timer for pointer lock center updates during window events.
|
||||||
|
// Uses kDefaultPointerLockCenterThrottleMs from consts.dart for the duration.
|
||||||
|
Timer? _pointerLockCenterDebounceTimer;
|
||||||
|
|
||||||
// We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar`
|
// We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar`
|
||||||
// to identify the toolbar instance and its callback function.
|
// to identify the toolbar instance and its callback function.
|
||||||
int? _instanceIdOnEnterOrLeaveImage4Toolbar;
|
int? _instanceIdOnEnterOrLeaveImage4Toolbar;
|
||||||
@@ -169,6 +174,16 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
widget.tabController?.onSelected?.call(widget.id);
|
widget.tabController?.onSelected?.call(widget.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Register callback to cancel debounce timer when relative mouse mode is disabled
|
||||||
|
_ffi.inputModel.onRelativeMouseModeDisabled =
|
||||||
|
_cancelPointerLockCenterDebounceTimer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cancel the pointer lock center debounce timer
|
||||||
|
void _cancelPointerLockCenterDebounceTimer() {
|
||||||
|
_pointerLockCenterDebounceTimer?.cancel();
|
||||||
|
_pointerLockCenterDebounceTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -184,6 +199,13 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_rawKeyFocusNode.unfocus();
|
_rawKeyFocusNode.unfocus();
|
||||||
}
|
}
|
||||||
stateGlobal.isFocused.value = false;
|
stateGlobal.isFocused.value = false;
|
||||||
|
|
||||||
|
// When window loses focus, temporarily release relative mouse mode constraints
|
||||||
|
// to allow user to interact with other applications normally.
|
||||||
|
// The cursor will be re-hidden and re-centered when window regains focus.
|
||||||
|
if (_ffi.inputModel.relativeMouseMode.value) {
|
||||||
|
_ffi.inputModel.onWindowBlur();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -194,6 +216,12 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_isWindowBlur = false;
|
_isWindowBlur = false;
|
||||||
}
|
}
|
||||||
stateGlobal.isFocused.value = true;
|
stateGlobal.isFocused.value = true;
|
||||||
|
|
||||||
|
// Restore relative mouse mode constraints when window regains focus.
|
||||||
|
if (_ffi.inputModel.relativeMouseMode.value) {
|
||||||
|
_rawKeyFocusNode.requestFocus();
|
||||||
|
_ffi.inputModel.onWindowFocus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -205,6 +233,8 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_isWindowBlur = false;
|
_isWindowBlur = false;
|
||||||
}
|
}
|
||||||
WakelockManager.enable(_uniqueKey);
|
WakelockManager.enable(_uniqueKey);
|
||||||
|
// Update pointer lock center when window is restored
|
||||||
|
_updatePointerLockCenterIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the window is unminimized, onWindowMaximize or onWindowRestore can be called when the old state was maximized or not.
|
// When the window is unminimized, onWindowMaximize or onWindowRestore can be called when the old state was maximized or not.
|
||||||
@@ -212,12 +242,50 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
void onWindowMaximize() {
|
void onWindowMaximize() {
|
||||||
super.onWindowMaximize();
|
super.onWindowMaximize();
|
||||||
WakelockManager.enable(_uniqueKey);
|
WakelockManager.enable(_uniqueKey);
|
||||||
|
// Update pointer lock center when window is maximized
|
||||||
|
_updatePointerLockCenterIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowResize() {
|
||||||
|
super.onWindowResize();
|
||||||
|
// Update pointer lock center when window is resized
|
||||||
|
_updatePointerLockCenterIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowMove() {
|
||||||
|
super.onWindowMove();
|
||||||
|
// Update pointer lock center when window is moved
|
||||||
|
_updatePointerLockCenterIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update pointer lock center with debouncing to avoid excessive updates
|
||||||
|
/// during rapid window move/resize events.
|
||||||
|
void _updatePointerLockCenterIfNeeded() {
|
||||||
|
if (!_ffi.inputModel.relativeMouseMode.value) return;
|
||||||
|
|
||||||
|
// Cancel any pending update and schedule a new one (debounce pattern)
|
||||||
|
_pointerLockCenterDebounceTimer?.cancel();
|
||||||
|
_pointerLockCenterDebounceTimer = Timer(
|
||||||
|
const Duration(milliseconds: kDefaultPointerLockCenterThrottleMs),
|
||||||
|
() {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (_ffi.inputModel.relativeMouseMode.value) {
|
||||||
|
_ffi.inputModel.updatePointerLockCenter();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onWindowMinimize() {
|
void onWindowMinimize() {
|
||||||
super.onWindowMinimize();
|
super.onWindowMinimize();
|
||||||
WakelockManager.disable(_uniqueKey);
|
WakelockManager.disable(_uniqueKey);
|
||||||
|
// Release cursor constraints when minimized
|
||||||
|
if (_ffi.inputModel.relativeMouseMode.value) {
|
||||||
|
_ffi.inputModel.onWindowBlur();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -243,6 +311,16 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
// https://github.com/flutter/flutter/issues/64935
|
// https://github.com/flutter/flutter/issues/64935
|
||||||
super.dispose();
|
super.dispose();
|
||||||
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
|
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
|
||||||
|
|
||||||
|
// Defensive cleanup: ensure host system-key propagation is reset even if
|
||||||
|
// MouseRegion.onExit never fired (e.g., tab closed while cursor inside).
|
||||||
|
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
|
||||||
|
|
||||||
|
_pointerLockCenterDebounceTimer?.cancel();
|
||||||
|
_pointerLockCenterDebounceTimer = null;
|
||||||
|
// Clear callback reference to prevent memory leaks and stale references
|
||||||
|
_ffi.inputModel.onRelativeMouseModeDisabled = null;
|
||||||
|
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
|
||||||
_ffi.textureModel.onRemotePageDispose(closeSession);
|
_ffi.textureModel.onRemotePageDispose(closeSession);
|
||||||
if (closeSession) {
|
if (closeSession) {
|
||||||
// ensure we leave this session, this is a double check
|
// ensure we leave this session, this is a double check
|
||||||
@@ -344,10 +422,15 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
// Use Overlay to enable rebuild every time on menu button click.
|
// Use Overlay to enable rebuild every time on menu button click.
|
||||||
_ffi.ffiModel.pi.isSet.isTrue
|
// Hide toolbar when relative mouse mode is active to prevent
|
||||||
? Overlay(
|
// cursor from escaping to toolbar area.
|
||||||
initialEntries: [OverlayEntry(builder: remoteToolbar)])
|
Obx(() => _ffi.inputModel.relativeMouseMode.value
|
||||||
: remoteToolbar(context),
|
? const Offstage()
|
||||||
|
: _ffi.ffiModel.pi.isSet.isTrue
|
||||||
|
? Overlay(initialEntries: [
|
||||||
|
OverlayEntry(builder: remoteToolbar)
|
||||||
|
])
|
||||||
|
: remoteToolbar(context)),
|
||||||
_ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(),
|
_ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -415,6 +498,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
//
|
//
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// See [onWindowBlur].
|
// See [onWindowBlur].
|
||||||
if (!isWindows) {
|
if (!isWindows) {
|
||||||
if (!_rawKeyFocusNode.hasFocus) {
|
if (!_rawKeyFocusNode.hasFocus) {
|
||||||
@@ -440,6 +524,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
//
|
//
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// See [onWindowBlur].
|
// See [onWindowBlur].
|
||||||
if (!isWindows) {
|
if (!isWindows) {
|
||||||
_ffi.inputModel.enterOrLeave(false);
|
_ffi.inputModel.enterOrLeave(false);
|
||||||
@@ -487,13 +572,17 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
|
|
||||||
Widget getBodyForDesktop(BuildContext context) {
|
Widget getBodyForDesktop(BuildContext context) {
|
||||||
var paints = <Widget>[
|
var paints = <Widget>[
|
||||||
MouseRegion(onEnter: (evt) {
|
MouseRegion(
|
||||||
|
onEnter: (evt) {
|
||||||
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false);
|
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false);
|
||||||
}, onExit: (evt) {
|
},
|
||||||
|
onExit: (evt) {
|
||||||
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
|
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
|
||||||
}, child: LayoutBuilder(builder: (context, constraints) {
|
},
|
||||||
final c = Provider.of<CanvasModel>(context, listen: false);
|
child: _ViewStyleUpdater(
|
||||||
Future.delayed(Duration.zero, () => c.updateViewStyle());
|
canvasModel: _ffi.canvasModel,
|
||||||
|
inputModel: _ffi.inputModel,
|
||||||
|
child: Builder(builder: (context) {
|
||||||
final peerDisplay = CurrentDisplayState.find(widget.id);
|
final peerDisplay = CurrentDisplayState.find(widget.id);
|
||||||
return Obx(
|
return Obx(
|
||||||
() => _ffi.ffiModel.pi.isSet.isFalse
|
() => _ffi.ffiModel.pi.isSet.isFalse
|
||||||
@@ -506,13 +595,16 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
cursorOverImage: _cursorOverImage,
|
cursorOverImage: _cursorOverImage,
|
||||||
keyboardEnabled: _keyboardEnabled,
|
keyboardEnabled: _keyboardEnabled,
|
||||||
remoteCursorMoved: _remoteCursorMoved,
|
remoteCursorMoved: _remoteCursorMoved,
|
||||||
listenerBuilder: (child) => _buildRawTouchAndPointerRegion(
|
listenerBuilder: (child) =>
|
||||||
|
_buildRawTouchAndPointerRegion(
|
||||||
child, enterView, leaveView),
|
child, enterView, leaveView),
|
||||||
ffi: _ffi,
|
ffi: _ffi,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}))
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!_ffi.canvasModel.cursorEmbedded) {
|
if (!_ffi.canvasModel.cursorEmbedded) {
|
||||||
@@ -541,6 +633,63 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A widget that tracks the view size and updates CanvasModel.updateViewStyle()
|
||||||
|
/// and InputModel.updateImageWidgetSize() only when size actually changes.
|
||||||
|
/// This avoids scheduling post-frame callbacks on every LayoutBuilder rebuild.
|
||||||
|
class _ViewStyleUpdater extends StatefulWidget {
|
||||||
|
final CanvasModel canvasModel;
|
||||||
|
final InputModel inputModel;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _ViewStyleUpdater({
|
||||||
|
Key? key,
|
||||||
|
required this.canvasModel,
|
||||||
|
required this.inputModel,
|
||||||
|
required this.child,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ViewStyleUpdater> createState() => _ViewStyleUpdaterState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ViewStyleUpdaterState extends State<_ViewStyleUpdater> {
|
||||||
|
Size? _lastSize;
|
||||||
|
bool _callbackScheduled = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final maxWidth = constraints.maxWidth;
|
||||||
|
final maxHeight = constraints.maxHeight;
|
||||||
|
// Guard against infinite constraints (e.g., unconstrained ancestor).
|
||||||
|
if (!maxWidth.isFinite || !maxHeight.isFinite) {
|
||||||
|
return widget.child;
|
||||||
|
}
|
||||||
|
final newSize = Size(maxWidth, maxHeight);
|
||||||
|
if (_lastSize != newSize) {
|
||||||
|
_lastSize = newSize;
|
||||||
|
// Schedule the update for after the current frame to avoid setState during build.
|
||||||
|
// Use _callbackScheduled flag to prevent accumulating multiple callbacks
|
||||||
|
// when size changes rapidly before any callback executes.
|
||||||
|
if (!_callbackScheduled) {
|
||||||
|
_callbackScheduled = true;
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_callbackScheduled = false;
|
||||||
|
final currentSize = _lastSize;
|
||||||
|
if (mounted && currentSize != null) {
|
||||||
|
widget.canvasModel.updateViewStyle();
|
||||||
|
widget.inputModel.updateImageWidgetSize(currentSize);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return widget.child;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ImagePaint extends StatefulWidget {
|
class ImagePaint extends StatefulWidget {
|
||||||
final FFI ffi;
|
final FFI ffi;
|
||||||
final String id;
|
final String id;
|
||||||
@@ -604,6 +753,9 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
cursor: cursorOverImage.isTrue
|
cursor: cursorOverImage.isTrue
|
||||||
? c.cursorEmbedded
|
? c.cursorEmbedded
|
||||||
|
? SystemMouseCursors.none
|
||||||
|
// Hide cursor when relative mouse mode is active
|
||||||
|
: widget.ffi.inputModel.relativeMouseMode.value
|
||||||
? SystemMouseCursors.none
|
? SystemMouseCursors.none
|
||||||
: keyboardEnabled.isTrue
|
: keyboardEnabled.isTrue
|
||||||
? (() {
|
? (() {
|
||||||
|
|||||||
@@ -135,7 +135,13 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
body: DesktopTab(
|
body: DesktopTab(
|
||||||
controller: tabController,
|
controller: tabController,
|
||||||
onWindowCloseButton: handleWindowCloseButton,
|
onWindowCloseButton: handleWindowCloseButton,
|
||||||
tail: const AddButton(),
|
tail: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_RelativeMouseModeHint(tabController: tabController),
|
||||||
|
const AddButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
selectedBorderColor: MyTheme.accent,
|
selectedBorderColor: MyTheme.accent,
|
||||||
pageViewBuilder: (pageView) => pageView,
|
pageViewBuilder: (pageView) => pageView,
|
||||||
labelGetter: DesktopTab.tablabelGetter,
|
labelGetter: DesktopTab.tablabelGetter,
|
||||||
@@ -374,6 +380,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
loopCloseWindow();
|
loopCloseWindow();
|
||||||
}
|
}
|
||||||
ConnectionTypeState.delete(id);
|
ConnectionTypeState.delete(id);
|
||||||
|
// Clean up relative mouse mode state for this peer.
|
||||||
|
stateGlobal.relativeMouseModeState.remove(id);
|
||||||
_update_remote_count();
|
_update_remote_count();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,3 +556,69 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
return returnValue;
|
return returnValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A widget that displays a hint in the tab bar when relative mouse mode is active.
|
||||||
|
/// This helps users remember how to exit relative mouse mode.
|
||||||
|
class _RelativeMouseModeHint extends StatelessWidget {
|
||||||
|
final DesktopTabController tabController;
|
||||||
|
|
||||||
|
const _RelativeMouseModeHint({Key? key, required this.tabController})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Obx(() {
|
||||||
|
// Check if there are any tabs
|
||||||
|
if (tabController.state.value.tabs.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current selected tab's RemotePage
|
||||||
|
final selectedTabInfo = tabController.state.value.selectedTabInfo;
|
||||||
|
if (selectedTabInfo.page is! RemotePage) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final remotePage = selectedTabInfo.page as RemotePage;
|
||||||
|
final String peerId = remotePage.id;
|
||||||
|
|
||||||
|
// Use global state to check relative mouse mode (synced from InputModel).
|
||||||
|
// This avoids timing issues with FFI registration.
|
||||||
|
final isRelativeMouseMode =
|
||||||
|
stateGlobal.relativeMouseModeState[peerId] ?? false;
|
||||||
|
|
||||||
|
if (!isRelativeMouseMode) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
margin: const EdgeInsets.only(right: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orange.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(color: Colors.orange.withOpacity(0.5)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.mouse,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.orange[700],
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
translate(
|
||||||
|
'rel-mouse-exit-{${isMacOS ? "Cmd+G" : "Ctrl+Alt"}}-tip'),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.orange[700],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -593,7 +593,6 @@ class _DesktopTabState extends State<DesktopTab>
|
|||||||
|
|
||||||
Widget _buildBar() {
|
Widget _buildBar() {
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
|
|||||||
@@ -569,7 +569,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool get showCursorPaint =>
|
bool get showCursorPaint =>
|
||||||
!gFFI.ffiModel.isPeerAndroid && !gFFI.canvasModel.cursorEmbedded;
|
!gFFI.ffiModel.isPeerAndroid &&
|
||||||
|
!gFFI.canvasModel.cursorEmbedded &&
|
||||||
|
!gFFI.inputModel.relativeMouseMode.value;
|
||||||
|
|
||||||
Widget getBodyForMobile() {
|
Widget getBodyForMobile() {
|
||||||
final keyboardIsVisible = keyboardVisibilityController.isVisible;
|
final keyboardIsVisible = keyboardVisibilityController.isVisible;
|
||||||
@@ -808,6 +810,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||||||
bind.mainSetLocalOption(key: kOptionTouchMode, value: v);
|
bind.mainSetLocalOption(key: kOptionTouchMode, value: v);
|
||||||
},
|
},
|
||||||
virtualMouseMode: gFFI.ffiModel.virtualMouseMode,
|
virtualMouseMode: gFFI.ffiModel.virtualMouseMode,
|
||||||
|
inputModel: gFFI.inputModel,
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,10 @@ class _FloatingMouseWidgetsState extends State<FloatingMouseWidgets> {
|
|||||||
cursorModel: _cursorModel,
|
cursorModel: _cursorModel,
|
||||||
),
|
),
|
||||||
if (virtualMouseMode.showVirtualJoystick)
|
if (virtualMouseMode.showVirtualJoystick)
|
||||||
VirtualJoystick(cursorModel: _cursorModel),
|
VirtualJoystick(
|
||||||
|
cursorModel: _cursorModel,
|
||||||
|
inputModel: _inputModel,
|
||||||
|
),
|
||||||
FloatingLeftRightButton(
|
FloatingLeftRightButton(
|
||||||
isLeft: true,
|
isLeft: true,
|
||||||
inputModel: _inputModel,
|
inputModel: _inputModel,
|
||||||
@@ -674,12 +677,18 @@ class _QuarterCirclePainter extends CustomPainter {
|
|||||||
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Virtual joystick sends the absolute movement for now.
|
// Virtual joystick can send either absolute movement (via updatePan)
|
||||||
// Maybe we need to change it to relative movement in the future.
|
// or relative movement (via sendMobileRelativeMouseMove) depending on the
|
||||||
|
// InputModel.relativeMouseMode setting.
|
||||||
class VirtualJoystick extends StatefulWidget {
|
class VirtualJoystick extends StatefulWidget {
|
||||||
final CursorModel cursorModel;
|
final CursorModel cursorModel;
|
||||||
|
final InputModel inputModel;
|
||||||
|
|
||||||
const VirtualJoystick({super.key, required this.cursorModel});
|
const VirtualJoystick({
|
||||||
|
super.key,
|
||||||
|
required this.cursorModel,
|
||||||
|
required this.inputModel,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<VirtualJoystick> createState() => _VirtualJoystickState();
|
State<VirtualJoystick> createState() => _VirtualJoystickState();
|
||||||
@@ -694,6 +703,10 @@ class _VirtualJoystickState extends State<VirtualJoystick> {
|
|||||||
final double _moveStep = 3.0;
|
final double _moveStep = 3.0;
|
||||||
final double _speed = 1.0;
|
final double _speed = 1.0;
|
||||||
|
|
||||||
|
/// Scale factor for relative mouse movement sensitivity.
|
||||||
|
/// Higher values result in faster cursor movement on the remote machine.
|
||||||
|
static const double _kRelativeMouseScale = 3.0;
|
||||||
|
|
||||||
// One-shot timer to detect a drag gesture
|
// One-shot timer to detect a drag gesture
|
||||||
Timer? _dragStartTimer;
|
Timer? _dragStartTimer;
|
||||||
// Periodic timer for continuous movement
|
// Periodic timer for continuous movement
|
||||||
@@ -701,6 +714,9 @@ class _VirtualJoystickState extends State<VirtualJoystick> {
|
|||||||
Size? _lastScreenSize;
|
Size? _lastScreenSize;
|
||||||
bool _isPressed = false;
|
bool _isPressed = false;
|
||||||
|
|
||||||
|
/// Check if relative mouse mode is enabled.
|
||||||
|
bool get _useRelativeMouse => widget.inputModel.relativeMouseMode.value;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -746,6 +762,18 @@ class _VirtualJoystickState extends State<VirtualJoystick> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send movement delta to remote machine.
|
||||||
|
/// Uses relative mouse mode if enabled, otherwise uses absolute updatePan.
|
||||||
|
void _sendMovement(Offset delta) {
|
||||||
|
if (_useRelativeMouse) {
|
||||||
|
widget.inputModel.sendMobileRelativeMouseMove(
|
||||||
|
delta.dx * _kRelativeMouseScale, delta.dy * _kRelativeMouseScale);
|
||||||
|
} else {
|
||||||
|
// In absolute mode, use cursorModel.updatePan which tracks position.
|
||||||
|
widget.cursorModel.updatePan(delta, Offset.zero, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _stopSendEventTimer() {
|
void _stopSendEventTimer() {
|
||||||
_dragStartTimer?.cancel();
|
_dragStartTimer?.cancel();
|
||||||
_continuousMoveTimer?.cancel();
|
_continuousMoveTimer?.cancel();
|
||||||
@@ -773,7 +801,7 @@ class _VirtualJoystickState extends State<VirtualJoystick> {
|
|||||||
// The movement is small for a gentle start.
|
// The movement is small for a gentle start.
|
||||||
final initialDelta = _offsetToPanDelta(_offset);
|
final initialDelta = _offsetToPanDelta(_offset);
|
||||||
if (initialDelta.distance > 0) {
|
if (initialDelta.distance > 0) {
|
||||||
widget.cursorModel.updatePan(initialDelta, Offset.zero, false);
|
_sendMovement(initialDelta);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Start a one-shot timer to check if the user is holding for a drag.
|
// 2. Start a one-shot timer to check if the user is holding for a drag.
|
||||||
@@ -784,10 +812,7 @@ class _VirtualJoystickState extends State<VirtualJoystick> {
|
|||||||
_continuousMoveTimer =
|
_continuousMoveTimer =
|
||||||
periodic_immediate(const Duration(milliseconds: 20), () async {
|
periodic_immediate(const Duration(milliseconds: 20), () async {
|
||||||
if (_offset != Offset.zero) {
|
if (_offset != Offset.zero) {
|
||||||
widget.cursorModel.updatePan(
|
_sendMovement(_offsetToPanDelta(_offset) * _moveStep * _speed);
|
||||||
_offsetToPanDelta(_offset) * _moveStep * _speed,
|
|
||||||
Offset.zero,
|
|
||||||
false);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/models/input_model.dart';
|
||||||
import 'package:flutter_hbb/models/model.dart';
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
import 'package:toggle_switch/toggle_switch.dart';
|
import 'package:toggle_switch/toggle_switch.dart';
|
||||||
|
|
||||||
class GestureIcons {
|
class GestureIcons {
|
||||||
@@ -39,11 +41,13 @@ class GestureHelp extends StatefulWidget {
|
|||||||
{Key? key,
|
{Key? key,
|
||||||
required this.touchMode,
|
required this.touchMode,
|
||||||
required this.onTouchModeChange,
|
required this.onTouchModeChange,
|
||||||
required this.virtualMouseMode})
|
required this.virtualMouseMode,
|
||||||
|
this.inputModel})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
final bool touchMode;
|
final bool touchMode;
|
||||||
final OnTouchModeChange onTouchModeChange;
|
final OnTouchModeChange onTouchModeChange;
|
||||||
final VirtualMouseMode virtualMouseMode;
|
final VirtualMouseMode virtualMouseMode;
|
||||||
|
final InputModel? inputModel;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() =>
|
State<StatefulWidget> createState() =>
|
||||||
@@ -61,6 +65,14 @@ class _GestureHelpState extends State<GestureHelp> {
|
|||||||
_selectedIndex = _touchMode ? 1 : 0;
|
_selectedIndex = _touchMode ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper to exit relative mouse mode when certain conditions are met.
|
||||||
|
/// This reduces code duplication across multiple UI callbacks.
|
||||||
|
void _exitRelativeMouseModeIf(bool condition) {
|
||||||
|
if (condition) {
|
||||||
|
widget.inputModel?.setRelativeMouseMode(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
@@ -103,6 +115,8 @@ class _GestureHelpState extends State<GestureHelp> {
|
|||||||
_selectedIndex = index ?? 0;
|
_selectedIndex = index ?? 0;
|
||||||
_touchMode = index == 0 ? false : true;
|
_touchMode = index == 0 ? false : true;
|
||||||
widget.onTouchModeChange(_touchMode);
|
widget.onTouchModeChange(_touchMode);
|
||||||
|
// Exit relative mouse mode when switching to touch mode
|
||||||
|
_exitRelativeMouseModeIf(_touchMode);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -117,12 +131,18 @@ class _GestureHelpState extends State<GestureHelp> {
|
|||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
await _virtualMouseMode.toggleVirtualMouse();
|
await _virtualMouseMode.toggleVirtualMouse();
|
||||||
|
// Exit relative mouse mode when virtual mouse is hidden
|
||||||
|
_exitRelativeMouseModeIf(
|
||||||
|
!_virtualMouseMode.showVirtualMouse);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await _virtualMouseMode.toggleVirtualMouse();
|
await _virtualMouseMode.toggleVirtualMouse();
|
||||||
|
// Exit relative mouse mode when virtual mouse is hidden
|
||||||
|
_exitRelativeMouseModeIf(
|
||||||
|
!_virtualMouseMode.showVirtualMouse);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
child: Text(translate('Show virtual mouse')),
|
child: Text(translate('Show virtual mouse')),
|
||||||
@@ -196,6 +216,10 @@ class _GestureHelpState extends State<GestureHelp> {
|
|||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
await _virtualMouseMode
|
await _virtualMouseMode
|
||||||
.toggleVirtualJoystick();
|
.toggleVirtualJoystick();
|
||||||
|
// Exit relative mouse mode when joystick is hidden
|
||||||
|
_exitRelativeMouseModeIf(
|
||||||
|
!_virtualMouseMode
|
||||||
|
.showVirtualJoystick);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -203,6 +227,10 @@ class _GestureHelpState extends State<GestureHelp> {
|
|||||||
onTap: () async {
|
onTap: () async {
|
||||||
await _virtualMouseMode
|
await _virtualMouseMode
|
||||||
.toggleVirtualJoystick();
|
.toggleVirtualJoystick();
|
||||||
|
// Exit relative mouse mode when joystick is hidden
|
||||||
|
_exitRelativeMouseModeIf(
|
||||||
|
!_virtualMouseMode
|
||||||
|
.showVirtualJoystick);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -211,6 +239,39 @@ class _GestureHelpState extends State<GestureHelp> {
|
|||||||
],
|
],
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
|
// Relative mouse mode option - only visible when joystick is shown
|
||||||
|
if (!_touchMode &&
|
||||||
|
_virtualMouseMode.showVirtualMouse &&
|
||||||
|
_virtualMouseMode.showVirtualJoystick &&
|
||||||
|
widget.inputModel != null)
|
||||||
|
Obx(() => Transform.translate(
|
||||||
|
offset: const Offset(-10.0, -24.0),
|
||||||
|
child: Padding(
|
||||||
|
// Indent further for 'Relative mouse mode'
|
||||||
|
padding: const EdgeInsets.only(left: 48.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: widget.inputModel!
|
||||||
|
.relativeMouseMode.value,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null) return;
|
||||||
|
widget.inputModel!
|
||||||
|
.setRelativeMouseMode(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
widget.inputModel!
|
||||||
|
.toggleRelativeMouseMode();
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
translate('Relative mouse mode')),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import 'package:get/get.dart';
|
|||||||
|
|
||||||
import '../../models/model.dart';
|
import '../../models/model.dart';
|
||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
|
import '../../models/state_model.dart';
|
||||||
|
import 'relative_mouse_model.dart';
|
||||||
import '../common.dart';
|
import '../common.dart';
|
||||||
import '../consts.dart';
|
import '../consts.dart';
|
||||||
|
|
||||||
@@ -349,15 +351,28 @@ class InputModel {
|
|||||||
double _trackpadSpeedInner = kDefaultTrackpadSpeed / 100.0;
|
double _trackpadSpeedInner = kDefaultTrackpadSpeed / 100.0;
|
||||||
var _trackpadScrollUnsent = Offset.zero;
|
var _trackpadScrollUnsent = Offset.zero;
|
||||||
|
|
||||||
|
// Mobile relative mouse delta accumulators (for slow/fine movements).
|
||||||
|
double _mobileDeltaRemainderX = 0.0;
|
||||||
|
double _mobileDeltaRemainderY = 0.0;
|
||||||
|
|
||||||
var _lastScale = 1.0;
|
var _lastScale = 1.0;
|
||||||
|
|
||||||
bool _pointerMovedAfterEnter = false;
|
bool _pointerMovedAfterEnter = false;
|
||||||
|
bool _pointerInsideImage = false;
|
||||||
|
|
||||||
// mouse
|
// mouse
|
||||||
final isPhysicalMouse = false.obs;
|
final isPhysicalMouse = false.obs;
|
||||||
int _lastButtons = 0;
|
int _lastButtons = 0;
|
||||||
Offset lastMousePos = Offset.zero;
|
Offset lastMousePos = Offset.zero;
|
||||||
|
|
||||||
|
// Relative mouse mode (for games/3D apps).
|
||||||
|
final relativeMouseMode = false.obs;
|
||||||
|
late final RelativeMouseModel _relativeMouse;
|
||||||
|
// Callback to cancel external throttle timer when relative mouse mode is disabled.
|
||||||
|
VoidCallback? onRelativeMouseModeDisabled;
|
||||||
|
// Disposer for the relativeMouseMode observer (to prevent memory leaks).
|
||||||
|
Worker? _relativeMouseModeDisposer;
|
||||||
|
|
||||||
bool _queryOtherWindowCoords = false;
|
bool _queryOtherWindowCoords = false;
|
||||||
Rect? _windowRect;
|
Rect? _windowRect;
|
||||||
List<RemoteWindowCoords> _remoteWindowCoords = [];
|
List<RemoteWindowCoords> _remoteWindowCoords = [];
|
||||||
@@ -367,15 +382,40 @@ class InputModel {
|
|||||||
bool get keyboardPerm => parent.target!.ffiModel.keyboard;
|
bool get keyboardPerm => parent.target!.ffiModel.keyboard;
|
||||||
String get id => parent.target?.id ?? '';
|
String get id => parent.target?.id ?? '';
|
||||||
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
|
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
|
||||||
|
String get peerVersion => parent.target?.ffiModel.pi.version ?? '';
|
||||||
bool get isViewOnly => parent.target!.ffiModel.viewOnly;
|
bool get isViewOnly => parent.target!.ffiModel.viewOnly;
|
||||||
bool get showMyCursor => parent.target!.ffiModel.showMyCursor;
|
bool get showMyCursor => parent.target!.ffiModel.showMyCursor;
|
||||||
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
|
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
|
||||||
bool get isViewCamera => parent.target!.connType == ConnType.viewCamera;
|
bool get isViewCamera => parent.target!.connType == ConnType.viewCamera;
|
||||||
int get trackpadSpeed => _trackpadSpeed;
|
int get trackpadSpeed => _trackpadSpeed;
|
||||||
bool get useEdgeScroll => parent.target!.canvasModel.scrollStyle == ScrollStyle.scrolledge;
|
bool get useEdgeScroll =>
|
||||||
|
parent.target!.canvasModel.scrollStyle == ScrollStyle.scrolledge;
|
||||||
|
|
||||||
|
/// Check if the connected server supports relative mouse mode.
|
||||||
|
bool get isRelativeMouseModeSupported => _relativeMouse.isSupported;
|
||||||
|
|
||||||
InputModel(this.parent) {
|
InputModel(this.parent) {
|
||||||
sessionId = parent.target!.sessionId;
|
sessionId = parent.target!.sessionId;
|
||||||
|
_relativeMouse = RelativeMouseModel(
|
||||||
|
sessionId: sessionId,
|
||||||
|
enabled: relativeMouseMode,
|
||||||
|
keyboardPerm: () => keyboardPerm,
|
||||||
|
isViewCamera: () => isViewCamera,
|
||||||
|
peerVersion: () => peerVersion,
|
||||||
|
peerPlatform: () => peerPlatform,
|
||||||
|
modify: (msg) => modify(msg),
|
||||||
|
getPointerInsideImage: () => _pointerInsideImage,
|
||||||
|
setPointerInsideImage: (inside) => _pointerInsideImage = inside,
|
||||||
|
);
|
||||||
|
_relativeMouse.onDisabled = () => onRelativeMouseModeDisabled?.call();
|
||||||
|
|
||||||
|
// Sync relative mouse mode state to global state for UI components (e.g., tab bar hint).
|
||||||
|
_relativeMouseModeDisposer = ever(relativeMouseMode, (bool value) {
|
||||||
|
final peerId = id;
|
||||||
|
if (peerId.isNotEmpty) {
|
||||||
|
stateGlobal.relativeMouseModeState[peerId] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function must be called after the peer info is received.
|
// This function must be called after the peer info is received.
|
||||||
@@ -506,6 +546,10 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_relativeMouse.handleRawKeyEvent(e)) {
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
|
||||||
final key = e.logicalKey;
|
final key = e.logicalKey;
|
||||||
if (e is RawKeyDownEvent) {
|
if (e is RawKeyDownEvent) {
|
||||||
if (!e.repeat) {
|
if (!e.repeat) {
|
||||||
@@ -568,6 +612,16 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_relativeMouse.handleKeyEvent(
|
||||||
|
e,
|
||||||
|
ctrlPressed: ctrl,
|
||||||
|
shiftPressed: shift,
|
||||||
|
altPressed: alt,
|
||||||
|
commandPressed: command,
|
||||||
|
)) {
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
|
||||||
if (e is KeyUpEvent) {
|
if (e is KeyUpEvent) {
|
||||||
handleKeyUpEventModifiers(e);
|
handleKeyUpEventModifiers(e);
|
||||||
} else if (e is KeyDownEvent) {
|
} else if (e is KeyDownEvent) {
|
||||||
@@ -853,11 +907,13 @@ class InputModel {
|
|||||||
toReleaseKeys.release(handleKeyEvent);
|
toReleaseKeys.release(handleKeyEvent);
|
||||||
toReleaseRawKeys.release(handleRawKeyEvent);
|
toReleaseRawKeys.release(handleRawKeyEvent);
|
||||||
_pointerMovedAfterEnter = false;
|
_pointerMovedAfterEnter = false;
|
||||||
|
_pointerInsideImage = enter;
|
||||||
|
|
||||||
// Fix status
|
// Fix status
|
||||||
if (!enter) {
|
if (!enter) {
|
||||||
resetModifiers();
|
resetModifiers();
|
||||||
}
|
}
|
||||||
|
_relativeMouse.onEnterOrLeaveImage(enter);
|
||||||
_flingTimer?.cancel();
|
_flingTimer?.cancel();
|
||||||
if (!isInputSourceFlutter) {
|
if (!isInputSourceFlutter) {
|
||||||
bind.sessionEnterOrLeave(sessionId: sessionId, enter: enter);
|
bind.sessionEnterOrLeave(sessionId: sessionId, enter: enter);
|
||||||
@@ -878,15 +934,134 @@ class InputModel {
|
|||||||
msg: json.encode(modify({'x': '$x2', 'y': '$y2'})));
|
msg: json.encode(modify({'x': '$x2', 'y': '$y2'})));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send relative mouse movement for mobile clients (virtual joystick).
|
||||||
|
/// This method is for touch-based controls that want to send delta values.
|
||||||
|
/// Uses the 'move_relative' type which bypasses absolute position tracking.
|
||||||
|
///
|
||||||
|
/// Accumulates fractional deltas to avoid losing slow/fine movements.
|
||||||
|
/// Only sends events when relative mouse mode is enabled and supported.
|
||||||
|
Future<void> sendMobileRelativeMouseMove(double dx, double dy) async {
|
||||||
|
if (!keyboardPerm) return;
|
||||||
|
if (isViewCamera) return;
|
||||||
|
// Only send relative mouse events when relative mode is enabled and supported.
|
||||||
|
if (!isRelativeMouseModeSupported || !relativeMouseMode.value) return;
|
||||||
|
_mobileDeltaRemainderX += dx;
|
||||||
|
_mobileDeltaRemainderY += dy;
|
||||||
|
final x = _mobileDeltaRemainderX.truncate();
|
||||||
|
final y = _mobileDeltaRemainderY.truncate();
|
||||||
|
_mobileDeltaRemainderX -= x;
|
||||||
|
_mobileDeltaRemainderY -= y;
|
||||||
|
if (x == 0 && y == 0) return;
|
||||||
|
await bind.sessionSendMouse(
|
||||||
|
sessionId: sessionId,
|
||||||
|
msg: json.encode(modify({
|
||||||
|
'type': 'move_relative',
|
||||||
|
'x': '$x',
|
||||||
|
'y': '$y',
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the pointer lock center position based on current window frame.
|
||||||
|
Future<void> updatePointerLockCenter({Offset? localCenter}) {
|
||||||
|
return _relativeMouse.updatePointerLockCenter(localCenter: localCenter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current image widget size (for comparison to avoid unnecessary updates).
|
||||||
|
Size? get imageWidgetSize => _relativeMouse.imageWidgetSize;
|
||||||
|
|
||||||
|
/// Update the image widget size for center calculation.
|
||||||
|
void updateImageWidgetSize(Size size) {
|
||||||
|
_relativeMouse.updateImageWidgetSize(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleRelativeMouseMode() {
|
||||||
|
_relativeMouse.toggleRelativeMouseMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool setRelativeMouseMode(bool enabled) {
|
||||||
|
return _relativeMouse.setRelativeMouseMode(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exit relative mouse mode and release all modifier keys to the remote.
|
||||||
|
/// This is called when the user presses the exit shortcut (Ctrl+Alt on Win/Linux, Cmd+G on macOS).
|
||||||
|
/// We need to send key-up events for all modifiers because the shortcut itself may have
|
||||||
|
/// blocked some key events, leaving the remote in a state where modifiers are stuck.
|
||||||
|
void exitRelativeMouseModeWithKeyRelease() {
|
||||||
|
if (!_relativeMouse.enabled.value) return;
|
||||||
|
|
||||||
|
// First, send release events for all modifier keys to the remote.
|
||||||
|
// This ensures the remote doesn't have stuck modifier keys after exiting.
|
||||||
|
// Use press: false, down: false to send key-up events without modifiers attached.
|
||||||
|
final modifiersToRelease = [
|
||||||
|
'Control_L',
|
||||||
|
'Control_R',
|
||||||
|
'Alt_L',
|
||||||
|
'Alt_R',
|
||||||
|
'Shift_L',
|
||||||
|
'Shift_R',
|
||||||
|
'Meta_L', // Command/Super left
|
||||||
|
'Meta_R', // Command/Super right
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final key in modifiersToRelease) {
|
||||||
|
bind.sessionInputKey(
|
||||||
|
sessionId: sessionId,
|
||||||
|
name: key,
|
||||||
|
down: false,
|
||||||
|
press: false,
|
||||||
|
alt: false,
|
||||||
|
ctrl: false,
|
||||||
|
shift: false,
|
||||||
|
command: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset local modifier state
|
||||||
|
resetModifiers();
|
||||||
|
|
||||||
|
// Now exit relative mouse mode
|
||||||
|
_relativeMouse.setRelativeMouseMode(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void disposeRelativeMouseMode() {
|
||||||
|
_relativeMouse.dispose();
|
||||||
|
onRelativeMouseModeDisabled = null;
|
||||||
|
// Cancel the relative mouse mode observer and clean up global state.
|
||||||
|
_relativeMouseModeDisposer?.dispose();
|
||||||
|
_relativeMouseModeDisposer = null;
|
||||||
|
final peerId = id;
|
||||||
|
if (peerId.isNotEmpty) {
|
||||||
|
stateGlobal.relativeMouseModeState.remove(peerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onWindowBlur() {
|
||||||
|
_relativeMouse.onWindowBlur();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onWindowFocus() {
|
||||||
|
_relativeMouse.onWindowFocus();
|
||||||
|
}
|
||||||
|
|
||||||
void onPointHoverImage(PointerHoverEvent e) {
|
void onPointHoverImage(PointerHoverEvent e) {
|
||||||
_stopFling = true;
|
_stopFling = true;
|
||||||
if (isViewOnly && !showMyCursor) return;
|
if (isViewOnly && !showMyCursor) return;
|
||||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||||
|
|
||||||
|
// Only update pointer region when relative mouse mode is enabled.
|
||||||
|
// This avoids unnecessary tracking when not in relative mode.
|
||||||
|
if (_relativeMouse.enabled.value) {
|
||||||
|
_relativeMouse.updatePointerRegionTopLeftGlobal(e);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isPhysicalMouse.value) {
|
if (!isPhysicalMouse.value) {
|
||||||
isPhysicalMouse.value = true;
|
isPhysicalMouse.value = true;
|
||||||
}
|
}
|
||||||
if (isPhysicalMouse.value) {
|
if (isPhysicalMouse.value) {
|
||||||
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position, edgeScroll: useEdgeScroll);
|
if (!_relativeMouse.handleRelativeMouseMove(e.localPosition)) {
|
||||||
|
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position,
|
||||||
|
edgeScroll: useEdgeScroll);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1043,30 +1218,59 @@ class InputModel {
|
|||||||
_windowRect = null;
|
_windowRect = null;
|
||||||
if (isViewOnly && !showMyCursor) return;
|
if (isViewOnly && !showMyCursor) return;
|
||||||
if (isViewCamera) return;
|
if (isViewCamera) return;
|
||||||
|
|
||||||
|
if (_relativeMouse.enabled.value) {
|
||||||
|
_relativeMouse.updatePointerRegionTopLeftGlobal(e);
|
||||||
|
}
|
||||||
|
|
||||||
if (e.kind != ui.PointerDeviceKind.mouse) {
|
if (e.kind != ui.PointerDeviceKind.mouse) {
|
||||||
if (isPhysicalMouse.value) {
|
if (isPhysicalMouse.value) {
|
||||||
isPhysicalMouse.value = false;
|
isPhysicalMouse.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isPhysicalMouse.value) {
|
if (isPhysicalMouse.value) {
|
||||||
|
// In relative mouse mode, send button events without position.
|
||||||
|
// Use _relativeMouse.enabled.value consistently with the guard above.
|
||||||
|
if (_relativeMouse.enabled.value) {
|
||||||
|
_relativeMouse
|
||||||
|
.sendRelativeMouseButton(_getMouseEvent(e, _kMouseEventDown));
|
||||||
|
} else {
|
||||||
handleMouse(_getMouseEvent(e, _kMouseEventDown), e.position);
|
handleMouse(_getMouseEvent(e, _kMouseEventDown), e.position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void onPointUpImage(PointerUpEvent e) {
|
void onPointUpImage(PointerUpEvent e) {
|
||||||
if (isDesktop) _queryOtherWindowCoords = false;
|
if (isDesktop) _queryOtherWindowCoords = false;
|
||||||
if (isViewOnly && !showMyCursor) return;
|
if (isViewOnly && !showMyCursor) return;
|
||||||
if (isViewCamera) return;
|
if (isViewCamera) return;
|
||||||
|
|
||||||
|
if (_relativeMouse.enabled.value) {
|
||||||
|
_relativeMouse.updatePointerRegionTopLeftGlobal(e);
|
||||||
|
}
|
||||||
|
|
||||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||||
if (isPhysicalMouse.value) {
|
if (isPhysicalMouse.value) {
|
||||||
|
// In relative mouse mode, send button events without position.
|
||||||
|
// Use _relativeMouse.enabled.value consistently with the guard above.
|
||||||
|
if (_relativeMouse.enabled.value) {
|
||||||
|
_relativeMouse
|
||||||
|
.sendRelativeMouseButton(_getMouseEvent(e, _kMouseEventUp));
|
||||||
|
} else {
|
||||||
handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position);
|
handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void onPointMoveImage(PointerMoveEvent e) {
|
void onPointMoveImage(PointerMoveEvent e) {
|
||||||
if (isViewOnly && !showMyCursor) return;
|
if (isViewOnly && !showMyCursor) return;
|
||||||
if (isViewCamera) return;
|
if (isViewCamera) return;
|
||||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||||
|
|
||||||
|
if (_relativeMouse.enabled.value) {
|
||||||
|
_relativeMouse.updatePointerRegionTopLeftGlobal(e);
|
||||||
|
}
|
||||||
|
|
||||||
if (_queryOtherWindowCoords) {
|
if (_queryOtherWindowCoords) {
|
||||||
Future.delayed(Duration.zero, () async {
|
Future.delayed(Duration.zero, () async {
|
||||||
_windowRect = await fillRemoteCoordsAndGetCurFrame(_remoteWindowCoords);
|
_windowRect = await fillRemoteCoordsAndGetCurFrame(_remoteWindowCoords);
|
||||||
@@ -1074,7 +1278,10 @@ class InputModel {
|
|||||||
_queryOtherWindowCoords = false;
|
_queryOtherWindowCoords = false;
|
||||||
}
|
}
|
||||||
if (isPhysicalMouse.value) {
|
if (isPhysicalMouse.value) {
|
||||||
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position, edgeScroll: useEdgeScroll);
|
if (!_relativeMouse.handleRelativeMouseMove(e.localPosition)) {
|
||||||
|
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position,
|
||||||
|
edgeScroll: useEdgeScroll);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1098,6 +1305,11 @@ class InputModel {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle scroll/wheel events.
|
||||||
|
/// Note: Scroll events intentionally use absolute positioning even in relative mouse mode.
|
||||||
|
/// This is because scroll events don't need relative positioning - they represent
|
||||||
|
/// scroll deltas that are independent of cursor position. Games and 3D applications
|
||||||
|
/// handle scroll events the same way regardless of mouse mode.
|
||||||
void onPointerSignalImage(PointerSignalEvent e) {
|
void onPointerSignalImage(PointerSignalEvent e) {
|
||||||
if (isViewOnly) return;
|
if (isViewOnly) return;
|
||||||
if (isViewCamera) return;
|
if (isViewCamera) return;
|
||||||
@@ -1285,14 +1497,18 @@ class InputModel {
|
|||||||
evt['y'] = '${pos.y.toInt()}';
|
evt['y'] = '${pos.y.toInt()}';
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<int, String> mapButtons = {
|
final buttons = evt['buttons'];
|
||||||
kPrimaryMouseButton: 'left',
|
if (buttons is int) {
|
||||||
kSecondaryMouseButton: 'right',
|
evt['buttons'] = mouseButtonsToPeer(buttons);
|
||||||
kMiddleMouseButton: 'wheel',
|
} else {
|
||||||
kBackMouseButton: 'back',
|
// Log warning if buttons exists but is not an int (unexpected caller).
|
||||||
kForwardMouseButton: 'forward'
|
// Keep empty string fallback for missing buttons to preserve move/hover behavior.
|
||||||
};
|
if (buttons != null) {
|
||||||
evt['buttons'] = mapButtons[evt['buttons']] ?? '';
|
debugPrint(
|
||||||
|
'[InputModel] processEventToPeer: unexpected buttons type: ${buttons.runtimeType}, value: $buttons');
|
||||||
|
}
|
||||||
|
evt['buttons'] = '';
|
||||||
|
}
|
||||||
return evt;
|
return evt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1303,8 +1519,8 @@ class InputModel {
|
|||||||
bool moveCanvas = true,
|
bool moveCanvas = true,
|
||||||
bool edgeScroll = false,
|
bool edgeScroll = false,
|
||||||
}) {
|
}) {
|
||||||
final evtToPeer =
|
final evtToPeer = processEventToPeer(evt, offset,
|
||||||
processEventToPeer(evt, offset, onExit: onExit, moveCanvas: moveCanvas, edgeScroll: edgeScroll);
|
onExit: onExit, moveCanvas: moveCanvas, edgeScroll: edgeScroll);
|
||||||
if (evtToPeer != null) {
|
if (evtToPeer != null) {
|
||||||
bind.sessionSendMouse(
|
bind.sessionSendMouse(
|
||||||
sessionId: sessionId, msg: json.encode(modify(evtToPeer)));
|
sessionId: sessionId, msg: json.encode(modify(evtToPeer)));
|
||||||
|
|||||||
@@ -213,6 +213,9 @@ class FfiModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updatePermission(Map<String, dynamic> evt, String id) {
|
updatePermission(Map<String, dynamic> evt, String id) {
|
||||||
|
// Track previous keyboard permission to detect revocation.
|
||||||
|
final hadKeyboardPerm = _permissions['keyboard'] != false;
|
||||||
|
|
||||||
evt.forEach((k, v) {
|
evt.forEach((k, v) {
|
||||||
if (k == 'name' || k.isEmpty) return;
|
if (k == 'name' || k.isEmpty) return;
|
||||||
_permissions[k] = v == 'true';
|
_permissions[k] = v == 'true';
|
||||||
@@ -221,6 +224,18 @@ class FfiModel with ChangeNotifier {
|
|||||||
if (parent.target?.connType == ConnType.defaultConn) {
|
if (parent.target?.connType == ConnType.defaultConn) {
|
||||||
KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false;
|
KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If keyboard permission was revoked while relative mouse mode is active,
|
||||||
|
// forcefully disable relative mouse mode to prevent the user from being trapped.
|
||||||
|
final hasKeyboardPerm = _permissions['keyboard'] != false;
|
||||||
|
if (hadKeyboardPerm && !hasKeyboardPerm) {
|
||||||
|
final inputModel = parent.target?.inputModel;
|
||||||
|
if (inputModel != null && inputModel.relativeMouseMode.value) {
|
||||||
|
inputModel.setRelativeMouseMode(false);
|
||||||
|
showToast(translate('rel-mouse-permission-lost-tip'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
debugPrint('updatePermission: $_permissions');
|
debugPrint('updatePermission: $_permissions');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -457,6 +472,9 @@ class FfiModel with ChangeNotifier {
|
|||||||
_handlePrinterRequest(evt, sessionId, peerId);
|
_handlePrinterRequest(evt, sessionId, peerId);
|
||||||
} else if (name == 'screenshot') {
|
} else if (name == 'screenshot') {
|
||||||
_handleScreenshot(evt, sessionId, peerId);
|
_handleScreenshot(evt, sessionId, peerId);
|
||||||
|
} else if (name == 'exit_relative_mouse_mode') {
|
||||||
|
// Handle exit shortcut from rdev grab loop (Ctrl+Alt on Win/Linux, Cmd+G on macOS)
|
||||||
|
parent.target?.inputModel.exitRelativeMouseModeWithKeyRelease();
|
||||||
} else {
|
} else {
|
||||||
debugPrint('Event is not handled in the fixed branch: $name');
|
debugPrint('Event is not handled in the fixed branch: $name');
|
||||||
}
|
}
|
||||||
@@ -765,7 +783,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCurDisplay(SessionID sessionId, {updateCursorPos = false}) {
|
Future<void> updateCurDisplay(SessionID sessionId, {updateCursorPos = false}) async {
|
||||||
final newRect = displaysRect();
|
final newRect = displaysRect();
|
||||||
if (newRect == null) {
|
if (newRect == null) {
|
||||||
return;
|
return;
|
||||||
@@ -777,9 +795,19 @@ class FfiModel with ChangeNotifier {
|
|||||||
updateCursorPos: updateCursorPos);
|
updateCursorPos: updateCursorPos);
|
||||||
}
|
}
|
||||||
_rect = newRect;
|
_rect = newRect;
|
||||||
parent.target?.canvasModel
|
// Await updateViewStyle to ensure view geometry is fully updated before
|
||||||
|
// updating pointer lock center. This prevents stale center calculations.
|
||||||
|
await parent.target?.canvasModel
|
||||||
.updateViewStyle(refreshMousePos: updateCursorPos);
|
.updateViewStyle(refreshMousePos: updateCursorPos);
|
||||||
_updateSessionWidthHeight(sessionId);
|
_updateSessionWidthHeight(sessionId);
|
||||||
|
|
||||||
|
// Keep pointer lock center in sync when using relative mouse mode.
|
||||||
|
// Note: updatePointerLockCenter is async-safe (handles errors internally),
|
||||||
|
// so we fire-and-forget here.
|
||||||
|
final inputModel = parent.target?.inputModel;
|
||||||
|
if (inputModel != null && inputModel.relativeMouseMode.value) {
|
||||||
|
inputModel.updatePointerLockCenter();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -863,6 +891,17 @@ class FfiModel with ChangeNotifier {
|
|||||||
final title = evt['title'];
|
final title = evt['title'];
|
||||||
final text = evt['text'];
|
final text = evt['text'];
|
||||||
final link = evt['link'];
|
final link = evt['link'];
|
||||||
|
|
||||||
|
// Disable relative mouse mode on any error-type message to ensure cursor is released.
|
||||||
|
// This includes connection errors, session-ending messages, elevation errors, etc.
|
||||||
|
// Safety: releasing pointer lock on errors prevents the user from being stuck.
|
||||||
|
if (title == 'Connection Error' ||
|
||||||
|
type == 'error' ||
|
||||||
|
type == 'restarting' ||
|
||||||
|
(type is String && type.contains('error'))) {
|
||||||
|
parent.target?.inputModel.setRelativeMouseMode(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (type == 're-input-password') {
|
if (type == 're-input-password') {
|
||||||
wrongPasswordDialog(sessionId, dialogManager, type, title, text);
|
wrongPasswordDialog(sessionId, dialogManager, type, title, text);
|
||||||
} else if (type == 'input-2fa') {
|
} else if (type == 'input-2fa') {
|
||||||
@@ -967,6 +1006,8 @@ class FfiModel with ChangeNotifier {
|
|||||||
|
|
||||||
void reconnect(OverlayDialogManager dialogManager, SessionID sessionId,
|
void reconnect(OverlayDialogManager dialogManager, SessionID sessionId,
|
||||||
bool forceRelay) {
|
bool forceRelay) {
|
||||||
|
// Disable relative mouse mode before reconnecting to ensure cursor is released.
|
||||||
|
parent.target?.inputModel.setRelativeMouseMode(false);
|
||||||
bind.sessionReconnect(sessionId: sessionId, forceRelay: forceRelay);
|
bind.sessionReconnect(sessionId: sessionId, forceRelay: forceRelay);
|
||||||
clearPermissions();
|
clearPermissions();
|
||||||
dialogManager.dismissAll();
|
dialogManager.dismissAll();
|
||||||
@@ -1192,9 +1233,6 @@ class FfiModel with ChangeNotifier {
|
|||||||
|
|
||||||
_queryAuditGuid(peerId);
|
_queryAuditGuid(peerId);
|
||||||
|
|
||||||
// This call is to ensuer the keyboard mode is updated depending on the peer version.
|
|
||||||
parent.target?.inputModel.updateKeyboardMode();
|
|
||||||
|
|
||||||
// Map clone is required here, otherwise "evt" may be changed by other threads through the reference.
|
// Map clone is required here, otherwise "evt" may be changed by other threads through the reference.
|
||||||
// Because this function is asynchronous, there's an "await" in this function.
|
// Because this function is asynchronous, there's an "await" in this function.
|
||||||
cachedPeerData.peerInfo = {...evt};
|
cachedPeerData.peerInfo = {...evt};
|
||||||
@@ -1206,6 +1244,17 @@ class FfiModel with ChangeNotifier {
|
|||||||
|
|
||||||
parent.target?.dialogManager.dismissAll();
|
parent.target?.dialogManager.dismissAll();
|
||||||
_pi.version = evt['version'];
|
_pi.version = evt['version'];
|
||||||
|
// Note: Relative mouse mode is NOT auto-enabled on connect.
|
||||||
|
// Users must manually enable it via toolbar or keyboard shortcut (Ctrl+Alt+Shift+M).
|
||||||
|
//
|
||||||
|
// For desktop/webDesktop, keyboard mode initialization is handled later by
|
||||||
|
// checkDesktopKeyboardMode() which may change the mode if not supported,
|
||||||
|
// followed by updateKeyboardMode() to sync InputModel.keyboardMode.
|
||||||
|
// For mobile, updateKeyboardMode() is currently a no-op (only executes on desktop/web),
|
||||||
|
// but we call it here for consistency and future-proofing.
|
||||||
|
if (isMobile) {
|
||||||
|
parent.target?.inputModel.updateKeyboardMode();
|
||||||
|
}
|
||||||
_pi.isSupportMultiUiSession =
|
_pi.isSupportMultiUiSession =
|
||||||
bind.isSupportMultiUiSession(version: _pi.version);
|
bind.isSupportMultiUiSession(version: _pi.version);
|
||||||
_pi.username = evt['username'];
|
_pi.username = evt['username'];
|
||||||
@@ -1307,7 +1356,11 @@ class FfiModel with ChangeNotifier {
|
|||||||
stateGlobal.resetLastResolutionGroupValues(peerId);
|
stateGlobal.resetLastResolutionGroupValues(peerId);
|
||||||
|
|
||||||
if (isDesktop || isWebDesktop) {
|
if (isDesktop || isWebDesktop) {
|
||||||
checkDesktopKeyboardMode();
|
// checkDesktopKeyboardMode may change the keyboard mode if the current
|
||||||
|
// mode is not supported. Re-sync InputModel.keyboardMode afterwards.
|
||||||
|
// Note: updateKeyboardMode() is a no-op on mobile (early-returns).
|
||||||
|
await checkDesktopKeyboardMode();
|
||||||
|
await parent.target?.inputModel.updateKeyboardMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -3768,6 +3821,8 @@ class FFI {
|
|||||||
ffiModel.clear();
|
ffiModel.clear();
|
||||||
canvasModel.clear();
|
canvasModel.clear();
|
||||||
inputModel.resetModifiers();
|
inputModel.resetModifiers();
|
||||||
|
// Dispose relative mouse mode resources to ensure cursor is restored
|
||||||
|
inputModel.disposeRelativeMouseMode();
|
||||||
if (closeSession) {
|
if (closeSession) {
|
||||||
await bind.sessionClose(sessionId: sessionId);
|
await bind.sessionClose(sessionId: sessionId);
|
||||||
}
|
}
|
||||||
|
|||||||
1061
flutter/lib/models/relative_mouse_model.dart
Normal file
1061
flutter/lib/models/relative_mouse_model.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
|||||||
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
@@ -30,6 +29,11 @@ class StateGlobal {
|
|||||||
|
|
||||||
String _inputSource = '';
|
String _inputSource = '';
|
||||||
|
|
||||||
|
// Track relative mouse mode state for each peer connection.
|
||||||
|
// Key: peerId, Value: true if relative mouse mode is active.
|
||||||
|
// Note: This is session-only runtime state, NOT persisted to config.
|
||||||
|
final RxMap<String, bool> relativeMouseModeState = <String, bool>{}.obs;
|
||||||
|
|
||||||
// Use for desktop -> remote toolbar -> resolution
|
// Use for desktop -> remote toolbar -> resolution
|
||||||
final Map<String, Map<int, String?>> _lastResolutionGroupValues = {};
|
final Map<String, Map<int, String?>> _lastResolutionGroupValues = {};
|
||||||
|
|
||||||
|
|||||||
58
flutter/lib/utils/relative_mouse_accumulator.dart
Normal file
58
flutter/lib/utils/relative_mouse_accumulator.dart
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/// A small helper for accumulating fractional mouse deltas and emitting integer deltas.
|
||||||
|
///
|
||||||
|
/// Relative mouse mode uses integer deltas on the wire, but Flutter pointer deltas
|
||||||
|
/// are doubles. This accumulator preserves sub-pixel movement by carrying the
|
||||||
|
/// fractional remainder across events.
|
||||||
|
class RelativeMouseDelta {
|
||||||
|
final int x;
|
||||||
|
final int y;
|
||||||
|
|
||||||
|
const RelativeMouseDelta(this.x, this.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accumulates fractional mouse deltas and returns integer deltas when available.
|
||||||
|
class RelativeMouseAccumulator {
|
||||||
|
double _fracX = 0.0;
|
||||||
|
double _fracY = 0.0;
|
||||||
|
|
||||||
|
/// Adds a delta and returns an integer delta when at least one axis reaches a
|
||||||
|
/// magnitude of 1px (after truncation towards zero).
|
||||||
|
///
|
||||||
|
/// If [maxDelta] is > 0, the returned integer delta is clamped to
|
||||||
|
/// [-maxDelta, maxDelta] on each axis.
|
||||||
|
RelativeMouseDelta? add(
|
||||||
|
double dx,
|
||||||
|
double dy, {
|
||||||
|
required int maxDelta,
|
||||||
|
}) {
|
||||||
|
// Guard against misuse: negative maxDelta would silently disable clamping.
|
||||||
|
assert(maxDelta >= 0, 'maxDelta must be non-negative');
|
||||||
|
|
||||||
|
_fracX += dx;
|
||||||
|
_fracY += dy;
|
||||||
|
|
||||||
|
int intX = _fracX.truncate();
|
||||||
|
int intY = _fracY.truncate();
|
||||||
|
|
||||||
|
if (intX == 0 && intY == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp before subtracting so excess movement is preserved in the accumulator
|
||||||
|
// rather than being permanently discarded during spikes.
|
||||||
|
if (maxDelta > 0) {
|
||||||
|
intX = intX.clamp(-maxDelta, maxDelta);
|
||||||
|
intY = intY.clamp(-maxDelta, maxDelta);
|
||||||
|
}
|
||||||
|
|
||||||
|
_fracX -= intX;
|
||||||
|
_fracY -= intY;
|
||||||
|
|
||||||
|
return RelativeMouseDelta(intX, intY);
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
_fracX = 0.0;
|
||||||
|
_fracY = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2020,5 +2020,19 @@ class RustdeskImpl {
|
|||||||
return js.context.callMethod('getByName', ['audit_guid']);
|
return js.context.callMethod('getByName', ['audit_guid']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool mainSetCursorPosition({required int x, required int y, dynamic hint}) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool mainClipCursor(
|
||||||
|
{required int left,
|
||||||
|
required int top,
|
||||||
|
required int right,
|
||||||
|
required int bottom,
|
||||||
|
required bool enable,
|
||||||
|
dynamic hint}) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void dispose() {}
|
void dispose() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,22 @@ import window_manager
|
|||||||
import window_size
|
import window_size
|
||||||
import texture_rgba_renderer
|
import texture_rgba_renderer
|
||||||
|
|
||||||
|
// Global state for relative mouse mode
|
||||||
|
// All properties and methods must be accessed on the main thread since they
|
||||||
|
// interact with NSEvent monitors, CoreGraphics APIs, and Flutter channels.
|
||||||
|
// Note: We avoid @MainActor to maintain macOS 10.14 compatibility.
|
||||||
|
class RelativeMouseState {
|
||||||
|
static let shared = RelativeMouseState()
|
||||||
|
|
||||||
|
var enabled = false
|
||||||
|
var eventMonitor: Any?
|
||||||
|
var deltaChannel: FlutterMethodChannel?
|
||||||
|
var accumulatedDeltaX: CGFloat = 0
|
||||||
|
var accumulatedDeltaY: CGFloat = 0
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
}
|
||||||
|
|
||||||
class MainFlutterWindow: NSWindow {
|
class MainFlutterWindow: NSWindow {
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
rustdesk_core_main();
|
rustdesk_core_main();
|
||||||
@@ -64,6 +80,104 @@ class MainFlutterWindow: NSWindow {
|
|||||||
window.appearance = NSAppearance(named: themeName == "light" ? .aqua : .darkAqua)
|
window.appearance = NSAppearance(named: themeName == "light" ? .aqua : .darkAqua)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func enableNativeRelativeMouseMode(channel: FlutterMethodChannel) -> Bool {
|
||||||
|
assert(Thread.isMainThread, "enableNativeRelativeMouseMode must be called on the main thread")
|
||||||
|
let state = RelativeMouseState.shared
|
||||||
|
if state.enabled {
|
||||||
|
// Already enabled: update the channel so this caller receives deltas.
|
||||||
|
state.deltaChannel = channel
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dissociate mouse from cursor position - this locks the cursor in place
|
||||||
|
// Do this FIRST before setting any state
|
||||||
|
let result = CGAssociateMouseAndMouseCursorPosition(0)
|
||||||
|
if result != CGError.success {
|
||||||
|
NSLog("[RustDesk] Failed to dissociate mouse from cursor position: %d", result.rawValue)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only set state after CG call succeeds
|
||||||
|
state.deltaChannel = channel
|
||||||
|
state.accumulatedDeltaX = 0
|
||||||
|
state.accumulatedDeltaY = 0
|
||||||
|
|
||||||
|
// Add local event monitor to capture mouse delta.
|
||||||
|
// Note: Local event monitors are always called on the main thread,
|
||||||
|
// so accessing main-thread-only state is safe here.
|
||||||
|
state.eventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved, .leftMouseDragged, .rightMouseDragged, .otherMouseDragged]) { [weak state] event in
|
||||||
|
guard let state = state else { return event }
|
||||||
|
// Guard against race: mode may be disabled between weak capture and this check.
|
||||||
|
guard state.enabled else { return event }
|
||||||
|
let deltaX = event.deltaX
|
||||||
|
let deltaY = event.deltaY
|
||||||
|
|
||||||
|
if deltaX != 0 || deltaY != 0 {
|
||||||
|
// Accumulate delta (main thread only - NSEvent local monitors always run on main thread)
|
||||||
|
state.accumulatedDeltaX += deltaX
|
||||||
|
state.accumulatedDeltaY += deltaY
|
||||||
|
|
||||||
|
// Only send if we have integer movement
|
||||||
|
let intX = Int(state.accumulatedDeltaX)
|
||||||
|
let intY = Int(state.accumulatedDeltaY)
|
||||||
|
|
||||||
|
if intX != 0 || intY != 0 {
|
||||||
|
state.accumulatedDeltaX -= CGFloat(intX)
|
||||||
|
state.accumulatedDeltaY -= CGFloat(intY)
|
||||||
|
|
||||||
|
// Send delta to Flutter (already on main thread)
|
||||||
|
state.deltaChannel?.invokeMethod("onMouseDelta", arguments: ["dx": intX, "dy": intY])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if monitor was created successfully
|
||||||
|
if state.eventMonitor == nil {
|
||||||
|
NSLog("[RustDesk] Failed to create event monitor for relative mouse mode")
|
||||||
|
// Re-associate mouse since we failed
|
||||||
|
CGAssociateMouseAndMouseCursorPosition(1)
|
||||||
|
state.deltaChannel = nil
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set enabled LAST after everything succeeds
|
||||||
|
state.enabled = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func disableNativeRelativeMouseMode() {
|
||||||
|
assert(Thread.isMainThread, "disableNativeRelativeMouseMode must be called on the main thread")
|
||||||
|
let state = RelativeMouseState.shared
|
||||||
|
if !state.enabled { return }
|
||||||
|
|
||||||
|
state.enabled = false
|
||||||
|
|
||||||
|
// Remove event monitor
|
||||||
|
if let monitor = state.eventMonitor {
|
||||||
|
NSEvent.removeMonitor(monitor)
|
||||||
|
state.eventMonitor = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
state.deltaChannel = nil
|
||||||
|
state.accumulatedDeltaX = 0
|
||||||
|
state.accumulatedDeltaY = 0
|
||||||
|
|
||||||
|
// Re-associate mouse with cursor position (non-blocking with async retry)
|
||||||
|
let result = CGAssociateMouseAndMouseCursorPosition(1)
|
||||||
|
if result != CGError.success {
|
||||||
|
NSLog("[RustDesk] Failed to re-associate mouse with cursor position: %d, scheduling retry...", result.rawValue)
|
||||||
|
// Non-blocking retry after 50ms
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||||
|
let retryResult = CGAssociateMouseAndMouseCursorPosition(1)
|
||||||
|
if retryResult != CGError.success {
|
||||||
|
NSLog("[RustDesk] Retry failed to re-associate mouse: %d. Cursor may remain locked.", retryResult.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func setMethodHandler(registrar: FlutterPluginRegistrar) {
|
public func setMethodHandler(registrar: FlutterPluginRegistrar) {
|
||||||
let channel = FlutterMethodChannel(name: "org.rustdesk.rustdesk/host", binaryMessenger: registrar.messenger)
|
let channel = FlutterMethodChannel(name: "org.rustdesk.rustdesk/host", binaryMessenger: registrar.messenger)
|
||||||
channel.setMethodCallHandler({
|
channel.setMethodCallHandler({
|
||||||
@@ -96,7 +210,9 @@ class MainFlutterWindow: NSWindow {
|
|||||||
}
|
}
|
||||||
case "requestRecordAudio":
|
case "requestRecordAudio":
|
||||||
AVCaptureDevice.requestAccess(for: .audio, completionHandler: { granted in
|
AVCaptureDevice.requestAccess(for: .audio, completionHandler: { granted in
|
||||||
|
DispatchQueue.main.async {
|
||||||
result(granted)
|
result(granted)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case "bumpMouse":
|
case "bumpMouse":
|
||||||
@@ -145,11 +261,22 @@ class MainFlutterWindow: NSWindow {
|
|||||||
// This function's main action is to toggle whether the mouse cursor is
|
// This function's main action is to toggle whether the mouse cursor is
|
||||||
// associated with the mouse position, but setting it to true when it's
|
// associated with the mouse position, but setting it to true when it's
|
||||||
// already true has the side-effect of cancelling this motion suppression.
|
// already true has the side-effect of cancelling this motion suppression.
|
||||||
|
//
|
||||||
|
// However, we must NOT call this when relative mouse mode is active,
|
||||||
|
// as it would break the pointer lock established by enableNativeRelativeMouseMode.
|
||||||
|
if !RelativeMouseState.shared.enabled {
|
||||||
CGAssociateMouseAndMouseCursorPosition(1 /* true */)
|
CGAssociateMouseAndMouseCursorPosition(1 /* true */)
|
||||||
|
}
|
||||||
|
|
||||||
result(true)
|
result(true)
|
||||||
|
|
||||||
break
|
case "enableNativeRelativeMouseMode":
|
||||||
|
let success = self.enableNativeRelativeMouseMode(channel: channel)
|
||||||
|
result(success)
|
||||||
|
|
||||||
|
case "disableNativeRelativeMouseMode":
|
||||||
|
self.disableNativeRelativeMouseMode()
|
||||||
|
result(true)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
result(FlutterMethodNotImplemented)
|
result(FlutterMethodNotImplemented)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||||
version: 1.4.4+62
|
version: 1.4.5+63
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '^3.1.0'
|
sdk: '^3.1.0'
|
||||||
|
|||||||
@@ -208,42 +208,56 @@ impl MouseControllable for Enigo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn mouse_move_to(&mut self, x: i32, y: i32) {
|
fn mouse_move_to(&mut self, x: i32, y: i32) {
|
||||||
let pressed = Self::pressed_buttons();
|
// For absolute movement, we don't set delta values
|
||||||
|
// This maintains backward compatibility
|
||||||
let event_type = if pressed & 1 > 0 {
|
self.mouse_move_to_impl(x, y, None);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mouse_move_relative(&mut self, x: i32, y: i32) {
|
fn mouse_move_relative(&mut self, x: i32, y: i32) {
|
||||||
let (display_width, display_height) = Self::main_display_size();
|
let (display_width, display_height) = Self::main_display_size();
|
||||||
let (current_x, y_inv) = Self::mouse_location_raw_coords();
|
let (current_x, y_inv) = Self::mouse_location_raw_coords();
|
||||||
let current_y = (display_height as i32) - y_inv;
|
let current_y = (display_height as i32) - y_inv;
|
||||||
let new_x = current_x + x;
|
// Use saturating arithmetic to prevent overflow/wraparound
|
||||||
let new_y = current_y + y;
|
let mut new_x = current_x.saturating_add(x);
|
||||||
|
let mut new_y = current_y.saturating_add(y);
|
||||||
|
|
||||||
if new_x < 0
|
// Define screen center and edge margins for cursor reset
|
||||||
|| new_x as usize > display_width
|
let center_x = (display_width / 2) as i32;
|
||||||
|| new_y < 0
|
let center_y = (display_height / 2) as i32;
|
||||||
|| new_y as usize > display_height
|
// 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
|
||||||
return;
|
// 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 {
|
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
|
/// Fetches the `(width, height)` in pixels of the main display
|
||||||
pub fn main_display_size() -> (usize, usize) {
|
pub fn main_display_size() -> (usize, usize) {
|
||||||
let display_id = unsafe { CGMainDisplayID() };
|
let display_id = unsafe { CGMainDisplayID() };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.4.4"
|
version = "1.4.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "RustDesk Remote Desktop"
|
description = "RustDesk Remote Desktop"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pkgname=rustdesk
|
pkgname=rustdesk
|
||||||
pkgver=1.4.4
|
pkgver=1.4.5
|
||||||
pkgrel=0
|
pkgrel=0
|
||||||
epoch=
|
epoch=
|
||||||
pkgdesc=""
|
pkgdesc=""
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.4.4
|
Version: 1.4.5
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.4.4
|
Version: 1.4.5
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: rustdesk
|
Name: rustdesk
|
||||||
Version: 1.4.4
|
Version: 1.4.5
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: RPM package
|
Summary: RPM package
|
||||||
License: GPL-3.0
|
License: GPL-3.0
|
||||||
|
|||||||
@@ -71,6 +71,19 @@ pub mod input {
|
|||||||
pub const MOUSE_TYPE_UP: i32 = 2;
|
pub const MOUSE_TYPE_UP: i32 = 2;
|
||||||
pub const MOUSE_TYPE_WHEEL: i32 = 3;
|
pub const MOUSE_TYPE_WHEEL: i32 = 3;
|
||||||
pub const MOUSE_TYPE_TRACKPAD: i32 = 4;
|
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_LEFT: i32 = 0x01;
|
||||||
pub const MOUSE_BUTTON_RIGHT: i32 = 0x02;
|
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")
|
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
|
// is server process, with "--server" args
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_server() -> bool {
|
pub fn is_server() -> bool {
|
||||||
@@ -2462,4 +2489,36 @@ mod tests {
|
|||||||
assert!(!is_public("https://rustdesk.computer.com"));
|
assert!(!is_public("https://rustdesk.computer.com"));
|
||||||
assert!(!is_public("rustdesk.comhello.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
pub fn main_get_my_id() -> String {
|
||||||
get_id()
|
get_id()
|
||||||
}
|
}
|
||||||
@@ -1748,8 +1808,99 @@ pub fn session_send_pointer(session_id: SessionID, msg: String) {
|
|||||||
super::flutter::session_send_pointer(session_id, msg);
|
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) {
|
pub fn session_send_mouse(session_id: SessionID, msg: String) {
|
||||||
if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(&msg) {
|
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 alt = m.get("alt").is_some();
|
||||||
let ctrl = m.get("ctrl").is_some();
|
let ctrl = m.get("ctrl").is_some();
|
||||||
let shift = m.get("shift").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,
|
"up" => MOUSE_TYPE_UP,
|
||||||
"wheel" => MOUSE_TYPE_WHEEL,
|
"wheel" => MOUSE_TYPE_WHEEL,
|
||||||
"trackpad" => MOUSE_TYPE_TRACKPAD,
|
"trackpad" => MOUSE_TYPE_TRACKPAD,
|
||||||
|
"move_relative" => MOUSE_TYPE_MOVE_RELATIVE,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
171
src/keyboard.rs
171
src/keyboard.rs
@@ -32,9 +32,33 @@ const OS_LOWER_MACOS: &str = "macos";
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
const OS_LOWER_ANDROID: &str = "android";
|
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);
|
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(feature = "flutter")]
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
static IS_RDEV_ENABLED: AtomicBool = AtomicBool::new(false);
|
static IS_RDEV_ENABLED: AtomicBool = AtomicBool::new(false);
|
||||||
@@ -82,7 +106,7 @@ pub mod client {
|
|||||||
GrabState::Run => {
|
GrabState::Run => {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
update_grab_get_key_name(keyboard_mode);
|
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);
|
KEYBOARD_HOOKED.swap(true, Ordering::SeqCst);
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@@ -94,7 +118,7 @@ pub mod client {
|
|||||||
|
|
||||||
release_remote_keys(keyboard_mode);
|
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);
|
KEYBOARD_HOOKED.swap(false, Ordering::SeqCst);
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@@ -266,6 +290,136 @@ fn get_keyboard_mode() -> String {
|
|||||||
"legacy".to_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() {
|
fn start_grab_loop() {
|
||||||
std::env::set_var("KEYBOARD_ONLY", "y");
|
std::env::set_var("KEYBOARD_ONLY", "y");
|
||||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||||
@@ -278,6 +432,12 @@ fn start_grab_loop() {
|
|||||||
|
|
||||||
let _scan_code = event.position_code;
|
let _scan_code = event.position_code;
|
||||||
let _code = event.platform_code as KeyCode;
|
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) {
|
let res = if KEYBOARD_HOOKED.load(Ordering::SeqCst) {
|
||||||
client::process_event(&get_keyboard_mode(), &event, None);
|
client::process_event(&get_keyboard_mode(), &event, None);
|
||||||
if is_press {
|
if is_press {
|
||||||
@@ -337,9 +497,14 @@ fn start_grab_loop() {
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
if let Err(err) = rdev::start_grab_listen(move |event: Event| match event.event_type {
|
if let Err(err) = rdev::start_grab_listen(move |event: Event| match event.event_type {
|
||||||
EventType::KeyPress(key) | EventType::KeyRelease(key) => {
|
EventType::KeyPress(key) | EventType::KeyRelease(key) => {
|
||||||
|
let is_press = matches!(event.event_type, EventType::KeyPress(_));
|
||||||
if let Key::Unknown(keycode) = key {
|
if let Key::Unknown(keycode) = key {
|
||||||
log::error!("rdev get unknown key, keycode is {:?}", keycode);
|
log::error!("rdev get unknown key, keycode is {:?}", keycode);
|
||||||
} else {
|
} else {
|
||||||
|
#[cfg(feature = "flutter")]
|
||||||
|
if should_block_relative_mouse_shortcut(key, is_press) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
client::process_event(&get_keyboard_mode(), &event, None);
|
client::process_event(&get_keyboard_mode(), &event, None);
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", "أدخل الملاحظة هنا"),
|
("input note here", "أدخل الملاحظة هنا"),
|
||||||
("note-at-conn-end-tip", "سيتم عرض هذه الملاحظة عند نهاية الاتصال"),
|
("note-at-conn-end-tip", "سيتم عرض هذه الملاحظة عند نهاية الاتصال"),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", "输入备注"),
|
("input note here", "输入备注"),
|
||||||
("note-at-conn-end-tip", "在连接结束时请求备注"),
|
("note-at-conn-end-tip", "在连接结束时请求备注"),
|
||||||
("Show terminal extra keys", "显示终端扩展键"),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", "Hier eine Notiz eingeben"),
|
("input note here", "Hier eine Notiz eingeben"),
|
||||||
("note-at-conn-end-tip", "Am Ende der Verbindung um eine Notiz bitten."),
|
("note-at-conn-end-tip", "Am Ende der Verbindung um eine Notiz bitten."),
|
||||||
("Show terminal extra keys", "Zusätzliche Tasten des Terminals anzeigen"),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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."),
|
("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."),
|
("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"),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", "یادداشت را اینجا وارد کنید"),
|
("input note here", "یادداشت را اینجا وارد کنید"),
|
||||||
("note-at-conn-end-tip", "در پایان اتصال، یادداشت بخواهید"),
|
("note-at-conn-end-tip", "در پایان اتصال، یادداشت بخواهید"),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", "saisir la note ici"),
|
("input note here", "saisir la note ici"),
|
||||||
("note-at-conn-end-tip", "Proposer de rédiger une note une fois la connexion terminée"),
|
("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"),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", "Megjegyzés bevitele"),
|
("input note here", "Megjegyzés bevitele"),
|
||||||
("note-at-conn-end-tip", "Megjegyzés a kapcsolat végén"),
|
("note-at-conn-end-tip", "Megjegyzés a kapcsolat végén"),
|
||||||
("Show terminal extra keys", "További terminálgombok megjelenítése"),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", "Inserisci nota qui"),
|
("input note here", "Inserisci nota qui"),
|
||||||
("note-at-conn-end-tip", "Visualizza nota alla fine della connessione"),
|
("note-at-conn-end-tip", "Visualizza nota alla fine della connessione"),
|
||||||
("Show terminal extra keys", "Visualizza tasti aggiuntivi terminale"),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", "ここにメモを入力"),
|
("input note here", "ここにメモを入力"),
|
||||||
("note-at-conn-end-tip", "接続終了時にメモを要求する"),
|
("note-at-conn-end-tip", "接続終了時にメモを要求する"),
|
||||||
("Show terminal extra keys", "ターミナルの追加キーを表示する"),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", "여기에 노트 입력"),
|
("input note here", "여기에 노트 입력"),
|
||||||
("note-at-conn-end-tip", "연결이 끝날 때 메모 요청"),
|
("note-at-conn-end-tip", "연결이 끝날 때 메모 요청"),
|
||||||
("Show terminal extra keys", "터미널 추가 키 표시"),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", "voeg hier een opmerking toe"),
|
("input note here", "voeg hier een opmerking toe"),
|
||||||
("note-at-conn-end-tip", "Vraag om een opmerking aan het einde van de verbinding"),
|
("note-at-conn-end-tip", "Vraag om een opmerking aan het einde van de verbinding"),
|
||||||
("Show terminal extra keys", "Toon extra toetsen voor terminal"),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", "Wstaw tutaj notatkę"),
|
("input note here", "Wstaw tutaj notatkę"),
|
||||||
("note-at-conn-end-tip", "Poproś o notatkę po zakończeniu połączenia."),
|
("note-at-conn-end-tip", "Poproś o notatkę po zakończeniu połączenia."),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", "введите заметку"),
|
("input note here", "введите заметку"),
|
||||||
("note-at-conn-end-tip", "Запрашивать заметку в конце соединения"),
|
("note-at-conn-end-tip", "Запрашивать заметку в конце соединения"),
|
||||||
("Show terminal extra keys", "Показывать дополнительные кнопки терминала"),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", "Notu buraya girin"),
|
("input note here", "Notu buraya girin"),
|
||||||
("note-at-conn-end-tip", "Bağlantı bittiğinde not sorulsun"),
|
("note-at-conn-end-tip", "Bağlantı bittiğinde not sorulsun"),
|
||||||
("Show terminal extra keys", "Terminal ek tuşlarını göster"),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", "輸入備註"),
|
("input note here", "輸入備註"),
|
||||||
("note-at-conn-end-tip", "在連接結束時請求備註"),
|
("note-at-conn-end-tip", "在連接結束時請求備註"),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||||||
("input note here", ""),
|
("input note here", ""),
|
||||||
("note-at-conn-end-tip", ""),
|
("note-at-conn-end-tip", ""),
|
||||||
("Show terminal extra keys", ""),
|
("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();
|
].iter().cloned().collect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ mod keyboard;
|
|||||||
pub mod platform;
|
pub mod platform;
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub use platform::{
|
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")))]
|
#[cfg(not(any(target_os = "ios")))]
|
||||||
/// cbindgen:ignore
|
/// cbindgen:ignore
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ extern "C" {
|
|||||||
y: *mut c_int,
|
y: *mut c_int,
|
||||||
screen_num: *mut c_int,
|
screen_num: *mut c_int,
|
||||||
) -> 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_new(display: *const c_char) -> Xdo;
|
||||||
fn xdo_get_active_window(xdo: Xdo, window: *mut *mut c_void) -> c_int;
|
fn xdo_get_active_window(xdo: Xdo, window: *mut *mut c_void) -> c_int;
|
||||||
fn xdo_get_window_location(
|
fn xdo_get_window_location(
|
||||||
@@ -174,6 +175,56 @@ pub fn get_cursor_pos() -> Option<(i32, i32)> {
|
|||||||
res
|
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 reset_input_cache() {}
|
||||||
|
|
||||||
pub fn get_focused_display(displays: Vec<DisplayInfo>) -> Option<usize> {
|
pub fn get_focused_display(displays: Vec<DisplayInfo>) -> Option<usize> {
|
||||||
|
|||||||
@@ -32,8 +32,12 @@ use std::{
|
|||||||
os::unix::process::CommandExt,
|
os::unix::process::CommandExt,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::{Command, Stdio},
|
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 =
|
static PRIVILEGES_SCRIPTS_DIR: Dir =
|
||||||
include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts");
|
include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts");
|
||||||
static mut LATEST_SEED: i32 = 0;
|
static mut LATEST_SEED: i32 = 0;
|
||||||
@@ -42,6 +46,11 @@ static mut LATEST_SEED: i32 = 0;
|
|||||||
// using one that includes the custom client name.
|
// using one that includes the custom client name.
|
||||||
const UPDATE_TEMP_DIR: &str = "/tmp/.rustdeskupdate";
|
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" {
|
extern "C" {
|
||||||
fn CGSCurrentCursorSeed() -> i32;
|
fn CGSCurrentCursorSeed() -> i32;
|
||||||
fn CGEventCreate(r: *const c_void) -> *const c_void;
|
fn CGEventCreate(r: *const c_void) -> *const c_void;
|
||||||
@@ -64,6 +73,8 @@ extern "C" {
|
|||||||
fn majorVersion() -> u32;
|
fn majorVersion() -> u32;
|
||||||
fn MacGetMode(display: u32, width: *mut u32, height: *mut u32) -> BOOL;
|
fn MacGetMode(display: u32, width: *mut u32, height: *mut u32) -> BOOL;
|
||||||
fn MacSetMode(display: u32, width: u32, height: u32, tryHiDPI: bool) -> 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 {
|
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> {
|
pub fn get_focused_display(displays: Vec<DisplayInfo>) -> Option<usize> {
|
||||||
autoreleasepool(|| unsafe_get_focused_display(displays))
|
autoreleasepool(|| unsafe_get_focused_display(displays))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,18 +26,13 @@ pub mod linux_desktop_manager;
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub mod gtk_sudo;
|
pub mod gtk_sudo;
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
||||||
use hbb_common::{
|
|
||||||
message_proto::CursorData,
|
|
||||||
sysinfo::Pid,
|
|
||||||
ResultType,
|
|
||||||
};
|
|
||||||
#[cfg(all(
|
#[cfg(all(
|
||||||
not(all(target_os = "windows", not(target_pointer_width = "64"))),
|
not(all(target_os = "windows", not(target_pointer_width = "64"))),
|
||||||
not(any(target_os = "android", target_os = "ios"))))]
|
not(any(target_os = "android", target_os = "ios"))
|
||||||
use hbb_common::{
|
))]
|
||||||
sysinfo::System,
|
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};
|
use std::sync::{Arc, Mutex};
|
||||||
#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))]
|
||||||
pub const SERVICE_INTERVAL: u64 = 300;
|
pub const SERVICE_INTERVAL: u64 = 300;
|
||||||
|
|||||||
@@ -116,12 +116,51 @@ pub fn get_focused_display(displays: Vec<DisplayInfo>) -> Option<usize> {
|
|||||||
|
|
||||||
pub fn get_cursor_pos() -> Option<(i32, i32)> {
|
pub fn get_cursor_pos() -> Option<(i32, i32)> {
|
||||||
unsafe {
|
unsafe {
|
||||||
#[allow(invalid_value)]
|
let mut out = mem::MaybeUninit::<POINT>::uninit();
|
||||||
let mut out = mem::MaybeUninit::uninit().assume_init();
|
if GetCursorPos(out.as_mut_ptr()) == FALSE {
|
||||||
if GetCursorPos(&mut out) == FALSE {
|
|
||||||
return None;
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5173,9 +5173,13 @@ impl Retina {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn on_mouse_event(&mut self, e: &mut MouseEvent, current: usize) {
|
fn on_mouse_event(&mut self, e: &mut MouseEvent, current: usize) {
|
||||||
let evt_type = e.mask & 0x7;
|
let evt_type = e.mask & crate::input::MOUSE_TYPE_MASK;
|
||||||
if evt_type == crate::input::MOUSE_TYPE_WHEEL {
|
// Delta-based events do not contain absolute coordinates.
|
||||||
// x and y are always 0, +1 or -1
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
let Some(d) = self.displays.get(current) else {
|
let Some(d) = self.displays.get(current) else {
|
||||||
@@ -5421,6 +5425,9 @@ mod raii {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.on_connection_close(self.0);
|
.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);
|
AUTHED_CONNS.lock().unwrap().retain(|c| c.conn_id != self.0);
|
||||||
let remote_count = AUTHED_CONNS
|
let remote_count = AUTHED_CONNS
|
||||||
.lock()
|
.lock()
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ use std::{
|
|||||||
thread,
|
thread,
|
||||||
time::{self, Duration, Instant},
|
time::{self, Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use winapi::um::winuser::WHEEL_DELTA;
|
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 KEYS_DOWN: Arc<Mutex<HashMap<KeysDown, Instant>>> = Default::default();
|
||||||
static ref LATEST_PEER_INPUT_CURSOR: Arc<Mutex<Input>> = 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))));
|
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);
|
static EXITING: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
const MOUSE_MOVE_PROTECTION_TIMEOUT: Duration = Duration::from_millis(1_000);
|
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 {
|
pub fn is_left_up(evt: &MouseEvent) -> bool {
|
||||||
let buttons = evt.mask >> 3;
|
let buttons = evt.mask >> 3;
|
||||||
let evt_type = evt.mask & 0x7;
|
let evt_type = evt.mask & MOUSE_TYPE_MASK;
|
||||||
return buttons == 1 && evt_type == 2;
|
buttons == MOUSE_BUTTON_LEFT && evt_type == MOUSE_TYPE_UP
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@@ -1003,10 +1033,18 @@ pub fn handle_mouse_(
|
|||||||
handle_mouse_simulation_(evt, conn);
|
handle_mouse_simulation_(evt, conn);
|
||||||
}
|
}
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
if _show_cursor {
|
{
|
||||||
|
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);
|
handle_mouse_show_cursor_(evt, conn, _username, _argb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) {
|
pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) {
|
||||||
if !active_mouse_(conn) {
|
if !active_mouse_(conn) {
|
||||||
@@ -1020,7 +1058,7 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) {
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
crate::platform::windows::try_change_desktop();
|
crate::platform::windows::try_change_desktop();
|
||||||
let buttons = evt.mask >> 3;
|
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();
|
let mut en = ENIGO.lock().unwrap();
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
en.set_ignore_flags(enigo_ignore_flags());
|
en.set_ignore_flags(enigo_ignore_flags());
|
||||||
@@ -1048,6 +1086,8 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) {
|
|||||||
}
|
}
|
||||||
match evt_type {
|
match evt_type {
|
||||||
MOUSE_TYPE_MOVE => {
|
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);
|
en.mouse_move_to(evt.x, evt.y);
|
||||||
*LATEST_PEER_INPUT_CURSOR.lock().unwrap() = Input {
|
*LATEST_PEER_INPUT_CURSOR.lock().unwrap() = Input {
|
||||||
conn,
|
conn,
|
||||||
@@ -1056,6 +1096,28 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) {
|
|||||||
y: evt.y,
|
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_TYPE_DOWN => match buttons {
|
||||||
MOUSE_BUTTON_LEFT => {
|
MOUSE_BUTTON_LEFT => {
|
||||||
allow_err!(en.mouse_down(MouseButton::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")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub fn handle_mouse_show_cursor_(evt: &MouseEvent, conn: i32, username: String, argb: u32) {
|
pub fn handle_mouse_show_cursor_(evt: &MouseEvent, conn: i32, username: String, argb: u32) {
|
||||||
let buttons = evt.mask >> 3;
|
let buttons = evt.mask >> 3;
|
||||||
let evt_type = evt.mask & 0x7;
|
let evt_type = evt.mask & MOUSE_TYPE_MASK;
|
||||||
match evt_type {
|
match evt_type {
|
||||||
MOUSE_TYPE_MOVE => {
|
MOUSE_TYPE_MOVE => {
|
||||||
whiteboard::update_whiteboard(
|
whiteboard::update_whiteboard(
|
||||||
@@ -1170,11 +1232,22 @@ pub fn handle_mouse_show_cursor_(evt: &MouseEvent, conn: i32, username: String,
|
|||||||
}
|
}
|
||||||
MOUSE_TYPE_UP => {
|
MOUSE_TYPE_UP => {
|
||||||
if buttons == MOUSE_BUTTON_LEFT {
|
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::update_whiteboard(
|
||||||
whiteboard::get_key_cursor(conn),
|
whiteboard::get_key_cursor(conn),
|
||||||
whiteboard::CustomEvent::Cursor(whiteboard::Cursor {
|
whiteboard::CustomEvent::Cursor(whiteboard::Cursor {
|
||||||
x: evt.x as _,
|
x: x as _,
|
||||||
y: evt.y as _,
|
y: y as _,
|
||||||
argb,
|
argb,
|
||||||
btns: buttons,
|
btns: buttons,
|
||||||
text: username,
|
text: username,
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
common::{get_supported_keyboard_modes, is_keyboard_mode_supported},
|
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,
|
ui_interface::use_texture_render,
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
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))
|
self.get_scroll_xy((x, y))
|
||||||
} else {
|
} else {
|
||||||
(x, y)
|
(x, y)
|
||||||
@@ -1231,8 +1236,6 @@ impl<T: InvokeUiSession> Session<T> {
|
|||||||
// #[cfg(not(any(target_os = "android", target_os = "ios")))]
|
// #[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
let (alt, ctrl, shift, command) =
|
let (alt, ctrl, shift, command) =
|
||||||
keyboard::client::get_modifiers_state(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_left = (mask & (MOUSE_BUTTON_LEFT << 3)) > 0;
|
||||||
let is_right = (mask & (MOUSE_BUTTON_RIGHT << 3)) > 0;
|
let is_right = (mask & (MOUSE_BUTTON_RIGHT << 3)) > 0;
|
||||||
if is_left ^ is_right {
|
if is_left ^ is_right {
|
||||||
@@ -1252,9 +1255,8 @@ impl<T: InvokeUiSession> Session<T> {
|
|||||||
// to-do: how about ctrl + left from win to macos
|
// to-do: how about ctrl + left from win to macos
|
||||||
if cfg!(target_os = "macos") {
|
if cfg!(target_os = "macos") {
|
||||||
let buttons = mask >> 3;
|
let buttons = mask >> 3;
|
||||||
let evt_type = mask & 0x7;
|
|
||||||
if buttons == MOUSE_BUTTON_LEFT
|
if buttons == MOUSE_BUTTON_LEFT
|
||||||
&& evt_type == MOUSE_TYPE_DOWN
|
&& event_type == MOUSE_TYPE_DOWN
|
||||||
&& ctrl
|
&& ctrl
|
||||||
&& self.peer_platform() != "Mac OS"
|
&& self.peer_platform() != "Mac OS"
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user