Files
rustdesk/flutter/lib/web/custom_cursor.dart
T
rustdeskandClaude Opus 5.5 cb4f35a540 cursor: keep native cursors for the session, as before
Evicting native cursors and deleting a shape's replaced raster made
Windows leak more than master. The engine's deleteCustomCursor/windows
frees the HCURSOR with DeleteObject, which does not take a cursor, so a
delete frees nothing and each cursor made again for a raster, or for a
shape shown once more, leaks another handle. Keeping every raster of
every shape until the session is cleared creates each one once, as
master does.

Fixing the engine instead is not cheap: x64 runs our fork on Flutter
3.24, where the fix is one more patch to rebuild and carry across every
upgrade; arm64 runs the stock engine of a newer Flutter with the same
code, where it means porting the fork and publishing an arm64 engine
too, or waiting for an upstream fix to reach stable. Content ids keep
the count to the shapes the peer really shows.

The painted images, the compressed shapes in the core, and a switch
showing the cursor shown before are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-24 02:46:53 +08:00

139 lines
3.7 KiB
Dart

import 'dart:convert';
import 'dart:js' as js;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/models/model.dart' as model;
class CursorData {
final String key;
final String url;
final double hotX;
final double hotY;
final int width;
final int height;
CursorData({
required this.key,
required this.url,
required this.hotX,
required this.hotY,
required this.width,
required this.height,
});
}
/// The cursor manager
class CursorManager {
final Map<String, CursorData> _cursors = <String, CursorData>{};
String latestKey = '';
CursorManager._();
static CursorManager instance = CursorManager._();
Future<void> registerCursor(CursorData data) async {
_cursors[data.key] = data;
}
Future<void> deleteCursor(String key) async {
_cursors.remove(key);
}
Future<void> setSystemCursor(String key) async {
if (latestKey == key) {
return;
}
latestKey = key;
final CursorData? cursorData = _cursors[key];
if (cursorData != null) {
js.context.callMethod('setByName', [
'cursor',
jsonEncode({
'url': cursorData.url,
'hotx': cursorData.hotX.toInt(),
'hoty': cursorData.hotY.toInt(),
})
]);
}
}
Future<void> resetSystemCursor() async {
latestKey = '';
js.context.callMethod('setByName', ['cursor', 'auto']);
}
}
class FlutterCustomMemoryImageCursor extends MouseCursor {
final String key;
const FlutterCustomMemoryImageCursor({required this.key});
@override
MouseCursorSession createSession(int device) =>
_FlutterCustomMemoryImageCursorSession(this, device);
@override
String get debugDescription =>
objectRuntimeType(this, 'FlutterCustomMemoryImageCursor');
}
class _FlutterCustomMemoryImageCursorSession extends MouseCursorSession {
_FlutterCustomMemoryImageCursorSession(
FlutterCustomMemoryImageCursor cursor, int device)
: super(cursor, device);
@override
FlutterCustomMemoryImageCursor get cursor =>
super.cursor as FlutterCustomMemoryImageCursor;
@override
Future<void> activate() async {
await CursorManager.instance.setSystemCursor(cursor.key);
}
@override
void dispose() {}
}
deleteCustomCursor(String key) => CursorManager.instance.deleteCursor(key);
resetSystemCursor() => CursorManager.instance.resetSystemCursor();
MouseCursor buildCursorOfCache(
model.CursorModel cursor, double scale, model.CursorData? cache) {
if (cache == null) {
return _shownCursor(cursor);
} else {
final key = cursor.nativeKey(cache, scale);
if (!cursor.cachedKeys.contains(key)) {
// data should be checked here, because it may be changed after `updateGetKey()`
final data = cache.data;
if (data == null) {
cursor.restorePixels(cache.id);
return _shownCursor(cursor);
}
debugPrint(
"Register custom cursor with key $key (${cache.hotx},${cache.hoty})");
CursorManager.instance.registerCursor(CursorData(
key: key,
url: 'data:image/rgba;base64,${base64Encode(data)}',
width: (cache.width * cache.scale).toInt(),
height: (cache.height * cache.scale).toInt(),
hotX: cache.hotx,
hotY: cache.hoty));
cursor.addKey(key);
cursor.registered(cache, key);
}
cursor.shownKey = key;
return FlutterCustomMemoryImageCursor(key: key);
}
}
// The cursor shown last stays while the one in use is made, rather than the system one.
MouseCursor _shownCursor(model.CursorModel cursor) {
final key = cursor.shownKey;
return key == null
? MouseCursor.defer
: FlutterCustomMemoryImageCursor(key: key);
}