From bbc8ee3bdb24d0ef11d3eb5e23b458094d387c07 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 13 Sep 2026 10:04:12 +0800 Subject: [PATCH] Align painted cursors with Linux texture origins Use the truncated Linux texture origin while preserving fractional cursor positions and hotspots. Compare painted cursor geometry with the actual video widget in the existing regression tests. --- flutter/lib/desktop/pages/remote_page.dart | 5 ++- flutter/test/cursor_paint_scale_test.dart | 43 ++++++++++++++++++---- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 091e69047..14a6a111c 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -1469,7 +1469,10 @@ class CursorPaint extends StatelessWidget { final ffi = canvas.parent.target!; final peer = ffi.ffiModel; if (ffi.imageModel.useTextureRender || peer.pi.forceTextureRender) { - return null; + // Match Linux's texture origin without rounding the cursor or hotspot. + return isLinux + ? Offset(canvas.x.toInt().toDouble(), canvas.y.toInt().toDouble()) + : null; } var scale = canvas.scale; final displays = peer.pi.getCurDisplays(); diff --git a/flutter/test/cursor_paint_scale_test.dart b/flutter/test/cursor_paint_scale_test.dart index 37424b0f6..857bdf968 100644 --- a/flutter/test/cursor_paint_scale_test.dart +++ b/flutter/test/cursor_paint_scale_test.dart @@ -4,6 +4,7 @@ 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/desktop_render_texture.dart'; import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/utils/image.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -12,7 +13,7 @@ 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 _canvasOffset = Offset(15.125, -10.25); const _viewport = Size(200, 160); class _CursorModel extends ChangeNotifier implements CursorModel { @@ -44,11 +45,18 @@ class _Density extends Fake implements CursorData { final double pixelRatio; } -class _ImageModel extends Fake implements ImageModel { +class _ImageModel extends ChangeNotifier implements ImageModel { _ImageModel(this.useTextureRender); @override final bool useTextureRender; + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _TextureModel extends Fake implements TextureModel { + @override + RxInt getTextureId(int display) => 0.obs; } class _Display extends Display { @@ -64,7 +72,10 @@ class _Display extends Display { class _Peer extends Fake implements FfiModel { @override - final pi = PeerInfo(); + final pi = PeerInfo() + ..displays.add(Display() + ..width = _viewport.width.toInt() + ..height = _viewport.height.toInt()); @override bool isPeerLinux = false; @override @@ -79,6 +90,8 @@ class _FFI extends Fake implements FFI { @override final _Peer ffiModel = _Peer(); @override + final textureModel = _TextureModel(); + @override late CanvasModel canvasModel; } @@ -151,16 +164,29 @@ Future _paintCursor(WidgetTester tester, CanvasModel canvas, final image = (await tester .runAsync(() => createTestImage(width: source.$1, height: source.$2)))!; addTearDown(image.dispose); - await tester.pumpWidget(MediaQuery( + tester.view.devicePixelRatio = dpr; + tester.view.physicalSize = _viewport * dpr; + addTearDown(tester.view.reset); + await tester.pumpWidget(Directionality(textDirection: TextDirection.ltr, + child: MediaQuery( data: MediaQueryData(devicePixelRatio: dpr), child: MultiProvider( providers: [ + ChangeNotifierProvider.value(value: canvas.parent.target!.imageModel), ChangeNotifierProvider(create: (_) => _CursorModel(image, position, density)), ChangeNotifierProvider(create: (_) => canvas), ], - child: CursorPaint(id: 'cursor-test', zoomCursor: zoom.obs), + child: Stack(fit: StackFit.expand, children: [ + // Measure the video's origin instead of copying its rounding policy. + if (canvas.parent.target!.imageModel.useTextureRender && + canvas.scrollStyle == ScrollStyle.scrollauto) + ImagePaint(ffi: canvas.parent.target!, id: 'cursor-test', + zoomCursor: zoom.obs, cursorOverImage: false.obs, + keyboardEnabled: true.obs, remoteCursorMoved: false.obs), + CursorPaint(id: 'cursor-test', zoomCursor: zoom.obs), + ]), ), - )); + ))); final painter = tester.widget(find.byType(CustomPaint)).painter! as ImagePainter; expect(painter.image, same(image)); @@ -191,7 +217,7 @@ void _linuxDisplayTests() { : _remotePosition); final pixelScale = 2 / displayScale; expect(painter.scale, pixelScale); - final origin = texture ? _canvasOffset + final origin = texture ? tester.getTopLeft(find.byType(Texture).first) : Offset((_canvasOffset.dx / pixelScale).toInt() * pixelScale, (_canvasOffset.dy / pixelScale).toInt() * pixelScale); final position = allDisplays @@ -228,7 +254,8 @@ void main() { tester, _CanvasModel(style, canvasScale, texture), dpr: dpr, zoom: zoom, source: source, density: density); expect(painter.scale, scale); - var imageOrigin = _canvasOffset; + var imageOrigin = texture + ? tester.getTopLeft(find.byType(Texture).first) : _canvasOffset; if (!texture) { final background = _Canvas(); ImagePainter(