Skip to content

Commit

Permalink
[cp:beta][web] Work around wrong pointerId in coalesced events in iOS…
Browse files Browse the repository at this point in the history
… Safari 18.2 (#56719) (#56905)

Manual cherry pick for #56719

Cherrypick request: flutter/flutter#159692
  • Loading branch information
mdebbar authored Dec 10, 2024
1 parent 45ac4d6 commit 83bacfc
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 16 deletions.
33 changes: 25 additions & 8 deletions lib/web_ui/lib/src/engine/pointer_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<ui.PointerData> pointerData = <ui.PointerData>[];
final List<DomPointerEvent> expandedEvents = _expandEvents(event);
final List<DomPointerEvent> 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) {
Expand Down Expand Up @@ -1077,20 +1089,25 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
required List<ui.PointerData> 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,
change: details.change,
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,32 @@ 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) {
return _computeOffsetForTalkbackEvent(event, actualTarget);
}

// 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) {
Expand Down
139 changes: 137 additions & 2 deletions lib/web_ui/test/engine/pointer_binding_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2526,6 +2526,88 @@ void testMain() {
},
);

test('ignores pointerId on coalesced events', () {
final _MultiPointerEventMixin context = _PointerEventContext();
final List<ui.PointerDataPacket> packets = <ui.PointerDataPacket>[];
List<ui.PointerData> 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',
() {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -3395,6 +3496,10 @@ class _PointerEventContext extends _BasicEventContext

@override
List<DomEvent> 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,
Expand Down Expand Up @@ -3458,6 +3563,7 @@ class _PointerEventContext extends _BasicEventContext
clientX: details.clientX,
clientY: details.clientY,
pointerType: 'touch',
coalescedEvents: details.coalescedEvents,
))
.toList();
}
Expand Down Expand Up @@ -3487,8 +3593,9 @@ class _PointerEventContext extends _BasicEventContext
int? buttons,
int? pointer,
String? pointerType,
List<_CoalescedTouchDetails>? coalescedEvents,
}) {
return createDomPointerEvent('pointermove', <String, dynamic>{
final event = createDomPointerEvent('pointermove', <String, dynamic>{
'bubbles': true,
'pointerId': pointer,
'button': button,
Expand All @@ -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
Expand Down Expand Up @@ -3537,6 +3664,10 @@ class _PointerEventContext extends _BasicEventContext

@override
List<DomEvent> 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,
Expand Down Expand Up @@ -3587,6 +3718,10 @@ class _PointerEventContext extends _BasicEventContext

@override
List<DomEvent> 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', <String, dynamic>{
Expand Down

0 comments on commit 83bacfc

Please sign in to comment.