mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-14 00:11:01 +03:00
fix(cursor): preserve unzoomed Web size across browser DPRs
This commit is contained in:
@@ -1127,7 +1127,7 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
} else {
|
||||
if (zoomCursor.value || isViewOriginal()) {
|
||||
cursorScale = s;
|
||||
} else {
|
||||
} else if (!isWeb) {
|
||||
// NSCursor and GdkCursor treat the bitmap size as logical
|
||||
// pixels, so an unzoomed cursor must be shrunk by the DPR to
|
||||
// keep 1 remote px == 1 physical px, the size Original view
|
||||
|
||||
@@ -9,10 +9,16 @@ import 'dart:js' as js;
|
||||
import 'dart:typed_data';
|
||||
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/input_model.dart';
|
||||
import 'package:flutter_hbb/models/model.dart' as model;
|
||||
import 'package:flutter_hbb/web/custom_cursor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class _CursorModel extends Fake implements model.CursorModel {
|
||||
@override
|
||||
@@ -23,6 +29,12 @@ class _CursorModel extends Fake implements model.CursorModel {
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
for (final style in [kRemoteViewStyleAdaptive, kRemoteViewStyleCustom]) {
|
||||
for (final dpr in [1.0, 2.0]) {
|
||||
testWidgets('ImagePaint Web $style zoom off DPR $dpr keeps source size',
|
||||
(tester) => tester.runAsync(() => _checkPolicy(tester, style, dpr)));
|
||||
}
|
||||
}
|
||||
test('Web cursor aligns CSS hotspots with rounded PNG dimensions', () async {
|
||||
Map<String, dynamic>? registered;
|
||||
final original = js.context['setByName'];
|
||||
@@ -71,3 +83,130 @@ model.CursorData _data(ui.Image nativeImage, (double, double) hotspot) {
|
||||
height: 48,
|
||||
);
|
||||
}
|
||||
|
||||
class _Image extends ChangeNotifier implements model.ImageModel {
|
||||
@override
|
||||
bool get useTextureRender => false;
|
||||
@override
|
||||
ui.Image? get image => null;
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
class _Canvas extends ChangeNotifier implements model.CanvasModel {
|
||||
_Canvas(String style)
|
||||
: viewStyle = model.ViewStyle(
|
||||
style: style,
|
||||
width: 200,
|
||||
height: 160,
|
||||
displayWidth: 400,
|
||||
displayHeight: 320);
|
||||
@override
|
||||
final model.ViewStyle viewStyle;
|
||||
@override
|
||||
final imageOverflow = false.obs;
|
||||
@override
|
||||
bool get cursorEmbedded => false;
|
||||
@override
|
||||
Size get size => const Size(200, 160);
|
||||
@override
|
||||
double get scale => 0.5;
|
||||
@override
|
||||
double get x => 0;
|
||||
@override
|
||||
double get y => 0;
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
class _Input extends Fake implements InputModel {
|
||||
@override
|
||||
final relativeMouseMode = false.obs;
|
||||
}
|
||||
|
||||
class _Peer extends Fake implements model.FfiModel {
|
||||
@override
|
||||
final pi = model.PeerInfo();
|
||||
@override
|
||||
bool get isPeerLinux => false;
|
||||
}
|
||||
|
||||
class _FFI extends Fake implements model.FFI {
|
||||
_FFI(this.canvasModel);
|
||||
@override
|
||||
final model.CanvasModel canvasModel;
|
||||
@override
|
||||
final inputModel = _Input();
|
||||
@override
|
||||
final ffiModel = _Peer();
|
||||
}
|
||||
|
||||
Future<model.CursorModel> _loadCursor(model.FFI ffi, String id) async {
|
||||
final cursor = model.CursorModel(WeakReference(ffi))..id = id;
|
||||
await cursor.updateCursorData({
|
||||
'id': id,
|
||||
'width': '48',
|
||||
'height': '48',
|
||||
'hotx': '7',
|
||||
'hoty': '9',
|
||||
'scale': '1',
|
||||
'colors': jsonEncode(List.filled(48 * 48 * 4, 255)),
|
||||
});
|
||||
addTearDown(() async {
|
||||
for (final key in cursor.cachedKeys) {
|
||||
await deleteCustomCursor(key);
|
||||
}
|
||||
cursor.disposeImages();
|
||||
cursor.dispose();
|
||||
});
|
||||
return cursor;
|
||||
}
|
||||
|
||||
Future<void> _checkPolicy(WidgetTester tester, String style, double dpr) async {
|
||||
final originals = {
|
||||
for (final key in ['isMobile', 'getByName', 'setByName'])
|
||||
key: js.context[key]
|
||||
};
|
||||
js.context['isMobile'] = js.allowInterop(() => false);
|
||||
js.context['getByName'] = js.allowInterop((String name, String value) => '');
|
||||
Map<String, dynamic>? registered;
|
||||
js.context['setByName'] = js.allowInterop((String name, String value) {
|
||||
if (name == 'cursor') registered = jsonDecode(value);
|
||||
});
|
||||
addTearDown(() {
|
||||
for (final entry in originals.entries) {
|
||||
js.context[entry.key] = entry.value;
|
||||
}
|
||||
});
|
||||
final canvas = _Canvas(style);
|
||||
addTearDown(canvas.dispose);
|
||||
final ffi = _FFI(canvas);
|
||||
final cursor = await _loadCursor(ffi, '$style-$dpr');
|
||||
await tester.pumpWidget(MediaQuery(
|
||||
data: MediaQueryData(devicePixelRatio: dpr),
|
||||
child: MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<model.ImageModel>(create: (_) => _Image()),
|
||||
ChangeNotifierProvider<model.CanvasModel>.value(value: canvas),
|
||||
ChangeNotifierProvider<model.CursorModel>.value(value: cursor),
|
||||
],
|
||||
child: ImagePaint(
|
||||
ffi: ffi,
|
||||
id: 'web-cursor-test',
|
||||
zoomCursor: false.obs,
|
||||
cursorOverImage: true.obs,
|
||||
keyboardEnabled: true.obs,
|
||||
remoteCursorMoved: false.obs)),
|
||||
));
|
||||
final session = tester
|
||||
.widget<MouseRegion>(find.byType(MouseRegion))
|
||||
.cursor
|
||||
.createSession(1);
|
||||
await session.activate();
|
||||
session.dispose();
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
final png =
|
||||
img.decodePng(Uri.parse(registered!['url']).data!.contentAsBytes())!;
|
||||
expect((png.width, png.height), (48, 48));
|
||||
expect((registered!['hotx'], registered!['hoty']), (7, 9));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user