diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 3abac2056..cf33ff48b 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -1401,22 +1401,20 @@ class CursorPaint extends StatelessWidget { final imageOffset = _softwareImageOffset(c); double cx = imageOffset?.dx ?? c.x; double cy = imageOffset?.dy ?? c.y; - if (c.viewStyle.style == kRemoteViewStyleOriginal && - c.scrollStyle == ScrollStyle.scrollbar) { + if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) { final rect = c.parent.target!.ffiModel.rect; if (rect == null) { // unreachable! debugPrint('unreachable! The displays rect is null.'); return Container(); } - if (cx < 0) { - final imageWidth = rect.width * c.scale; - cx = -imageWidth * c.scrollX; - } - if (cy < 0) { - final imageHeight = rect.height * c.scale; - cy = -imageHeight * c.scrollY; - } + // Pan offsets can be stale after leaving the image; scrollbars do not use them. + final imageWidth = rect.width * c.scale; + final imageHeight = rect.height * c.scale; + cx = (c.size.width > imageWidth ? (c.size.width - imageWidth) / 2 : 0) - + imageWidth * c.scrollX; + cy = (c.size.height > imageHeight ? (c.size.height - imageHeight) / 2 : 0) - + imageHeight * c.scrollY; } final image = m.image ?? preDefaultCursor.image; diff --git a/flutter/test/cursor_paint_scale_test.dart b/flutter/test/cursor_paint_scale_test.dart index bb4632772..26a4e1917 100644 --- a/flutter/test/cursor_paint_scale_test.dart +++ b/flutter/test/cursor_paint_scale_test.dart @@ -45,6 +45,8 @@ class _Peer extends Fake implements FfiModel { final pi = PeerInfo(); @override bool get isPeerLinux => false; + @override + Rect get rect => Offset.zero & _viewport; } class _FFI extends Fake implements FFI { @@ -82,11 +84,26 @@ class _CanvasModel extends ChangeNotifier implements CanvasModel { final double scale; @override ScrollStyle get scrollStyle => ScrollStyle.scrollauto; + @override + Size get size => _viewport; @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } +class _ScrollbarCanvasModel extends _CanvasModel { + _ScrollbarCanvasModel(String style) : super(style, 2, true) { + imageOverflow.value = true; + } + + @override + ScrollStyle get scrollStyle => ScrollStyle.scrollbar; + @override + double get scrollX => 0.1; + @override + double get scrollY => 0.2; +} + class _Canvas extends Fake implements Canvas { double factor = 1; Offset? position; @@ -161,4 +178,31 @@ void main() { expect(position.dy, closeTo(target.dy, 1e-9)); }); } + for (final style in [kRemoteViewStyleOriginal, kRemoteViewStyleCustom]) { + testWidgets('$style painted cursor follows scrollbar position', + (tester) async { + final image = (await tester.runAsync( + () => createTestImage(width: 48, height: 64)))!; + addTearDown(image.dispose); + await tester.pumpWidget(MediaQuery( + data: const MediaQueryData(devicePixelRatio: 2), + child: MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (_) => _CursorModel(image)), + ChangeNotifierProvider( + create: (_) => _ScrollbarCanvasModel(style)), + ], + child: CursorPaint(id: 'cursor-test', zoomCursor: true.obs), + ), + )); + final painter = tester + .widget(find.byType(CustomPaint)) + .painter! as ImagePainter; + final target = _remotePosition * 2 - + Offset(_viewport.width * 2 * 0.1, _viewport.height * 2 * 0.2); + expect( + (Offset(painter.x, painter.y) + _hotspot) * painter.scale, target); + }); + } }