From 16088cfc807712650d6994df75ddd2c99b4c4e32 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 13 Sep 2026 14:53:47 +0800 Subject: [PATCH] Synchronize custom-scrollbar cursor geometry after relayout --- flutter/lib/desktop/pages/remote_page.dart | 10 ++++++---- flutter/lib/models/model.dart | 2 +- flutter/test/cursor_scroll_edge_test.dart | 20 +++++++++++--------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 1c9aa9901..735820e07 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -1173,7 +1173,7 @@ class _ImagePaintState extends State { child: child); }); if (c.imageOverflow.isTrue && c.scrollStyle != ScrollStyle.scrollauto) { - _syncEdgeScrollAfterLayout(c); + _syncScrollAfterLayout(c); final paintWidth = c.getDisplayWidth() * s; final paintHeight = c.getDisplayHeight() * s; final paintSize = Size(paintWidth, paintHeight); @@ -1218,13 +1218,15 @@ class _ImagePaintState extends State { } } - void _syncEdgeScrollAfterLayout(CanvasModel canvas) { - if (canvas.scrollStyle != ScrollStyle.scrolledge) return; + void _syncScrollAfterLayout(CanvasModel canvas) { + // Custom scrollbars also paint from scroll fractions; preserve Original's path. + if (canvas.scrollStyle != ScrollStyle.scrolledge && + canvas.viewStyle.style != kRemoteViewStyleCustom) return; final renderedScroll = (canvas.scrollX, canvas.scrollY); // Relayout can clamp or detach scroll positions without a scroll event. SchedulerBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - canvas.updateEdgeScrollAfterLayout(renderedScroll); + canvas.updateScrollAfterLayout(renderedScroll); }); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 0582571a7..01d5d79b8 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2702,7 +2702,7 @@ class CanvasModel with ChangeNotifier { notifyListeners(); } - void updateEdgeScrollAfterLayout((double, double) renderedScroll) { + void updateScrollAfterLayout((double, double) renderedScroll) { updateScrollPercent(); // A delayed refresh may already have changed the model without repainting. if (renderedScroll != (_scrollX, _scrollY)) { diff --git a/flutter/test/cursor_scroll_edge_test.dart b/flutter/test/cursor_scroll_edge_test.dart index dc7e2ef8a..9a7f4f1aa 100644 --- a/flutter/test/cursor_scroll_edge_test.dart +++ b/flutter/test/cursor_scroll_edge_test.dart @@ -47,7 +47,7 @@ class _Texture extends Fake implements TextureModel { } class _ScrollCanvas extends CanvasModel { - _ScrollCanvas(FFI ffi, String style) + _ScrollCanvas(FFI ffi, String style, this.scrollStyle) : viewStyle = ViewStyle( style: style, width: _viewport.width, @@ -69,7 +69,7 @@ class _ScrollCanvas extends CanvasModel { @override double get y => (size.height - getDisplayHeight() * scale) / 2; @override - ScrollStyle get scrollStyle => ScrollStyle.scrolledge; + final ScrollStyle scrollStyle; @override final ViewStyle viewStyle; @@ -100,10 +100,10 @@ class _Cursor extends CursorModel { class _FFI extends Fake implements FFI { _FFI(ui.Image frame, ui.Image cursor, - {required bool texture, required String style, required Offset position}) + {required bool texture, required (String, ScrollStyle) style, required Offset position}) : ffiModel = _Peer(frame) { imageModel = _Image(this, frame, texture); - canvasModel = _ScrollCanvas(this, style); + canvasModel = _ScrollCanvas(this, style.$1, style.$2); cursorModel = _Cursor(this, cursor, position); } @override @@ -131,7 +131,9 @@ class _Draw extends Fake implements Canvas { } void main() { - for (final style in [kRemoteViewStyleOriginal, kRemoteViewStyleCustom]) { + for (final style in [(kRemoteViewStyleOriginal, ScrollStyle.scrolledge), + (kRemoteViewStyleCustom, ScrollStyle.scrolledge), + (kRemoteViewStyleCustom, ScrollStyle.scrollbar)]) { for (final texture in [false, true]) { for (final (frame, dpr) in [ (const Size(199, 320), 1.0), @@ -140,13 +142,13 @@ void main() { (const Size(400, 158), 1.0), ]) { testWidgets( - 'ScrollEdge $style texture=$texture frame=$frame DPR=$dpr', + 'Scroll $style texture=$texture frame=$frame DPR=$dpr', (tester) => tester .runAsync(() => _check(tester, (style, texture, frame, dpr)))); } for (final refreshBeforeLayout in [true, false]) { testWidgets( - 'ScrollEdge relayout $style texture=$texture early=$refreshBeforeLayout', + 'Scroll relayout $style texture=$texture early=$refreshBeforeLayout', (tester) => tester.runAsync(() => _checkRelayout(tester, (style, texture, refreshBeforeLayout)))); } @@ -155,7 +157,7 @@ void main() { } Future _check( - WidgetTester tester, (String, bool, Size, double) testCase) async { + WidgetTester tester, ((String, ScrollStyle), bool, Size, double) testCase) async { final (style, texture, frame, dpr) = testCase; tester.view.devicePixelRatio = dpr; tester.view.physicalSize = _viewport * dpr; @@ -195,7 +197,7 @@ Future _check( } Future _checkRelayout( - WidgetTester tester, (String, bool, bool) testCase) async { + WidgetTester tester, ((String, ScrollStyle), bool, bool) testCase) async { final (style, texture, refreshBeforeLayout) = testCase; tester.view.devicePixelRatio = 1; tester.view.physicalSize = _viewport;