fix(cursor): preserve native pixels and hotspots across display scales

Capture actual Mutter and DXGI cursor metadata, preserve Retina artwork, and invalidate cursor identities when physical pixels or DPI change. Keep straight-alpha colors during native cursor resizing and correct the Windows XOR outline offset.

Verified all six Mac/Linux/Windows directions with real pointer movement, arrow/I-beam/crosshair transitions, adaptive zoom off/on/off, Original view, and live DPI changes. Requested debug builds and focused native/Flutter regressions pass.
This commit is contained in:
fufesou
2026-09-11 11:49:04 +08:00
parent f96d00d9d1
commit 72ea38ca4f
16 changed files with 1644 additions and 12 deletions

View File

@@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/services.dart';
@@ -31,6 +32,11 @@ class _CursorModel implements CursorModel {
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _CursorFFI implements FFI {
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
CursorData _cursorData((int, int) size, (double, double) hotspot) {
final image = img.Image(width: size.$1, height: size.$2, numChannels: 4);
for (var y = 0; y < image.height; y++) {
@@ -83,6 +89,46 @@ void _expectArtwork(img.Image native, img.Image artwork) {
}
void main() {
testWidgets('received cursor keeps edge colors across scale changes',
(tester) async {
const side = 4;
for (final (rgba, expected) in [
([128, 64, 32, 128], [255, 128, 64, 128]),
([0, 0, 0, 0], [0, 0, 0, 0]),
([32, 64, 128, 255], [32, 64, 128, 255]),
]) {
final cursor = CursorModel(WeakReference<FFI>(_CursorFFI()))
..id = 'edge-colors';
addTearDown(cursor.disposeImages);
addTearDown(cursor.dispose);
await tester.runAsync(() => cursor.updateCursorData({
'id': 'edge-colors',
'hotx': '1',
'hoty': '1',
'width': '$side',
'height': '$side',
'colors': jsonEncode(List.generate(side * side, (_) => rgba)
.expand((pixel) => pixel)
.toList()),
}));
final data = cursor.cache!;
for (final scale in [1.0, 0.5, 1.0]) {
data.updateGetKey(scale);
final image = Platform.isWindows
? img.Image.fromBytes(
width: data.scaledWidth,
height: data.scaledHeight,
bytes: data.data!.buffer,
order: img.ChannelOrder.bgra)
: img.decodePng(data.data!)!;
for (final pixel in image) {
expect([pixel.r, pixel.g, pixel.b, pixel.a], expected,
reason: 'Edge color must survive scale $scale');
}
}
}
});
for (final (source, hotspot, scale, size, linuxHotspot) in _cases) {
testWidgets('${source.$1}x${source.$2} cursor at scale $scale',
(tester) async {