Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
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
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
Loading