mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-16 01:11:03 +03:00
Preserve thin cursor proportions at the visibility minimum
This commit is contained in:
@@ -1125,9 +1125,8 @@ class _ImagePaintState extends State<ImagePaint> {
|
||||
// the existing unzoomed sizing policy for other hosts.
|
||||
return (isWindows ? dpr : 1.0) / peerDpr;
|
||||
}
|
||||
// Without density, keep the legacy unzoomed scale of 1. Dividing by
|
||||
// controller DPR would also trigger the legacy short-edge minimum,
|
||||
// enlarging thin artwork that previously needed no resizing.
|
||||
// Without density, preserve the legacy unzoomed size. Controller
|
||||
// DPR alone cannot determine the remote bitmap's logical size.
|
||||
final imageScale = isViewScaled() && zoomCursor.isTrue
|
||||
? _cursorImageScale(widget.ffi, cursor, useLocalPointer: true)
|
||||
: s;
|
||||
@@ -1456,10 +1455,8 @@ class CursorPaint extends StatelessWidget {
|
||||
if (image != null && (logicalMinimum || scale * nativePixels != 1.0)) {
|
||||
final sx = kMinCursorSize / (image.width * nativePixels);
|
||||
final sy = kMinCursorSize / (image.height * nativePixels);
|
||||
// Preserve Original's short-edge minimum; scaled views use the long edge.
|
||||
final minimumScale = c.viewStyle.style == kRemoteViewStyleOriginal
|
||||
? (sx > sy ? sx : sy)
|
||||
: (sx < sy ? sx : sy);
|
||||
// Match native resizing: a thin axis must not enlarge the whole cursor.
|
||||
final minimumScale = sx < sy ? sx : sy;
|
||||
if (scale < minimumScale) scale = minimumScale;
|
||||
}
|
||||
// Anchor the hotspot to the video position even when the minimum size
|
||||
|
||||
@@ -2915,19 +2915,11 @@ class CursorData {
|
||||
debugPrint('Rejected cursor $id: invalid scale $scale');
|
||||
return null;
|
||||
}
|
||||
if (!useLegacyMinimum) {
|
||||
// Preserve unscaled legacy artwork, but never enlarge a thin cursor just
|
||||
// to make its short edge reach the visibility minimum.
|
||||
if (!useLegacyMinimum || scale != 1.0) {
|
||||
scale = max(scale, kMinCursorSize / max(width, height));
|
||||
}
|
||||
if (useLegacyMinimum && scale != 1.0) {
|
||||
// Update data if scale changed.
|
||||
final tgtWidth = width * scale;
|
||||
final tgtHeight = height * scale;
|
||||
if (tgtWidth < kMinCursorSize || tgtHeight < kMinCursorSize) {
|
||||
double sw = kMinCursorSize.toDouble() / width;
|
||||
double sh = kMinCursorSize.toDouble() / height;
|
||||
scale = sw < sh ? sh : sw;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_validCursorRasterSize(width * scale, height * scale,
|
||||
rasterScale: rasterScale)) {
|
||||
|
||||
@@ -26,8 +26,8 @@ MouseCursor buildCursorOfCache(
|
||||
// bitmap even when the remote view scale has not changed.
|
||||
final dpr = WidgetsBinding
|
||||
.instance.platformDispatcher.views.single.devicePixelRatio;
|
||||
// Keep Original and older peers unchanged. A long-edge minimum preserves
|
||||
// the proportions of thin remote cursors when normalizing their DPI.
|
||||
// Keep Original and older peers' pixel units and scale-1 behavior.
|
||||
// Resizing uses a long-edge minimum regardless of source density.
|
||||
final legacyMinimum = cache.pixelRatio == 0 ||
|
||||
cursor.parent.target?.canvasModel.viewStyle.style == kRemoteViewStyleOriginal;
|
||||
// The minimum is logical, while Windows callers pass a physical scale.
|
||||
|
||||
@@ -38,7 +38,6 @@ void _rasterBoundsTests() {
|
||||
for (final (width, height, scale, legacy, rasterScale) in [
|
||||
(32, 32, 1e300, false, 1.0),
|
||||
(32, 32, 100.0, false, 2.0),
|
||||
(512, 1, 0.1, true, 1.0),
|
||||
]) {
|
||||
test('rejects raster ${width}x$height scale=$scale before key creation',
|
||||
() async {
|
||||
|
||||
@@ -35,10 +35,11 @@ class _LinuxDisplay extends Display {
|
||||
double get scale => 2;
|
||||
}
|
||||
|
||||
Future<CursorData> _data(int density, String id) async {
|
||||
Future<CursorData> _data(int density, String id,
|
||||
{(int, int) source = (9, 18)}) async {
|
||||
final bitmapDensity = density == 0 ? 1 : density;
|
||||
final image = await createTestImage(
|
||||
width: 9 * bitmapDensity, height: 18 * bitmapDensity);
|
||||
width: source.$1 * bitmapDensity, height: source.$2 * bitmapDensity);
|
||||
return CursorData(
|
||||
peerId: 'dpi-policy',
|
||||
id: id,
|
||||
@@ -46,8 +47,8 @@ Future<CursorData> _data(int density, String id) async {
|
||||
nativeImage: image,
|
||||
scale: 1,
|
||||
data: Uint8List.fromList([1, 2]),
|
||||
hotxOrigin: 4.0 * bitmapDensity,
|
||||
hotyOrigin: 9.0 * bitmapDensity,
|
||||
hotxOrigin: (source.$1 ~/ 2) * bitmapDensity.toDouble(),
|
||||
hotyOrigin: (source.$2 ~/ 2) * bitmapDensity.toDouble(),
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
pixelRatio: density.toDouble(),
|
||||
@@ -76,7 +77,8 @@ void main() {
|
||||
(kRemoteViewStyleCustom, false, 2, 0.25, windows ? 1.0 : 0.5),
|
||||
(kRemoteViewStyleAdaptive, true, 2, 0.25, windows ? 2 / 3 : 1 / 3),
|
||||
(kRemoteViewStyleCustom, true, 2, 0.25, windows ? 2 / 3 : 1 / 3),
|
||||
(kRemoteViewStyleOriginal, false, 2, 0.5, windows ? 1.0 : 2 / 3),
|
||||
(kRemoteViewStyleOriginal, false, 1, 0.5, windows ? 1.0 : 2 / 3),
|
||||
(kRemoteViewStyleOriginal, false, 2, 0.5, windows ? 1.0 : 0.5),
|
||||
]) {
|
||||
testWidgets(
|
||||
'${testCase.$1} zoom=${testCase.$2} peerDPR=${testCase.$3}',
|
||||
@@ -86,6 +88,12 @@ void main() {
|
||||
test('live DPR changes invalidate a cached native cursor',
|
||||
() => _checkDprChange(view, registrations));
|
||||
for (final style in [kRemoteViewStyleAdaptive, kRemoteViewStyleCustom]) {
|
||||
final dpr = style == kRemoteViewStyleAdaptive ? 1.0 : 2.0;
|
||||
final source = style == kRemoteViewStyleAdaptive ? (64, 4) : (4, 64);
|
||||
testWidgets('$style unknown-density thin cursor preserves zoom', (tester) =>
|
||||
tester.runAsync(() => _checkPolicy(tester,
|
||||
(style, true, 0, 0.5, windows ? 0.5 * dpr : 0.5), registrations,
|
||||
dpr: dpr, source: source)));
|
||||
testWidgets('$style forbidden cursor ignores remote DPR', (tester) =>
|
||||
tester.runAsync(() => _checkPolicy(tester,
|
||||
(style, false, 2, 0.25, 0.5), registrations, dpr: 1)));
|
||||
@@ -99,10 +107,10 @@ Future<void> _checkPolicy(
|
||||
WidgetTester tester,
|
||||
(String, bool, int, double, double) testCase,
|
||||
List<Map<dynamic, dynamic>> registrations,
|
||||
{bool linux = false, double dpr = 2}) async {
|
||||
{bool linux = false, double dpr = 2, (int, int) source = (9, 18)}) async {
|
||||
final (style, zoom, density, viewScale, expectedScale) = testCase;
|
||||
tester.view.devicePixelRatio = dpr;
|
||||
final data = await _data(density, '$style-$zoom-$density');
|
||||
final data = await _data(density, '$style-$zoom-$density', source: source);
|
||||
final originalBytes = data.data;
|
||||
// A stale cached DPR must not affect the cursor when the window moves.
|
||||
final canvas = CursorTestCanvas(1, style: style, scale: viewScale);
|
||||
@@ -146,6 +154,16 @@ Future<void> _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 (zoom && density == 0) {
|
||||
final args = registrations.single;
|
||||
final rasterScale = expectedScale * (Platform.isWindows ? 1 : dpr);
|
||||
final width = source.$1 * rasterScale;
|
||||
final height = source.$2 * rasterScale;
|
||||
expect((args['width'], args['height']),
|
||||
(Platform.isLinux && height > width ? height : width, height));
|
||||
expect((args['hotX'], args['hotY']),
|
||||
((source.$1 ~/ 2) * rasterScale, (source.$2 ~/ 2) * rasterScale));
|
||||
}
|
||||
if (revoke) {
|
||||
final args = registrations.last;
|
||||
expect(args['name'], contains('_${kPreForbiddenCursorId}_'));
|
||||
|
||||
@@ -18,19 +18,20 @@ const _canvasOffset = Offset(15.125, -10.25);
|
||||
const _viewport = Size(200, 160);
|
||||
|
||||
class _CursorModel extends ChangeNotifier implements CursorModel {
|
||||
_CursorModel(this.image, this.position, this.density);
|
||||
_CursorModel(this.image, this.position, this.density, {this.hotspot = _hotspot});
|
||||
|
||||
final Offset position;
|
||||
final double density;
|
||||
final Offset hotspot;
|
||||
@override
|
||||
CursorData get cache => _Density(density);
|
||||
|
||||
@override
|
||||
final ui.Image image;
|
||||
@override
|
||||
double get hotx => _hotspot.dx;
|
||||
double get hotx => hotspot.dx;
|
||||
@override
|
||||
double get hoty => _hotspot.dy;
|
||||
double get hoty => hotspot.dy;
|
||||
@override
|
||||
double get x => position.dx;
|
||||
@override
|
||||
@@ -143,7 +144,8 @@ class _ScrollbarCanvasModel extends _CanvasModel {
|
||||
|
||||
Future<ImagePainter> _paintCursor(WidgetTester tester, CanvasModel canvas,
|
||||
{double dpr = 2, bool zoom = true, (int, int) source = (48, 64),
|
||||
Offset position = _remotePosition, double density = 0}) async {
|
||||
Offset position = _remotePosition, double density = 0,
|
||||
Offset hotspot = _hotspot}) async {
|
||||
final image = (await tester
|
||||
.runAsync(() => createTestImage(width: source.$1, height: source.$2)))!;
|
||||
addTearDown(image.dispose);
|
||||
@@ -156,7 +158,7 @@ Future<ImagePainter> _paintCursor(WidgetTester tester, CanvasModel canvas,
|
||||
child: MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<ImageModel>.value(value: canvas.parent.target!.imageModel),
|
||||
ChangeNotifierProvider<CursorModel>(create: (_) => _CursorModel(image, position, density)),
|
||||
ChangeNotifierProvider<CursorModel>(create: (_) => _CursorModel(image, position, density, hotspot: hotspot)),
|
||||
ChangeNotifierProvider<CanvasModel>(create: (_) => canvas),
|
||||
],
|
||||
child: Stack(fit: StackFit.expand, children: [
|
||||
@@ -221,21 +223,26 @@ void main() {
|
||||
(kRemoteViewStyleCustom, true, 2.0, (48, 64), 2.0, 2.0, false, 0.0),
|
||||
(kRemoteViewStyleAdaptive, false, 2.25, (48, 48), 0.375, 0.375, false, 0.0),
|
||||
(kRemoteViewStyleAdaptive, true, 2.0, (9, 18), 0.1, minimumScale, true, 0.0),
|
||||
(kRemoteViewStyleAdaptive, true, 1.0, (64, 4), 0.5, 0.5, true, 0.0),
|
||||
(kRemoteViewStyleCustom, true, 2.0, (4, 64), 0.5, 0.5, true, 0.0),
|
||||
(kRemoteViewStyleAdaptive, true, 1.0, (48, 64), 0.05, 0.1875, true, 2.0),
|
||||
(kRemoteViewStyleAdaptive, true, 2.0, (48, 64), 0.05, 0.1875, true, 2.0),
|
||||
(kRemoteViewStyleCustom, true, 1.0, (48, 64), 0.05, 0.1875, true, 2.0),
|
||||
(kRemoteViewStyleCustom, true, 2.0, (48, 64), 0.05, 0.1875, true, 2.0),
|
||||
(kRemoteViewStyleCustom, true, 2.0, (8, 10), 1.0, 1.2, true, 2.0),
|
||||
(kRemoteViewStyleOriginal, true, 1.0, (48, 64), 0.05, 0.25, true, 2.0),
|
||||
(kRemoteViewStyleOriginal, true, 1.0, (48, 64), 0.05, 0.1875, true, 2.0),
|
||||
(kRemoteViewStyleOriginal, true, 2.0, (48, 64), 0.05,
|
||||
Platform.isWindows ? 0.125 : 0.25, true, 2.0),
|
||||
Platform.isWindows ? 0.09375 : 0.1875, true, 2.0),
|
||||
]) {
|
||||
testWidgets(
|
||||
'$style zoom=$zoom dpr=$dpr source=$source texture=$texture density=$density keeps remote geometry',
|
||||
(tester) async {
|
||||
final sourceHotspot = source.$1 <= _hotspot.dx || source.$2 <= _hotspot.dy
|
||||
? Offset(source.$1 / 2, source.$2 / 2) : _hotspot;
|
||||
final painter = await _paintCursor(
|
||||
tester, _CanvasModel(style, canvasScale, texture),
|
||||
dpr: dpr, zoom: zoom, source: source, density: density);
|
||||
dpr: dpr, zoom: zoom, source: source, density: density,
|
||||
hotspot: sourceHotspot);
|
||||
expect(painter.scale, scale);
|
||||
var imageOrigin = texture
|
||||
? tester.getTopLeft(find.byType(Texture).first) : _canvasOffset;
|
||||
@@ -250,12 +257,12 @@ void main() {
|
||||
imageOrigin = background.position!;
|
||||
}
|
||||
final target = _remotePosition * canvasScale + imageOrigin;
|
||||
final hotspot = (Offset(painter.x, painter.y) + _hotspot) * scale;
|
||||
final hotspot = (Offset(painter.x, painter.y) + sourceHotspot) * scale;
|
||||
expect(hotspot.dx, closeTo(target.dx, 1e-9));
|
||||
expect(hotspot.dy, closeTo(target.dy, 1e-9));
|
||||
final canvas = CursorTestDraw();
|
||||
painter.paint(canvas, _viewport);
|
||||
final position = canvas.position! + _hotspot * canvas.factor;
|
||||
final position = canvas.position! + sourceHotspot * canvas.factor;
|
||||
expect(position.dx, closeTo(target.dx, 1e-9));
|
||||
expect(position.dy, closeTo(target.dy, 1e-9));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user