Files
rustdesk/flutter/lib/native/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

79 lines
2.8 KiB
Dart

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_hbb/common.dart' show isLinux;
import 'package:flutter_hbb/models/model.dart';
import 'package:image/image.dart' as img;
deleteCustomCursor(String key) =>
custom_cursor_manager.CursorManager.instance.deleteCursor(key);
resetSystemCursor() {}
MouseCursor buildCursorOfCache(
CursorModel cursor, double scale, 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);
}
// Square canvases avoid clipping or stray edge pixels on Linux.
final width = isLinux && cache.rasterWidth < cache.rasterHeight
? cache.rasterHeight
: cache.rasterWidth;
final height = isLinux ? width : cache.rasterHeight;
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 =
width == cache.rasterWidth && height == cache.rasterHeight
? data
: _padCursor(data, width)
..width = width
..height = height
..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(CursorModel cursor) {
final key = cursor.shownKey;
return key == null
? MouseCursor.defer
: FlutterCustomMemoryImageCursor(key: key);
}
Uint8List _padCursor(Uint8List data, int size) {
final bitmap = img.decodePng(data);
if (bitmap == null) {
throw const FormatException('Invalid native cursor PNG');
}
final padded = img.copyExpandCanvas(bitmap,
newWidth: size,
newHeight: size,
position: img.ExpandCanvasPosition.topLeft,
toImage: img.Image(width: size, height: size, numChannels: 4));
return Uint8List.fromList(img.encodePng(padded));
}