forked from firka/flutter
Web trackpad pan (flutter/engine#36346)
* Guess at trackpad pans on web * Add test * Update comment * Handle macOS accelerated scroll wheel * Fix test after last commit * Disable on firefox * Pull out _isTrackpadEvent and add doc links * Fix issue with floating point / integer conversion error. * Workaround for magic mouse events which happen to be divisible by 120. * Refactor to handle bad luck in accelerated mouse deltas. Basically, bias towards choosing mouse, but if timestamps are available, we can check the previous event and ensure that false-mouses are avoided. * Use 120 wheelDelta to identify mouse-accelerated events instead of 240 Apparently some high-precision mice use 120 instead of 240 as the wheelDelta per tick. * Handle multiple bad-luck events in a row. Also fix setting of timeStamp in test. * Cleanup parameters
This commit is contained in:
@@ -1127,6 +1127,8 @@ class DomWheelEvent extends DomMouseEvent {}
|
||||
extension DomWheelEventExtension on DomWheelEvent {
|
||||
external double get deltaX;
|
||||
external double get deltaY;
|
||||
external double? get wheelDeltaX;
|
||||
external double? get wheelDeltaY;
|
||||
external double get deltaMode;
|
||||
}
|
||||
|
||||
|
||||
@@ -263,6 +263,8 @@ abstract class _BaseAdapter {
|
||||
final _PointerDataCallback _callback;
|
||||
final PointerDataConverter _pointerDataConverter;
|
||||
final KeyboardConverter _keyboardConverter;
|
||||
DomWheelEvent? _lastWheelEvent;
|
||||
bool _lastWheelEventWasTrackpad = false;
|
||||
|
||||
/// Each subclass is expected to override this method to attach its own event
|
||||
/// listeners and convert events into pointer events.
|
||||
@@ -333,6 +335,71 @@ abstract class _BaseAdapter {
|
||||
mixin _WheelEventListenerMixin on _BaseAdapter {
|
||||
static double? _defaultScrollLineHeight;
|
||||
|
||||
bool _isAcceleratedMouseWheelDelta(num delta, num? wheelDelta) {
|
||||
// On macOS, scrolling using a mouse wheel by default uses an acceleration
|
||||
// curve, so delta values ramp up and are not at fixed multiples of 120.
|
||||
// But in this case, the wheelDelta properties of the event still keep
|
||||
// their original values.
|
||||
// For all events without this acceleration curve applied, the wheelDelta
|
||||
// values are by convention three times greater than the delta values and with
|
||||
// the opposite sign.
|
||||
if (wheelDelta == null) {
|
||||
return false;
|
||||
}
|
||||
// Account for observed issues with integer truncation by allowing +-1px error.
|
||||
return (wheelDelta - (-3 * delta)).abs() > 1;
|
||||
}
|
||||
|
||||
bool _isTrackpadEvent(DomWheelEvent event) {
|
||||
// This function relies on deprecated and non-standard implementation
|
||||
// details. Useful reference material can be found below.
|
||||
//
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:ui/events/event.cc
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:ui/events/cocoa/events_mac.mm
|
||||
// https://github.com/WebKit/WebKit/blob/main/Source/WebCore/platform/mac/PlatformEventFactoryMac.mm
|
||||
// https://searchfox.org/mozilla-central/source/dom/events/WheelEvent.h
|
||||
// https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-mousewheel
|
||||
if (browserEngine == BrowserEngine.firefox) {
|
||||
// Firefox has restricted the wheelDelta properties, they do not provide
|
||||
// enough information to accurately disambiguate trackpad events from mouse
|
||||
// wheel events.
|
||||
return false;
|
||||
}
|
||||
if (_isAcceleratedMouseWheelDelta(event.deltaX, event.wheelDeltaX) ||
|
||||
_isAcceleratedMouseWheelDelta(event.deltaY, event.wheelDeltaY)) {
|
||||
return false;
|
||||
}
|
||||
if (((event.deltaX % 120 == 0) && (event.deltaY % 120 == 0)) ||
|
||||
(((event.wheelDeltaX ?? 1) % 120 == 0) && ((event.wheelDeltaY ?? 1) % 120) == 0)) {
|
||||
// While not in any formal web standard, `blink` and `webkit` browsers use
|
||||
// a delta of 120 to represent one mouse wheel turn. If both dimensions of
|
||||
// the delta are divisible by 120, this event is probably from a mouse.
|
||||
// Checking if wheelDeltaX and wheelDeltaY are both divisible by 120
|
||||
// catches any macOS accelerated mouse wheel deltas which by random chance
|
||||
// are not caught by _isAcceleratedMouseWheelDelta.
|
||||
final num deltaXChange = (event.deltaX - (_lastWheelEvent?.deltaX ?? 0)).abs();
|
||||
final num deltaYChange = (event.deltaY - (_lastWheelEvent?.deltaY ?? 0)).abs();
|
||||
if ((_lastWheelEvent == null) ||
|
||||
(deltaXChange == 0 && deltaYChange == 0) ||
|
||||
!(deltaXChange < 20 && deltaYChange < 20)) {
|
||||
// A trackpad event might by chance have a delta of exactly 120, so
|
||||
// make sure this event does not have a similar delta to the previous
|
||||
// one before calling it a mouse event.
|
||||
if (event.timeStamp != null && _lastWheelEvent?.timeStamp != null) {
|
||||
// If the event has a large delta to the previous event, check if
|
||||
// it was preceded within 50 milliseconds by a trackpad event. This
|
||||
// handles unlucky 120-delta trackpad events during rapid movement.
|
||||
final num diffMs = event.timeStamp! - _lastWheelEvent!.timeStamp!;
|
||||
if (diffMs < 50 && _lastWheelEventWasTrackpad) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
List<ui.PointerData> _convertWheelEventToPointerData(
|
||||
DomWheelEvent event
|
||||
) {
|
||||
@@ -340,6 +407,11 @@ mixin _WheelEventListenerMixin on _BaseAdapter {
|
||||
const int domDeltaLine = 0x01;
|
||||
const int domDeltaPage = 0x02;
|
||||
|
||||
ui.PointerDeviceKind kind = ui.PointerDeviceKind.mouse;
|
||||
if (_isTrackpadEvent(event)) {
|
||||
kind = ui.PointerDeviceKind.trackpad;
|
||||
}
|
||||
|
||||
// Flutter only supports pixel scroll delta. Convert deltaMode values
|
||||
// to pixels.
|
||||
double deltaX = event.deltaX;
|
||||
@@ -371,7 +443,7 @@ mixin _WheelEventListenerMixin on _BaseAdapter {
|
||||
data,
|
||||
change: ui.PointerChange.hover,
|
||||
timeStamp: _BaseAdapter._eventTimeStampToDuration(event.timeStamp!),
|
||||
kind: ui.PointerDeviceKind.mouse,
|
||||
kind: kind,
|
||||
signalKind: ui.PointerSignalKind.scroll,
|
||||
device: _mouseDeviceId,
|
||||
physicalX: event.clientX * ui.window.devicePixelRatio,
|
||||
@@ -382,6 +454,8 @@ mixin _WheelEventListenerMixin on _BaseAdapter {
|
||||
scrollDeltaX: deltaX,
|
||||
scrollDeltaY: deltaY,
|
||||
);
|
||||
_lastWheelEvent = event;
|
||||
_lastWheelEventWasTrackpad = kind == ui.PointerDeviceKind.trackpad;
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1147,6 +1147,229 @@ void testMain() {
|
||||
},
|
||||
);
|
||||
|
||||
_testEach<_ButtonedEventMixin>(
|
||||
<_ButtonedEventMixin>[
|
||||
if (!isIosSafari) _PointerEventContext(),
|
||||
if (!isIosSafari) _MouseEventContext(),
|
||||
],
|
||||
'does set pointer device kind based on delta precision and wheelDelta',
|
||||
(_ButtonedEventMixin context) {
|
||||
if (isFirefox) {
|
||||
// Firefox does not support trackpad events, as they cannot be
|
||||
// disambiguated from smoothed mouse wheel events.
|
||||
return;
|
||||
}
|
||||
PointerBinding.instance!.debugOverrideDetector(context);
|
||||
final List<ui.PointerDataPacket> packets = <ui.PointerDataPacket>[];
|
||||
ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) {
|
||||
packets.add(packet);
|
||||
};
|
||||
|
||||
glassPane.dispatchEvent(context.wheel(
|
||||
buttons: 0,
|
||||
clientX: 10,
|
||||
clientY: 10,
|
||||
deltaX: 119,
|
||||
deltaY: 119,
|
||||
wheelDeltaX: -357,
|
||||
wheelDeltaY: -357,
|
||||
timeStamp: 0,
|
||||
));
|
||||
|
||||
glassPane.dispatchEvent(context.wheel(
|
||||
buttons: 0,
|
||||
clientX: 10,
|
||||
clientY: 10,
|
||||
deltaX: 120,
|
||||
deltaY: 120,
|
||||
wheelDeltaX: -360,
|
||||
wheelDeltaY: -360,
|
||||
timeStamp: 10,
|
||||
));
|
||||
|
||||
glassPane.dispatchEvent(context.wheel(
|
||||
buttons: 0,
|
||||
clientX: 10,
|
||||
clientY: 10,
|
||||
deltaX: 120,
|
||||
deltaY: 120,
|
||||
wheelDeltaX: -360,
|
||||
wheelDeltaY: -360,
|
||||
timeStamp: 20,
|
||||
));
|
||||
|
||||
glassPane.dispatchEvent(context.wheel(
|
||||
buttons: 0,
|
||||
clientX: 10,
|
||||
clientY: 10,
|
||||
deltaX: 119,
|
||||
deltaY: 119,
|
||||
wheelDeltaX: -357,
|
||||
wheelDeltaY: -357,
|
||||
timeStamp: 1000,
|
||||
));
|
||||
|
||||
glassPane.dispatchEvent(context.wheel(
|
||||
buttons: 0,
|
||||
clientX: 10,
|
||||
clientY: 10,
|
||||
deltaX: -120,
|
||||
deltaY: -120,
|
||||
wheelDeltaX: 360,
|
||||
wheelDeltaY: 360,
|
||||
timeStamp: 1010,
|
||||
));
|
||||
|
||||
glassPane.dispatchEvent(context.wheel(
|
||||
buttons: 0,
|
||||
clientX: 10,
|
||||
clientY: 10,
|
||||
deltaX: 0,
|
||||
deltaY: -120,
|
||||
wheelDeltaX: 0,
|
||||
wheelDeltaY: 360,
|
||||
timeStamp: 2000,
|
||||
));
|
||||
|
||||
glassPane.dispatchEvent(context.wheel(
|
||||
buttons: 0,
|
||||
clientX: 10,
|
||||
clientY: 10,
|
||||
deltaX: 0,
|
||||
deltaY: 40,
|
||||
wheelDeltaX: 0,
|
||||
wheelDeltaY: -360,
|
||||
timeStamp: 3000,
|
||||
));
|
||||
|
||||
expect(packets, hasLength(7));
|
||||
|
||||
// An add will be synthesized.
|
||||
expect(packets[0].data, hasLength(2));
|
||||
expect(packets[0].data[0].change, equals(ui.PointerChange.add));
|
||||
expect(packets[0].data[0].pointerIdentifier, equals(0));
|
||||
expect(packets[0].data[0].synthesized, isTrue);
|
||||
expect(packets[0].data[0].physicalX, equals(10.0 * dpi));
|
||||
expect(packets[0].data[0].physicalY, equals(10.0 * dpi));
|
||||
expect(packets[0].data[0].physicalDeltaX, equals(0.0));
|
||||
expect(packets[0].data[0].physicalDeltaY, equals(0.0));
|
||||
// Because the delta is not in increments of 120 and has matching wheelDelta,
|
||||
// it will be a trackpad event.
|
||||
expect(packets[0].data[1].change, equals(ui.PointerChange.hover));
|
||||
expect(
|
||||
packets[0].data[1].signalKind, equals(ui.PointerSignalKind.scroll));
|
||||
expect(
|
||||
packets[0].data[1].kind, equals(ui.PointerDeviceKind.trackpad));
|
||||
expect(packets[0].data[1].pointerIdentifier, equals(0));
|
||||
expect(packets[0].data[1].synthesized, isFalse);
|
||||
expect(packets[0].data[1].physicalX, equals(10.0 * dpi));
|
||||
expect(packets[0].data[1].physicalY, equals(10.0 * dpi));
|
||||
expect(packets[0].data[1].physicalDeltaX, equals(0.0));
|
||||
expect(packets[0].data[1].physicalDeltaY, equals(0.0));
|
||||
expect(packets[0].data[1].scrollDeltaX, equals(119.0));
|
||||
expect(packets[0].data[1].scrollDeltaY, equals(119.0));
|
||||
|
||||
// Because the delta is in increments of 120, but is similar to the
|
||||
// previous event, it will be a trackpad event.
|
||||
expect(packets[1].data[0].change, equals(ui.PointerChange.hover));
|
||||
expect(
|
||||
packets[1].data[0].signalKind, equals(ui.PointerSignalKind.scroll));
|
||||
expect(
|
||||
packets[1].data[0].kind, equals(ui.PointerDeviceKind.trackpad));
|
||||
expect(packets[1].data[0].pointerIdentifier, equals(0));
|
||||
expect(packets[1].data[0].synthesized, isFalse);
|
||||
expect(packets[1].data[0].physicalX, equals(10.0 * dpi));
|
||||
expect(packets[1].data[0].physicalY, equals(10.0 * dpi));
|
||||
expect(packets[1].data[0].physicalDeltaX, equals(0.0));
|
||||
expect(packets[1].data[0].physicalDeltaY, equals(0.0));
|
||||
expect(packets[1].data[0].scrollDeltaX, equals(120.0));
|
||||
expect(packets[1].data[0].scrollDeltaY, equals(120.0));
|
||||
|
||||
// Because the delta is in increments of 120, but is again similar to the
|
||||
// previous event, it will be a trackpad event.
|
||||
expect(packets[2].data[0].change, equals(ui.PointerChange.hover));
|
||||
expect(
|
||||
packets[2].data[0].signalKind, equals(ui.PointerSignalKind.scroll));
|
||||
expect(
|
||||
packets[2].data[0].kind, equals(ui.PointerDeviceKind.trackpad));
|
||||
expect(packets[2].data[0].pointerIdentifier, equals(0));
|
||||
expect(packets[2].data[0].synthesized, isFalse);
|
||||
expect(packets[2].data[0].physicalX, equals(10.0 * dpi));
|
||||
expect(packets[2].data[0].physicalY, equals(10.0 * dpi));
|
||||
expect(packets[2].data[0].physicalDeltaX, equals(0.0));
|
||||
expect(packets[2].data[0].physicalDeltaY, equals(0.0));
|
||||
expect(packets[2].data[0].scrollDeltaX, equals(120.0));
|
||||
expect(packets[2].data[0].scrollDeltaY, equals(120.0));
|
||||
|
||||
// Because the delta is not in increments of 120 and has matching wheelDelta,
|
||||
// it will be a trackpad event.
|
||||
expect(packets[3].data[0].change, equals(ui.PointerChange.hover));
|
||||
expect(
|
||||
packets[3].data[0].signalKind, equals(ui.PointerSignalKind.scroll));
|
||||
expect(
|
||||
packets[3].data[0].kind, equals(ui.PointerDeviceKind.trackpad));
|
||||
expect(packets[3].data[0].pointerIdentifier, equals(0));
|
||||
expect(packets[3].data[0].synthesized, isFalse);
|
||||
expect(packets[3].data[0].physicalX, equals(10.0 * dpi));
|
||||
expect(packets[3].data[0].physicalY, equals(10.0 * dpi));
|
||||
expect(packets[3].data[0].physicalDeltaX, equals(0.0));
|
||||
expect(packets[3].data[0].physicalDeltaY, equals(0.0));
|
||||
expect(packets[3].data[0].scrollDeltaX, equals(119.0));
|
||||
expect(packets[3].data[0].scrollDeltaY, equals(119.0));
|
||||
|
||||
// Because the delta is in increments of 120, and is not similar to the
|
||||
// previous event, but occured soon after the previous event, it will be
|
||||
// a trackpad event.
|
||||
expect(packets[4].data[0].change, equals(ui.PointerChange.hover));
|
||||
expect(
|
||||
packets[4].data[0].signalKind, equals(ui.PointerSignalKind.scroll));
|
||||
expect(
|
||||
packets[4].data[0].kind, equals(ui.PointerDeviceKind.trackpad));
|
||||
expect(packets[4].data[0].pointerIdentifier, equals(0));
|
||||
expect(packets[4].data[0].synthesized, isFalse);
|
||||
expect(packets[4].data[0].physicalX, equals(10.0 * dpi));
|
||||
expect(packets[4].data[0].physicalY, equals(10.0 * dpi));
|
||||
expect(packets[4].data[0].physicalDeltaX, equals(0.0));
|
||||
expect(packets[4].data[0].physicalDeltaY, equals(0.0));
|
||||
expect(packets[4].data[0].scrollDeltaX, equals(-120.0));
|
||||
expect(packets[4].data[0].scrollDeltaY, equals(-120.0));
|
||||
|
||||
// Because the delta is in increments of 120, and is not similar to
|
||||
// the previous event, and occured long after the previous event, it will be a mouse event.
|
||||
expect(packets[5].data, hasLength(1));
|
||||
expect(packets[5].data[0].change, equals(ui.PointerChange.hover));
|
||||
expect(
|
||||
packets[5].data[0].signalKind, equals(ui.PointerSignalKind.scroll));
|
||||
expect(
|
||||
packets[5].data[0].kind, equals(ui.PointerDeviceKind.mouse));
|
||||
expect(packets[5].data[0].pointerIdentifier, equals(0));
|
||||
expect(packets[5].data[0].synthesized, isFalse);
|
||||
expect(packets[5].data[0].physicalX, equals(10.0 * dpi));
|
||||
expect(packets[5].data[0].physicalY, equals(10.0 * dpi));
|
||||
expect(packets[5].data[0].physicalDeltaX, equals(0.0));
|
||||
expect(packets[5].data[0].physicalDeltaY, equals(0.0));
|
||||
expect(packets[5].data[0].scrollDeltaX, equals(0.0));
|
||||
expect(packets[5].data[0].scrollDeltaY, equals(-120.0));
|
||||
|
||||
// Because the delta is not in increments of 120 and has non-matching
|
||||
// wheelDelta, it will be a mouse event.
|
||||
expect(packets[6].data, hasLength(1));
|
||||
expect(packets[6].data[0].change, equals(ui.PointerChange.hover));
|
||||
expect(
|
||||
packets[6].data[0].signalKind, equals(ui.PointerSignalKind.scroll));
|
||||
expect(
|
||||
packets[6].data[0].kind, equals(ui.PointerDeviceKind.mouse));
|
||||
expect(packets[6].data[0].pointerIdentifier, equals(0));
|
||||
expect(packets[6].data[0].synthesized, isFalse);
|
||||
expect(packets[6].data[0].physicalX, equals(10.0 * dpi));
|
||||
expect(packets[6].data[0].physicalY, equals(10.0 * dpi));
|
||||
expect(packets[6].data[0].physicalDeltaX, equals(0.0));
|
||||
expect(packets[6].data[0].physicalDeltaY, equals(0.0));
|
||||
expect(packets[6].data[0].scrollDeltaX, equals(0.0));
|
||||
expect(packets[6].data[0].scrollDeltaY, equals(40.0));
|
||||
},
|
||||
);
|
||||
|
||||
_testEach<_ButtonedEventMixin>(
|
||||
<_ButtonedEventMixin>[
|
||||
if (!isIosSafari) _PointerEventContext(),
|
||||
@@ -2854,6 +3077,9 @@ mixin _ButtonedEventMixin on _BasicEventContext {
|
||||
required double? clientY,
|
||||
required double? deltaX,
|
||||
required double? deltaY,
|
||||
double? wheelDeltaX,
|
||||
double? wheelDeltaY,
|
||||
int? timeStamp,
|
||||
}) {
|
||||
final Function jsWheelEvent = js_util.getProperty<Function>(domWindow, 'WheelEvent');
|
||||
final List<dynamic> eventArgs = <dynamic>[
|
||||
@@ -2864,12 +3090,30 @@ mixin _ButtonedEventMixin on _BasicEventContext {
|
||||
'clientY': clientY,
|
||||
'deltaX': deltaX,
|
||||
'deltaY': deltaY,
|
||||
'wheelDeltaX': wheelDeltaX,
|
||||
'wheelDeltaY': wheelDeltaY,
|
||||
}
|
||||
];
|
||||
return js_util.callConstructor<DomEvent>(
|
||||
final DomEvent event = js_util.callConstructor<DomEvent>(
|
||||
jsWheelEvent,
|
||||
js_util.jsify(eventArgs) as List<Object?>,
|
||||
);
|
||||
// timeStamp can't be set in the constructor, need to override the getter.
|
||||
if (timeStamp != null) {
|
||||
js_util.callMethod(
|
||||
objectConstructor,
|
||||
'defineProperty',
|
||||
<dynamic>[
|
||||
event,
|
||||
'timeStamp',
|
||||
js_util.jsify(<String, dynamic>{
|
||||
'value': timeStamp,
|
||||
'configurable': true
|
||||
})
|
||||
]
|
||||
);
|
||||
}
|
||||
return event;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user