Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:ui';

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import '../platform_util.dart';
import '_static_channel.dart';
Expand Down Expand Up @@ -50,6 +51,21 @@ const Map<String, SystemMouseCursor> _cursors = {
SystemMouseCursor _getCursorByName(String name) =>
_cursors[name] ?? SystemMouseCursors.basic;

/// The trackpad scroll-speed knob: how far the page moves per pixel of
/// finger travel. 1.0 is exact 1:1 tracking (measured); it was reported to
/// feel sluggish next to native precision-touchpad scrolling, which applies
/// gain plus inertia. Synthetic fling distances pass through this factor
/// too, so the glide scales together with the drag.
const double _kTrackpadGain = 1.5;

/// Converts trackpad pan pixels to WebView2 wheel units.
/// Mouse-wheel deltas are already wheel-derived and are not scaled.
///
/// WHEEL_DELTA (120) is one wheel notch and Chromium scrolls ~100px per
/// notch (measured against a real WebView2), so pan*120/100 gives exact 1:1
/// finger-to-page tracking, multiplied by the felt [_kTrackpadGain].
double _panToWheelUnits(double pan) => (pan * 120.0 / 100.0) * _kTrackpadGain;

/// Pointer button type
// Order must match InAppWebViewPointerEventKind (see in_app_webview.h)
enum PointerButton { none, primary, secondary, tertiary }
Expand Down Expand Up @@ -294,12 +310,25 @@ class CustomPlatformView extends StatefulWidget {
}

class _CustomPlatformViewState extends State<CustomPlatformView>
with PlatformUtilListener {
with PlatformUtilListener, SingleTickerProviderStateMixin {
final GlobalKey _key = GlobalKey();
final _downButtons = <int, PointerButton>{};

PointerDeviceKind _pointerKind = PointerDeviceKind.unknown;

// Accumulates fractional wheel deltas so sub-pixel trackpad movement is not
// lost when the native side truncates to short.
double _scrollRemainderX = 0;
double _scrollRemainderY = 0;

// Synthetic trackpad inertia; this view bypasses Flutter Scrollable, so it
// must continue a fast pan after the fingers lift.
VelocityTracker? _panVelocityTracker;
Ticker? _flingTicker;
ClampingScrollSimulation? _flingSimulation;
Offset _flingDirection = Offset.zero;
double _flingLastDistance = 0;

MouseCursor _cursor = SystemMouseCursors.basic;

final _controller = CustomPlatformViewController();
Expand Down Expand Up @@ -388,6 +417,7 @@ class _CustomPlatformViewState extends State<CustomPlatformView>
_controller._setCursorPos(ev.localPosition);
},
onPointerDown: (ev) {
_stopFling();
_reportSurfaceSize();
_reportWidgetPosition();

Expand Down Expand Up @@ -464,14 +494,41 @@ class _CustomPlatformViewState extends State<CustomPlatformView>
},
onPointerSignal: (signal) {
if (signal is PointerScrollEvent) {
_controller._setScrollDelta(
_controller._setCursorPos(signal.localPosition);
_stopFling();
_sendScrollDelta(
-signal.scrollDelta.dx,
-signal.scrollDelta.dy,
);
} else if (signal is PointerScrollInertiaCancelEvent) {
// Sent by the engine when the user touches the trackpad
// during inertia — that touch must also halt the
// synthetic glide.
_stopFling();
}
},
onPointerPanZoomStart: (ev) {
_controller._setCursorPos(ev.localPosition);
_stopFling();
_scrollRemainderX = 0;
_scrollRemainderY = 0;
_panVelocityTracker = VelocityTracker.withKind(
PointerDeviceKind.trackpad,
);
},
onPointerPanZoomUpdate: (ev) {
_controller._setScrollDelta(ev.panDelta.dx, ev.panDelta.dy);
_panVelocityTracker?.addPosition(ev.timeStamp, ev.pan);
_sendScrollDelta(
_panToWheelUnits(ev.panDelta.dx),
_panToWheelUnits(ev.panDelta.dy),
);
},
onPointerPanZoomEnd: (ev) {
final tracker = _panVelocityTracker;
_panVelocityTracker = null;
if (tracker != null) {
_startFling(tracker.getVelocity());
}
},
child: MouseRegion(
cursor: _cursor,
Expand Down Expand Up @@ -500,6 +557,72 @@ class _CustomPlatformViewState extends State<CustomPlatformView>
);
}

/// Forwards scroll deltas, preserving fractional remainders.
///
/// Forwarding is intentionally immediate, NOT batched per Flutter frame:
/// tying sends to frame scheduling adds 0-16ms of *variable* latency, and
/// in a busy app a delayed UI frame holds scroll input back and releases it
/// in a burst — scrolling that intermittently "ignores" the fingers and
/// then catches up. Chromium coalesces a per-event wheel stream by itself
/// (this is what high-resolution mouse wheels produce natively).
void _sendScrollDelta(double dx, double dy) {
_scrollRemainderX += dx;
_scrollRemainderY += dy;
final flushX = _scrollRemainderX.truncateToDouble();
final flushY = _scrollRemainderY.truncateToDouble();
if (flushX == 0 && flushY == 0) {
return;
}
_scrollRemainderX -= flushX;
_scrollRemainderY -= flushY;
_controller._setScrollDelta(flushX, flushY);
}

/// Starts synthetic inertia after a fast lifted pan.
/// The decay follows Flutter's clamping scroll simulation.
void _startFling(Velocity velocity) {
final speed = velocity.pixelsPerSecond.distance.clamp(
0.0,
kMaxFlingVelocity,
);
if (speed < kMinFlingVelocity) {
return;
}
_flingDirection =
velocity.pixelsPerSecond / velocity.pixelsPerSecond.distance;
_flingSimulation = ClampingScrollSimulation(position: 0, velocity: speed);
_flingLastDistance = 0;
final ticker = _flingTicker ??= createTicker(_onFlingTick);
ticker.stop();
ticker.start();
}

void _stopFling() {
_flingSimulation = null;
_flingTicker?.stop();
}

void _onFlingTick(Duration elapsed) {
final simulation = _flingSimulation;
if (simulation == null) {
_flingTicker?.stop();
return;
}
final seconds = elapsed.inMicroseconds / Duration.microsecondsPerSecond;
final distance = simulation.x(seconds);
final step = distance - _flingLastDistance;
_flingLastDistance = distance;
if (simulation.isDone(seconds)) {
_stopFling();
}
if (step != 0) {
_sendScrollDelta(
_panToWheelUnits(_flingDirection.dx * step),
_panToWheelUnits(_flingDirection.dy * step),
);
}
}

void _reportSurfaceSize() async {
final box = _key.currentContext?.findRenderObject() as RenderBox?;
if (box != null) {
Expand Down Expand Up @@ -530,6 +653,7 @@ class _CustomPlatformViewState extends State<CustomPlatformView>
@override
void dispose() {
super.dispose();
_flingTicker?.dispose();
_platformUtil.removeListener(this);
_cursorSubscription?.cancel();
_controller.dispose();
Expand Down
Loading