revert(cursor): return to f5b98b32 before splitting plugin changes

This commit is contained in:
fufesou
2026-09-12 00:32:58 +08:00
parent 650a6e21cc
commit 8da629c57d
34 changed files with 68 additions and 3177 deletions

View File

@@ -1,196 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/native/custom_cursor.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:image/image.dart' as img;
// Source size, source hotspot, requested/effective scale, artwork size, integer hotspot.
final _cases = [
((17, 23), (4.0, 4.0), 1 / 3, 12 / 17, (12, 16), (3.0, 3.0)),
((32, 16), (16.0, 8.0), 0.5, 0.75, (24, 12), (12.0, 6.0)),
((34, 46), (8.0, 8.0), 0.5, 0.5, (17, 23), (4.0, 4.0)),
((24, 24), (11.0, 11.0), 1 / 3, 0.5, (12, 12), (6.0, 6.0)),
((1, 48), (0.0, 24.0), 1.0, 1.0, (1, 48), (0.0, 24.0)),
((9, 18), (4.0, 9.0), 1.0, 1.0, (9, 18), (4.0, 9.0)),
((24, 24), (4.0, 4.0), 0.1, 0.5, (12, 12), (2.0, 2.0)),
(
(19, 27),
(6.0, 13.0),
1.37,
1.37,
Platform.isWindows ? (26, 36) : (26, 37),
Platform.isWindows ? (8.0, 17.0) : (8.0, 18.0)
),
(
(18, 36),
(8.0, 18.0),
0.75,
0.75,
Platform.isWindows ? (13, 27) : (14, 27),
(6.0, 14.0)
),
];
const _hotspotTolerance = 1e-9;
class _CursorModel implements CursorModel {
@override
final Set<String> cachedKeys = {};
@override
void addKey(String key) => cachedKeys.add(key);
@override
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++) {
image.setPixelRgba(image.width ~/ 2, y, 255, 255, 255, 255);
}
for (var x = 0; x < image.width; x++) {
image.setPixelRgba(x, 0, 255, 0, 0, 255);
image.setPixelRgba(x, image.height - 1, 0, 0, 255, 255);
}
return CursorData(
peerId: 'cursor-test',
id: 'native',
image: image,
scale: 1,
data: Platform.isWindows
? image.getBytes(order: img.ChannelOrder.bgra)
: Uint8List.fromList(img.encodePng(image)),
hotxOrigin: hotspot.$1,
hotyOrigin: hotspot.$2,
width: size.$1,
height: size.$2,
);
}
Future<Map<dynamic, dynamic>> _register(
WidgetTester tester, CursorData data, double scale) async {
final channel = Platform.isWindows
? SystemChannels.mouseCursor
: const MethodChannel('flutter_custom_cursor');
Map<dynamic, dynamic>? registered;
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel,
(call) async {
expect(
call.method,
Platform.isWindows
? 'createCustomCursor/windows'
: 'createCustomCursor');
registered = call.arguments as Map<dynamic, dynamic>;
return registered!['name'];
});
addTearDown(() => tester.binding.defaultBinaryMessenger
.setMockMethodCallHandler(channel, null));
buildCursorOfCache(_CursorModel(), scale, data);
await tester.pump();
expect(registered, isNotNull);
return registered!;
}
void _expectArtwork(img.Image native, img.Image artwork) {
final content = img.copyCrop(native,
x: 0, y: 0, width: artwork.width, height: artwork.height);
expect(content.getBytes(), artwork.getBytes());
for (final pixel in native) {
if (pixel.x >= artwork.width) {
expect(pixel.a, 0, reason: 'Cursor padding must be transparent');
}
}
}
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, effectiveScale, size, integerHotspot)
in _cases) {
testWidgets('${source.$1}x${source.$2} cursor at scale $scale',
(tester) async {
final data = _cursorData(source, hotspot);
final cursor = await _register(tester, data, scale);
final artwork = Platform.isWindows
? img.Image.fromBytes(
width: data.scaledWidth,
height: data.scaledHeight,
bytes: data.data!.buffer,
order: img.ChannelOrder.bgra)
: img.decodePng(data.data!)!;
final native = Platform.isWindows
? img.Image.fromBytes(
width: cursor['width'],
height: cursor['height'],
bytes: (cursor['buffer'] as Uint8List).buffer,
bytesOffset: (cursor['buffer'] as Uint8List).offsetInBytes,
order: img.ChannelOrder.bgra)
: img.decodePng(cursor['buffer'] as Uint8List)!;
final width = Platform.isLinux && size.$2 > size.$1 ? size.$2 : size.$1;
expect(data.scale, effectiveScale);
expect((artwork.width, artwork.height), size);
expect(data.hotx / size.$1,
closeTo(hotspot.$1 / source.$1, _hotspotTolerance));
expect(data.hoty / size.$2,
closeTo(hotspot.$2 / source.$2, _hotspotTolerance));
expect((native.width, native.height), (width, size.$2));
expect((cursor['width'], cursor['height']), (width, size.$2));
expect(
(cursor['hotX'], cursor['hotY']),
Platform.isLinux || Platform.isWindows
? integerHotspot
: (data.hotx, data.hoty));
_expectArtwork(native, artwork);
if (width == artwork.width) {
expect(cursor['buffer'], data.data);
}
});
}
}

View File

@@ -1,292 +0,0 @@
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
import 'package:flutter_hbb/models/input_model.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/utils/cursor_size.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:get/get.dart';
import 'package:image/image.dart' as img;
import 'package:provider/provider.dart';
const _viewport = Size(200, 160);
class _Image extends ChangeNotifier implements ImageModel {
@override
bool get useTextureRender => false;
@override
ui.Image? get image => null;
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _Canvas extends ChangeNotifier implements CanvasModel {
_Canvas(this.devicePixelRatio, {required this.style, required this.scale});
final String style;
@override
final double devicePixelRatio;
@override
final imageOverflow = false.obs;
@override
late final viewStyle = ViewStyle(
style: style,
width: _viewport.width,
height: _viewport.height,
displayWidth: 400,
displayHeight: 320,
);
@override
bool get cursorEmbedded => false;
@override
Size get size => _viewport;
@override
final double scale;
@override
double get x => 0;
@override
double get y => 0;
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _Cursor extends ChangeNotifier implements CursorModel {
_Cursor(this.cache);
@override
CursorData cache;
@override
ui.Image? get image => null;
@override
double get hotx => cache.hotxOrigin;
@override
double get hoty => cache.hotyOrigin;
@override
final Set<String> cachedKeys = {};
@override
void addKey(String key) => cachedKeys.add(key);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _Input extends Fake implements InputModel {
@override
final relativeMouseMode = false.obs;
}
class _Peer extends Fake implements FfiModel {
@override
final pi = PeerInfo();
@override
bool get isPeerLinux => false;
}
class _FFI extends Fake implements FFI {
@override
final inputModel = _Input();
@override
final ffiModel = _Peer();
}
CursorData _cursor(img.Image image,
{String id = 'sprite', double hotspot = 0}) {
return CursorData(
peerId: 'local-size-test',
id: id,
image: image,
scale: 1,
data: Platform.isWindows
? image.getBytes(order: img.ChannelOrder.bgra)
: Uint8List.fromList(img.encodePng(image)),
hotxOrigin: hotspot,
hotyOrigin: hotspot,
width: image.width,
height: image.height,
);
}
CursorData _arrow(int density) {
final image =
img.Image(width: 17 * density, height: 23 * density, numChannels: 4);
img.fill(image, color: img.ColorRgba8(255, 255, 255, 255));
return _cursor(image, id: 'arrow-$density', hotspot: 4.0 * density);
}
void main() {
test('local size preserves remote pixels, padding and asymmetric hotspot',
() {
final artwork = img.Image(width: 40, height: 48, numChannels: 4);
img.fillRect(artwork,
x1: 8,
y1: 12,
x2: 22,
y2: 35,
color: img.ColorRgba8(180, 90, 45, 128),
alphaBlend: false);
for (final density in [1, 2, 3]) {
final source = img.copyResize(artwork,
width: artwork.width * density,
height: artwork.height * density,
interpolation: img.Interpolation.nearest);
expect(cursorVisibleSize(source), 24 * density);
final cursor = CursorData(
peerId: 'local-size-test',
id: 'padded-$density',
image: source,
scale: 1,
data: Platform.isWindows
? source.getBytes(order: img.ChannelOrder.bgra)
: Uint8List.fromList(img.encodePng(source)),
hotxOrigin: 10.0 * density,
hotyOrigin: 17.0 * density,
width: source.width,
height: source.height,
)..localSize = 24;
cursor.updateGetKey(0.01);
final output = Platform.isWindows
? img.Image.fromBytes(
width: cursor.scaledWidth,
height: cursor.scaledHeight,
bytes: cursor.data!.buffer,
order: img.ChannelOrder.bgra)
: img.decodePng(cursor.data!)!;
expect(cursorVisibleSize(output), 24);
expect(output.getBytes(), artwork.getBytes());
expect((cursor.hotx, cursor.hoty), (10, 17));
}
});
test('local cursor minimum keeps a thin remote cursor visible', () {
final thin = img.Image(width: 1, height: 48, numChannels: 4);
img.fill(thin, color: img.ColorRgba8(255, 255, 255, 255));
final cursor = _cursor(thin)..localSize = 2;
cursor.updateGetKey(1);
expect(cursor.scaledWidth, 1);
expect(cursor.scaledHeight, kMinCursorSize);
final blank = _cursor(img.Image(width: 17, height: 23, numChannels: 4))
..localSize = 24;
blank.updateGetKey(1);
final output = Platform.isWindows
? img.Image.fromBytes(
width: 17,
height: 23,
bytes: blank.data!.buffer,
order: img.ChannelOrder.bgra)
: img.decodePng(blank.data!)!;
expect(cursorVisibleSize(output), 0);
expect(blank.scale, 1);
});
test('switching local sizing keeps bitmap data and cache geometry consistent',
() {
final pending = _arrow(1)..data = null;
pending.updateGetKey(1);
expect(pending.data, isNull);
final cursor = _arrow(1);
const scale = 1.5;
final originalKey = cursor.updateGetKey(scale);
cursor.localSize = 23 * scale;
final localKey = cursor.updateGetKey(scale);
expect(cursor.scaledWidth, 26);
if (Platform.isWindows) {
expect(localKey, isNot(originalKey));
expect(cursor.data!.length, 26 * 35 * 4);
}
cursor.localSize = null;
expect(cursor.updateGetKey(scale), originalKey);
if (Platform.isWindows) expect(cursor.data!.length, 25 * 34 * 4);
});
for (final (dpr, style, scale) in [
for (final dpr in [1.0, 2.0, 3.0])
for (final (style, scale) in [
(kRemoteViewStyleAdaptive, 0.5),
(kRemoteViewStyleCustom, 0.25),
(kRemoteViewStyleCustom, 2.0),
]) (dpr, style, scale),
]) {
testWidgets('$style scale=$scale DPR=$dpr preserves local size and hotspot',
(tester) async {
final channel = Platform.isWindows
? SystemChannels.mouseCursor
: const MethodChannel('flutter_custom_cursor');
final registered = <Map<dynamic, dynamic>>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel,
(call) async {
if (call.method.startsWith('createCustomCursor')) {
final arguments = call.arguments as Map<dynamic, dynamic>;
registered.add(arguments);
return arguments['name'];
}
return null;
});
addTearDown(() => tester.binding.defaultBinaryMessenger
.setMockMethodCallHandler(channel, null));
const sizeChannel = MethodChannel('org.rustdesk.rustdesk/cursor');
tester.binding.defaultBinaryMessenger
.setMockMethodCallHandler(sizeChannel, (call) async {
expect(call.method, 'getSystemCursorSize');
return Platform.isWindows ? 23.0 * dpr : 23.0;
});
addTearDown(() => tester.binding.defaultBinaryMessenger
.setMockMethodCallHandler(sizeChannel, null));
final cursor = _Cursor(_arrow(1));
final zoom = false.obs;
await tester.pumpWidget(MediaQuery(
data: MediaQueryData(devicePixelRatio: dpr),
child: MultiProvider(
providers: [
ChangeNotifierProvider<ImageModel>(create: (_) => _Image()),
ChangeNotifierProvider<CanvasModel>(
create: (_) => _Canvas(dpr, style: style, scale: scale)),
ChangeNotifierProvider<CursorModel>.value(value: cursor),
],
child: ImagePaint(
ffi: _FFI(),
id: 'local-size-test',
zoomCursor: zoom,
cursorOverImage: true.obs,
keyboardEnabled: true.obs,
remoteCursorMoved: false.obs,
),
),
));
await tester.pump();
expect(registered, isNotEmpty);
expect(cursor.cache.localSize, Platform.isWindows ? 23.0 * dpr : 23.0);
final original = registered.last;
final beforeDensityChange = registered.length;
cursor.cache = _arrow(2);
cursor.notifyListeners();
await tester.pump();
expect(registered, hasLength(beforeDensityChange + 1));
final retina = registered.last;
expect(
[retina['width'], retina['height'], retina['hotX'], retina['hotY']],
[
original['width'],
original['height'],
original['hotX'],
original['hotY']
],
reason: 'Only the remote raster density changed; the local cursor must '
'retain its size and hotspot.',
);
zoom.value = true;
await tester.pump();
expect(cursor.cache.localSize, isNull);
final zoomScale = scale * (Platform.isWindows ? dpr : 1);
expect(cursor.cache.scale,
zoomScale < kMinCursorSize / 34 ? kMinCursorSize / 34 : zoomScale);
expect(cursor.cache.hotx / cursor.cache.scaledWidth, closeTo(4 / 17, 1e-9));
expect(cursor.cache.hoty / cursor.cache.scaledHeight, closeTo(4 / 23, 1e-9));
await tester.pumpWidget(const SizedBox.shrink());
cursor.dispose();
});
}
}

View File

@@ -1,181 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
import 'package:flutter_hbb/models/input_model.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:get/get.dart';
import 'package:image/image.dart' as img;
import 'package:provider/provider.dart';
const _viewport = Size(200, 160);
class _Image extends ChangeNotifier implements ImageModel {
@override
bool get useTextureRender => false;
@override
ui.Image? get image => null;
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _Canvas extends ChangeNotifier implements CanvasModel {
_Canvas(this.devicePixelRatio, {required this.style, required this.scale});
final String style;
@override
final double devicePixelRatio;
@override
final imageOverflow = false.obs;
@override
late final viewStyle = ViewStyle(
style: style,
width: _viewport.width,
height: _viewport.height,
displayWidth: 400,
displayHeight: 320,
);
@override
bool get cursorEmbedded => false;
@override
Size get size => _viewport;
@override
final double scale;
@override
double get x => 0;
@override
double get y => 0;
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _Cursor extends ChangeNotifier implements CursorModel {
_Cursor(this.cache);
@override
CursorData cache;
@override
ui.Image? get image => null;
@override
double get hotx => cache.hotxOrigin;
@override
double get hoty => cache.hotyOrigin;
@override
final Set<String> cachedKeys = {};
@override
void addKey(String key) => cachedKeys.add(key);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _Input extends Fake implements InputModel {
@override
final relativeMouseMode = false.obs;
}
class _Peer extends Fake implements FfiModel {
@override
final pi = PeerInfo();
@override
bool get isPeerLinux => false;
}
class _FFI extends Fake implements FFI {
@override
final inputModel = _Input();
@override
final ffiModel = _Peer();
}
CursorData _cursor(img.Image image,
{String id = 'sprite', double hotspot = 0}) {
return CursorData(
peerId: 'local-size-test',
id: id,
image: image,
scale: 1,
data: Platform.isWindows
? image.getBytes(order: img.ChannelOrder.bgra)
: Uint8List.fromList(img.encodePng(image)),
hotxOrigin: hotspot,
hotyOrigin: hotspot,
width: image.width,
height: image.height,
);
}
CursorData _arrow(int density) {
final image =
img.Image(width: 17 * density, height: 23 * density, numChannels: 4);
img.fill(image, color: img.ColorRgba8(255, 255, 255, 255));
return _cursor(image, id: 'arrow-$density', hotspot: 4.0 * density);
}
void main() {
testWidgets('pending or failed size measurement preserves the remote cursor',
(tester) async {
final channel = Platform.isWindows
? SystemChannels.mouseCursor
: const MethodChannel('flutter_custom_cursor');
final registered = <Map<dynamic, dynamic>>[];
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel,
(call) async {
if (call.method.startsWith('createCustomCursor')) {
final arguments = call.arguments as Map<dynamic, dynamic>;
registered.add(arguments);
return arguments['name'];
}
return null;
});
addTearDown(() => tester.binding.defaultBinaryMessenger
.setMockMethodCallHandler(channel, null));
const sizeChannel = MethodChannel('org.rustdesk.rustdesk/cursor');
final measurement = Completer<double>();
tester.binding.defaultBinaryMessenger
.setMockMethodCallHandler(sizeChannel, (_) => measurement.future);
addTearDown(() => tester.binding.defaultBinaryMessenger
.setMockMethodCallHandler(sizeChannel, null));
final cursor = _Cursor(_arrow(2));
await tester.pumpWidget(_view(cursor));
expect(registered, isNotEmpty);
measurement.completeError(PlatformException(code: 'cursor_size'));
await tester.pump();
expect(tester.takeException(), isA<PlatformException>());
expect(cursor.cache.localSize, isNull);
final scale = Platform.isWindows ? 1.0 : 0.5;
expect(cursor.cache.scaledWidth, 34 * scale);
expect(cursor.cache.scaledHeight, 46 * scale);
expect(cursor.cache.hotx, 8 * scale);
expect(cursor.cache.hoty, 8 * scale);
expect(registered.last['hotX'], 8 * scale);
expect(registered.last['hotY'], 8 * scale);
await tester.pumpWidget(const SizedBox.shrink());
cursor.dispose();
});
}
Widget _view(_Cursor cursor) => MediaQuery(
data: const MediaQueryData(devicePixelRatio: 2),
child: MultiProvider(
providers: [
ChangeNotifierProvider<ImageModel>(create: (_) => _Image()),
ChangeNotifierProvider<CanvasModel>(create: (_) => _Canvas(2,
style: kRemoteViewStyleAdaptive, scale: 0.25)),
ChangeNotifierProvider<CursorModel>.value(value: cursor),
],
child: ImagePaint(
ffi: _FFI(),
id: 'measurement-failure-test',
zoomCursor: false.obs,
cursorOverImage: true.obs,
keyboardEnabled: true.obs,
remoteCursorMoved: false.obs,
),
),
);

View File

@@ -1,187 +0,0 @@
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/utils/image.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
const _hotspot = Offset(4, 9);
const _remotePosition = Offset(100.25, 80.75);
const _canvasOffset = Offset(15.125, 10.25);
const _viewport = Size(200, 160);
class _CursorModel extends ChangeNotifier implements CursorModel {
_CursorModel(this.image);
@override
final ui.Image image;
@override
double get hotx => _hotspot.dx;
@override
double get hoty => _hotspot.dy;
@override
double get x => _remotePosition.dx;
@override
double get y => _remotePosition.dy;
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _ImageModel extends Fake implements ImageModel {
_ImageModel(this.useTextureRender);
@override
final bool useTextureRender;
}
class _Peer extends Fake implements FfiModel {
@override
final pi = PeerInfo();
@override
bool get isPeerLinux => false;
}
class _FFI extends Fake implements FFI {
_FFI(bool useTexture) : imageModel = _ImageModel(useTexture);
@override
final ImageModel imageModel;
@override
final ffiModel = _Peer();
}
class _CanvasModel extends ChangeNotifier implements CanvasModel {
_CanvasModel(String style, this.scale, bool useTexture)
: _ffi = _FFI(useTexture),
viewStyle = ViewStyle(
style: style,
width: _viewport.width,
height: _viewport.height,
displayWidth: _viewport.width.toInt(),
displayHeight: _viewport.height.toInt(),
);
final FFI _ffi;
@override
WeakReference<FFI> get parent => WeakReference(_ffi);
@override
final imageOverflow = false.obs;
@override
final ViewStyle viewStyle;
@override
double get x => _canvasOffset.dx;
@override
double get y => _canvasOffset.dy;
@override
final double scale;
@override
ScrollStyle get scrollStyle => ScrollStyle.scrollauto;
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _Canvas extends Fake implements Canvas {
double factor = 1;
Offset? position;
@override
void scale(double sx, [double? sy]) => factor *= sx;
@override
void drawImage(ui.Image image, Offset offset, Paint paint) {
position = offset * factor;
}
}
void main() {
for (final (style, zoom, dpr, source, canvasScale, scale, texture) in [
(kRemoteViewStyleAdaptive, false, 2.0, (48, 64), 0.375, 0.375, true),
(kRemoteViewStyleAdaptive, false, 3.0, (48, 64), 0.25, 0.25, true),
(kRemoteViewStyleAdaptive, true, 3.0, (48, 64), 0.375, 0.375, true),
(kRemoteViewStyleOriginal, false, 2.0, (48, 64), 0.5, 0.5, true),
(kRemoteViewStyleOriginal, true, 2.0, (48, 64), 0.5, 0.5, true),
for (final zoom in [false, true])
for (final scale in [0.25, 2.0])
for (final texture in [false, true])
(kRemoteViewStyleCustom, zoom, 2.0, (48, 64), scale, scale, texture),
(
kRemoteViewStyleAdaptive,
false,
2.0,
(9, 18),
0.1,
Platform.isWindows ? 2 / 3 : 4 / 3,
true
),
(
kRemoteViewStyleAdaptive,
true,
2.0,
(9, 18),
0.1,
Platform.isWindows ? 2 / 3 : 4 / 3,
true
),
(kRemoteViewStyleAdaptive, false, 2.25, (48, 48), 0.375, 0.375, false),
(
kRemoteViewStyleAdaptive,
true,
2.0,
(9, 18),
0.1,
Platform.isWindows ? 2 / 3 : 4 / 3,
false
),
]) {
testWidgets(
'$style zoom=$zoom dpr=$dpr source=$source texture=$texture keeps remote geometry',
(tester) async {
final image = (await tester.runAsync(
() => createTestImage(width: source.$1, height: source.$2)))!;
addTearDown(image.dispose);
await tester.pumpWidget(MediaQuery(
data: MediaQueryData(devicePixelRatio: dpr),
child: MultiProvider(
providers: [
ChangeNotifierProvider<CursorModel>(
create: (_) => _CursorModel(image)),
ChangeNotifierProvider<CanvasModel>(
create: (_) => _CanvasModel(style, canvasScale, texture)),
],
child: CursorPaint(id: 'cursor-test', zoomCursor: zoom.obs),
),
));
final painter = tester
.widget<CustomPaint>(find.byType(CustomPaint))
.painter! as ImagePainter;
expect(painter.image, same(image));
expect(painter.scale, scale);
var imageOrigin = _canvasOffset;
if (!texture) {
final background = _Canvas();
ImagePainter(
image: image,
x: _canvasOffset.dx / canvasScale,
y: _canvasOffset.dy / canvasScale,
scale: canvasScale,
).paint(background, _viewport);
imageOrigin = background.position!;
}
final target = _remotePosition * canvasScale + imageOrigin;
expect((Offset(painter.x, painter.y) + _hotspot) * scale, target);
final canvas = _Canvas();
painter.paint(canvas, _viewport);
final position = canvas.position! + _hotspot * canvas.factor;
expect(position.dx, closeTo(target.dx, 1e-9));
expect(position.dy, closeTo(target.dy, 1e-9));
});
}
}

View File

@@ -1,43 +0,0 @@
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:image/image.dart' as img;
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('sparse remote artwork cannot amplify the native bitmap without limit',
() {
const sourceSize = 64;
const bitmapLimit = 512;
final image = img.Image(width: sourceSize, height: sourceSize, numChannels: 4)
..setPixelRgba(0, 0, 255, 255, 255, 255);
final cache = CursorData(
peerId: 'sparse-cursor',
id: '1',
image: image,
scale: 1,
data: Uint8List.fromList(img.encodePng(image)),
hotxOrigin: 32,
hotyOrigin: 48,
width: sourceSize,
height: sourceSize,
)..localSize = sourceSize.toDouble();
final messages = <String>[];
final previous = debugPrint;
debugPrint = (String? message, {int? wrapWidth}) {
messages.add(message ?? '');
};
addTearDown(() => debugPrint = previous);
cache.updateGetKey(1);
expect(max(cache.scaledWidth, cache.scaledHeight), bitmapLimit);
expect(cache.hotx, 32 * cache.scaledWidth / sourceSize);
expect(cache.hoty, 48 * cache.scaledHeight / sourceSize);
expect(messages.single, contains('bitmap limit'));
});
}

View File

@@ -1,64 +0,0 @@
@TestOn('browser')
library;
import 'dart:convert';
import 'dart:js' as js;
import 'dart:typed_data';
import 'package:flutter_hbb/models/model.dart' as model;
import 'package:flutter_hbb/web/custom_cursor.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:image/image.dart' as img;
class _CursorModel extends Fake implements model.CursorModel {
@override
final Set<String> cachedKeys = {};
@override
void addKey(String key) => cachedKeys.add(key);
}
void main() {
test('Web cursor rounds its bitmap and hotspot together', () async {
Map<String, dynamic>? registered;
final original = js.context['setByName'];
js.context['setByName'] = js.allowInterop((String name, String value) {
expect(name, 'cursor');
registered = jsonDecode(value) as Map<String, dynamic>;
});
addTearDown(() => js.context['setByName'] = original);
const sourceSide = 48;
final image = img.Image(width: sourceSide, height: sourceSide, numChannels: 4);
img.fill(image, color: img.ColorRgba8(255, 255, 255, 255));
for (final (sourceHotspot, scale, outputSide, expectedHotspot) in [
((7.0, 7.0), 633 / 1600, 19, (3, 3)),
((21.0, 23.0), 633 / 1600, 19, (8, 9)),
((22.0, 22.0), 633 / 1600, 19, (9, 9)),
((21.0, 23.0), 0.05, 12, (5, 6)),
((21.0, 23.0), 0.5, 24, (11, 12)),
((7.0, 7.0), 1.0, 48, (7, 7)),
]) {
final cache = model.CursorData(
peerId: 'web-cursor-test',
id: '$sourceHotspot',
image: image,
scale: 1,
data: Uint8List.fromList(img.encodePng(image)),
hotxOrigin: sourceHotspot.$1,
hotyOrigin: sourceHotspot.$2,
width: sourceSide,
height: sourceSide,
);
final cursor = buildCursorOfCache(_CursorModel(), scale, cache);
final session = cursor.createSession(1);
await session.activate();
final uri = Uri.parse(registered!['url'] as String);
final bitmap = img.decodePng(uri.data!.contentAsBytes())!;
expect((bitmap.width, bitmap.height), (outputSide, outputSide));
expect((registered!['hotx'], registered!['hoty']), expectedHotspot);
session.dispose();
await deleteCustomCursor(cache.updateGetKey(scale));
}
});
}