feat: mobile, virtual mouse (#12911)

* feat: mobile, virtual mouse

Signed-off-by: fufesou <linlong1266@gmail.com>

* feat: mobile, virtual mouse, mouse mode

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact: mobile, virtual mouse, mouse mode

Signed-off-by: fufesou <linlong1266@gmail.com>

* feat: mobile, virtual mouse mode

Signed-off-by: fufesou <linlong1266@gmail.com>

* feat: mobile virtual mouse, options

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
This commit is contained in:
fufesou
2025-10-08 20:23:55 -04:00
committed by GitHub
parent 02f455b0cc
commit 0f3a03aab7
56 changed files with 2714 additions and 49 deletions

View File

@@ -766,6 +766,11 @@ class InputModel {
command: command);
}
static Map<String, dynamic> getMouseEventMove() => {
'type': _kMouseEventMove,
'buttons': 0,
};
Map<String, dynamic> _getMouseEvent(PointerEvent evt, String type) {
final Map<String, dynamic> out = {};
@@ -1222,16 +1227,17 @@ class InputModel {
return false;
}
void handleMouse(
Map<String, dynamic>? processEventToPeer(
Map<String, dynamic> evt,
Offset offset, {
bool onExit = false,
bool moveCanvas = true,
}) {
if (isViewCamera) return;
if (isViewCamera) return null;
double x = offset.dx;
double y = max(0.0, offset.dy);
if (_checkPeerControlProtected(x, y)) {
return;
return null;
}
var type = kMouseEventTypeDefault;
@@ -1248,7 +1254,7 @@ class InputModel {
isMove = true;
break;
default:
return;
return null;
}
evt['type'] = type;
@@ -1266,9 +1272,10 @@ class InputModel {
type,
onExit: onExit,
buttons: evt['buttons'],
moveCanvas: moveCanvas,
);
if (pos == null) {
return;
return null;
}
if (type != '') {
evt['x'] = '0';
@@ -1286,7 +1293,22 @@ class InputModel {
kForwardMouseButton: 'forward'
};
evt['buttons'] = mapButtons[evt['buttons']] ?? '';
bind.sessionSendMouse(sessionId: sessionId, msg: json.encode(modify(evt)));
return evt;
}
Map<String, dynamic>? handleMouse(
Map<String, dynamic> evt,
Offset offset, {
bool onExit = false,
bool moveCanvas = true,
}) {
final evtToPeer =
processEventToPeer(evt, offset, onExit: onExit, moveCanvas: moveCanvas);
if (evtToPeer != null) {
bind.sessionSendMouse(
sessionId: sessionId, msg: json.encode(modify(evtToPeer)));
}
return evtToPeer;
}
Point? handlePointerDevicePos(
@@ -1297,6 +1319,7 @@ class InputModel {
String evtType, {
bool onExit = false,
int buttons = kPrimaryMouseButton,
bool moveCanvas = true,
}) {
final ffiModel = parent.target!.ffiModel;
CanvasCoords canvas =
@@ -1325,7 +1348,7 @@ class InputModel {
y -= CanvasModel.topToEdge;
x -= CanvasModel.leftToEdge;
if (isMove) {
if (isMove && moveCanvas) {
parent.target!.canvasModel.moveDesktopMouse(x, y);
}

View File

@@ -114,6 +114,7 @@ class FfiModel with ChangeNotifier {
bool? _secure;
bool? _direct;
bool _touchMode = false;
late VirtualMouseMode virtualMouseMode;
Timer? _timer;
var _reconnects = 1;
bool _viewOnly = false;
@@ -166,6 +167,7 @@ class FfiModel with ChangeNotifier {
clear();
sessionId = parent.target!.sessionId;
cachedPeerData.permissions = _permissions;
virtualMouseMode = VirtualMouseMode(this);
}
Rect? globalDisplaysRect() => _getDisplaysRect(_pi.displays, true);
@@ -1109,6 +1111,9 @@ class FfiModel with ChangeNotifier {
sessionId: sessionId, arg: kOptionTouchMode) !=
'';
}
if (isMobile) {
virtualMouseMode.loadOptions();
}
if (connType == ConnType.fileTransfer) {
parent.target?.fileModel.onReady();
} else if (connType == ConnType.terminal) {
@@ -1508,6 +1513,72 @@ class FfiModel with ChangeNotifier {
}
}
class VirtualMouseMode with ChangeNotifier {
bool _showVirtualMouse = false;
double _virtualMouseScale = 1.0;
bool _showVirtualJoystick = false;
bool get showVirtualMouse => _showVirtualMouse;
double get virtualMouseScale => _virtualMouseScale;
bool get showVirtualJoystick => _showVirtualJoystick;
FfiModel ffiModel;
VirtualMouseMode(this.ffiModel);
bool _shouldShow() => !ffiModel.isPeerAndroid;
setShowVirtualMouse(bool b) {
if (b == _showVirtualMouse) return;
if (_shouldShow()) {
_showVirtualMouse = b;
notifyListeners();
}
}
setVirtualMouseScale(double s) {
if (s <= 0) return;
if (s == _virtualMouseScale) return;
_virtualMouseScale = s;
bind.mainSetLocalOption(key: kOptionVirtualMouseScale, value: s.toString());
notifyListeners();
}
setShowVirtualJoystick(bool b) {
if (b == _showVirtualJoystick) return;
if (_shouldShow()) {
_showVirtualJoystick = b;
notifyListeners();
}
}
void loadOptions() {
_showVirtualMouse =
bind.mainGetLocalOption(key: kOptionShowVirtualMouse) == 'Y';
_virtualMouseScale = double.tryParse(
bind.mainGetLocalOption(key: kOptionVirtualMouseScale)) ??
1.0;
_showVirtualJoystick =
bind.mainGetLocalOption(key: kOptionShowVirtualJoystick) == 'Y';
notifyListeners();
}
Future<void> toggleVirtualMouse() async {
await bind.mainSetLocalOption(
key: kOptionShowVirtualMouse, value: showVirtualMouse ? 'N' : 'Y');
setShowVirtualMouse(
bind.mainGetLocalOption(key: kOptionShowVirtualMouse) == 'Y');
}
Future<void> toggleVirtualJoystick() async {
await bind.mainSetLocalOption(
key: kOptionShowVirtualJoystick,
value: showVirtualJoystick ? 'N' : 'Y');
setShowVirtualJoystick(
bind.mainGetLocalOption(key: kOptionShowVirtualJoystick) == 'Y');
}
}
class ImageModel with ChangeNotifier {
ui.Image? _image;
@@ -2289,9 +2360,25 @@ class CursorModel with ChangeNotifier {
Rect? get keyHelpToolsRectToAdjustCanvas =>
_lastKeyboardIsVisible ? _keyHelpToolsRect : null;
keyHelpToolsVisibilityChanged(Rect? r, bool keyboardIsVisible) {
_keyHelpToolsRect = r;
if (r == null) {
// The blocked rect is used to block the pointer/touch events in the remote page.
final List<Rect> _blockedRects = [];
// Used in shouldBlock().
// _blockEvents is a flag to block pointer/touch events on the remote image.
// It is set to true to prevent accidental touch events in the following scenarios:
// 1. In floating mouse mode, when the scroll circle is shown.
// 2. In floating mouse widgets mode, when the left/right buttons are moving.
// 3. In floating mouse widgets mode, when using the virtual joystick.
// When _blockEvents is true, all pointer/touch events are blocked regardless of the contents of _blockedRects.
// _blockedRects contains specific rectangular regions where events are blocked; these are checked when _blockEvents is false.
// In summary: _blockEvents acts as a global block, while _blockedRects provides fine-grained blocking.
bool _blockEvents = false;
List<Rect> get blockedRects => List.unmodifiable(_blockedRects);
set blockEvents(bool v) => _blockEvents = v;
keyHelpToolsVisibilityChanged(Rect? rect, bool keyboardIsVisible) {
_keyHelpToolsRect = rect;
if (rect == null) {
_lastIsBlocked = false;
} else {
// Block the touch event is safe here.
@@ -2306,6 +2393,14 @@ class CursorModel with ChangeNotifier {
_lastKeyboardIsVisible = keyboardIsVisible;
}
addBlockedRect(Rect rect) {
_blockedRects.add(rect);
}
removeBlockedRect(Rect rect) {
_blockedRects.remove(rect);
}
get lastIsBlocked => _lastIsBlocked;
ui.Image? get image => _image;
@@ -2372,13 +2467,22 @@ class CursorModel with ChangeNotifier {
// mobile Soft keyboard, block touch event from the KeyHelpTools
shouldBlock(double x, double y) {
if (_blockEvents) {
return true;
}
final offset = Offset(x, y);
for (final rect in _blockedRects) {
if (isPointInRect(offset, rect)) {
return true;
}
}
// For help tools rectangle, only block touch event when in touch mode.
if (!(parent.target?.ffiModel.touchMode ?? false)) {
return false;
}
if (_keyHelpToolsRect == null) {
return false;
}
if (isPointInRect(Offset(x, y), _keyHelpToolsRect!)) {
if (_keyHelpToolsRect != null &&
isPointInRect(offset, _keyHelpToolsRect!)) {
return true;
}
return false;
@@ -2398,6 +2502,10 @@ class CursorModel with ChangeNotifier {
return true;
}
Future<void> syncCursorPosition() async {
await parent.target?.inputModel.moveMouse(_x, _y);
}
bool isInRemoteRect(Offset offset) {
return getRemotePosInRect(offset) != null;
}