Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 83bacfc

Browse files
authored
[cp:beta][web] Work around wrong pointerId in coalesced events in iOS Safari 18.2 (#56719) (#56905)
Manual cherry pick for #56719 Cherrypick request: flutter/flutter#159692
1 parent 45ac4d6 commit 83bacfc

File tree

3 files changed

+174
-16
lines changed

3 files changed

+174
-16
lines changed

lib/web_ui/lib/src/engine/pointer_binding.dart

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,20 +1010,32 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
10101010
});
10111011

10121012
// Why `domWindow` you ask? See this fiddle: https://jsfiddle.net/ditman/7towxaqp
1013-
_addPointerEventListener(_globalTarget, 'pointermove', (DomPointerEvent event) {
1014-
final int device = _getPointerId(event);
1013+
_addPointerEventListener(_globalTarget, 'pointermove', (DomPointerEvent moveEvent) {
1014+
final int device = _getPointerId(moveEvent);
10151015
final _ButtonSanitizer sanitizer = _ensureSanitizer(device);
10161016
final List<ui.PointerData> pointerData = <ui.PointerData>[];
1017-
final List<DomPointerEvent> expandedEvents = _expandEvents(event);
1017+
final List<DomPointerEvent> expandedEvents = _expandEvents(moveEvent);
10181018
for (final DomPointerEvent event in expandedEvents) {
10191019
final _SanitizedDetails? up = sanitizer.sanitizeMissingRightClickUp(buttons: event.buttons!.toInt());
10201020
if (up != null) {
1021-
_convertEventsToPointerData(data: pointerData, event: event, details: up);
1021+
_convertEventsToPointerData(
1022+
data: pointerData,
1023+
event: event,
1024+
details: up,
1025+
pointerId: device,
1026+
eventTarget: moveEvent.target,
1027+
);
10221028
}
10231029
final _SanitizedDetails move = sanitizer.sanitizeMoveEvent(buttons: event.buttons!.toInt());
1024-
_convertEventsToPointerData(data: pointerData, event: event, details: move);
1030+
_convertEventsToPointerData(
1031+
data: pointerData,
1032+
event: event,
1033+
details: move,
1034+
pointerId: device,
1035+
eventTarget: moveEvent.target,
1036+
);
10251037
}
1026-
_callback(event, pointerData);
1038+
_callback(moveEvent, pointerData);
10271039
});
10281040

10291041
_addPointerEventListener(_viewTarget, 'pointerleave', (DomPointerEvent event) {
@@ -1077,20 +1089,25 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
10771089
required List<ui.PointerData> data,
10781090
required DomPointerEvent event,
10791091
required _SanitizedDetails details,
1092+
// `pointerId` and `eventTarget` are optional but useful when it's not
1093+
// desired to get those values from the event object. For example, when the
1094+
// event is a coalesced event.
1095+
int? pointerId,
1096+
DomEventTarget? eventTarget,
10801097
}) {
10811098
final ui.PointerDeviceKind kind = _pointerTypeToDeviceKind(event.pointerType!);
10821099
final double tilt = _computeHighestTilt(event);
10831100
final Duration timeStamp = _BaseAdapter._eventTimeStampToDuration(event.timeStamp!);
10841101
final num? pressure = event.pressure;
1085-
final ui.Offset offset = computeEventOffsetToTarget(event, _view);
1102+
final ui.Offset offset = computeEventOffsetToTarget(event, _view, eventTarget: eventTarget);
10861103
_pointerDataConverter.convert(
10871104
data,
10881105
viewId: _view.viewId,
10891106
change: details.change,
10901107
timeStamp: timeStamp,
10911108
kind: kind,
10921109
signalKind: ui.PointerSignalKind.none,
1093-
device: _getPointerId(event),
1110+
device: pointerId ?? _getPointerId(event),
10941111
physicalX: offset.dx * _view.devicePixelRatio,
10951112
physicalY: offset.dy * _view.devicePixelRatio,
10961113
buttons: details.buttons,

lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,32 @@ import '../text_editing/text_editing.dart';
1212
import '../vector_math.dart';
1313
import '../window.dart';
1414

15-
/// Returns an [ui.Offset] of the position of [event], relative to the position of [actualTarget].
15+
/// Returns an [ui.Offset] of the position of [event], relative to the position
16+
/// of the Flutter [view].
1617
///
1718
/// The offset is *not* multiplied by DPR or anything else, it's the closest
1819
/// to what the DOM would return if we had currentTarget readily available.
1920
///
20-
/// This needs an `actualTarget`, because the `event.currentTarget` (which is what
21-
/// this would really need to use) gets lost when the `event` comes from a "coalesced"
22-
/// event.
21+
/// This needs an `eventTarget`, because the `event.target` (which is what
22+
/// this would really need to use) gets lost when the `event` comes from a
23+
/// "coalesced" event (see https://github.com/flutter/flutter/issues/155987).
2324
///
2425
/// It also takes into account semantics being enabled to fix the case where
2526
/// offsetX, offsetY == 0 (TalkBack events).
26-
ui.Offset computeEventOffsetToTarget(DomMouseEvent event, EngineFlutterView view) {
27+
ui.Offset computeEventOffsetToTarget(
28+
DomMouseEvent event,
29+
EngineFlutterView view, {
30+
DomEventTarget? eventTarget,
31+
}) {
2732
final DomElement actualTarget = view.dom.rootElement;
2833
// On a TalkBack event
2934
if (EngineSemantics.instance.semanticsEnabled && event.offsetX == 0 && event.offsetY == 0) {
3035
return _computeOffsetForTalkbackEvent(event, actualTarget);
3136
}
3237

3338
// On one of our text-editing nodes
34-
final bool isInput = view.dom.textEditingHost.contains(event.target! as DomNode);
39+
eventTarget ??= event.target!;
40+
final bool isInput = view.dom.textEditingHost.contains(eventTarget as DomNode);
3541
if (isInput) {
3642
final EditableTextGeometry? inputGeometry = textEditing.strategy.geometry;
3743
if (inputGeometry != null) {

lib/web_ui/test/engine/pointer_binding_test.dart

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2526,6 +2526,88 @@ void testMain() {
25262526
},
25272527
);
25282528

2529+
test('ignores pointerId on coalesced events', () {
2530+
final _MultiPointerEventMixin context = _PointerEventContext();
2531+
final List<ui.PointerDataPacket> packets = <ui.PointerDataPacket>[];
2532+
List<ui.PointerData> data;
2533+
ui.PlatformDispatcher.instance.onPointerDataPacket = (ui.PointerDataPacket packet) {
2534+
packets.add(packet);
2535+
};
2536+
2537+
context.multiTouchDown(const <_TouchDetails>[
2538+
_TouchDetails(pointer: 52, clientX: 100, clientY: 101),
2539+
]).forEach(rootElement.dispatchEvent);
2540+
expect(packets.length, 1);
2541+
2542+
data = packets.single.data;
2543+
expect(data, hasLength(2));
2544+
expect(data[0].change, equals(ui.PointerChange.add));
2545+
expect(data[0].synthesized, isTrue);
2546+
expect(data[0].device, equals(52));
2547+
expect(data[0].physicalX, equals(100 * dpi));
2548+
expect(data[0].physicalY, equals(101 * dpi));
2549+
2550+
expect(data[1].change, equals(ui.PointerChange.down));
2551+
expect(data[1].device, equals(52));
2552+
expect(data[1].buttons, equals(1));
2553+
expect(data[1].physicalX, equals(100 * dpi));
2554+
expect(data[1].physicalY, equals(101 * dpi));
2555+
expect(data[1].physicalDeltaX, equals(0));
2556+
expect(data[1].physicalDeltaY, equals(0));
2557+
packets.clear();
2558+
2559+
// Pointer move with coaleasced events
2560+
context.multiTouchMove(const <_TouchDetails>[
2561+
_TouchDetails(pointer: 52, coalescedEvents: <_CoalescedTouchDetails>[
2562+
_CoalescedTouchDetails(pointer: 0, clientX: 301, clientY: 302),
2563+
_CoalescedTouchDetails(pointer: 0, clientX: 401, clientY: 402),
2564+
]),
2565+
]).forEach(rootElement.dispatchEvent);
2566+
expect(packets.length, 1);
2567+
2568+
data = packets.single.data;
2569+
expect(data, hasLength(2));
2570+
expect(data[0].change, equals(ui.PointerChange.move));
2571+
expect(data[0].device, equals(52));
2572+
expect(data[0].buttons, equals(1));
2573+
expect(data[0].physicalX, equals(301 * dpi));
2574+
expect(data[0].physicalY, equals(302 * dpi));
2575+
expect(data[0].physicalDeltaX, equals(201 * dpi));
2576+
expect(data[0].physicalDeltaY, equals(201 * dpi));
2577+
2578+
expect(data[1].change, equals(ui.PointerChange.move));
2579+
expect(data[1].device, equals(52));
2580+
expect(data[1].buttons, equals(1));
2581+
expect(data[1].physicalX, equals(401 * dpi));
2582+
expect(data[1].physicalY, equals(402 * dpi));
2583+
expect(data[1].physicalDeltaX, equals(100 * dpi));
2584+
expect(data[1].physicalDeltaY, equals(100 * dpi));
2585+
packets.clear();
2586+
2587+
// Pointer up
2588+
context.multiTouchUp(const <_TouchDetails>[
2589+
_TouchDetails(pointer: 52, clientX: 401, clientY: 402),
2590+
]).forEach(rootElement.dispatchEvent);
2591+
expect(packets, hasLength(1));
2592+
expect(packets[0].data, hasLength(2));
2593+
expect(packets[0].data[0].change, equals(ui.PointerChange.up));
2594+
expect(packets[0].data[0].device, equals(52));
2595+
expect(packets[0].data[0].buttons, equals(0));
2596+
expect(packets[0].data[0].physicalX, equals(401 * dpi));
2597+
expect(packets[0].data[0].physicalY, equals(402 * dpi));
2598+
expect(packets[0].data[0].physicalDeltaX, equals(0));
2599+
expect(packets[0].data[0].physicalDeltaY, equals(0));
2600+
2601+
expect(packets[0].data[1].change, equals(ui.PointerChange.remove));
2602+
expect(packets[0].data[1].device, equals(52));
2603+
expect(packets[0].data[1].buttons, equals(0));
2604+
expect(packets[0].data[1].physicalX, equals(401 * dpi));
2605+
expect(packets[0].data[1].physicalY, equals(402 * dpi));
2606+
expect(packets[0].data[1].physicalDeltaX, equals(0));
2607+
expect(packets[0].data[1].physicalDeltaY, equals(0));
2608+
packets.clear();
2609+
});
2610+
25292611
test(
25302612
'correctly parses cancel event',
25312613
() {
@@ -3336,7 +3418,26 @@ mixin _ButtonedEventMixin on _BasicEventContext {
33363418
}
33373419

33383420
class _TouchDetails {
3339-
const _TouchDetails({this.pointer, this.clientX, this.clientY});
3421+
const _TouchDetails({
3422+
this.pointer,
3423+
this.clientX,
3424+
this.clientY,
3425+
this.coalescedEvents,
3426+
});
3427+
3428+
final int? pointer;
3429+
final double? clientX;
3430+
final double? clientY;
3431+
3432+
final List<_CoalescedTouchDetails>? coalescedEvents;
3433+
}
3434+
3435+
class _CoalescedTouchDetails {
3436+
const _CoalescedTouchDetails({
3437+
this.pointer,
3438+
this.clientX,
3439+
this.clientY,
3440+
});
33403441

33413442
final int? pointer;
33423443
final double? clientX;
@@ -3395,6 +3496,10 @@ class _PointerEventContext extends _BasicEventContext
33953496

33963497
@override
33973498
List<DomEvent> multiTouchDown(List<_TouchDetails> touches) {
3499+
assert(
3500+
touches.every((_TouchDetails details) => details.coalescedEvents == null),
3501+
'Coalesced events are not allowed for pointerdown events.',
3502+
);
33983503
return touches
33993504
.map((_TouchDetails details) => _downWithFullDetails(
34003505
pointer: details.pointer,
@@ -3458,6 +3563,7 @@ class _PointerEventContext extends _BasicEventContext
34583563
clientX: details.clientX,
34593564
clientY: details.clientY,
34603565
pointerType: 'touch',
3566+
coalescedEvents: details.coalescedEvents,
34613567
))
34623568
.toList();
34633569
}
@@ -3487,8 +3593,9 @@ class _PointerEventContext extends _BasicEventContext
34873593
int? buttons,
34883594
int? pointer,
34893595
String? pointerType,
3596+
List<_CoalescedTouchDetails>? coalescedEvents,
34903597
}) {
3491-
return createDomPointerEvent('pointermove', <String, dynamic>{
3598+
final event = createDomPointerEvent('pointermove', <String, dynamic>{
34923599
'bubbles': true,
34933600
'pointerId': pointer,
34943601
'button': button,
@@ -3497,6 +3604,26 @@ class _PointerEventContext extends _BasicEventContext
34973604
'clientY': clientY,
34983605
'pointerType': pointerType,
34993606
});
3607+
3608+
if (coalescedEvents != null) {
3609+
// There's no JS API for setting coalesced events, so we need to
3610+
// monkey-patch the `getCoalescedEvents` method to return what we want.
3611+
final coalescedEventJs = coalescedEvents
3612+
.map((_CoalescedTouchDetails details) => _moveWithFullDetails(
3613+
pointer: details.pointer,
3614+
button: button,
3615+
buttons: buttons,
3616+
clientX: details.clientX,
3617+
clientY: details.clientY,
3618+
pointerType: 'touch',
3619+
)).toJSAnyDeep;
3620+
3621+
js_util.setProperty(event, 'getCoalescedEvents', js_util.allowInterop(() {
3622+
return coalescedEventJs;
3623+
}));
3624+
}
3625+
3626+
return event;
35003627
}
35013628

35023629
@override
@@ -3537,6 +3664,10 @@ class _PointerEventContext extends _BasicEventContext
35373664

35383665
@override
35393666
List<DomEvent> multiTouchUp(List<_TouchDetails> touches) {
3667+
assert(
3668+
touches.every((_TouchDetails details) => details.coalescedEvents == null),
3669+
'Coalesced events are not allowed for pointerup events.',
3670+
);
35403671
return touches
35413672
.map((_TouchDetails details) => _upWithFullDetails(
35423673
pointer: details.pointer,
@@ -3587,6 +3718,10 @@ class _PointerEventContext extends _BasicEventContext
35873718

35883719
@override
35893720
List<DomEvent> multiTouchCancel(List<_TouchDetails> touches) {
3721+
assert(
3722+
touches.every((_TouchDetails details) => details.coalescedEvents == null),
3723+
'Coalesced events are not allowed for pointercancel events.',
3724+
);
35903725
return touches
35913726
.map((_TouchDetails details) =>
35923727
createDomPointerEvent('pointercancel', <String, dynamic>{

0 commit comments

Comments
 (0)