[web] Fix drag failure when RMB pointer up event is not received (#22946)
diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart
index 74f21a5..b4edd40 100644
--- a/lib/web_ui/lib/src/engine/pointer_binding.dart
+++ b/lib/web_ui/lib/src/engine/pointer_binding.dart
@@ -199,7 +199,13 @@
}
if (_debugLogPointerEvents) {
- print(event.type);
+ if (event is html.PointerEvent) {
+ print('${event.type} '
+ '${event.client.x.toStringAsFixed(1)},'
+ '${event.client.y.toStringAsFixed(1)}');
+ } else {
+ print(event.type);
+ }
}
// Report the event to semantics. This information is used to debounce
// browser gestures. Semantics tells us whether it is safe to forward
@@ -381,6 +387,7 @@
}
_pressedButtons = _inferDownFlutterButtons(button, buttons);
+
return _SanitizedDetails(
change: ui.PointerChange.down,
buttons: _pressedButtons,
@@ -389,18 +396,6 @@
_SanitizedDetails sanitizeMoveEvent({required int buttons}) {
final int newPressedButtons = _htmlButtonsToFlutterButtons(buttons);
- // This could happen when the context menu is active and the user clicks
- // RMB somewhere else. The browser sends a down event with `buttons:0`.
- //
- // In this case, we keep the old `buttons` value so we don't confuse the
- // framework.
- if (_pressedButtons != 0 && newPressedButtons == 0) {
- return _SanitizedDetails(
- change: ui.PointerChange.move,
- buttons: _pressedButtons,
- );
- }
-
// This could happen when the user clicks RMB then moves the mouse quickly.
// The brower sends a move event with `buttons:2` even though there's no
// buttons down yet.
@@ -434,6 +429,30 @@
);
}
+ _SanitizedDetails? sanitizeUpEventWithButtons({required int buttons}) {
+ final int newPressedButtons = _htmlButtonsToFlutterButtons(buttons);
+ // This could happen when the context menu is active and the user clicks
+ // RMB somewhere else. The browser sends a down event with `buttons:0`.
+ //
+ // In this case, we keep the old `buttons` value so we don't confuse the
+ // framework.
+ if (_pressedButtons != 0 && newPressedButtons == 0) {
+ return _SanitizedDetails(
+ change: ui.PointerChange.move,
+ buttons: _pressedButtons,
+ );
+ }
+
+ _pressedButtons = newPressedButtons;
+
+ return _SanitizedDetails(
+ change: _pressedButtons == 0
+ ? ui.PointerChange.hover
+ : ui.PointerChange.move,
+ buttons: _pressedButtons,
+ );
+ }
+
_SanitizedDetails sanitizeCancelEvent() {
_pressedButtons = 0;
return _SanitizedDetails(
@@ -444,6 +463,7 @@
}
typedef _PointerEventListener = dynamic Function(html.PointerEvent event);
+const int kContextMenuButton = 2;
/// Adapter class to be used with browsers that support native pointer events.
///
@@ -492,8 +512,16 @@
_addPointerEventListener('pointerdown', (html.PointerEvent event) {
final int device = event.pointerId!;
final List<ui.PointerData> pointerData = <ui.PointerData>[];
+ final _ButtonSanitizer sanitizer = _ensureSanitizer(device);
+ if (event.button == kContextMenuButton) {
+ _handleMissingRightMouseUpEvent(sanitizer,
+ sanitizer._pressedButtons,
+ sanitizer._pressedButtons & ~kContextMenuButton,
+ event,
+ pointerData);
+ }
final _SanitizedDetails details =
- _ensureSanitizer(device).sanitizeDownEvent(
+ sanitizer.sanitizeDownEvent(
button: event.button,
buttons: event.buttons!,
);
@@ -505,9 +533,19 @@
final int device = event.pointerId!;
final _ButtonSanitizer sanitizer = _ensureSanitizer(device);
final List<ui.PointerData> pointerData = <ui.PointerData>[];
+ final int buttonsBeforeEvent = sanitizer._pressedButtons;
final Iterable<_SanitizedDetails> detailsList = _expandEvents(event).map(
- (html.PointerEvent expandedEvent) => sanitizer.sanitizeMoveEvent(buttons: expandedEvent.buttons!),
+ (html.PointerEvent expandedEvent) {
+ return sanitizer.sanitizeMoveEvent(buttons: expandedEvent.buttons!);
+ },
);
+ _handleMissingRightMouseUpEvent(
+ sanitizer,
+ buttonsBeforeEvent,
+ (sanitizer._inferDownFlutterButtons(event.button, event.buttons!)
+ & kContextMenuButton),
+ event,
+ pointerData);
for (_SanitizedDetails details in detailsList) {
_convertEventsToPointerData(data: pointerData, event: event, details: details);
}
@@ -541,6 +579,39 @@
});
}
+ // Handle special case where right mouse button no longer is pressed.
+ // We need to synthesize right mouse up, otherwise drag gesture will fail
+ // to complete or multiple RMB down events will lead to wrong state.
+ void _handleMissingRightMouseUpEvent(_ButtonSanitizer sanitizer,
+ int buttonsBeforeEvent, int buttonsAfterEvent, html.PointerEvent event,
+ List<ui.PointerData> pointerData) {
+ if ((buttonsBeforeEvent & kContextMenuButton) != 0 &&
+ buttonsAfterEvent == 0) {
+ final ui.PointerDeviceKind kind =
+ _pointerTypeToDeviceKind(event.pointerType!);
+ final int device = kind == ui.PointerDeviceKind.mouse
+ ? _mouseDeviceId : event.pointerId!;
+ final double tilt = _computeHighestTilt(event);
+ final Duration timeStamp = _BaseAdapter._eventTimeStampToDuration(event.timeStamp!);
+ sanitizer._pressedButtons &= ~kContextMenuButton;
+ _pointerDataConverter.convert(
+ pointerData,
+ change: ui.PointerChange.up,
+ timeStamp: timeStamp,
+ kind: kind,
+ signalKind: ui.PointerSignalKind.none,
+ device: device,
+ physicalX: event.client.x.toDouble() * ui.window.devicePixelRatio,
+ physicalY: event.client.y.toDouble() * ui.window.devicePixelRatio,
+ buttons: sanitizer._pressedButtons,
+ pressure: event.pressure as double,
+ pressureMin: 0.0,
+ pressureMax: 1.0,
+ tilt: tilt,
+ );
+ }
+ }
+
// For each event that is de-coalesced from `event` and described in
// `details`, convert it to pointer data and store in `data`.
void _convertEventsToPointerData({
@@ -782,6 +853,13 @@
void setup() {
_addMouseEventListener('mousedown', (html.MouseEvent event) {
final List<ui.PointerData> pointerData = <ui.PointerData>[];
+ if (event.button == kContextMenuButton) {
+ _handleMissingRightMouseUpEvent(_sanitizer,
+ _sanitizer._pressedButtons,
+ _sanitizer._pressedButtons & ~kContextMenuButton,
+ event,
+ pointerData);
+ }
final _SanitizedDetails sanitizedDetails =
_sanitizer.sanitizeDownEvent(
button: event.button,
@@ -793,6 +871,14 @@
_addMouseEventListener('mousemove', (html.MouseEvent event) {
final List<ui.PointerData> pointerData = <ui.PointerData>[];
+ final int buttonsBeforeEvent = _sanitizer._pressedButtons;
+ _handleMissingRightMouseUpEvent(
+ _sanitizer,
+ buttonsBeforeEvent,
+ (_sanitizer._inferDownFlutterButtons(event.button, event.buttons!)
+ & kContextMenuButton),
+ event,
+ pointerData);
final _SanitizedDetails sanitizedDetails = _sanitizer.sanitizeMoveEvent(buttons: event.buttons!);
_convertEventsToPointerData(data: pointerData, event: event, details: sanitizedDetails);
_callback(pointerData);
@@ -803,7 +889,7 @@
final bool isEndOfDrag = event.buttons == 0;
final _SanitizedDetails sanitizedDetails = isEndOfDrag ?
_sanitizer.sanitizeUpEvent()! :
- _sanitizer.sanitizeMoveEvent(buttons: event.buttons!);
+ _sanitizer.sanitizeUpEventWithButtons(buttons: event.buttons!)!;
_convertEventsToPointerData(data: pointerData, event: event, details: sanitizedDetails);
_callback(pointerData);
}, acceptOutsideGlasspane: true);
@@ -813,6 +899,32 @@
});
}
+ // Handle special case where right mouse button no longer is pressed.
+ // We need to synthesize right mouse up, otherwise drag gesture will fail
+ // to complete or multiple RMB down events will lead to wrong state.
+ void _handleMissingRightMouseUpEvent(_ButtonSanitizer sanitizer,
+ int buttonsBeforeEvent, int buttonsAfterEvent, html.MouseEvent event,
+ List<ui.PointerData> pointerData) {
+ if ((buttonsBeforeEvent & kContextMenuButton) != 0 &&
+ buttonsAfterEvent == 0) {
+ sanitizer._pressedButtons &= ~2;
+ _pointerDataConverter.convert(
+ pointerData,
+ change: ui.PointerChange.up,
+ timeStamp: _BaseAdapter._eventTimeStampToDuration(event.timeStamp!),
+ kind: ui.PointerDeviceKind.mouse,
+ signalKind: ui.PointerSignalKind.none,
+ device: _mouseDeviceId,
+ physicalX: event.client.x.toDouble() * ui.window.devicePixelRatio,
+ physicalY: event.client.y.toDouble() * ui.window.devicePixelRatio,
+ buttons: _sanitizer._pressedButtons,
+ pressure: 1.0,
+ pressureMin: 0.0,
+ pressureMax: 1.0,
+ );
+ }
+ }
+
// For each event that is de-coalesced from `event` and described in
// `detailsList`, convert it to pointer data and store in `data`.
void _convertEventsToPointerData({
diff --git a/lib/web_ui/lib/src/engine/pointer_converter.dart b/lib/web_ui/lib/src/engine/pointer_converter.dart
index c76e1a8..26b72a0 100644
--- a/lib/web_ui/lib/src/engine/pointer_converter.dart
+++ b/lib/web_ui/lib/src/engine/pointer_converter.dart
@@ -5,6 +5,8 @@
// @dart = 2.12
part of engine;
+const bool _debugLogPointerConverter = false;
+
class _PointerState {
_PointerState(this.x, this.y);
@@ -237,6 +239,9 @@
double scrollDeltaX = 0.0,
double scrollDeltaY = 0.0,
}) {
+ if (_debugLogPointerConverter) {
+ print('>> device=$device change = $change buttons = $buttons');
+ }
assert(change != null); // ignore: unnecessary_null_comparison
if (signalKind == null ||
signalKind == ui.PointerSignalKind.none) {
diff --git a/lib/web_ui/test/engine/pointer_binding_test.dart b/lib/web_ui/test/engine/pointer_binding_test.dart
index 4c100b9..693f43c 100644
--- a/lib/web_ui/test/engine/pointer_binding_test.dart
+++ b/lib/web_ui/test/engine/pointer_binding_test.dart
@@ -1227,6 +1227,57 @@
_testEach<_ButtonedEventMixin>(
[
_PointerEventContext(),
+ ],
+ 'correctly handles missing right mouse button up when followed by move',
+ (_ButtonedEventMixin context) {
+ PointerBinding.instance.debugOverrideDetector(context);
+ // This can happen with the following gesture sequence:
+ //
+ // - Pops up the context menu by right clicking;
+ // - Clicks LMB to close context menu.
+ // - Moves mouse.
+
+ List<ui.PointerDataPacket> packets = <ui.PointerDataPacket>[];
+ ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) {
+ packets.add(packet);
+ };
+
+ // Press RMB popping up the context menu, then release by LMB down and up.
+ // Browser won't send up event in that case.
+ glassPane.dispatchEvent(context.mouseDown(
+ button: 2,
+ buttons: 2,
+ ));
+ expect(packets, hasLength(1));
+ expect(packets[0].data, hasLength(2));
+ expect(packets[0].data[0].change, equals(ui.PointerChange.add));
+ expect(packets[0].data[0].synthesized, equals(true));
+
+ expect(packets[0].data[1].change, equals(ui.PointerChange.down));
+ expect(packets[0].data[1].synthesized, equals(false));
+ expect(packets[0].data[1].buttons, equals(2));
+ packets.clear();
+
+ // User now hovers.
+ glassPane.dispatchEvent(context.mouseMove(
+ button: _kNoButtonChange,
+ buttons: 0,
+ ));
+ 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].synthesized, equals(false));
+ expect(packets[0].data[0].buttons, equals(0));
+ expect(packets[0].data[1].change, equals(ui.PointerChange.hover));
+ expect(packets[0].data[1].synthesized, equals(false));
+ expect(packets[0].data[1].buttons, equals(0));
+ packets.clear();
+ },
+ );
+
+ _testEach<_ButtonedEventMixin>(
+ [
+ _PointerEventContext(),
_MouseEventContext(),
],
'handles RMB click when the browser sends it as a move',
@@ -1303,10 +1354,16 @@
clientY: 20.0,
));
expect(packets, hasLength(1));
- expect(packets[0].data, hasLength(1));
+ expect(packets[0].data, hasLength(3));
expect(packets[0].data[0].change, equals(ui.PointerChange.move));
- expect(packets[0].data[0].synthesized, equals(false));
+ expect(packets[0].data[0].synthesized, equals(true));
expect(packets[0].data[0].buttons, equals(2));
+ expect(packets[0].data[1].change, equals(ui.PointerChange.up));
+ expect(packets[0].data[1].synthesized, equals(false));
+ expect(packets[0].data[1].buttons, equals(0));
+ expect(packets[0].data[2].change, equals(ui.PointerChange.hover));
+ expect(packets[0].data[2].synthesized, equals(false));
+ expect(packets[0].data[2].buttons, equals(0));
packets.clear();
},
);
@@ -1416,7 +1473,7 @@
// Press RMB again. In Chrome, when RMB is clicked again while the
// context menu is still active, it sends a pointerdown/mousedown event
- // with "buttons:0".
+ // with "buttons:0". We convert this to pointer up, pointer down.
glassPane.dispatchEvent(context.mouseDown(
button: 2,
buttons: 0,
@@ -1424,10 +1481,16 @@
clientY: 20.0,
));
expect(packets, hasLength(1));
- expect(packets[0].data, hasLength(1));
+ expect(packets[0].data, hasLength(3));
expect(packets[0].data[0].change, equals(ui.PointerChange.move));
- expect(packets[0].data[0].synthesized, equals(false));
+ expect(packets[0].data[0].synthesized, equals(true));
expect(packets[0].data[0].buttons, equals(2));
+ expect(packets[0].data[1].change, equals(ui.PointerChange.up));
+ expect(packets[0].data[1].synthesized, equals(false));
+ expect(packets[0].data[1].buttons, equals(0));
+ expect(packets[0].data[2].change, equals(ui.PointerChange.down));
+ expect(packets[0].data[2].synthesized, equals(false));
+ expect(packets[0].data[2].buttons, equals(2));
packets.clear();
// Release RMB.