From 3244213f6072219814833bd1a2f1fc436b2a509c Mon Sep 17 00:00:00 2001 From: palmoni5 Date: Tue, 1 Sep 2026 02:05:47 +0300 Subject: [PATCH] fix(windows): reverse horizontal scroll direction for synthetic wheel input Two-finger horizontal trackpad pans (and mouse tilt-wheel scrolls) moved the page content in the opposite direction inside the WebView. Plugins that scroll horizontally had to work around it on their side, and the reversal was misattributed to RTL documents - it is actually document-independent, but only RTL horizontal-scrolling content made it visible in practice. The synthetic wheel injection treated both axes with the same sign, but the WM conventions are asymmetric: positive WM_MOUSEWHEEL data scrolls up, while positive WM_MOUSEHWHEEL data scrolls right. The Flutter engine mirrors this asymmetry when producing PointerScrollEvent (it negates only the vertical axis), so the round trip back to WebView2 must too. The trackpad path now negates dx so the content follows the fingers (this also covers the synthetic fling, which shares the path), and the wheel path forwards dx unnegated to reproduce the original native delta. Updates the two tests that pinned the reversed sign and adds an explicit direction test per input path. --- flutter_inappwebview_windows/CHANGELOG.md | 3 ++ .../in_app_webview/custom_platform_view.dart | 9 ++-- .../custom_platform_view_scroll_test.dart | 50 +++++++++++++++++-- 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/flutter_inappwebview_windows/CHANGELOG.md b/flutter_inappwebview_windows/CHANGELOG.md index 742f8f958c..0671432a87 100644 --- a/flutter_inappwebview_windows/CHANGELOG.md +++ b/flutter_inappwebview_windows/CHANGELOG.md @@ -1,5 +1,8 @@ ## 0.7.0-beta.3 +- Fixed reversed horizontal scrolling: two-finger trackpad pans and mouse + tilt-wheel scrolls moved the content in the opposite direction, because + `WM_MOUSEHWHEEL`'s sign convention is inverted relative to `WM_MOUSEWHEEL`. - Fixed WebView2 input windows intercepting desktop clicks while the Flutter host window is minimized, including WebViews created or resumed during minimization. diff --git a/flutter_inappwebview_windows/lib/src/in_app_webview/custom_platform_view.dart b/flutter_inappwebview_windows/lib/src/in_app_webview/custom_platform_view.dart index 7ab8a738d6..5a5ea9b7f9 100644 --- a/flutter_inappwebview_windows/lib/src/in_app_webview/custom_platform_view.dart +++ b/flutter_inappwebview_windows/lib/src/in_app_webview/custom_platform_view.dart @@ -634,7 +634,7 @@ class _CustomPlatformViewState extends State _controller._setCursorPos(signal.localPosition); _stopFling(); _sendScrollDelta( - -signal.scrollDelta.dx, + signal.scrollDelta.dx, -signal.scrollDelta.dy, ); } else if (signal is PointerScrollInertiaCancelEvent) { @@ -728,7 +728,8 @@ class _CustomPlatformViewState extends State ); } - /// Forwards scroll deltas immediately, preserving fractional remainders. + /// Forwards raw WM wheel data immediately, preserving fractional + /// remainders. WM sign asymmetry: positive dy = up, positive dx = right. void _sendScrollDelta(double dx, double dy) { _scrollRemainderX += dx; _scrollRemainderY += dy; @@ -744,7 +745,9 @@ class _CustomPlatformViewState extends State void _sendTrackpadScrollDelta(double dx, double dy) { final delta = _dominantAxis(Offset(dx, dy)); - _sendScrollDelta(delta.dx, delta.dy); + // Content follows the fingers: fingers right = scroll left = negative + // hwheel (WM_MOUSEHWHEEL positive means right, unlike the vertical axis). + _sendScrollDelta(-delta.dx, delta.dy); } /// Starts synthetic inertia after a fast lifted pan. diff --git a/flutter_inappwebview_windows/test/custom_platform_view_scroll_test.dart b/flutter_inappwebview_windows/test/custom_platform_view_scroll_test.dart index 145fe19ee6..60aac2fc8c 100644 --- a/flutter_inappwebview_windows/test/custom_platform_view_scroll_test.dart +++ b/flutter_inappwebview_windows/test/custom_platform_view_scroll_test.dart @@ -224,10 +224,31 @@ void main() { await tester.pump(); // 0.5px × 1.2 × 1.5 = 0.9 units per update: whole units flush on the - // 2nd and 3rd updates. + // 2nd and 3rd updates. Fingers left → positive hwheel (scroll right is + // negative in WM terms only for the vertical axis). expect(scrollDeltaCalls(), [ - [-1.0, 0.0], - [-1.0, 0.0], + [1.0, 0.0], + [1.0, 0.0], + ]); + }); + + testWidgets('horizontal pan follows the fingers (hwheel sign is inverted ' + 'relative to the vertical axis)', (tester) async { + final center = await pumpView(tester); + + final pointer = TestPointer(1, PointerDeviceKind.trackpad); + await tester.sendEventToBinding(pointer.panZoomStart(center)); + // Fingers move right 10px: content must follow them, i.e. scroll left, + // which is a NEGATIVE WM_MOUSEHWHEEL delta (positive hwheel = right). + await tester.sendEventToBinding( + pointer.panZoomUpdate(center, pan: const Offset(10, 0)), + ); + await tester.sendEventToBinding(pointer.panZoomEnd()); + await tester.pump(); + + // 10px × 1.2 × 1.5 = 18 units, negated for the horizontal axis. + expect(scrollDeltaCalls(), [ + [-18.0, 0.0], ]); }); @@ -246,7 +267,7 @@ void main() { await tester.pump(); expect(scrollDeltaCalls(), [ - [-18.0, 0.0], + [18.0, 0.0], [0.0, -18.0], ]); }); @@ -435,6 +456,27 @@ void main() { ]); }); + testWidgets('horizontal wheel deltas are forwarded unnegated ' + '(WM_MOUSEHWHEEL positive = right, unlike the vertical axis)', ( + tester, + ) async { + final center = await pumpView(tester); + + final pointer = TestPointer(1, PointerDeviceKind.mouse); + await tester.sendEventToBinding(pointer.hover(center)); + // The engine maps WM_MOUSEHWHEEL without negation, so reproducing the + // original native delta means forwarding dx as-is. + await tester.sendEventToBinding(pointer.scroll(const Offset(120, 0))); + await tester.pump(); + await tester.sendEventToBinding(pointer.scroll(const Offset(-120, 0))); + await tester.pump(); + + expect(scrollDeltaCalls(), [ + [120.0, 0.0], + [-120.0, 0.0], + ]); + }); + testWidgets('wheel deltas are still forwarded negated', (tester) async { final center = await pumpView(tester);