Files
rustdesk/flutter/lib/native/custom_cursor.dart
rustdeskandClaude Opus 5.5 f0acecb8af cursor: fall back to the default cursor for a shape the core cannot give
The shape shown before stays while the one in use decodes, so a switch
does not flash the default cursor. It also stayed once the shape in use
was known to be lost (rejected by the core, or not decoding): the
controller kept drawing, say, an I-beam for a pointer it never got,
until the peer changed shape. Both the native and the painted cursor
now leave the default one, as master did for the native cursor. A shape
that does not decode is marked in updateCursorData, whether it was sent
or fetched again, so both paths agree.

The client also selects a shape it rejects as it arrives. It showed the
shape before until the peer selected the rejected one again, and the
default cursor from then on; it now looks the same both times. Sciter
ignores an id it has no shape for, as it did.

The default and forbidden cursors are made with the model. They were
made the first time a build asked for one, and their decode lands
later with nothing to draw again, so that first build showed the shape
before in their place.

A fetch failing after the session was cleared marks nothing in the new
one, and a fetch that throws at once tells the listeners after the
build that asked for it, not during it.

The forbidden cursor, shown while input is off, became the cursor shown
last, so a raster made after input came back showed it until the
shape's pixels were fetched, or for good if they never came. Only a
shape stands in for a shape now; CursorModel.shown keeps the rule for
both builders.

_checkView asks for a single registration again: every native cursor
is made once per session, and one made twice leaks an HCURSOR on
Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
2026-09-25 10:11:54 +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.shown(cache, key);
return FlutterCustomMemoryImageCursor(key: key);
}
}
// The last shape's cursor 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));
}