mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Fix drag failure when RMB pointer up event is not received (flutter/engine#22946)
This commit is contained in:
parent
465e165cc6
commit
86f6fe0ef2
@ -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({
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user