mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-10-20 10:42:13 +00:00
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
139 lines
3.7 KiB
Dart
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);
|
|
}
|