diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index 11502593eb97e..228b30a7f5a19 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -1010,20 +1010,32 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { }); // Why `domWindow` you ask? See this fiddle: https://jsfiddle.net/ditman/7towxaqp - _addPointerEventListener(_globalTarget, 'pointermove', (DomPointerEvent event) { - final int device = _getPointerId(event); + _addPointerEventListener(_globalTarget, 'pointermove', (DomPointerEvent moveEvent) { + final int device = _getPointerId(moveEvent); final _ButtonSanitizer sanitizer = _ensureSanitizer(device); final List pointerData = []; - final List expandedEvents = _expandEvents(event); + final List expandedEvents = _expandEvents(moveEvent); for (final DomPointerEvent event in expandedEvents) { final _SanitizedDetails? up = sanitizer.sanitizeMissingRightClickUp(buttons: event.buttons!.toInt()); if (up != null) { - _convertEventsToPointerData(data: pointerData, event: event, details: up); + _convertEventsToPointerData( + data: pointerData, + event: event, + details: up, + pointerId: device, + eventTarget: moveEvent.target, + ); } final _SanitizedDetails move = sanitizer.sanitizeMoveEvent(buttons: event.buttons!.toInt()); - _convertEventsToPointerData(data: pointerData, event: event, details: move); + _convertEventsToPointerData( + data: pointerData, + event: event, + details: move, + pointerId: device, + eventTarget: moveEvent.target, + ); } - _callback(event, pointerData); + _callback(moveEvent, pointerData); }); _addPointerEventListener(_viewTarget, 'pointerleave', (DomPointerEvent event) { @@ -1077,12 +1089,17 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { required List data, required DomPointerEvent event, required _SanitizedDetails details, + // `pointerId` and `eventTarget` are optional but useful when it's not + // desired to get those values from the event object. For example, when the + // event is a coalesced event. + int? pointerId, + DomEventTarget? eventTarget, }) { final ui.PointerDeviceKind kind = _pointerTypeToDeviceKind(event.pointerType!); final double tilt = _computeHighestTilt(event); final Duration timeStamp = _BaseAdapter._eventTimeStampToDuration(event.timeStamp!); final num? pressure = event.pressure; - final ui.Offset offset = computeEventOffsetToTarget(event, _view); + final ui.Offset offset = computeEventOffsetToTarget(event, _view, eventTarget: eventTarget); _pointerDataConverter.convert( data, viewId: _view.viewId, @@ -1090,7 +1107,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { timeStamp: timeStamp, kind: kind, signalKind: ui.PointerSignalKind.none, - device: _getPointerId(event), + device: pointerId ?? _getPointerId(event), physicalX: offset.dx * _view.devicePixelRatio, physicalY: offset.dy * _view.devicePixelRatio, buttons: details.buttons, diff --git a/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart b/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart index 319a3620cd741..494f940d58eb4 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart @@ -12,18 +12,23 @@ import '../text_editing/text_editing.dart'; import '../vector_math.dart'; import '../window.dart'; -/// Returns an [ui.Offset] of the position of [event], relative to the position of [actualTarget]. +/// Returns an [ui.Offset] of the position of [event], relative to the position +/// of the Flutter [view]. /// /// The offset is *not* multiplied by DPR or anything else, it's the closest /// to what the DOM would return if we had currentTarget readily available. /// -/// This needs an `actualTarget`, because the `event.currentTarget` (which is what -/// this would really need to use) gets lost when the `event` comes from a "coalesced" -/// event. +/// This needs an `eventTarget`, because the `event.target` (which is what +/// this would really need to use) gets lost when the `event` comes from a +/// "coalesced" event (see https://github.com/flutter/flutter/issues/155987). /// /// It also takes into account semantics being enabled to fix the case where /// offsetX, offsetY == 0 (TalkBack events). -ui.Offset computeEventOffsetToTarget(DomMouseEvent event, EngineFlutterView view) { +ui.Offset computeEventOffsetToTarget( + DomMouseEvent event, + EngineFlutterView view, { + DomEventTarget? eventTarget, +}) { final DomElement actualTarget = view.dom.rootElement; // On a TalkBack event if (EngineSemantics.instance.semanticsEnabled && event.offsetX == 0 && event.offsetY == 0) { @@ -31,7 +36,8 @@ ui.Offset computeEventOffsetToTarget(DomMouseEvent event, EngineFlutterView view } // On one of our text-editing nodes - final bool isInput = view.dom.textEditingHost.contains(event.target! as DomNode); + eventTarget ??= event.target!; + final bool isInput = view.dom.textEditingHost.contains(eventTarget as DomNode); if (isInput) { final EditableTextGeometry? inputGeometry = textEditing.strategy.geometry; if (inputGeometry != null) { diff --git a/lib/web_ui/test/engine/pointer_binding_test.dart b/lib/web_ui/test/engine/pointer_binding_test.dart index f765c492f7226..374ed872f428e 100644 --- a/lib/web_ui/test/engine/pointer_binding_test.dart +++ b/lib/web_ui/test/engine/pointer_binding_test.dart @@ -2526,6 +2526,88 @@ void testMain() { }, ); + test('ignores pointerId on coalesced events', () { + final _MultiPointerEventMixin context = _PointerEventContext(); + final List packets = []; + List data; + ui.PlatformDispatcher.instance.onPointerDataPacket = (ui.PointerDataPacket packet) { + packets.add(packet); + }; + + context.multiTouchDown(const <_TouchDetails>[ + _TouchDetails(pointer: 52, clientX: 100, clientY: 101), + ]).forEach(rootElement.dispatchEvent); + expect(packets.length, 1); + + data = packets.single.data; + expect(data, hasLength(2)); + expect(data[0].change, equals(ui.PointerChange.add)); + expect(data[0].synthesized, isTrue); + expect(data[0].device, equals(52)); + expect(data[0].physicalX, equals(100 * dpi)); + expect(data[0].physicalY, equals(101 * dpi)); + + expect(data[1].change, equals(ui.PointerChange.down)); + expect(data[1].device, equals(52)); + expect(data[1].buttons, equals(1)); + expect(data[1].physicalX, equals(100 * dpi)); + expect(data[1].physicalY, equals(101 * dpi)); + expect(data[1].physicalDeltaX, equals(0)); + expect(data[1].physicalDeltaY, equals(0)); + packets.clear(); + + // Pointer move with coaleasced events + context.multiTouchMove(const <_TouchDetails>[ + _TouchDetails(pointer: 52, coalescedEvents: <_CoalescedTouchDetails>[ + _CoalescedTouchDetails(pointer: 0, clientX: 301, clientY: 302), + _CoalescedTouchDetails(pointer: 0, clientX: 401, clientY: 402), + ]), + ]).forEach(rootElement.dispatchEvent); + expect(packets.length, 1); + + data = packets.single.data; + expect(data, hasLength(2)); + expect(data[0].change, equals(ui.PointerChange.move)); + expect(data[0].device, equals(52)); + expect(data[0].buttons, equals(1)); + expect(data[0].physicalX, equals(301 * dpi)); + expect(data[0].physicalY, equals(302 * dpi)); + expect(data[0].physicalDeltaX, equals(201 * dpi)); + expect(data[0].physicalDeltaY, equals(201 * dpi)); + + expect(data[1].change, equals(ui.PointerChange.move)); + expect(data[1].device, equals(52)); + expect(data[1].buttons, equals(1)); + expect(data[1].physicalX, equals(401 * dpi)); + expect(data[1].physicalY, equals(402 * dpi)); + expect(data[1].physicalDeltaX, equals(100 * dpi)); + expect(data[1].physicalDeltaY, equals(100 * dpi)); + packets.clear(); + + // Pointer up + context.multiTouchUp(const <_TouchDetails>[ + _TouchDetails(pointer: 52, clientX: 401, clientY: 402), + ]).forEach(rootElement.dispatchEvent); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(2)); + expect(packets[0].data[0].change, equals(ui.PointerChange.up)); + expect(packets[0].data[0].device, equals(52)); + expect(packets[0].data[0].buttons, equals(0)); + expect(packets[0].data[0].physicalX, equals(401 * dpi)); + expect(packets[0].data[0].physicalY, equals(402 * dpi)); + expect(packets[0].data[0].physicalDeltaX, equals(0)); + expect(packets[0].data[0].physicalDeltaY, equals(0)); + + expect(packets[0].data[1].change, equals(ui.PointerChange.remove)); + expect(packets[0].data[1].device, equals(52)); + expect(packets[0].data[1].buttons, equals(0)); + expect(packets[0].data[1].physicalX, equals(401 * dpi)); + expect(packets[0].data[1].physicalY, equals(402 * dpi)); + expect(packets[0].data[1].physicalDeltaX, equals(0)); + expect(packets[0].data[1].physicalDeltaY, equals(0)); + packets.clear(); + }); + test( 'correctly parses cancel event', () { @@ -3336,7 +3418,26 @@ mixin _ButtonedEventMixin on _BasicEventContext { } class _TouchDetails { - const _TouchDetails({this.pointer, this.clientX, this.clientY}); + const _TouchDetails({ + this.pointer, + this.clientX, + this.clientY, + this.coalescedEvents, + }); + + final int? pointer; + final double? clientX; + final double? clientY; + + final List<_CoalescedTouchDetails>? coalescedEvents; +} + +class _CoalescedTouchDetails { + const _CoalescedTouchDetails({ + this.pointer, + this.clientX, + this.clientY, + }); final int? pointer; final double? clientX; @@ -3395,6 +3496,10 @@ class _PointerEventContext extends _BasicEventContext @override List multiTouchDown(List<_TouchDetails> touches) { + assert( + touches.every((_TouchDetails details) => details.coalescedEvents == null), + 'Coalesced events are not allowed for pointerdown events.', + ); return touches .map((_TouchDetails details) => _downWithFullDetails( pointer: details.pointer, @@ -3458,6 +3563,7 @@ class _PointerEventContext extends _BasicEventContext clientX: details.clientX, clientY: details.clientY, pointerType: 'touch', + coalescedEvents: details.coalescedEvents, )) .toList(); } @@ -3487,8 +3593,9 @@ class _PointerEventContext extends _BasicEventContext int? buttons, int? pointer, String? pointerType, + List<_CoalescedTouchDetails>? coalescedEvents, }) { - return createDomPointerEvent('pointermove', { + final event = createDomPointerEvent('pointermove', { 'bubbles': true, 'pointerId': pointer, 'button': button, @@ -3497,6 +3604,26 @@ class _PointerEventContext extends _BasicEventContext 'clientY': clientY, 'pointerType': pointerType, }); + + if (coalescedEvents != null) { + // There's no JS API for setting coalesced events, so we need to + // monkey-patch the `getCoalescedEvents` method to return what we want. + final coalescedEventJs = coalescedEvents + .map((_CoalescedTouchDetails details) => _moveWithFullDetails( + pointer: details.pointer, + button: button, + buttons: buttons, + clientX: details.clientX, + clientY: details.clientY, + pointerType: 'touch', + )).toJSAnyDeep; + + js_util.setProperty(event, 'getCoalescedEvents', js_util.allowInterop(() { + return coalescedEventJs; + })); + } + + return event; } @override @@ -3537,6 +3664,10 @@ class _PointerEventContext extends _BasicEventContext @override List multiTouchUp(List<_TouchDetails> touches) { + assert( + touches.every((_TouchDetails details) => details.coalescedEvents == null), + 'Coalesced events are not allowed for pointerup events.', + ); return touches .map((_TouchDetails details) => _upWithFullDetails( pointer: details.pointer, @@ -3587,6 +3718,10 @@ class _PointerEventContext extends _BasicEventContext @override List multiTouchCancel(List<_TouchDetails> touches) { + assert( + touches.every((_TouchDetails details) => details.coalescedEvents == null), + 'Coalesced events are not allowed for pointercancel events.', + ); return touches .map((_TouchDetails details) => createDomPointerEvent('pointercancel', {