mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-21 03:40:59 +03:00
Keep resized Web cursors within a proportional minimum
This commit is contained in:
@@ -2906,6 +2906,9 @@ class CursorData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Web's long-edge minimum can round a thin axis below one raster pixel.
|
||||||
|
final webWidth = max(1, (width * scale).round());
|
||||||
|
final webHeight = max(1, (height * scale).round());
|
||||||
if (resizeImage && _doubleToInt(oldScale) != _doubleToInt(scale)) {
|
if (resizeImage && _doubleToInt(oldScale) != _doubleToInt(scale)) {
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
data = img2
|
data = img2
|
||||||
@@ -2921,8 +2924,8 @@ class CursorData {
|
|||||||
img2.encodePng(
|
img2.encodePng(
|
||||||
img2.copyResize(
|
img2.copyResize(
|
||||||
image,
|
image,
|
||||||
width: isWeb ? (width * scale).round() : (width * scale).toInt(),
|
width: isWeb ? webWidth : (width * scale).toInt(),
|
||||||
height: isWeb ? (height * scale).round() : (height * scale).toInt(),
|
height: isWeb ? webHeight : (height * scale).toInt(),
|
||||||
interpolation: img2.Interpolation.average,
|
interpolation: img2.Interpolation.average,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -2935,8 +2938,8 @@ class CursorData {
|
|||||||
hoty = hotyOrigin * scale;
|
hoty = hotyOrigin * scale;
|
||||||
if (isWeb) {
|
if (isWeb) {
|
||||||
// CSS hotspots must follow the actual rounded PNG dimensions.
|
// CSS hotspots must follow the actual rounded PNG dimensions.
|
||||||
hotx = hotxOrigin * (width * scale).round() / width;
|
hotx = hotxOrigin * webWidth / width;
|
||||||
hoty = hotyOrigin * (height * scale).round() / height;
|
hoty = hotyOrigin * webHeight / height;
|
||||||
}
|
}
|
||||||
return scale;
|
return scale;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:js' as js;
|
import 'dart:js' as js;
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -52,8 +53,9 @@ class CursorManager {
|
|||||||
'cursor',
|
'cursor',
|
||||||
jsonEncode({
|
jsonEncode({
|
||||||
'url': cursorData.url,
|
'url': cursorData.url,
|
||||||
'hotx': cursorData.hotX.round(),
|
// Rounding must keep the hotspot inside even a one-pixel raster.
|
||||||
'hoty': cursorData.hotY.round(),
|
'hotx': cursorData.hotX.round().clamp(0, cursorData.width - 1),
|
||||||
|
'hoty': cursorData.hotY.round().clamp(0, cursorData.height - 1),
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -104,7 +106,9 @@ MouseCursor buildCursorOfCache(
|
|||||||
if (cache == null) {
|
if (cache == null) {
|
||||||
return MouseCursor.defer;
|
return MouseCursor.defer;
|
||||||
} else {
|
} else {
|
||||||
final key = cache.updateGetKey(scale);
|
// A short-edge minimum can enlarge thin artwork beyond CSS cursor limits.
|
||||||
|
// Keep unzoomed images unchanged and use the long edge when resizing.
|
||||||
|
final key = cache.updateGetKey(scale, useLegacyMinimum: scale == 1.0);
|
||||||
if (!cursor.cachedKeys.contains(key)) {
|
if (!cursor.cachedKeys.contains(key)) {
|
||||||
// data should be checked here, because it may be changed after `updateGetKey()`
|
// data should be checked here, because it may be changed after `updateGetKey()`
|
||||||
final data = cache.data;
|
final data = cache.data;
|
||||||
@@ -116,8 +120,8 @@ MouseCursor buildCursorOfCache(
|
|||||||
CursorManager.instance.registerCursor(CursorData(
|
CursorManager.instance.registerCursor(CursorData(
|
||||||
key: key,
|
key: key,
|
||||||
url: 'data:image/rgba;base64,${base64Encode(data)}',
|
url: 'data:image/rgba;base64,${base64Encode(data)}',
|
||||||
width: (cache.width * cache.scale).round(),
|
width: max(1, (cache.width * cache.scale).round()),
|
||||||
height: (cache.height * cache.scale).round(),
|
height: max(1, (cache.height * cache.scale).round()),
|
||||||
hotX: cache.hotx,
|
hotX: cache.hotx,
|
||||||
hotY: cache.hoty));
|
hotY: cache.hoty));
|
||||||
cursor.addKey(key);
|
cursor.addKey(key);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import 'package:provider/provider.dart';
|
|||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
_alphaTests();
|
_alphaTests();
|
||||||
|
_thinCursorTests();
|
||||||
for (final style in [kRemoteViewStyleAdaptive, kRemoteViewStyleCustom]) {
|
for (final style in [kRemoteViewStyleAdaptive, kRemoteViewStyleCustom]) {
|
||||||
for (final dpr in [1.0, 2.0]) {
|
for (final dpr in [1.0, 2.0]) {
|
||||||
testWidgets('ImagePaint Web $style zoom off DPR $dpr keeps source size',
|
testWidgets('ImagePaint Web $style zoom off DPR $dpr keeps source size',
|
||||||
@@ -55,6 +56,42 @@ void main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _thinCursorTests() {
|
||||||
|
for (final style in [kRemoteViewStyleAdaptive, kRemoteViewStyleCustom]) {
|
||||||
|
for (final zoom in [false, true]) {
|
||||||
|
testWidgets('ImagePaint Web $style thin cursor zoom=$zoom', (tester) =>
|
||||||
|
tester.runAsync(() => _checkPolicy(tester, style, 1,
|
||||||
|
zoom: zoom, source: (64, 4), hotspot: (32, 2),
|
||||||
|
expectedSize: zoom ? (32, 2) : (64, 4),
|
||||||
|
expectedHotspot: zoom ? (16, 1) : (32, 2))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
test('Web thin cursors keep a raster pixel and an in-bounds hotspot', () async {
|
||||||
|
final registered = _captureCursor();
|
||||||
|
final canvas = _Canvas(kRemoteViewStyleAdaptive);
|
||||||
|
addTearDown(canvas.dispose);
|
||||||
|
final ffi = _FFI(canvas);
|
||||||
|
for (final (source, hotspot, scale, size, expected) in [
|
||||||
|
((4, 64), (2.0, 32.0), 0.5, (2, 32), (1, 16)),
|
||||||
|
((2, 128), (1.0, 64.0), 0.01, (1, 12), (0, 6)),
|
||||||
|
((128, 2), (64.0, 1.0), 0.01, (12, 1), (6, 0)),
|
||||||
|
]) {
|
||||||
|
final cursor = await _loadCursor(ffi, '$source',
|
||||||
|
source: source, hotspot: hotspot);
|
||||||
|
for (final factor in [1.0, scale, 1.0]) {
|
||||||
|
final session =
|
||||||
|
buildCursorOfCache(cursor, factor, cursor.cache).createSession(1);
|
||||||
|
await session.activate();
|
||||||
|
session.dispose();
|
||||||
|
final png = img.decodePng(Uri.parse(registered['url']).data!.contentAsBytes())!;
|
||||||
|
expect((png.width, png.height), factor == 1 ? source : size);
|
||||||
|
expect((registered['hotx'], registered['hoty']),
|
||||||
|
factor == 1 ? (hotspot.$1, hotspot.$2) : expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class _Image extends ChangeNotifier implements model.ImageModel {
|
class _Image extends ChangeNotifier implements model.ImageModel {
|
||||||
@override
|
@override
|
||||||
bool get useTextureRender => false;
|
bool get useTextureRender => false;
|
||||||
@@ -114,17 +151,18 @@ class _FFI extends Fake implements model.FFI {
|
|||||||
|
|
||||||
Future<model.CursorModel> _loadCursor(model.FFI ffi, String id,
|
Future<model.CursorModel> _loadCursor(model.FFI ffi, String id,
|
||||||
{List<int> pixel = const [255, 255, 255, 255],
|
{List<int> pixel = const [255, 255, 255, 255],
|
||||||
|
(int, int) source = (48, 48),
|
||||||
(double, double) hotspot = (7, 9),
|
(double, double) hotspot = (7, 9),
|
||||||
double? pixelRatio = 1}) async {
|
double? pixelRatio = 1}) async {
|
||||||
final cursor = model.CursorModel(WeakReference(ffi))..id = id;
|
final cursor = model.CursorModel(WeakReference(ffi))..id = id;
|
||||||
await cursor.updateCursorData({
|
await cursor.updateCursorData({
|
||||||
'id': id,
|
'id': id,
|
||||||
'width': '48',
|
'width': '${source.$1}',
|
||||||
'height': '48',
|
'height': '${source.$2}',
|
||||||
'hotx': '${hotspot.$1}',
|
'hotx': '${hotspot.$1}',
|
||||||
'hoty': '${hotspot.$2}',
|
'hoty': '${hotspot.$2}',
|
||||||
if (pixelRatio != null) 'scale': '$pixelRatio',
|
if (pixelRatio != null) 'scale': '$pixelRatio',
|
||||||
'colors': jsonEncode([for (var i = 0; i < 48 * 48; i++) ...pixel]),
|
'colors': jsonEncode([for (var i = 0; i < source.$1 * source.$2; i++) ...pixel]),
|
||||||
});
|
});
|
||||||
addTearDown(() async {
|
addTearDown(() async {
|
||||||
// Keep the session owner alive across asynchronous image decoding.
|
// Keep the session owner alive across asynchronous image decoding.
|
||||||
@@ -161,12 +199,16 @@ Map<String, dynamic> _captureCursor() {
|
|||||||
return registered;
|
return registered;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _checkPolicy(WidgetTester tester, String style, double dpr) async {
|
Future<void> _checkPolicy(WidgetTester tester, String style, double dpr,
|
||||||
|
{bool zoom = false, (int, int) source = (48, 48),
|
||||||
|
(double, double) hotspot = (7, 9), (int, int) expectedSize = (48, 48),
|
||||||
|
(int, int) expectedHotspot = (7, 9)}) async {
|
||||||
final registered = _captureCursor();
|
final registered = _captureCursor();
|
||||||
final canvas = _Canvas(style);
|
final canvas = _Canvas(style);
|
||||||
addTearDown(canvas.dispose);
|
addTearDown(canvas.dispose);
|
||||||
final ffi = _FFI(canvas);
|
final ffi = _FFI(canvas);
|
||||||
final cursor = await _loadCursor(ffi, '$style-$dpr');
|
final cursor = await _loadCursor(ffi, '$style-$dpr-$zoom-$source',
|
||||||
|
source: source, hotspot: hotspot);
|
||||||
await tester.pumpWidget(MediaQuery(
|
await tester.pumpWidget(MediaQuery(
|
||||||
data: MediaQueryData(devicePixelRatio: dpr),
|
data: MediaQueryData(devicePixelRatio: dpr),
|
||||||
child: MultiProvider(
|
child: MultiProvider(
|
||||||
@@ -178,7 +220,7 @@ Future<void> _checkPolicy(WidgetTester tester, String style, double dpr) async {
|
|||||||
child: ImagePaint(
|
child: ImagePaint(
|
||||||
ffi: ffi,
|
ffi: ffi,
|
||||||
id: 'web-cursor-test',
|
id: 'web-cursor-test',
|
||||||
zoomCursor: false.obs,
|
zoomCursor: zoom.obs,
|
||||||
cursorOverImage: true.obs,
|
cursorOverImage: true.obs,
|
||||||
keyboardEnabled: true.obs,
|
keyboardEnabled: true.obs,
|
||||||
remoteCursorMoved: false.obs)),
|
remoteCursorMoved: false.obs)),
|
||||||
@@ -192,8 +234,8 @@ Future<void> _checkPolicy(WidgetTester tester, String style, double dpr) async {
|
|||||||
await tester.pumpWidget(const SizedBox.shrink());
|
await tester.pumpWidget(const SizedBox.shrink());
|
||||||
final png =
|
final png =
|
||||||
img.decodePng(Uri.parse(registered['url']).data!.contentAsBytes())!;
|
img.decodePng(Uri.parse(registered['url']).data!.contentAsBytes())!;
|
||||||
expect((png.width, png.height), (48, 48));
|
expect((png.width, png.height), expectedSize);
|
||||||
expect((registered['hotx'], registered['hoty']), (7, 9));
|
expect((registered['hotx'], registered['hoty']), expectedHotspot);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _alphaTests() {
|
void _alphaTests() {
|
||||||
|
|||||||
Reference in New Issue
Block a user