From 6a62b7bbd93ddec21e0990d1bcdb16edf32d6b7a Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 14 Sep 2026 11:18:03 +0800 Subject: [PATCH] Fix cursor alignment when a scrollbar axis stops overflowing --- flutter/lib/desktop/pages/remote_page.dart | 18 +++++++++++------- flutter/test/cursor_scroll_edge_test.dart | 15 ++++++++++++--- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 40f0fbe2d..ef6794f86 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -1220,9 +1220,10 @@ class _ImagePaintState extends State { // Layout can change scroll metrics without notification. Compare against this // build's fractions: a delayed refresh may already have changed the model. void _syncScrollAfterLayout(CanvasModel canvas) { - // Custom scrollbars also paint from scroll fractions; preserve Original's path. + // Both supported scrollbar view styles also paint from scroll fractions. if (canvas.scrollStyle != ScrollStyle.scrolledge && - canvas.viewStyle.style != kRemoteViewStyleCustom) return; + canvas.viewStyle.style != kRemoteViewStyleCustom && + canvas.viewStyle.style != kRemoteViewStyleOriginal) return; final renderedScroll = (canvas.scrollX, canvas.scrollY); SchedulerBinding.instance.addPostFrameCallback((_) { if (!mounted) return; @@ -1434,11 +1435,14 @@ class CursorPaint extends StatelessWidget { // Scrollbar and edge scrolling share a layout that ignores canvas pan offsets. final imageWidth = rect.width * c.scale; final imageHeight = rect.height * c.scale; - // Match the integer centering in _buildCrossScrollbarFromLayout. - cx = (c.size.width > imageWidth ? (c.size.width - imageWidth) ~/ 2 : 0) - - imageWidth * c.scrollX; - cy = (c.size.height > imageHeight ? (c.size.height - imageHeight) ~/ 2 : 0) - - imageHeight * c.scrollY; + // Match the video's integer centering on each fitting axis, even if a + // detached scroll controller left a stale fraction before layout sync. + cx = c.size.width < imageWidth + ? -imageWidth * c.scrollX + : ((c.size.width - imageWidth) ~/ 2).toDouble(); + cy = c.size.height < imageHeight + ? -imageHeight * c.scrollY + : ((c.size.height - imageHeight) ~/ 2).toDouble(); } final image = m.image ?? preDefaultCursor.image; diff --git a/flutter/test/cursor_scroll_edge_test.dart b/flutter/test/cursor_scroll_edge_test.dart index cf1921935..f3f62afd7 100644 --- a/flutter/test/cursor_scroll_edge_test.dart +++ b/flutter/test/cursor_scroll_edge_test.dart @@ -16,6 +16,12 @@ import 'cursor_test_utils.dart'; const _viewport = Size(200, 160); const _hotspot = Offset(4, 9); const _scrollDeltas = [80.0, -20.0, 1000.0, 1000.0, -1000.0, -1000.0, 80.0, 0.0]; +const _fittingViewports = [ + Size(500, 160), // Fit horizontally while vertical scrolling remains. + Size(200, 400), // Fit vertically while horizontal scrolling remains. + Size(400, 160), // Exact fit also detaches the controller. + Size(200, 320), +]; class _Peer extends Fake implements FfiModel { _Peer(ui.Image frame) @@ -118,6 +124,7 @@ class _FFI extends Fake implements FFI { void main() { for (final style in [(kRemoteViewStyleOriginal, ScrollStyle.scrolledge), + (kRemoteViewStyleOriginal, ScrollStyle.scrollbar), (kRemoteViewStyleCustom, ScrollStyle.scrolledge), (kRemoteViewStyleCustom, ScrollStyle.scrollbar)]) { for (final texture in [false, true]) { @@ -203,17 +210,19 @@ Future _checkRelayout( cursor.dispose(); }); await _mount(tester, ffi, 1); - canvas.performEdgeScroll(Vector2(20, 20)); - await tester.pump(); final videoWidget = texture ? find.byType(Texture) : _paintOf(video); - _expectAlignment(tester, ffi, videoWidget); for (final (viewport, dpr) in [ + for (final viewport in _fittingViewports) + ...[(viewport, 1.0), (_viewport, 1.0)], (const Size(160, 120), 2.0), (const Size(195, 155), 2.0), // Clamp both existing scroll positions. (const Size(240, 155), 2.0), // Detach the horizontal scroll controller. (const Size(240, 200), 2.0), // No scrolling remains. (_viewport, 1.0), ]) { + canvas.performEdgeScroll(Vector2(20, 20)); + await tester.pump(); + _expectAlignment(tester, ffi, videoWidget); tester.view.devicePixelRatio = dpr; tester.view.physicalSize = viewport * dpr; canvas.relayout(viewport, dpr);