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.
This commit is contained in:
fufesou
2026-09-13 10:04:12 +08:00
parent 32e9895294
commit bbc8ee3bdb
2 changed files with 39 additions and 9 deletions

View File

@@ -1469,7 +1469,10 @@ class CursorPaint extends StatelessWidget {
final ffi = canvas.parent.target!; final ffi = canvas.parent.target!;
final peer = ffi.ffiModel; final peer = ffi.ffiModel;
if (ffi.imageModel.useTextureRender || peer.pi.forceTextureRender) { 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; var scale = canvas.scale;
final displays = peer.pi.getCurDisplays(); final displays = peer.pi.getCurDisplays();

View File

@@ -4,6 +4,7 @@ import 'dart:ui' as ui;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/remote_page.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/models/model.dart';
import 'package:flutter_hbb/utils/image.dart'; import 'package:flutter_hbb/utils/image.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@@ -12,7 +13,7 @@ import 'package:provider/provider.dart';
const _hotspot = Offset(4, 9); const _hotspot = Offset(4, 9);
const _remotePosition = Offset(100.25, 80.75); 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); const _viewport = Size(200, 160);
class _CursorModel extends ChangeNotifier implements CursorModel { class _CursorModel extends ChangeNotifier implements CursorModel {
@@ -44,11 +45,18 @@ class _Density extends Fake implements CursorData {
final double pixelRatio; final double pixelRatio;
} }
class _ImageModel extends Fake implements ImageModel { class _ImageModel extends ChangeNotifier implements ImageModel {
_ImageModel(this.useTextureRender); _ImageModel(this.useTextureRender);
@override @override
final bool useTextureRender; 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 { class _Display extends Display {
@@ -64,7 +72,10 @@ class _Display extends Display {
class _Peer extends Fake implements FfiModel { class _Peer extends Fake implements FfiModel {
@override @override
final pi = PeerInfo(); final pi = PeerInfo()
..displays.add(Display()
..width = _viewport.width.toInt()
..height = _viewport.height.toInt());
@override @override
bool isPeerLinux = false; bool isPeerLinux = false;
@override @override
@@ -79,6 +90,8 @@ class _FFI extends Fake implements FFI {
@override @override
final _Peer ffiModel = _Peer(); final _Peer ffiModel = _Peer();
@override @override
final textureModel = _TextureModel();
@override
late CanvasModel canvasModel; late CanvasModel canvasModel;
} }
@@ -151,16 +164,29 @@ Future<ImagePainter> _paintCursor(WidgetTester tester, CanvasModel canvas,
final image = (await tester final image = (await tester
.runAsync(() => createTestImage(width: source.$1, height: source.$2)))!; .runAsync(() => createTestImage(width: source.$1, height: source.$2)))!;
addTearDown(image.dispose); 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), data: MediaQueryData(devicePixelRatio: dpr),
child: MultiProvider( child: MultiProvider(
providers: [ providers: [
ChangeNotifierProvider<ImageModel>.value(value: canvas.parent.target!.imageModel),
ChangeNotifierProvider<CursorModel>(create: (_) => _CursorModel(image, position, density)), ChangeNotifierProvider<CursorModel>(create: (_) => _CursorModel(image, position, density)),
ChangeNotifierProvider<CanvasModel>(create: (_) => canvas), ChangeNotifierProvider<CanvasModel>(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<CustomPaint>(find.byType(CustomPaint)).painter! final painter = tester.widget<CustomPaint>(find.byType(CustomPaint)).painter!
as ImagePainter; as ImagePainter;
expect(painter.image, same(image)); expect(painter.image, same(image));
@@ -191,7 +217,7 @@ void _linuxDisplayTests() {
: _remotePosition); : _remotePosition);
final pixelScale = 2 / displayScale; final pixelScale = 2 / displayScale;
expect(painter.scale, pixelScale); expect(painter.scale, pixelScale);
final origin = texture ? _canvasOffset final origin = texture ? tester.getTopLeft(find.byType(Texture).first)
: Offset((_canvasOffset.dx / pixelScale).toInt() * pixelScale, : Offset((_canvasOffset.dx / pixelScale).toInt() * pixelScale,
(_canvasOffset.dy / pixelScale).toInt() * pixelScale); (_canvasOffset.dy / pixelScale).toInt() * pixelScale);
final position = allDisplays final position = allDisplays
@@ -228,7 +254,8 @@ void main() {
tester, _CanvasModel(style, canvasScale, texture), tester, _CanvasModel(style, canvasScale, texture),
dpr: dpr, zoom: zoom, source: source, density: density); dpr: dpr, zoom: zoom, source: source, density: density);
expect(painter.scale, scale); expect(painter.scale, scale);
var imageOrigin = _canvasOffset; var imageOrigin = texture
? tester.getTopLeft(find.byType(Texture).first) : _canvasOffset;
if (!texture) { if (!texture) {
final background = _Canvas(); final background = _Canvas();
ImagePainter( ImagePainter(