[web] Fix drag failure when RMB pointer up event is not received (flutter/engine#22946)

This commit is contained in:
Ferhat 2020-12-10 10:32:04 -08:00 committed by GitHub
parent 465e165cc6
commit 86f6fe0ef2
3 changed files with 201 additions and 21 deletions

View File

@ -199,7 +199,13 @@ abstract class _BaseAdapter {
}
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 @@ class _ButtonSanitizer {
}
_pressedButtons = _inferDownFlutterButtons(button, buttons);
return _SanitizedDetails(
change: ui.PointerChange.down,
buttons: _pressedButtons,
@ -389,18 +396,6 @@ class _ButtonSanitizer {
_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 @@ class _ButtonSanitizer {
);
}
_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 @@ class _ButtonSanitizer {
}
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 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
_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 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
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 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
});
}
// 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 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin {
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 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin {
_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 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin {
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 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin {
});
}
// 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({

View File

@ -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 @@ class PointerDataConverter {
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) {

View File

@ -1224,6 +1224,57 @@ void testMain() {
},
);
_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(),
@ -1303,10 +1354,16 @@ void testMain() {
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 @@ void testMain() {
// 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 @@ void testMain() {
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.