diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 3110f08b6..091e69047 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -1113,7 +1113,11 @@ class _ImagePaintState extends State { mouseRegion({child}) => Obx(() { double getCursorScale() { final cursor = Provider.of(context); - final peerDpr = cursor.cache?.pixelRatio ?? 0; + // Predefined artwork must not inherit the cached remote bitmap's DPI. + final cache = keyboardEnabled.isTrue + ? cursor.cache ?? preDefaultCursor.cache + : preForbiddenCursor.cache; + final peerDpr = cache?.pixelRatio ?? 0; if (!isWeb && isViewScaled() && !zoomCursor.value && peerDpr > 0) { return (isWindows ? dpr : 1.0) / peerDpr; } @@ -1417,9 +1421,10 @@ class CursorPaint extends StatelessWidget { // 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) - + // Match the integer centering in _buildCrossScrollbarFromLayout. + 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) - + cy = (c.size.height > imageHeight ? (c.size.height - imageHeight) ~/ 2 : 0) - imageHeight * c.scrollY; } diff --git a/flutter/test/cursor_dpi_policy_test.dart b/flutter/test/cursor_dpi_policy_test.dart index 6c61eb5ff..08673ba82 100644 --- a/flutter/test/cursor_dpi_policy_test.dart +++ b/flutter/test/cursor_dpi_policy_test.dart @@ -4,6 +4,7 @@ import 'dart:ui' as ui; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_custom_cursor/cursor_manager.dart' show CursorManager; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/remote_page.dart'; import 'package:flutter_hbb/models/input_model.dart'; @@ -167,6 +168,9 @@ void main() { test('live DPR changes invalidate a cached native cursor', () => _checkDprChange(view, registrations)); for (final style in [kRemoteViewStyleAdaptive, kRemoteViewStyleCustom]) { + testWidgets('$style forbidden cursor ignores remote DPR', (tester) => + tester.runAsync(() => _checkPolicy(tester, + (style, false, 2, 0.25, 0.5), registrations, dpr: 1))); testWidgets('Linux $style zoom follows video pixels', (tester) => tester.runAsync( () => _checkPolicy(tester, (style, true, 2, 1.0, windows ? 1.0 : 0.5), registrations, linux: true))); @@ -176,9 +180,10 @@ void main() { Future _checkPolicy( WidgetTester tester, (String, bool, int, double, double) testCase, - List> registrations, {bool linux = false}) async { + List> registrations, + {bool linux = false, double dpr = 2}) async { final (style, zoom, density, viewScale, expectedScale) = testCase; - tester.view.devicePixelRatio = 2; + tester.view.devicePixelRatio = dpr; final data = await _data(density, '$style-$zoom-$density'); final originalBytes = data.data; // A stale cached DPR must not affect the cursor when the window moves. @@ -187,8 +192,9 @@ Future _checkPolicy( ffi.ffiModel.isPeerLinux = linux; if (linux) ffi.ffiModel.pi.displays.add(_LinuxDisplay()); final cursor = _Cursor(data, ffi); + final keyboardEnabled = true.obs; await tester.pumpWidget(MediaQuery( - data: const MediaQueryData(devicePixelRatio: 2), + data: MediaQueryData(devicePixelRatio: dpr), child: MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => _Image()), @@ -200,10 +206,17 @@ Future _checkPolicy( id: 'dpi-policy', zoomCursor: zoom.obs, cursorOverImage: true.obs, - keyboardEnabled: true.obs, + keyboardEnabled: keyboardEnabled, remoteCursorMoved: false.obs, )), )); + final revoke = !zoom && style != kRemoteViewStyleOriginal; + if (revoke) { + await Future.wait(cursor.cachedKeys + .map(CursorManager.instance.ensureCursorRegistered)); + keyboardEnabled.value = false; + await tester.pump(); + } await tester.pumpWidget(const SizedBox.shrink()); for (final key in cursor.cachedKeys) { await deleteCustomCursor(key); @@ -215,11 +228,17 @@ Future _checkPolicy( expect(data.hotx, closeTo(data.hotxOrigin * expectedScale, 1e-9)); expect(data.hoty, closeTo(data.hotyOrigin * expectedScale, 1e-9)); expect(data.data, same(originalBytes)); + if (revoke) { + final args = registrations.last; + expect(args['name'], contains('_${kPreForbiddenCursorId}_')); + expect((args['width'], args['height']), (32, 32)); + expect((args['hotX'], args['hotY']), (0.0, 0.0)); + } if (style == kRemoteViewStyleAdaptive && !zoom && density > 0) { - final args = registrations.single; - expect((args['width'], args['height']), (Platform.isLinux ? 36 : 18, 36)); - expect((args['hotX'], args['hotY']), (8.0, 18.0)); - expect(args['imagePixelRatio'], 2.0); + final args = registrations.first; + expect((args['width'], args['height']), ((Platform.isLinux ? 18 : 9) * dpr, 18 * dpr)); + expect((args['hotX'], args['hotY']), (4 * dpr, 9 * dpr)); + expect(args['imagePixelRatio'], dpr); } } diff --git a/flutter/test/cursor_paint_scale_test.dart b/flutter/test/cursor_paint_scale_test.dart index 389998303..37424b0f6 100644 --- a/flutter/test/cursor_paint_scale_test.dart +++ b/flutter/test/cursor_paint_scale_test.dart @@ -118,16 +118,18 @@ class _CanvasModel extends ChangeNotifier implements CanvasModel { } class _ScrollbarCanvasModel extends _CanvasModel { - _ScrollbarCanvasModel(String style) : super(style, 2, true) { + _ScrollbarCanvasModel(String style, {Size? frame, double scale = 2}) + : super(style, scale, true) { imageOverflow.value = true; + if (frame != null) _ffi.ffiModel.rect = Offset.zero & frame; } @override ScrollStyle get scrollStyle => ScrollStyle.scrollbar; @override - double get scrollX => 0.1; + double get scrollX => _ffi.ffiModel.rect.width * scale > size.width ? 0.1 : 0; @override - double get scrollY => 0.2; + double get scrollY => _ffi.ffiModel.rect.height * scale > size.height ? 0.2 : 0; } class _Canvas extends Fake implements Canvas { @@ -249,12 +251,20 @@ void main() { }); } for (final style in [kRemoteViewStyleOriginal, kRemoteViewStyleCustom]) { - testWidgets('$style painted cursor follows scrollbar position', - (tester) async { - final painter = await _paintCursor(tester, _ScrollbarCanvasModel(style)); - 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); - }); + for (final (frame, scale, offset) in [ + (_viewport, 2.0, const Offset(-40, -64)), + (const Size(199, 320), 1.0, const Offset(0, -64)), + (const Size(198, 320), 1.0, const Offset(1, -64)), + (const Size(400, 159), 1.0, const Offset(-40, 0)), + (const Size(400, 158), 1.0, const Offset(-40, 1)), + ]) { + testWidgets('$style frame=$frame painted cursor follows scrollbar layout', + (tester) async { + final painter = await _paintCursor( + tester, _ScrollbarCanvasModel(style, frame: frame, scale: scale)); + final target = _remotePosition * scale + offset; + expect((Offset(painter.x, painter.y) + _hotspot) * painter.scale, target); + }); + } } }