fix(cursor): delegate native image scaling to cursor plugin

Use the original Flutter image and source hotspot with explicit DPR, retaining the existing view and minimum-size policies. Pin the plugin implementation from rustdesk-org/flutter_custom_cursor#1.
This commit is contained in:
fufesou
2026-09-12 01:40:26 +08:00
parent 8da629c57d
commit 1061670368
5 changed files with 155 additions and 29 deletions

View File

@@ -2855,6 +2855,9 @@ class CursorData {
final String peerId;
final String id;
final img2.Image image;
// Borrowed from CursorModel/PredefinedCursor, which own its lifetime.
// The plugin clones the handle before starting asynchronous encoding.
final ui.Image nativeImage;
double scale;
Uint8List? data;
final double hotxOrigin;
@@ -2868,6 +2871,7 @@ class CursorData {
required this.peerId,
required this.id,
required this.image,
required this.nativeImage,
required this.scale,
required this.data,
required this.hotxOrigin,
@@ -2879,7 +2883,9 @@ class CursorData {
int _doubleToInt(double v) => (v * 10e6).round().toInt();
double _checkUpdateScale(double scale) {
// Keep the minimum-size policy here. Native callers let the plugin rasterize
// the original ui.Image; Web keeps the encoded-image resizing path.
double _checkUpdateScale(double scale, {bool resizeImage = true}) {
double oldScale = this.scale;
if (scale != 1.0) {
// Update data if scale changed.
@@ -2892,7 +2898,7 @@ class CursorData {
}
}
if (_doubleToInt(oldScale) != _doubleToInt(scale)) {
if (resizeImage && _doubleToInt(oldScale) != _doubleToInt(scale)) {
if (isWindows) {
data = img2
.copyResize(
@@ -2922,8 +2928,8 @@ class CursorData {
return scale;
}
String updateGetKey(double scale) {
scale = _checkUpdateScale(scale);
String updateGetKey(double scale, {bool resizeImage = true}) {
scale = _checkUpdateScale(scale, resizeImage: resizeImage);
return '${peerId}_${id}_${_doubleToInt(width * scale)}_${_doubleToInt(height * scale)}';
}
}
@@ -2976,9 +2982,10 @@ class PredefinedCursor {
// This function is called only one time, no need to care about the performance.
Uint8List data = defaultImg.getBytes(order: img2.ChannelOrder.rgba);
_image?.dispose();
_image = await img.decodeImageFromPixels(
final nativeImage = await img.decodeImageFromPixels(
data, defaultImg.width, defaultImg.height, ui.PixelFormat.rgba8888);
if (_image == null) {
_image = nativeImage;
if (nativeImage == null) {
print("decodeImageFromPixels failed, pre-defined cursor $id");
return;
}
@@ -2993,6 +3000,7 @@ class PredefinedCursor {
peerId: '',
id: id,
image: _image2!.clone(),
nativeImage: nativeImage,
scale: scale,
data: data,
hotxOrigin:
@@ -3471,6 +3479,7 @@ class CursorModel with ChangeNotifier {
peerId: peerId,
id: id,
image: imgOrigin,
nativeImage: image,
scale: 1.0,
data: data,
hotxOrigin: hotx,

View File

@@ -1,10 +1,14 @@
import 'dart:async';
import 'package:flutter_custom_cursor/cursor_manager.dart'
as custom_cursor_manager;
import 'package:flutter_custom_cursor/flutter_custom_cursor.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart' show WidgetsBinding;
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/model.dart';
deleteCustomCursor(String key) =>
@@ -16,27 +20,32 @@ MouseCursor buildCursorOfCache(
if (cache == null) {
return MouseCursor.defer;
} else {
final key = cache.updateGetKey(scale);
// Include the live DPR so moving between monitors rebuilds the native
// bitmap even when the remote view scale has not changed.
final dpr = WidgetsBinding
.instance.platformDispatcher.views.single.devicePixelRatio;
final key = '${cache.updateGetKey(scale, resizeImage: false)}_$dpr';
if (!cursor.cachedKeys.contains(key)) {
// data should be checked here, because it may be changed after `updateGetKey()`
final data = cache.data;
if (data == null) {
return MouseCursor.defer;
}
debugPrint(
"Register custom cursor with key $key (${cache.hotx},${cache.hoty})");
// [Safety]
// It's ok to call async registerCursor in current synchronous context,
// because activating the cursor is also an async call and will always
// be executed after this.
custom_cursor_manager.CursorManager.instance
.registerCursor(custom_cursor_manager.CursorData()
..name = key
..buffer = data
..width = (cache.width * cache.scale).toInt()
..height = (cache.height * cache.scale).toInt()
..hotX = cache.hotx
..hotY = cache.hoty);
unawaited(custom_cursor_manager.CursorManager.instance
.registerCursorImage(
name: key,
image: cache.nativeImage,
hotSpot: Offset(cache.hotxOrigin, cache.hotyOrigin),
// Windows callers already express scale in physical pixels.
// The plugin takes logical scale and applies DPR during rasterization.
scale: isWindows ? cache.scale / dpr : cache.scale,
devicePixelRatio: dpr,
)
.then<void>((_) {}, onError: (Object error, StackTrace stack) {
cursor.cachedKeys.remove(key);
FlutterError.reportError(FlutterErrorDetails(
exception: error,
stack: stack,
library: 'native cursor',
context: ErrorDescription('registering cursor $key')));
}));
cursor.addKey(key);
}
return FlutterCustomMemoryImageCursor(key: key);