diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index b988638d2..4cd520742 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -19,6 +19,7 @@ import '../../models/input_model.dart'; import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; import '../../utils/image.dart'; +import '../../utils/cursor_size.dart'; import '../widgets/remote_toolbar.dart'; import '../widgets/kb_layout_type_chooser.dart'; import '../widgets/tabbar_widget.dart'; @@ -1088,6 +1089,21 @@ class ImagePaint extends StatefulWidget { class _ImagePaintState extends State { bool _lastRemoteCursorMoved = false; + final _localCursorSize = LocalCursorSize(); + + @override + void initState() { + super.initState(); + _localCursorSize.addListener(_cursorSizeChanged); + } + + void _cursorSizeChanged() => setState(() {}); + + @override + void dispose() { + _localCursorSize.dispose(); + super.dispose(); + } String get id => widget.id; RxBool get zoomCursor => widget.zoomCursor; @@ -1109,6 +1125,11 @@ class _ImagePaintState extends State { bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal; mouseRegion({child}) => Obx(() { + final useLocalSize = !isWeb && + (isLinux || isMacOS || isWindows) && + !zoomCursor.value && + isViewAdaptive(); + if (useLocalSize) _localCursorSize.ensureLoaded(dpr); double getCursorScale() { var c = Provider.of(context); var cursorScale = 1.0; @@ -1149,11 +1170,16 @@ class _ImagePaintState extends State { _firstEnterImage.value = true; } return _buildCustomCursor( - context, getCursorScale()); + context, getCursorScale(), + useLocalSize: useLocalSize); } }()) - : _buildDisabledCursor(context, getCursorScale()) + : _buildDisabledCursor(context, getCursorScale(), + useLocalSize: useLocalSize) : MouseCursor.defer, + onEnter: (_) { + if (useLocalSize) _localCursorSize.refresh(); + }, onHover: (evt) {}, child: child); }); @@ -1266,15 +1292,21 @@ class _ImagePaintState extends State { ); } - MouseCursor _buildCustomCursor(BuildContext context, double scale) { + MouseCursor _buildCustomCursor(BuildContext context, double scale, + {bool useLocalSize = false}) { final cursor = Provider.of(context); final cache = cursor.cache ?? preDefaultCursor.cache; + if (useLocalSize && _localCursorSize.value == null) return MouseCursor.defer; + cache?.localSize = useLocalSize ? _localCursorSize.value : null; return buildCursorOfCache(cursor, scale, cache); } - MouseCursor _buildDisabledCursor(BuildContext context, double scale) { + MouseCursor _buildDisabledCursor(BuildContext context, double scale, + {bool useLocalSize = false}) { final cursor = Provider.of(context); final cache = preForbiddenCursor.cache; + if (useLocalSize && _localCursorSize.value == null) return MouseCursor.defer; + cache?.localSize = useLocalSize ? _localCursorSize.value : null; return buildCursorOfCache(cursor, scale, cache); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index b83d3c303..ece33f5a4 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -39,6 +39,7 @@ import 'package:vector_math/vector_math.dart' show Vector2; import '../common.dart'; import '../utils/image.dart' as img; +import '../utils/cursor_size.dart'; import '../common/widgets/dialog.dart'; import 'input_model.dart'; import 'platform_model.dart'; @@ -2863,6 +2864,8 @@ class CursorData { double hoty; final int width; final int height; + double? localSize; + late final int _visibleSize = cursorVisibleSize(image); CursorData({ required this.peerId, @@ -2886,14 +2889,18 @@ class CursorData { int _scaledDimension(int dimension, double scale) { const minBitmapSize = 1; final pixels = dimension * scale; - return _usesLogicalCursorPixels + return _usesLogicalCursorPixels || localSize != null ? max(minBitmapSize, pixels.round()) : pixels.toInt(); } double _checkUpdateScale(double scale) { double oldScale = this.scale; - if (scale != 1.0) { + if (localSize != null) { + scale = _visibleSize == 0 + ? 1.0 + : max(localSize!, kMinCursorSize) / _visibleSize; + } else if (scale != 1.0) { // Update data if scale changed. final tgtWidth = (width * scale).toInt(); final tgtHeight = (height * scale).toInt(); @@ -2904,13 +2911,17 @@ class CursorData { } } - if (_doubleToInt(oldScale) != _doubleToInt(scale)) { + const bytesPerPixel = 4; + final byteLength = _scaledDimension(width, scale) * + _scaledDimension(height, scale) * bytesPerPixel; + if (_doubleToInt(oldScale) != _doubleToInt(scale) || + (isWindows && data != null && data!.length != byteLength)) { if (isWindows) { data = img2 .copyResize( image, - width: (width * scale).toInt(), - height: (height * scale).toInt(), + width: _scaledDimension(width, scale), + height: _scaledDimension(height, scale), interpolation: img2.Interpolation.average, ) .getBytes(order: img2.ChannelOrder.bgra); @@ -2936,7 +2947,7 @@ class CursorData { String updateGetKey(double scale) { scale = _checkUpdateScale(scale); - return '${peerId}_${id}_${_doubleToInt(width * scale)}_${_doubleToInt(height * scale)}'; + return '${peerId}_${id}_${_doubleToInt(width * scale)}_${_doubleToInt(height * scale)}${localSize == null ? '' : '_local'}'; } } diff --git a/flutter/lib/utils/cursor_size.dart b/flutter/lib/utils/cursor_size.dart new file mode 100644 index 000000000..9c04d1bd4 --- /dev/null +++ b/flutter/lib/utils/cursor_size.dart @@ -0,0 +1,55 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:image/image.dart' as img; + +int cursorVisibleSize(img.Image image) { + var left = image.width; + var top = image.height; + var right = -1; + var bottom = -1; + for (final pixel in image) { + if (pixel.a == 0) continue; + if (pixel.x < left) left = pixel.x; + if (pixel.x > right) right = pixel.x; + if (pixel.y < top) top = pixel.y; + if (pixel.y > bottom) bottom = pixel.y; + } + if (right < left) return 0; + final width = right - left + 1; + final height = bottom - top + 1; + return width > height ? width : height; +} + +class LocalCursorSize extends ValueNotifier { + LocalCursorSize() : super(null); + + static const _channel = MethodChannel('org.rustdesk.rustdesk/cursor'); + double? _devicePixelRatio; + int _generation = 0; + + void ensureLoaded(double devicePixelRatio) { + if (_devicePixelRatio == devicePixelRatio) return; + _devicePixelRatio = devicePixelRatio; + refresh(); + } + + Future refresh() async { + final generation = ++_generation; + try { + final size = await _channel.invokeMethod('getSystemCursorSize'); + if (size == null || !size.isFinite || size <= 0) { + throw const FormatException('Invalid local system cursor size'); + } + if (generation == _generation) value = size; + } catch (error, stack) { + FlutterError.reportError(FlutterErrorDetails( + exception: error, stack: stack, library: 'local cursor size')); + } + } + + @override + void dispose() { + ++_generation; + super.dispose(); + } +}