diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index ccc135df5..30cfc1173 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -1423,10 +1423,14 @@ class CursorPaint extends StatelessWidget { } final image = m.image ?? preDefaultCursor.image; - final nativePixels = isWindows ? MediaQuery.devicePixelRatioOf(context) : 1.0; + // Match native registration's logical minimum for density-aware scaled views. + final logicalMinimum = (m.cache?.pixelRatio ?? 0) > 0 && + c.viewStyle.style != kRemoteViewStyleOriginal; + final nativePixels = isWindows && !logicalMinimum + ? MediaQuery.devicePixelRatioOf(context) : 1.0; // Show remote cursor follows the image scale, independently of Zoom cursor. double scale = _cursorImageScale(c.parent.target!, m); - if (image != null && scale * nativePixels != 1.0) { + if (image != null && (logicalMinimum || scale * nativePixels != 1.0)) { final sx = kMinCursorSize / (image.width * nativePixels); final sy = kMinCursorSize / (image.height * nativePixels); // Preserve Original's short-edge minimum; scaled views use the long edge. diff --git a/flutter/test/cursor_paint_scale_test.dart b/flutter/test/cursor_paint_scale_test.dart index 78d037089..1a5c84827 100644 --- a/flutter/test/cursor_paint_scale_test.dart +++ b/flutter/test/cursor_paint_scale_test.dart @@ -16,9 +16,12 @@ const _canvasOffset = Offset(15.125, 10.25); const _viewport = Size(200, 160); class _CursorModel extends ChangeNotifier implements CursorModel { - _CursorModel(this.image, this.position); + _CursorModel(this.image, this.position, this.density); final Offset position; + final double density; + @override + CursorData get cache => _Density(density); @override final ui.Image image; @@ -35,6 +38,12 @@ class _CursorModel extends ChangeNotifier implements CursorModel { dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } +class _Density extends Fake implements CursorData { + _Density(this.pixelRatio); + @override + final double pixelRatio; +} + class _ImageModel extends Fake implements ImageModel { _ImageModel(this.useTextureRender); @@ -136,7 +145,7 @@ class _Canvas extends Fake implements Canvas { Future _paintCursor(WidgetTester tester, CanvasModel canvas, {double dpr = 2, bool zoom = true, (int, int) source = (48, 64), - Offset position = _remotePosition}) async { + Offset position = _remotePosition, double density = 0}) async { final image = (await tester .runAsync(() => createTestImage(width: source.$1, height: source.$2)))!; addTearDown(image.dispose); @@ -144,7 +153,7 @@ Future _paintCursor(WidgetTester tester, CanvasModel canvas, data: MediaQueryData(devicePixelRatio: dpr), child: MultiProvider( providers: [ - ChangeNotifierProvider(create: (_) => _CursorModel(image, position)), + ChangeNotifierProvider(create: (_) => _CursorModel(image, position, density)), ChangeNotifierProvider(create: (_) => canvas), ], child: CursorPaint(id: 'cursor-test', zoomCursor: zoom.obs), @@ -156,7 +165,7 @@ Future _paintCursor(WidgetTester tester, CanvasModel canvas, return painter; } -void main() { +void _linuxDisplayTests() { for (final (texture, displayScale, allDisplays) in [ (false, 2.0, false), (true, 2.0, false), (true, 2.0, true), (false, 1.0, false), (true, 1.0, false), @@ -175,7 +184,7 @@ void main() { : peer.pi.displays.length - 1; peer.rect = Rect.fromLTWH(allDisplays ? -960 : 0, 0, 3840, 2160); final painter = await _paintCursor(tester, canvas, - dpr: 1, zoom: false, source: (64, 64), + dpr: 1, zoom: false, source: (64, 64), density: 2, position: allDisplays ? _remotePosition + const Offset(960, 0) : _remotePosition); final pixelScale = 2 / displayScale; @@ -189,6 +198,35 @@ void main() { position * 2 + origin); }); } +} + +void _minimumTests() { + for (final (style, dpr, source, viewScale, expected) in [ + (kRemoteViewStyleAdaptive, 1.0, (48, 64), 0.05, 0.1875), + (kRemoteViewStyleAdaptive, 2.0, (48, 64), 0.05, 0.1875), + (kRemoteViewStyleCustom, 1.0, (48, 64), 0.05, 0.1875), + (kRemoteViewStyleCustom, 2.0, (48, 64), 0.05, 0.1875), + (kRemoteViewStyleCustom, 2.0, (8, 10), 1.0, 1.2), + (kRemoteViewStyleOriginal, 1.0, (48, 64), 0.05, 0.25), + (kRemoteViewStyleOriginal, 2.0, (48, 64), 0.05, Platform.isWindows ? 0.125 : 0.25), + ]) { + testWidgets('density-aware $style DPR=$dpr source=$source minimum', + (tester) async { + final painter = await _paintCursor(tester, + _CanvasModel(style, viewScale, true), + dpr: dpr, source: source, density: 2); + expect(painter.scale, expected); + final hotspot = (Offset(painter.x, painter.y) + _hotspot) * painter.scale; + final target = _remotePosition * viewScale + _canvasOffset; + expect(hotspot.dx, closeTo(target.dx, 1e-9)); + expect(hotspot.dy, closeTo(target.dy, 1e-9)); + }); + } +} + +void main() { + _linuxDisplayTests(); + _minimumTests(); final minimumScale = Platform.isWindows ? 1 / 3 : 2 / 3; for (final (style, zoom, dpr, source, canvasScale, scale, texture) in [ (kRemoteViewStyleAdaptive, false, 2.0, (48, 64), 0.375, 0.375, true),