diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 433d37be6..b83d3c303 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2893,7 +2893,7 @@ class CursorData { double _checkUpdateScale(double scale) { double oldScale = this.scale; - if (!_usesLogicalCursorPixels && scale != 1.0) { + if (scale != 1.0) { // Update data if scale changed. final tgtWidth = (width * scale).toInt(); final tgtHeight = (height * scale).toInt(); @@ -2929,8 +2929,8 @@ class CursorData { } this.scale = scale; - hotx = hotxOrigin * (_usesLogicalCursorPixels ? scaledWidth / width : scale); - hoty = hotyOrigin * (_usesLogicalCursorPixels ? scaledHeight / height : scale); + hotx = hotxOrigin * scaledWidth / width; + hoty = hotyOrigin * scaledHeight / height; return scale; } diff --git a/flutter/lib/native/custom_cursor.dart b/flutter/lib/native/custom_cursor.dart index 0dcb476f8..af73f2694 100644 --- a/flutter/lib/native/custom_cursor.dart +++ b/flutter/lib/native/custom_cursor.dart @@ -16,8 +16,8 @@ deleteCustomCursor(String key) => resetSystemCursor() {} double _nativeHotspot(double hotspot, int bitmapSize) { - if (!isLinux) return hotspot; - // GDK takes integer hotspots inside the bitmap; avoid truncating toward zero. + if (!isLinux && !isWindows) return hotspot; + // GDK and Win32 take integer hotspots inside the bitmap; use the nearest pixel. return min(hotspot.round(), bitmapSize - 1).toDouble(); } diff --git a/flutter/test/cursor_dpi_scale_test.dart b/flutter/test/cursor_dpi_scale_test.dart index cafc40610..0bbb475ff 100644 --- a/flutter/test/cursor_dpi_scale_test.dart +++ b/flutter/test/cursor_dpi_scale_test.dart @@ -7,17 +7,31 @@ 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, scale, artwork size, Linux hotspot. -const _cases = [ - ((9, 18), (4.0, 9.0), 1 / 3, (3, 6), (1.0, 3.0)), - ((17, 23), (4.0, 4.0), 1 / 3, (6, 8), (1.0, 1.0)), - ((32, 16), (16.0, 8.0), 0.5, (16, 8), (8.0, 4.0)), - ((24, 24), (11.0, 11.0), 1 / 3, (8, 8), (4.0, 4.0)), - ((3, 3), (2.0, 2.0), 1 / 3, (1, 1), (0.0, 0.0)), - ((1, 48), (0.0, 24.0), 1 / 3, (1, 16), (0.0, 8.0)), - ((9, 18), (4.0, 9.0), 1.0, (9, 18), (4.0, 9.0)), - ((9, 18), (4.0, 9.0), 7 / 18, (4, 7), (2.0, 4.0)), - ((24, 24), (4.0, 4.0), 0.1, (2, 2), (0.0, 0.0)), +// 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; @@ -51,7 +65,9 @@ CursorData _cursorData((int, int) size, (double, double) hotspot) { id: 'native', image: image, scale: 1, - data: Uint8List.fromList(img.encodePng(image)), + data: Platform.isWindows + ? image.getBytes(order: img.ChannelOrder.bgra) + : Uint8List.fromList(img.encodePng(image)), hotxOrigin: hotspot.$1, hotyOrigin: hotspot.$2, width: size.$1, @@ -61,11 +77,17 @@ CursorData _cursorData((int, int) size, (double, double) hotspot) { Future> _register( WidgetTester tester, CursorData data, double scale) async { - const channel = MethodChannel('flutter_custom_cursor'); + final channel = Platform.isWindows + ? SystemChannels.mouseCursor + : const MethodChannel('flutter_custom_cursor'); Map? registered; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, (call) async { - expect(call.method, 'createCustomCursor'); + expect( + call.method, + Platform.isWindows + ? 'createCustomCursor/windows' + : 'createCustomCursor'); registered = call.arguments as Map; return registered!['name']; }); @@ -129,16 +151,30 @@ void main() { } }); - for (final (source, hotspot, scale, size, linuxHotspot) in _cases) { + 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 = img.decodePng(data.data!)!; - final native = img.decodePng(cursor['buffer'] as Uint8List)!; + 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, scale); + expect(data.scale, effectiveScale); expect((artwork.width, artwork.height), size); expect(data.hotx / size.$1, closeTo(hotspot.$1 / source.$1, _hotspotTolerance)); @@ -146,12 +182,15 @@ void main() { 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 ? linuxHotspot : (data.hotx, data.hoty)); + 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); } - }, skip: Platform.isWindows); + }); } }