Edge scrolling (#13247)

* Repurposed the MacOS-specific platform channel mechanism for all platforms:
- Renamed the channel from "org.rustdesk.rustdesk/macos" to "org.rustdesk.rustdesk/host".
- Renamed _osxMethodChannel in platform_channel.dart to _hostMethodChannel.
- Updated linux/my_application.cc to use the fl_* API to set up a Method Channel and to dispose it during my_application_dispose.
- Updated windows/runner/flutter_window.cpp to use the C++ API to set up a Method Channel.
- Updated the channel name in macos/Runner/MainFlutterWindow.swift.

Signed-off-by: Jonathan Gilbert <logic@deltaq.org>

* Added a method "bumpMouse" to the Platform Channel.
Added a thunk to call the method through the channel to platform_channel.dart.
Added implementation bump_mouse() in linux/my_application.cc using Gdk API calls. Updated host_channel_call_handler to process "bumpMouse" method call messages by calling bump_mouse.
Added implementation Win32Desktop::BumpMouse in windows/runner/win32_desktop.cpp/.h.  Updated the inline method call handler in flutter_window.cpp to handle "bumpMouse" method calls by calling Win32Desktop::BumpMouse.
Updated the method call handler in macos/Runner/MainFlutterWindow.swift to handle "bumpMouse" method call messages. Updated MainFlutterWindow to use a subclass of FlutterViewController exposing access to mouseLocationOutsideOfEventStream.

Signed-off-by: Jonathan Gilbert <logic@deltaq.org>

* Added message type kWindowBumpMouse to the multiwindow window event model:
- Added constant kWindowBumpMouse to consts.dart.
- Updated the method handler attached to rustDeskWinManager by DesktopHomePageState to recognize kWindowBumpMouse and translate it to a call to RdPlatformChannel.bumpMouse.

Signed-off-by: Jonathan Gilbert <logic@deltaq.org>

* Centralized serialization of ScrollStyle values, moving JSON and string conversions into methods toString/fromString and toJson/fromJson within the type.

Signed-off-by: Jonathan Gilbert <logic@deltaq.org>

* Added new scroll style for edge scrolling:
- Added ScrollStyle enum member "scrolledge". Added corresponding constant kRemoteScrollStyleEdge to consts.dart for the string serialized form.
- Updated sites checking specifically for ScrollStyle.scrollbar to instead check for NOT ScrollStyle.scrollauto.
- Added radio buttons for the new "ScrollEdge" style to desktop_setting_page.dart and remote_toolbar.dart. Added new string "ScrollEdge" to lang/template.rs.

Signed-off-by: Jonathan Gilbert <logic@deltaq.org>

* Implemented edge scrolling:
- Added methods edgeScrollMouse and pushScrollPositionToUI to class CanvasModel in model.dart.
- Added boolean parameter edgeScroll to handleMouse, handlePointerDevicePos and processEventToPeer in input_model.dart.
- Updated handlePointerDevicePos in input_model.dart to call edgeScrollMouse on move events when the edgeScroll parameter is true.
- Added convenience accessor useEdgeScroll to the InputModel class. Updated call sites to handleMouse to use it to supply the value for the edgeScroll parameter.

Signed-off-by: Jonathan Gilbert <logic@deltaq.org>

* Updated CanvasModel.edgeScrollMouse to be resilient to receiving events when _horizontal/_vertical aren't wired up to any UI.

* Updated CanvasModel to take notifications of resizes via method notifyResize and to suppress edge scrolling briefly after a resize.
Updated the onWindowResized handler in tabbar_widget.dart to call notifyResize on the canvasModel of any RemotePage tabs.

* Half a go at fixing MainFlutterWindow.swift.

* Copilot feedback.

* Applied fix suggested by Copilot in its explanation of the build error.

* Fixed a couple of silly errors in windows/runner/flutter_window.cpp.

* Fixed MainFlutterWindow.swift build errors.

Co-Authored-By: fufesou <linlong1266@gmail.com>
Signed-off-by: Jonathan Gilbert <logic@deltaq.org>

* Moved new translation to the end of template.rs.
Reran res/lang.py.

Signed-off-by: Jonathan Gilbert <logic@deltaq.org>

* Switched MainFlutterWindow.swift to use NSEvent.mouseLocation.

* Updated MainFlutterWindow.swift code based on build error.

* Fixed silly typo.

* Reintroduced the coordinate system translation in MainFlutterWindow.swift.

* Updated edgeScrollMouse in model.dart to add a "safe zone" around the window frame that doesn't trigger edge scrolling.

* Updated the bumpMouse handler in MainFlutterWindow.swift to call CGAssociateMouseAndMouseCursorPosition to cancel event suppression.

* Added debug annotation to the onWindowResized event in tabbar_widget.dart.

* Fix parameter type for CGAssociateMouseAndMouseCursorPosition in MainFlutterWindow.swift.

* tabbar_widget.dart: onWindowResized -> onWindowResize

* Removed temporary diagnostic debugPrint from tabbar_widget.dart.

* Updated MainFlutterWindow.swift to obtain the mouse position by creating a dummy CGEvent. The old NSEvent.mouseLocation code is left as a fallback.

* The documentation said to be sure to call CFRelease, but apparently it's a build error to do so. :-P

* Replaced CGEvent calls in MainFlutterWindow.swift with uses of the CGEvent wrapper struct.

* Added argument label to call to CGEvent.init.

* Changed mouseLoc from piecewise assignment to assignment of the whole structure, as it is not yet initialized at that point.

* Linux platform channel: Refactored bump_mouse, setting the stage for a future Wayland implementation.
- Made a new top-level bump_mouse method in bump_mouse.cc/.h.
- Moved the X11-specific implementation to bump_mouse_x11 in bump_mouse_x11.cc/h.
Reworked the bumpMouse operation to have a boolean return value:
- Updated bumpMouse in platform_channel.dart to return a Future<bool> instead of a Future<void>.
- Windows platform channel: Updated BumpMouse in win32_desktop.cpp to return a bool value. Updated the method call handler "bumpMouse" branch in flutter_window.cpp to propagate the BumpMouse return value back to the originating MethodCall.
- MacOS platform channel: Updated the "bumpMouse" branch in the method call handler in MainFlutterWindow.swift to pass true or false into the 'result()' call.
- Linux platform channel: Updated the bump_mouse top-level method and its underlying implementation bump_mouse_x11 to return bool values. Updated the "bumpMouse" branch of host_channel_call_handler in my_application.cc to propagate the result value back up the method channel.
- Updated the kWindowBumpMouse branch of the method handler registered in desktop_home_page.dart to propagate a return value from RdplatformChannel.bumpMouse.

* Reworked the edge scrolling computations in model.dart to use Vector2 from the vector_math package. Updated pubspec.yaml to declare a dependency on vector_math.

* Added an alternative edge scrolling mechanism for when "Bump Mouse" functionality is unavailable:
- Added methods setEdgeScrollTimer and cancelEdgeScrollTimer to model.dart, along with a few state fields.
- Updated edgeScrollMouse to latch the (x, y) coordinate of the last edge scroll event, in case it will be autorepeating.
- Updated edgeScrollMouse to check whether the call to the kWindowBumpMouse method of rustDeskWinManager (and thus the underlying bump_mouse method) succeeded, and to switch to timer-based autorepeat if it fails. Made edgeScrollMouse async to allow awaiting the result of the kWindowBumpMouse method call.
- Updated input_model.dart to call cancelEdgeScrollTimer when a new move event is being processed.
- Updated remote_page.dart to call cancelEdgeScrollTimer when the pointer exits the area represented by the view.

* Fixed scroll percentage math in edgeScrollMouse in model.dart.

* Fixed declared return value for Win32Desktop::BumpMouse in win32_desktop.h.

* Fixed vector_math dependency version in pubspec.yaml to be compatible with the codebase standard Flutter version.

* Added class EdgeScrollFallbackState to model.dart for tracking the state of the edge scroll fallback strategy. Factored out the actual edge scrolling action from CanvasModel.edgeScrollMouse to new method performEdgeScroll so that EdgeScrollFallbackState can call it. Updated edgeScrollMouse to not call performEdgeScroll when it's enabling the fallback strategy.
Updated CanvasModel to use EdgeScrollFallbackState instead of directly tracking the state. Removed method setEdgeScrollTimer.
Added method initializeEdgeScrollFallback to CanvasModel that takes a TickerProvider. Updated _RemotePageState to include the mixin TickerProviderStateMixin. Updated _RemotePageState.initState to call canvasModel.initializeEdgeScrollFallback.
Updated handlePointerDevicePos in input_model.dart to not call cancelEdgeScrollTimer before edgeScrollMouse.
Renamed CanvasModel.cancelEdgeScrollTimer to CanvasModel.cancelEdgeScroll.
Updated the calculations in CanvasModel.edgeScrollMouse to only factor in the safe zone if BumpMouse is working. (Otherwise the problem with resizing can't possibly occur.)

* Updated CanvasModel.edgeScrollMouse in model.dart to handle the situation where only one of the scrollbars is active. Factored extraction of scrollbar data into new function getScrollInfo.

* Updated onWindowResize in tabbar_widget.dart to be resilient to RemotePage instances that don't yet have an ffi reference. Added property hasFFI to remote_page.dart.

* Removed debug output from model.dart.

* PR feedback:
- Added filtering to diagnostic output in the method handler in desktop_home_page.dart to exclude the very chatty kWindowBumpMouse-related output.
- Removed the diagnostic output from bumpMouse in platform_channel.dart for the same reason.
- Updated setScrollPercent to coalesce NaN values for x and y to 0.
- Initialized the GError pointer variable passed into fl_method_call_respond_success in linux/my_application.cc to NULL.
- Added bounds checking of the argument values in the EncodableList branch of the "bumpMouse" method call handler in windows/runner/flutter_window.cpp.

* Added a latch mechanism that keeps edge scrolling disabled until the cursor is observed to be in the inner area bounded by the edge scroll areas:
- Added tristate enumerated type EdgeScrollState to model.dart. In addition to inactive and active states, there is state armed which behaves like inactive but can transition to active when conditions are met.
- Added a field to CanvasModel of type EdgeScrollState. Added methods disableEdgeScroll and rearmEdgeScroll.
- Updated enterView to call canvasModel.rearmEdgeScroll and leaveView to call canvasModel.disableEdgeScroll in remote_page.dart.
- Updated edgeScrollMouse to check the state, disabling edge scrolling when the state is not active and transitioning from armed to active when the mouse is in the interior space.
- Removed the notifyResize/_suppressEdgeScroll mechanism from CanvasModel in model.dart as it is no longer necessary.
- Removed the "safe zone" mechanism from CanvasModel.edgeScrollMouse in model.dart as it is no longer necessary.
- Switched the onWindowResize handler in DesktopTabState in tabbar_widget.dart back to onWindowResized, now that it is no longer delivering canvasModel.notifyResize to all RemotePage tabs.

* Fixed memory leak: Added call to free GError object returned by Flutter API in the event of an error.

* PR feedback:
- Copilot: Use type annotations.
- Copilot: Condition to stop edge scrolling when fallback strategy is in use and the mouse is moved back to the centre.
- Copilot: Check FLValue type before calling fl_value_get_int.
- Copilot: Support list-style method channel dispatch in "bumpMouse" handler for macos as the linux and windows implementations already do.
- Naming convention for constants.
- Left-over variable from previous strategy: _suppressEdgeScroll.
- Unnecessary extra parentheses in edge scroll area conditions.

* Removed property suppressEdgeScroll referencing now-removed field _suppressEdgeScroll in model.dart.
Removed accidental extra blank line in MainFlutterWindow.swift.

* Switched CanvasModel.setScrollPercent to use double.isFinite instead of double.isNaN to test for proper numerical values.

* PR feedback:
- Copilot: Use Vector2.length2 instead of Vector2.length to avoid an unnecessary sqrt in comparison with zero.
- Copilot: Baleet unnecessary semicolons from Swift code.

* PR feedback:
- Copilot: Check argList.count before indexing it

* Oops with the semicolons again.

* Edge scroll, active local cursor

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

* Remove duplicated condition checks

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

* Chore

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

* PR feedback:
- Copilot: Removed unused property hasFFI from remote_page.dart.
- Copilot: Updated updateScrollStyle in model.dart to be resilient to the possibility of bind.sessionGetScrollStyle returning null.

* Factored local cursor updates out of CanvasModel.moveDesktopMouse in model.dart, adding new methods activateLocalCursor and updateLocalCursor.
Updated handlePointerDevicePos in input_model.dart to call canvasModel.updateLocalCursor on every mouse event.
Updated initState in remote_page.dart to schedule a call to canvasModel.activateLocalCursor as a first-image callback.

* Updated the explanation for rounding away from 0 in edgeScrollMouse in model.dart.

---------

Signed-off-by: Jonathan Gilbert <logic@deltaq.org>
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
Jonathan Gilbert 2025-10-30 06:54:11 -05:00 committed by GitHub
parent a30582c840
commit 055826e26f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
67 changed files with 669 additions and 52 deletions

View File

@ -2948,7 +2948,7 @@ Future<void> updateSystemWindowTheme() async {
///
/// Note: not found a general solution for rust based AVFoundation bingding.
/// [AVFoundation] crate has compile error.
const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/macos");
const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/host");
enum PermissionAuthorizeType {
undetermined,

View File

@ -58,6 +58,7 @@ const String kWindowActionRebuild = "rebuild";
const String kWindowEventHide = "hide";
const String kWindowEventShow = "show";
const String kWindowConnect = "connect";
const String kWindowBumpMouse = "bump_mouse";
const String kWindowEventNewRemoteDesktop = "new_remote_desktop";
const String kWindowEventNewFileTransfer = "new_file_transfer";
@ -326,6 +327,9 @@ const kRemoteScrollStyleAuto = 'scrollauto';
/// [kRemoteScrollStyleBar] Scroll image with scroll bar.
const kRemoteScrollStyleBar = 'scrollbar';
/// [kRemoteScrollStyleEdge] Scroll image auto at edges.
const kRemoteScrollStyleEdge = 'scrolledge';
/// [kScrollModeDefault] Mouse or touchpad, the default scroll mode.
const kScrollModeDefault = 'default';

View File

@ -18,6 +18,7 @@ import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/plugin/ui_manager.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
@ -760,9 +761,19 @@ class _DesktopHomePageState extends State<DesktopHomePage>
'scaleFactor': screen.scaleFactor,
};
bool isChattyMethod(String methodName) {
switch (methodName) {
case kWindowBumpMouse: return true;
}
return false;
}
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
debugPrint(
if (!isChattyMethod(call.method)) {
debugPrint(
"[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId");
}
if (call.method == kWindowMainWindowOnTop) {
windowOnTop(null);
} else if (call.method == kWindowGetWindowInfo) {
@ -793,6 +804,10 @@ class _DesktopHomePageState extends State<DesktopHomePage>
forceRelay: call.arguments['forceRelay'],
connToken: call.arguments['connToken'],
);
} else if (call.method == kWindowBumpMouse) {
return RdPlatformChannel.instance.bumpMouse(
dx: call.arguments['dx'],
dy: call.arguments['dy']);
} else if (call.method == kWindowEventMoveTabToNewWindow) {
final args = call.arguments.split(',');
int? windowId;

View File

@ -1691,6 +1691,11 @@ class _DisplayState extends State<_Display> {
groupValue: groupValue,
label: 'ScrollAuto',
onChanged: isOptFixed ? null : onChanged),
_Radio(context,
value: kRemoteScrollStyleEdge,
groupValue: groupValue,
label: 'ScrollEdge',
onChanged: isOptFixed ? null : onChanged),
_Radio(context,
value: kRemoteScrollStyleBar,
groupValue: groupValue,

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/scheduler.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
@ -72,7 +73,7 @@ class RemotePage extends StatefulWidget {
}
class _RemotePageState extends State<RemotePage>
with AutomaticKeepAliveClientMixin, MultiWindowListener {
with AutomaticKeepAliveClientMixin, MultiWindowListener, TickerProviderStateMixin {
Timer? _timer;
String keyboardMode = "legacy";
bool _isWindowBlur = false;
@ -112,11 +113,13 @@ class _RemotePageState extends State<RemotePage>
_ffi = FFI(widget.sessionId);
Get.put<FFI>(_ffi, tag: widget.id);
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
_ffi.canvasModel.activateLocalCursor();
showKBLayoutTypeChooserIfNeeded(
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
_ffi.recordingModel
.updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
});
_ffi.canvasModel.initializeEdgeScrollFallback(this);
_ffi.start(
widget.id,
password: widget.password,
@ -408,6 +411,8 @@ class _RemotePageState extends State<RemotePage>
}
void enterView(PointerEnterEvent evt) {
_ffi.canvasModel.rearmEdgeScroll();
_cursorOverImage.value = true;
_firstEnterImage.value = true;
if (_onEnterOrLeaveImage4Toolbar != null) {
@ -427,6 +432,8 @@ class _RemotePageState extends State<RemotePage>
}
void leaveView(PointerExitEvent evt) {
_ffi.canvasModel.disableEdgeScroll();
if (_ffi.ffiModel.keyboard) {
_ffi.inputModel.tryMoveEdgeOnExit(evt.position);
}
@ -625,7 +632,7 @@ class _ImagePaintState extends State<ImagePaint> {
onHover: (evt) {},
child: child);
});
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
if (c.imageOverflow.isTrue && c.scrollStyle != ScrollStyle.scrollauto) {
final paintWidth = c.getDisplayWidth() * s;
final paintHeight = c.getDisplayHeight() * s;
final paintSize = Size(paintWidth, paintHeight);

View File

@ -527,7 +527,7 @@ class _ImagePaintState extends State<ImagePaint> {
bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal;
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
if (c.imageOverflow.isTrue && c.scrollStyle != ScrollStyle.scrollauto) {
final paintWidth = c.getDisplayWidth() * s;
final paintHeight = c.getDisplayHeight() * s;
final paintSize = Size(paintWidth, paintHeight);

View File

@ -1088,6 +1088,15 @@ class _DisplayMenuState extends State<_DisplayMenu> {
: null,
ffi: widget.ffi,
),
RdoMenuButton<String>(
child: Text(translate('ScrollEdge')),
value: kRemoteScrollStyleEdge,
groupValue: groupValue,
onChanged: widget.ffi.canvasModel.imageOverflow.value
? (value) => onChange(value)
: null,
ffi: widget.ffi,
),
RdoMenuButton<String>(
child: Text(translate('Scrollbar')),
value: kRemoteScrollStyleBar,

View File

@ -42,8 +42,7 @@ class CanvasCoords {
'scale': scale,
'scrollX': scrollX,
'scrollY': scrollY,
'scrollStyle':
scrollStyle == ScrollStyle.scrollauto ? 'scrollauto' : 'scrollbar',
'scrollStyle': scrollStyle.toJson(),
'size': {
'w': size.width,
'h': size.height,
@ -58,9 +57,7 @@ class CanvasCoords {
model.scale = json['scale'];
model.scrollX = json['scrollX'];
model.scrollY = json['scrollY'];
model.scrollStyle = json['scrollStyle'] == 'scrollauto'
? ScrollStyle.scrollauto
: ScrollStyle.scrollbar;
model.scrollStyle = ScrollStyle.fromJson(json['scrollStyle'], ScrollStyle.scrollauto);
model.size = Size(json['size']['w'], json['size']['h']);
return model;
}
@ -375,6 +372,7 @@ class InputModel {
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
bool get isViewCamera => parent.target!.connType == ConnType.viewCamera;
int get trackpadSpeed => _trackpadSpeed;
bool get useEdgeScroll => parent.target!.canvasModel.scrollStyle == ScrollStyle.scrolledge;
InputModel(this.parent) {
sessionId = parent.target!.sessionId;
@ -888,7 +886,7 @@ class InputModel {
isPhysicalMouse.value = true;
}
if (isPhysicalMouse.value) {
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position, edgeScroll: useEdgeScroll);
}
}
@ -1076,7 +1074,7 @@ class InputModel {
_queryOtherWindowCoords = false;
}
if (isPhysicalMouse.value) {
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position, edgeScroll: useEdgeScroll);
}
}
@ -1125,7 +1123,7 @@ class InputModel {
void refreshMousePos() => handleMouse({
'buttons': 0,
'type': _kMouseEventMove,
}, lastMousePos);
}, lastMousePos, edgeScroll: useEdgeScroll);
void tryMoveEdgeOnExit(Offset pos) => handleMouse(
{
@ -1232,6 +1230,7 @@ class InputModel {
Offset offset, {
bool onExit = false,
bool moveCanvas = true,
bool edgeScroll = false,
}) {
if (isViewCamera) return null;
double x = offset.dx;
@ -1273,6 +1272,7 @@ class InputModel {
onExit: onExit,
buttons: evt['buttons'],
moveCanvas: moveCanvas,
edgeScroll: edgeScroll,
);
if (pos == null) {
return null;
@ -1301,9 +1301,10 @@ class InputModel {
Offset offset, {
bool onExit = false,
bool moveCanvas = true,
bool edgeScroll = false,
}) {
final evtToPeer =
processEventToPeer(evt, offset, onExit: onExit, moveCanvas: moveCanvas);
processEventToPeer(evt, offset, onExit: onExit, moveCanvas: moveCanvas, edgeScroll: edgeScroll);
if (evtToPeer != null) {
bind.sessionSendMouse(
sessionId: sessionId, msg: json.encode(modify(evtToPeer)));
@ -1320,6 +1321,7 @@ class InputModel {
bool onExit = false,
int buttons = kPrimaryMouseButton,
bool moveCanvas = true,
bool edgeScroll = false,
}) {
final ffiModel = parent.target!.ffiModel;
CanvasCoords canvas =
@ -1348,8 +1350,16 @@ class InputModel {
y -= CanvasModel.topToEdge;
x -= CanvasModel.leftToEdge;
if (isMove && moveCanvas) {
parent.target!.canvasModel.moveDesktopMouse(x, y);
if (isMove) {
final canvasModel = parent.target!.canvasModel;
if (edgeScroll) {
canvasModel.edgeScrollMouse(x, y);
} else if (moveCanvas) {
canvasModel.moveDesktopMouse(x, y);
}
canvasModel.updateLocalCursor(x, y);
}
return _handlePointerDevicePos(
@ -1412,7 +1422,7 @@ class InputModel {
var nearBottom = (canvas.size.height - y) < nearThr;
final imageWidth = rect.width * canvas.scale;
final imageHeight = rect.height * canvas.scale;
if (canvas.scrollStyle == ScrollStyle.scrollbar) {
if (canvas.scrollStyle != ScrollStyle.scrollauto) {
x += imageWidth * canvas.scrollX;
y += imageHeight * canvas.scrollY;

View File

@ -9,6 +9,7 @@ import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/models/ab_model.dart';
@ -36,6 +37,7 @@ import 'package:get/get.dart';
import 'package:uuid/uuid.dart';
import 'package:window_manager/window_manager.dart';
import 'package:file_picker/file_picker.dart';
import 'package:vector_math/vector_math.dart' show Vector2;
import '../common.dart';
import '../utils/image.dart' as img;
@ -1713,8 +1715,56 @@ class ImageModel with ChangeNotifier {
}
enum ScrollStyle {
scrollbar,
scrollauto,
scrollbar(kRemoteScrollStyleBar),
scrollauto(kRemoteScrollStyleAuto),
scrolledge(kRemoteScrollStyleEdge);
const ScrollStyle(this.stringValue);
final String stringValue;
String toJson() {
return name;
}
static ScrollStyle fromJson(String json, [ScrollStyle? fallbackValue]) {
switch (json) {
case 'scrollbar':
return scrollbar;
case 'scrollauto':
return scrollauto;
case 'scrolledge':
return scrolledge;
}
if (fallbackValue != null) {
return fallbackValue;
}
throw ArgumentError("Unknown ScrollStyle JSON value: '$json'");
}
@override
String toString() {
return stringValue;
}
static ScrollStyle fromString(String string, [ScrollStyle? fallbackValue]) {
switch (string) {
case kRemoteScrollStyleBar:
return scrollbar;
case kRemoteScrollStyleAuto:
return scrollauto;
case kRemoteScrollStyleEdge:
return scrolledge;
}
if (fallbackValue != null) {
return fallbackValue;
}
throw ArgumentError("Unknown ScrollStyle string value: '$string'");
}
}
class ViewStyle {
@ -1789,6 +1839,60 @@ class ViewStyle {
}
}
enum EdgeScrollState {
inactive,
armed,
active,
}
class EdgeScrollFallbackState {
final CanvasModel _owner;
late Ticker _ticker;
Duration _lastTotalElapsed = Duration.zero;
bool _nextEventIsFirst = true;
Vector2 _encroachment = Vector2.zero();
EdgeScrollFallbackState(this._owner, TickerProvider tickerProvider) {
_ticker = tickerProvider.createTicker(emitTick);
}
void setEncroachment(Vector2 encroachment) {
_encroachment = encroachment;
}
void emitTick(Duration totalElapsed) {
if (_nextEventIsFirst) {
_lastTotalElapsed = totalElapsed;
_nextEventIsFirst = false;
} else {
final thisTickElapsed = totalElapsed - _lastTotalElapsed;
const double kFrameTime = 1000.0 / 60.0;
const double kSpeedFactor = 0.1;
var delta = _encroachment *
(kSpeedFactor * thisTickElapsed.inMilliseconds / kFrameTime);
_owner.performEdgeScroll(delta);
_lastTotalElapsed = totalElapsed;
}
}
void start() {
if (!_ticker.isActive) {
_nextEventIsFirst = true;
_ticker.start();
}
}
void stop() {
_ticker.stop();
}
}
class CanvasModel with ChangeNotifier {
// image offset of canvas
double _x = 0;
@ -1810,6 +1914,13 @@ class CanvasModel with ChangeNotifier {
// scroll offset y percent
double _scrollY = 0.0;
ScrollStyle _scrollStyle = ScrollStyle.scrollauto;
// tracks whether edge scroll should be active, prevents spurious
// scrolling when the cursor enters the view from outside
EdgeScrollState _edgeScrollState = EdgeScrollState.inactive;
// fallback strategy for when Bump Mouse isn't available
late EdgeScrollFallbackState _edgeScrollFallbackState;
// to avoid hammering a non-functional Bump Mouse
bool _bumpMouseIsWorking = true;
ViewStyle _lastViewStyle = ViewStyle.defaultViewStyle();
Timer? _timerMobileFocusCanvasCursor;
@ -1840,9 +1951,18 @@ class CanvasModel with ChangeNotifier {
_resetScroll() => setScrollPercent(0.0, 0.0);
setScrollPercent(double x, double y) {
_scrollX = x;
_scrollY = y;
void setScrollPercent(double x, double y) {
_scrollX = x.isFinite ? x : 0.0;
_scrollY = y.isFinite ? y : 0.0;
}
void pushScrollPositionToUI(double scrollPixelX, double scrollPixelY) {
if (_horizontal.hasClients) {
_horizontal.jumpTo(scrollPixelX);
}
if (_vertical.hasClients) {
_vertical.jumpTo(scrollPixelY);
}
}
ScrollController get scrollHorizontal => _horizontal;
@ -1957,13 +2077,14 @@ class CanvasModel with ChangeNotifier {
}
tryUpdateScrollStyle(Duration duration, String? style) async {
if (_scrollStyle != ScrollStyle.scrollbar) return;
if (_scrollStyle == ScrollStyle.scrollauto) return;
style ??= await bind.sessionGetViewStyle(sessionId: sessionId);
if (style != kRemoteViewStyleOriginal && style != kRemoteViewStyleCustom) {
return;
}
_resetScroll();
Future.delayed(duration, () async {
updateScrollPercent();
});
@ -1971,12 +2092,15 @@ class CanvasModel with ChangeNotifier {
updateScrollStyle() async {
final style = await bind.sessionGetScrollStyle(sessionId: sessionId);
if (style == kRemoteScrollStyleBar) {
_scrollStyle = ScrollStyle.scrollbar;
_scrollStyle = style != null
? ScrollStyle.fromString(style!)
: ScrollStyle.scrollauto;
if (_scrollStyle != ScrollStyle.scrollauto) {
_resetScroll();
} else {
_scrollStyle = ScrollStyle.scrollauto;
}
notifyListeners();
}
@ -2007,7 +2131,33 @@ class CanvasModel with ChangeNotifier {
static double get windowBorderWidth => stateGlobal.windowBorderWidth.value;
static double get tabBarHeight => stateGlobal.tabBarHeight;
moveDesktopMouse(double x, double y) {
void activateLocalCursor() {
if (isDesktop || isWebDesktop) {
try {
RemoteCursorMovedState.find(id).value = false;
} catch (e) {
//
}
}
}
void updateLocalCursor(double x, double y) {
// If keyboard is not permitted, do not move cursor when mouse is moving.
if (parent.target != null && parent.target!.ffiModel.keyboard) {
// Draw cursor if is not desktop.
if (!(isDesktop || isWebDesktop)) {
parent.target!.cursorModel.moveLocal(x, y);
} else {
try {
RemoteCursorMovedState.find(id).value = false;
} catch (e) {
//
}
}
}
}
void moveDesktopMouse(double x, double y) {
if (size.width == 0 || size.height == 0) {
return;
}
@ -2036,20 +2186,132 @@ class CanvasModel with ChangeNotifier {
if (dxOffset != 0 || dyOffset != 0) {
notifyListeners();
}
}
// If keyboard is not permitted, do not move cursor when mouse is moving.
if (parent.target != null && parent.target!.ffiModel.keyboard) {
// Draw cursor if is not desktop.
if (!(isDesktop || isWebDesktop)) {
parent.target!.cursorModel.moveLocal(x, y);
void initializeEdgeScrollFallback(TickerProvider tickerProvider) {
_edgeScrollFallbackState = EdgeScrollFallbackState(this, tickerProvider);
}
void disableEdgeScroll() {
_edgeScrollState = EdgeScrollState.inactive;
cancelEdgeScroll();
}
void rearmEdgeScroll() {
_edgeScrollState = EdgeScrollState.armed;
}
void cancelEdgeScroll() {
_edgeScrollFallbackState.stop();
}
(Vector2, Vector2) getScrollInfo() {
final scrollPixel = Vector2(
_horizontal.hasClients ? _horizontal.position.pixels : 0,
_vertical.hasClients ? _vertical.position.pixels : 0);
final max = Vector2(
_horizontal.hasClients ? _horizontal.position.maxScrollExtent : 0,
_vertical.hasClients ? _vertical.position.maxScrollExtent : 0);
return (scrollPixel, max);
}
void edgeScrollMouse(double x, double y) async {
if ((_edgeScrollState == EdgeScrollState.inactive) ||
(size.width == 0 || size.height == 0) ||
!(_horizontal.hasClients || _vertical.hasClients)) {
return;
}
// Trigger scrolling when the cursor is close to an edge
const double edgeThickness = 100;
if (_edgeScrollState == EdgeScrollState.armed) {
// Edge scroll is armed to become active once the cursor
// is observed within the rectangle interior to the
// edge scroll regions. If the user has just moved the
// cursor in from outside of the window, edge scrolling
// doesn't happen yet.
final clientArea = Rect.fromLTWH(0, 0, size.width, size.height);
final innerZone = clientArea.deflate(edgeThickness);
if (innerZone.contains(Offset(x, y))) {
_edgeScrollState = EdgeScrollState.active;
} else {
try {
RemoteCursorMovedState.find(id).value = false;
} catch (e) {
//
}
// Not yet.
return;
}
}
var dxOffset = 0.0;
var dyOffset = 0.0;
if (x < edgeThickness) {
dxOffset = x - edgeThickness;
} else if (x >= size.width - edgeThickness) {
dxOffset = x - (size.width - edgeThickness);
}
if (y < edgeThickness) {
dyOffset = y - edgeThickness;
} else if (y >= size.height - edgeThickness) {
dyOffset = y - (size.height - edgeThickness);
}
var encroachment = Vector2(dxOffset, dyOffset);
var (scrollPixel, max) = getScrollInfo();
encroachment.clamp(-scrollPixel, max - scrollPixel);
if (encroachment.length2 == 0) {
_edgeScrollFallbackState.stop();
} else {
var bumpAmount = -encroachment;
// Round away from 0: this ensures that the mouse will be bumped clear of
// whichever edge scroll zone(s) it is in
bumpAmount.x += bumpAmount.x.sign * 0.5;
bumpAmount.y += bumpAmount.y.sign * 0.5;
var bumpMouseSucceeded = _bumpMouseIsWorking &&
(await rustDeskWinManager.call(WindowType.Main, kWindowBumpMouse,
{"dx": bumpAmount.x.round(), "dy": bumpAmount.y.round()}))
.result;
if (bumpMouseSucceeded) {
performEdgeScroll(encroachment);
} else {
// If we can't BumpMouse, then we switch to slower scrolling with autorepeat
// Don't keep hammering BumpMouse if it's not working.
_bumpMouseIsWorking = false;
// Keep scrolling as long as the user is overtop of an edge.
_edgeScrollFallbackState.setEncroachment(encroachment);
_edgeScrollFallbackState.start();
}
}
}
void performEdgeScroll(Vector2 delta) {
var (scrollPixel, max) = getScrollInfo();
scrollPixel += delta;
scrollPixel.clamp(Vector2.zero(), max);
var scrollPixelPercent = scrollPixel.clone();
scrollPixelPercent.divide(max);
scrollPixelPercent.scale(100.0);
setScrollPercent(scrollPixelPercent.x, scrollPixelPercent.y);
pushScrollPositionToUI(scrollPixel.x, scrollPixel.y);
notifyListeners();
}
set scale(v) {

View File

@ -13,8 +13,18 @@ class RdPlatformChannel {
static RdPlatformChannel get instance => _windowUtil;
final MethodChannel _osxMethodChannel =
MethodChannel("org.rustdesk.rustdesk/macos");
final MethodChannel _hostMethodChannel =
MethodChannel("org.rustdesk.rustdesk/host");
/// Bump the position of the mouse cursor, if applicable
Future<bool> bumpMouse({required int dx, required int dy}) async {
// No debug output; this call is too chatty.
bool? result = await _hostMethodChannel
.invokeMethod("bumpMouse", {"dx": dx, "dy": dy});
return result ?? false;
}
/// Change the theme of the system window
Future<void> changeSystemWindowTheme(SystemWindowTheme theme) {
@ -23,13 +33,13 @@ class RdPlatformChannel {
print(
"[Window ${kWindowId ?? 'Main'}] change system window theme to ${theme.name}");
}
return _osxMethodChannel
return _hostMethodChannel
.invokeMethod("setWindowTheme", {"themeName": theme.name});
}
/// Terminate .app manually.
Future<void> terminate() {
assert(isMacOS);
return _osxMethodChannel.invokeMethod("terminate");
return _hostMethodChannel.invokeMethod("terminate");
}
}

View File

@ -63,6 +63,8 @@ add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"bump_mouse.cc"
"bump_mouse_x11.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)

View File

@ -0,0 +1,18 @@
#include "bump_mouse.h"
#include "bump_mouse_x11.h"
#include <gdk/gdkx.h>
bool bump_mouse(int dx, int dy)
{
GdkDisplay *display = gdk_display_get_default();
if (GDK_IS_X11_DISPLAY(display)) {
return bump_mouse_x11(dx, dy);
}
else {
// Don't know how to support this.
return false;
}
}

View File

@ -0,0 +1,3 @@
#pragma once
bool bump_mouse(int dx, int dy);

View File

@ -0,0 +1,30 @@
#include "bump_mouse.h"
#include <gtk/gtk.h>
#include <gdk/gdkx.h>
#include <iostream>
bool bump_mouse_x11(int dx, int dy)
{
GdkDevice *mouse_device;
#if GTK_CHECK_VERSION(3, 20, 0)
auto seat = gdk_display_get_default_seat(gdk_display_get_default());
mouse_device = gdk_seat_get_pointer(seat);
#else
auto devman = gdk_display_get_device_manager(gdk_display_get_default());
mouse_device = gdk_device_manager_get_client_pointer(devman);
#endif
GdkScreen *screen;
gint x, y;
gdk_device_get_position(mouse_device, &screen, &x, &y);
gdk_device_warp(mouse_device, screen, x + dx, y + dy);
return true;
}

View File

@ -0,0 +1,3 @@
#pragma once
bool bump_mouse_x11(int dx, int dy);

View File

@ -1,5 +1,7 @@
#include "my_application.h"
#include "bump_mouse.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
@ -10,10 +12,13 @@
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
FlMethodChannel* host_channel;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
void host_channel_call_handler(FlMethodChannel* channel, FlMethodCall* method_call, gpointer user_data);
GtkWidget *find_gl_area(GtkWidget *widget);
void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view);
@ -24,10 +29,11 @@ GtkWidget *find_gl_area(GtkWidget *widget);
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
gtk_window_set_decorated(window, FALSE);
// try setting icon for rustdesk, which uses the system cache
// try setting icon for rustdesk, which uses the system cache
GtkIconTheme* theme = gtk_icon_theme_get_default();
gint icons[4] = {256, 128, 64, 32};
for (int i = 0; i < 4; i++) {
@ -87,6 +93,17 @@ static void my_application_activate(GApplication* application) {
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
self->host_channel = fl_method_channel_new(
fl_engine_get_binary_messenger(fl_view_get_engine(view)),
"org.rustdesk.rustdesk/host",
FL_METHOD_CODEC(codec));
fl_method_channel_set_method_call_handler(
self->host_channel,
host_channel_call_handler,
self,
nullptr);
gtk_widget_grab_focus(GTK_WIDGET(view));
}
@ -113,6 +130,7 @@ static gboolean my_application_local_command_line(GApplication* application, gch
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
g_clear_object(&self->host_channel);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
@ -131,6 +149,61 @@ MyApplication* my_application_new() {
nullptr));
}
void host_channel_call_handler(FlMethodChannel* channel, FlMethodCall* method_call, gpointer user_data)
{
if (strcmp(fl_method_call_get_name(method_call), "bumpMouse") == 0) {
FlValue *args = fl_method_call_get_args(method_call);
FlValue *dxValue = nullptr;
FlValue *dyValue = nullptr;
switch (fl_value_get_type(args))
{
case FL_VALUE_TYPE_MAP:
{
dxValue = fl_value_lookup_string(args, "dx");
dyValue = fl_value_lookup_string(args, "dy");
break;
}
case FL_VALUE_TYPE_LIST:
{
int listSize = fl_value_get_length(args);
dxValue = (listSize >= 1) ? fl_value_get_list_value(args, 0) : nullptr;
dyValue = (listSize >= 2) ? fl_value_get_list_value(args, 1) : nullptr;
break;
}
default: break;
}
int dx = 0, dy = 0;
if (dxValue && (fl_value_get_type(dxValue) == FL_VALUE_TYPE_INT)) {
dx = fl_value_get_int(dxValue);
}
if (dyValue && (fl_value_get_type(dyValue) == FL_VALUE_TYPE_INT)) {
dy = fl_value_get_int(dyValue);
}
bool result = bump_mouse(dx, dy);
FlValue *result_value = fl_value_new_bool(result);
GError *error = nullptr;
if (!fl_method_call_respond_success(method_call, result_value, &error)) {
g_warning("Failed to send Flutter Platform Channel response: %s", error->message);
g_error_free(error);
}
fl_value_unref(result_value);
}
}
GtkWidget *find_gl_area(GtkWidget *widget)
{
if (GTK_IS_GL_AREA(widget)) {
@ -160,7 +233,7 @@ void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view)
GtkWidget *gl_area = NULL;
printf("Try setting transparent\n");
gl_area = find_gl_area(GTK_WIDGET(view));
if (gl_area != NULL) {
gtk_gl_area_set_has_alpha(GTK_GL_AREA(gl_area), TRUE);

View File

@ -29,7 +29,7 @@ class MainFlutterWindow: NSWindow {
// register self method handler
let registrar = flutterViewController.registrar(forPlugin: "RustDeskPlugin")
setMethodHandler(registrar: registrar)
RegisterGeneratedPlugins(registry: flutterViewController)
FlutterMultiWindowPlugin.setOnWindowCreatedCallback { controller in
@ -50,22 +50,22 @@ class MainFlutterWindow: NSWindow {
WindowSizePlugin.register(with: controller.registrar(forPlugin: "WindowSizePlugin"))
TextureRgbaRendererPlugin.register(with: controller.registrar(forPlugin: "TextureRgbaRendererPlugin"))
}
super.awakeFromNib()
}
override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) {
super.order(place, relativeTo: otherWin)
hiddenWindowAtLaunch()
}
/// Override window theme.
public func setWindowInterfaceMode(window: NSWindow, themeName: String) {
window.appearance = NSAppearance(named: themeName == "light" ? .aqua : .darkAqua)
}
public func setMethodHandler(registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "org.rustdesk.rustdesk/macos", binaryMessenger: registrar.messenger)
let channel = FlutterMethodChannel(name: "org.rustdesk.rustdesk/host", binaryMessenger: registrar.messenger)
channel.setMethodCallHandler({
(call, result) -> Void in
switch call.method {
@ -99,6 +99,58 @@ class MainFlutterWindow: NSWindow {
result(granted)
})
break
case "bumpMouse":
var dx = 0
var dy = 0
if let argMap = call.arguments as? [String: Any] {
dx = (argMap["dx"] as? Int) ?? 0
dy = (argMap["dy"] as? Int) ?? 0
}
else if let argList = call.arguments as? [Any] {
dx = argList.count >= 1 ? (argList[0] as? Int) ?? 0 : 0
dy = argList.count >= 2 ? (argList[1] as? Int) ?? 0 : 0
}
var mouseLoc: CGPoint
if let dummyEvent = CGEvent(source: nil) { // can this ever fail?
mouseLoc = dummyEvent.location
}
else if let screenFrame = NSScreen.screens.first?.frame {
// NeXTStep: Origin is lower-left of primary screen, positive is up
// Cocoa Core Graphics: Origin is upper-left of primary screen, positive is down
let nsMouseLoc = NSEvent.mouseLocation
mouseLoc = CGPoint(
x: nsMouseLoc.x,
y: NSHeight(screenFrame) - nsMouseLoc.y)
}
else {
result(false)
break
}
let newLoc = CGPoint(x: mouseLoc.x + CGFloat(dx), y: mouseLoc.y + CGFloat(dy))
CGDisplayMoveCursorToPoint(0, newLoc)
// By default, Cocoa suppresses mouse events briefly after a call to warp the
// cursor to a new location. This is good if you want to draw the user's
// attention to the fact that the mouse is now in a particular location, but
// it's bad in this case; we get called as part of the handling of edge
// scrolling, which means the mouse is typically still in motion, and we want
// the cursor to keep moving smoothly uninterrupted.
//
// 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
// already true has the side-effect of cancelling this motion suppression.
CGAssociateMouseAndMouseCursorPosition(1 /* true */)
result(true)
break
default:
result(FlutterMethodNotImplemented)
}

View File

@ -109,6 +109,7 @@ dependencies:
xterm: 4.0.0
sqflite: 2.2.0
google_fonts: ^6.2.1
vector_math: ^2.1.4
dev_dependencies:
icons_launcher: ^2.0.4

View File

@ -1,13 +1,24 @@
#include "flutter_window.h"
#include <optional>
#include <desktop_multi_window/desktop_multi_window_plugin.h>
#include <texture_rgba_renderer/texture_rgba_renderer_plugin_c_api.h>
#include <flutter_gpu_texture_renderer/flutter_gpu_texture_renderer_plugin_c_api.h>
#include "flutter/generated_plugin_registrant.h"
#include <flutter/event_channel.h>
#include <flutter/event_sink.h>
#include <flutter/event_stream_handler_functions.h>
#include <flutter/method_channel.h>
#include <flutter/standard_method_codec.h>
#include <windows.h>
#include <optional>
#include <memory>
#include "win32_desktop.h"
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
: project_(project) {}
@ -29,6 +40,48 @@ bool FlutterWindow::OnCreate() {
return false;
}
RegisterPlugins(flutter_controller_->engine());
flutter::MethodChannel<> channel(
flutter_controller_->engine()->messenger(),
"org.rustdesk.rustdesk/host",
&flutter::StandardMethodCodec::GetInstance());
channel.SetMethodCallHandler(
[](const flutter::MethodCall<>& call, std::unique_ptr<flutter::MethodResult<>> result) {
if (call.method_name() == "bumpMouse") {
auto arguments = call.arguments();
int dx = 0, dy = 0;
if (std::holds_alternative<flutter::EncodableMap>(*arguments)) {
auto argsMap = std::get<flutter::EncodableMap>(*arguments);
auto dxIt = argsMap.find(flutter::EncodableValue("dx"));
auto dyIt = argsMap.find(flutter::EncodableValue("dy"));
if ((dxIt != argsMap.end()) && std::holds_alternative<int>(dxIt->second)) {
dx = std::get<int>(dxIt->second);
}
if ((dyIt != argsMap.end()) && std::holds_alternative<int>(dyIt->second)) {
dy = std::get<int>(dyIt->second);
}
} else if (std::holds_alternative<flutter::EncodableList>(*arguments)) {
auto argsList = std::get<flutter::EncodableList>(*arguments);
if ((argsList.size() >= 1) && std::holds_alternative<int>(argsList[0])) {
dx = std::get<int>(argsList[0]);
}
if ((argsList.size() >= 2) && std::holds_alternative<int>(argsList[1])) {
dy = std::get<int>(argsList[1]);
}
}
bool succeeded = Win32Desktop::BumpMouse(dx, dy);
result->Success(succeeded);
}
});
DesktopMultiWindowSetWindowCreatedCallback([](void *controller) {
auto *flutter_view_controller =
reinterpret_cast<flutter::FlutterViewController *>(controller);

View File

@ -66,4 +66,17 @@ namespace Win32Desktop
size.width = std::min(size.width, workarea_bottom_right.x - origin.x);
size.height = std::min(size.height, workarea_bottom_right.y - origin.y);
}
bool BumpMouse(int dx, int dy)
{
POINT pos;
if (GetCursorPos(&pos))
{
SetCursorPos(pos.x + dx, pos.y + dy);
return true;
}
return false;
}
}

View File

@ -7,6 +7,7 @@ namespace Win32Desktop
{
void GetWorkArea(Win32Window::Point& origin, Win32Window::Size& size);
void FitToWorkArea(Win32Window::Point& origin, Win32Window::Size& size);
bool BumpMouse(int dx, int dy);
}
#endif // RUNNER_WIN32_DESKTOP_H_

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", "إظهار عصا التحكم الافتراضية"),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", "Mostra el joystick virtual"),
("Edit note", "Edita la nota"),
("Alias", "Alias"),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", "显示虚拟摇杆"),
("Edit note", "编辑备注"),
("Alias", "别名"),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", "Virtuellen Joystick anzeigen"),
("Edit note", "Hinweis bearbeiten"),
("Alias", "Alias"),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", "Mostrar joystick virtual"),
("Edit note", "Editar nota"),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", "نمایش جوی‌استیک مجازی"),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", "Afficher le joystick virtuel"),
("Edit note", "Modifier la note"),
("Alias", "Alias"),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", "Virtuális vezérlő megjelenítése"),
("Edit note", "Jegyzet szerkesztése"),
("Alias", "Álnév"),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", "Visualizza joystick virtuale"),
("Edit note", "Modifica nota"),
("Alias", "Alias"),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", "仮想ジョイスティックを表示する"),
("Edit note", "メモを編集"),
("Alias", "エイリアス"),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", "가상 조이스틱 표시"),
("Edit note", "노트 편집"),
("Alias", "별명"),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", "Virtuele joystick weergeven"),
("Edit note", "Opmerking bewerken"),
("Alias", "Alias"),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", "Pokaz wirtualny joystick"),
("Edit note", "Edytuj notatkę"),
("Alias", "Alias"),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", "Показать виртуальный джойстик"),
("Edit note", "Изменить заметку"),
("Alias", "Псевдоним"),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", "顯示虛擬搖桿"),
("Edit note", "編輯備註"),
("Alias", "別名"),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}

View File

@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
].iter().cloned().collect();
}