diff --git a/lib/web_ui/lib/src/engine/dom_renderer.dart b/lib/web_ui/lib/src/engine/dom_renderer.dart index 814afba8a9e86..a3b8578f685df 100644 --- a/lib/web_ui/lib/src/engine/dom_renderer.dart +++ b/lib/web_ui/lib/src/engine/dom_renderer.dart @@ -411,7 +411,7 @@ flt-glass-pane * { glassPaneElement .insertBefore(_accesibilityPlaceholder, _sceneHostElement); - PointerBinding(this); + PointerBinding.initInstance(_glassPaneElement); // Hide the DOM nodes used to render the scene from accessibility, because // the accessibility tree is built from the SemanticsNode tree as a parallel diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index 46e0c2f751142..d7fb941d41741 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -8,33 +8,79 @@ part of engine; const bool _debugLogPointerEvents = false; /// The signature of a callback that handles pointer events. -typedef PointerDataCallback = void Function(List); +typedef _PointerDataCallback = void Function(Iterable); + +// The mask for the bitfield of event buttons. Buttons not contained in this +// mask are cut off. +// +// In Flutter we used `kMaxUnsignedSMI`, but since that value is not available +// here, we use an already very large number (30 bits). +const int _kButtonsMask = 0x3FFFFFFF; + +// Intentionally set to -1 so it doesn't conflict with other device IDs. +const int _mouseDeviceId = -1; + +const int _kPrimaryMouseButton = 0x1; +const int _kSecondaryMouseButton = 0x2; +const int _kMiddleMouseButton =0x4; + +int _nthButton(int n) => 0x1 << n; + +/// Convert the `button` property of PointerEvent or MouseEvent to a bit mask of +/// its `buttons` property. +/// +/// The `button` property is a integer describing the button changed in an event, +/// which is sequentially 0 for LMB, 1 for MMB, 2 for RMB, 3 for backward and +/// 4 for forward, etc. +/// +/// The `buttons` property is a bitfield describing the buttons pressed after an +/// event, which is 0x1 for LMB, 0x4 for MMB, 0x2 for RMB, 0x8 for backward +/// and 0x10 for forward, etc. +@visibleForTesting +int convertButtonToButtons(int button) { + assert(button >= 0, 'Unexpected negative button $button.'); + switch(button) { + case 0: + return _kPrimaryMouseButton; + case 1: + return _kMiddleMouseButton; + case 2: + return _kSecondaryMouseButton; + default: + return _nthButton(button); + } +} class PointerBinding { /// The singleton instance of this object. static PointerBinding get instance => _instance; static PointerBinding _instance; - PointerBinding(this.domRenderer) { + static void initInstance(html.Element glassPaneElement) { if (_instance == null) { - _instance = this; - _pointerDataConverter = PointerDataConverter(); - _detector = const PointerSupportDetector(); - _adapter = _createAdapter(); + _instance = PointerBinding._(glassPaneElement); + assert(() { + registerHotRestartListener(() { + _instance._adapter?.clearListeners(); + _instance._pointerDataConverter?.clearPointerState(); + }); + return true; + }()); } - assert(() { - registerHotRestartListener(() { - _adapter?.clearListeners(); - _pointerDataConverter?.clearPointerState(); - }); - return true; - }()); } - final DomRenderer domRenderer; + PointerBinding._(this.glassPaneElement) { + _pointerDataConverter = PointerDataConverter(); + _detector = const PointerSupportDetector(); + _adapter = _createAdapter(); + } + + final html.Element glassPaneElement; + PointerSupportDetector _detector; - BaseAdapter _adapter; + _BaseAdapter _adapter; PointerDataConverter _pointerDataConverter; + /// Should be used in tests to define custom detection of pointer support. /// /// ```dart @@ -63,21 +109,21 @@ class PointerBinding { } } - BaseAdapter _createAdapter() { + _BaseAdapter _createAdapter() { if (_detector.hasPointerEvents) { - return PointerAdapter(_onPointerData, domRenderer, _pointerDataConverter); + return _PointerAdapter(_onPointerData, glassPaneElement, _pointerDataConverter); } if (_detector.hasTouchEvents) { - return TouchAdapter(_onPointerData, domRenderer, _pointerDataConverter); + return _TouchAdapter(_onPointerData, glassPaneElement, _pointerDataConverter); } if (_detector.hasMouseEvents) { - return MouseAdapter(_onPointerData, domRenderer, _pointerDataConverter); + return _MouseAdapter(_onPointerData, glassPaneElement, _pointerDataConverter); } return null; } - void _onPointerData(List data) { - final ui.PointerDataPacket packet = ui.PointerDataPacket(data: data); + void _onPointerData(Iterable data) { + final ui.PointerDataPacket packet = ui.PointerDataPacket(data: data.toList()); final ui.PointerDataPacketCallback callback = ui.window.onPointerDataPacket; if (callback != null) { callback(packet); @@ -97,28 +143,10 @@ class PointerSupportDetector { 'pointers:$hasPointerEvents, touch:$hasTouchEvents, mouse:$hasMouseEvents'; } -class _PressedButton { - const _PressedButton(this.deviceId, this.button); - - // The id of the device pressing the button. - final int deviceId; - - // The id of the button being pressed. - final int button; - - bool operator ==(other) { - if (other is! _PressedButton) return false; - final _PressedButton otherButton = other; - return deviceId == otherButton.deviceId && button == otherButton.button; - } - - int get hashCode => ((13801 + deviceId) * 37) + button; -} - /// Common functionality that's shared among adapters. -abstract class BaseAdapter { - BaseAdapter(this._callback, this.domRenderer, this._pointerDataConverter) { - _setup(); +abstract class _BaseAdapter { + _BaseAdapter(this._callback, this.glassPaneElement, this._pointerDataConverter) { + setup(); } /// Listeners that are registered through dart to js api. @@ -127,40 +155,24 @@ abstract class BaseAdapter { /// Listeners that are registered through native javascript api. static final Map _nativeListeners = {}; - final DomRenderer domRenderer; - PointerDataCallback _callback; + final html.Element glassPaneElement; + _PointerDataCallback _callback; PointerDataConverter _pointerDataConverter; - // A set of the buttons that are currently being pressed. - Set<_PressedButton> _pressedButtons = Set<_PressedButton>(); - - bool _isButtonDown(int device, int button) { - return _pressedButtons.contains(_PressedButton(device, button)); - } - - void _updateButtonDownState(int device, int button, bool value) { - if (value) { - _pressedButtons.add(_PressedButton(device, button)); - } else { - _pressedButtons.remove(_PressedButton(device, button)); - } - } - /// Each subclass is expected to override this method to attach its own event /// listeners and convert events into pointer events. - void _setup(); + void setup(); /// Remove all active event listeners. void clearListeners() { - final html.Element glassPane = domRenderer.glassPaneElement; _listeners.forEach((String eventName, html.EventListener listener) { - glassPane.removeEventListener(eventName, listener, true); + glassPaneElement.removeEventListener(eventName, listener, true); }); // For native listener, we will need to remove it through native javascript // api. _nativeListeners.forEach((String eventName, html.EventListener listener) { js_util.callMethod( - domRenderer.glassPaneElement, + glassPaneElement, 'removeEventListener', [ 'wheel', listener, @@ -171,7 +183,7 @@ abstract class BaseAdapter { _nativeListeners.clear(); } - void _addEventListener(String eventName, html.EventListener handler) { + void addEventListener(String eventName, html.EventListener handler) { final html.EventListener loggedHandler = (html.Event event) { if (_debugLogPointerEvents) { print(event.type); @@ -184,22 +196,24 @@ abstract class BaseAdapter { } }; _listeners[eventName] = loggedHandler; - domRenderer.glassPaneElement + glassPaneElement .addEventListener(eventName, loggedHandler, true); } /// Converts a floating number timestamp (in milliseconds) to a [Duration] by /// splitting it into two integer components: milliseconds + microseconds. - Duration _eventTimeStampToDuration(num milliseconds) { + static Duration _eventTimeStampToDuration(num milliseconds) { final int ms = milliseconds.toInt(); final int micro = ((milliseconds - ms) * Duration.microsecondsPerMillisecond).toInt(); return Duration(milliseconds: ms, microseconds: micro); } +} +mixin _WheelEventListenerMixin on _BaseAdapter { List _convertWheelEventToPointerData( - html.WheelEvent event, - ) { + html.WheelEvent event + ) { const int domDeltaPixel = 0x00; const int domDeltaLine = 0x01; const int domDeltaPage = 0x02; @@ -225,7 +239,7 @@ abstract class BaseAdapter { _pointerDataConverter.convert( data, change: ui.PointerChange.hover, - timeStamp: _eventTimeStampToDuration(event.timeStamp), + timeStamp: _BaseAdapter._eventTimeStampToDuration(event.timeStamp), kind: ui.PointerDeviceKind.mouse, signalKind: ui.PointerSignalKind.scroll, device: _mouseDeviceId, @@ -244,103 +258,178 @@ abstract class BaseAdapter { void _addWheelEventListener(html.EventListener handler) { final dynamic eventOptions = js_util.newObject(); final html.EventListener jsHandler = js.allowInterop((html.Event event) => handler(event)); - _nativeListeners['wheel'] = jsHandler; + _BaseAdapter._nativeListeners['wheel'] = jsHandler; js_util.setProperty(eventOptions, 'passive', false); js_util.callMethod( - domRenderer.glassPaneElement, + glassPaneElement, 'addEventListener', [ 'wheel', jsHandler, eventOptions ] ); - } } -const int _kPrimaryMouseButton = 0x1; -const int _kSecondaryMouseButton = 0x2; +@immutable +class _SanitizedDetails { + const _SanitizedDetails({ + @required this.buttons, + @required this.change, + }); -int _pointerButtonFromHtmlEvent(html.Event event) { - if (event is html.PointerEvent) { - final html.PointerEvent pointerEvent = event; - return pointerEvent.button == 2 - ? _kSecondaryMouseButton - : _kPrimaryMouseButton; - } else if (event is html.MouseEvent) { - final html.MouseEvent mouseEvent = event; - return mouseEvent.button == 2 - ? _kSecondaryMouseButton - : _kPrimaryMouseButton; - } - return _kPrimaryMouseButton; + final ui.PointerChange change; + final int buttons; + + @override + String toString() => '$runtimeType(change: $change, buttons: $buttons)'; } -int _deviceFromHtmlEvent(event) { - if (event is html.PointerEvent) { - final html.PointerEvent pointerEvent = event; - return pointerEvent.pointerId; +class _ButtonSanitizer { + int _pressedButtons = 0; + + // Transform html.PointerEvent.buttons to Flutter's PointerEvent buttons. + int _htmlButtonsToFlutterButtons(int buttons) { + // Flutter's button definition conveniently matches that of JavaScript + // from primary button (0x1) to forward button (0x10), which allows us to + // avoid transforming it bit by bit. + return buttons & _kButtonsMask; + } + + List<_SanitizedDetails> sanitizeDownEvent({@required int buttons}) { + final List<_SanitizedDetails> result = <_SanitizedDetails>[]; + // TODO(flutter_web): Remove this temporary fix for right click + // on web platform once context gesture is implemented. + if (_pressedButtons != 0) { + _pressedButtons = 0; + result.add(_SanitizedDetails( + change: ui.PointerChange.up, + buttons: 0, + )); + } + _pressedButtons = _htmlButtonsToFlutterButtons(buttons); + result.add(_SanitizedDetails( + change: ui.PointerChange.down, + buttons: _pressedButtons, + )); + return result; + } + + List<_SanitizedDetails> sanitizeMoveEvent({@required int buttons}) { + _pressedButtons = _htmlButtonsToFlutterButtons(buttons); + return <_SanitizedDetails>[_SanitizedDetails( + change: _pressedButtons == 0 + ? ui.PointerChange.hover + : ui.PointerChange.move, + buttons: _pressedButtons, + )]; + } + + List<_SanitizedDetails> sanitizeUpEvent() { + // The pointer could have been released by a `pointerout` event, in which + // case `pointerup` should have no effect. + if (_pressedButtons == 0) { + return <_SanitizedDetails>[]; + } + _pressedButtons = 0; + return <_SanitizedDetails>[_SanitizedDetails( + change: ui.PointerChange.up, + buttons: _pressedButtons, + )]; + } + + List<_SanitizedDetails> sanitizeCancelEvent() { + _pressedButtons = 0; + return <_SanitizedDetails>[_SanitizedDetails( + change: ui.PointerChange.cancel, + buttons: _pressedButtons, + )]; } - return _mouseDeviceId; } +typedef _PointerEventListener = dynamic Function(html.PointerEvent event); + /// Adapter class to be used with browsers that support native pointer events. -class PointerAdapter extends BaseAdapter { - PointerAdapter( - PointerDataCallback callback, - DomRenderer domRenderer, +/// +/// For the difference between MouseEvent and PointerEvent, see _MouseAdapter. +class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { + _PointerAdapter( + _PointerDataCallback callback, + html.Element glassPaneElement, PointerDataConverter _pointerDataConverter - ) : super(callback, domRenderer, _pointerDataConverter); + ) : super(callback, glassPaneElement, _pointerDataConverter); + + final Map _sanitizers = {}; + + @visibleForTesting + Iterable debugTrackedDevices() => _sanitizers.keys; + + _ButtonSanitizer _ensureSanitizer(int device) { + return _sanitizers.putIfAbsent(device, () => _ButtonSanitizer()); + } + + _ButtonSanitizer _getSanitizer(int device) { + final _ButtonSanitizer sanitizer = _sanitizers[device]; + assert(sanitizer != null); + return sanitizer; + } + + void _removePointerIfUnhoverable(List<_SanitizedDetails> details, html.PointerEvent event) { + if (event.pointerType == 'touch') { + _sanitizers.remove(event.pointerId); + details.add(_SanitizedDetails( + buttons: 0, + change: ui.PointerChange.remove, + )); + } + } + + void _addPointerEventListener(String eventName, _PointerEventListener handler) { + addEventListener(eventName, (html.Event event) { + final html.PointerEvent pointerEvent = event; + return handler(pointerEvent); + }); + } @override - void _setup() { - _addEventListener('pointerdown', (html.Event event) { - final int pointerButton = _pointerButtonFromHtmlEvent(event); - final int device = _deviceFromHtmlEvent(event); - if (_isButtonDown(device, pointerButton)) { - // TODO(flutter_web): Remove this temporary fix for right click - // on web platform once context guesture is implemented. - _callback(_convertEventToPointerData(ui.PointerChange.up, event)); - } - _updateButtonDownState(device, pointerButton, true); - _callback(_convertEventToPointerData(ui.PointerChange.down, event)); + void setup() { + _addPointerEventListener('pointerdown', (html.PointerEvent event) { + final int device = event.pointerId; + final List pointerData = []; + final List<_SanitizedDetails> detailsList = _ensureSanitizer(device).sanitizeDownEvent(buttons: event.buttons); + _convertEventsToPointerData(data: pointerData, event: event, detailsList: detailsList); + _callback(pointerData); }); - _addEventListener('pointermove', (html.Event event) { - // TODO(flutter_web): During a drag operation pointermove will set - // button to -1 as opposed to mouse move which sets it to 2. - // This check is currently defaulting to primary button for now. - // Change this when context gesture is implemented in flutter framework. - final html.PointerEvent pointerEvent = event; - final int pointerButton = _pointerButtonFromHtmlEvent(pointerEvent); - final int device = _deviceFromHtmlEvent(event); - final List data = _convertEventToPointerData( - _isButtonDown(device, pointerButton) - ? ui.PointerChange.move - : ui.PointerChange.hover, - pointerEvent); - _callback(data); + _addPointerEventListener('pointermove', (html.PointerEvent event) { + final int device = event.pointerId; + final _ButtonSanitizer sanitizer = _ensureSanitizer(device); + final List pointerData = []; + final Iterable<_SanitizedDetails> detailsList = _expandEvents(event).expand( + (html.PointerEvent expandedEvent) => sanitizer.sanitizeMoveEvent(buttons: expandedEvent.buttons), + ); + _convertEventsToPointerData(data: pointerData, event: event, detailsList: detailsList); + _callback(pointerData); }); - _addEventListener('pointerup', (html.Event event) { - // The pointer could have been released by a `pointerout` event, in which - // case `pointerup` should have no effect. - final int pointerButton = _pointerButtonFromHtmlEvent(event); - final int device = _deviceFromHtmlEvent(event); - if (!_isButtonDown(device, pointerButton)) { - return; - } - _updateButtonDownState(device, pointerButton, false); - _callback(_convertEventToPointerData(ui.PointerChange.up, event)); + _addPointerEventListener('pointerup', (html.PointerEvent event) { + final int device = event.pointerId; + final List pointerData = []; + final List<_SanitizedDetails> detailsList = _getSanitizer(device).sanitizeUpEvent(); + _removePointerIfUnhoverable(detailsList, event); + _convertEventsToPointerData(data: pointerData, event: event, detailsList: detailsList); + _callback(pointerData); }); // A browser fires cancel event if it concludes the pointer will no longer // be able to generate events (example: device is deactivated) - _addEventListener('pointercancel', (html.Event event) { - final int pointerButton = _pointerButtonFromHtmlEvent(event); - final int device = _deviceFromHtmlEvent(event); - _updateButtonDownState(pointerButton, device, false); - _callback(_convertEventToPointerData(ui.PointerChange.cancel, event)); + _addPointerEventListener('pointercancel', (html.PointerEvent event) { + final int device = event.pointerId; + final List pointerData = []; + final List<_SanitizedDetails> detailsList = _getSanitizer(device).sanitizeCancelEvent(); + _removePointerIfUnhoverable(detailsList, event); + _convertEventsToPointerData(data: pointerData, event: event, detailsList: detailsList); + _callback(pointerData); }); _addWheelEventListener((html.Event event) { @@ -355,30 +444,40 @@ class PointerAdapter extends BaseAdapter { }); } - List _convertEventToPointerData( - ui.PointerChange change, - html.PointerEvent evt, - ) { - final List allEvents = _expandEvents(evt); - final List data = []; - for (int i = 0; i < allEvents.length; i++) { - final html.PointerEvent event = allEvents[i]; + // For each event that is de-coalesced from `event` and described in + // `detailsList`, convert it to pointer data and store in `data`. + void _convertEventsToPointerData({ + @required List data, + @required html.PointerEvent event, + @required Iterable<_SanitizedDetails> detailsList, + }) { + assert(data != null); + assert(event != null); + assert(detailsList != null); + final ui.PointerDeviceKind kind = _pointerTypeToDeviceKind(event.pointerType); + // We force `device: _mouseDeviceId` on mouse pointers because Wheel events + // might come before any PointerEvents, and since wheel events don't contain + // pointerId we always assign `device: _mouseDeviceId` to them. + final int device = kind == ui.PointerDeviceKind.mouse ? _mouseDeviceId : event.pointerId; + final double tilt = _computeHighestTilt(event); + final Duration timeStamp = _BaseAdapter._eventTimeStampToDuration(event.timeStamp); + for (_SanitizedDetails details in detailsList) { _pointerDataConverter.convert( data, - change: change, - timeStamp: _eventTimeStampToDuration(event.timeStamp), - kind: _pointerTypeToDeviceKind(event.pointerType), - device: event.pointerId, + change: details.change, + timeStamp: timeStamp, + kind: kind, + signalKind: ui.PointerSignalKind.none, + device: device, physicalX: event.client.x * ui.window.devicePixelRatio, physicalY: event.client.y * ui.window.devicePixelRatio, - buttons: event.buttons, + buttons: details.buttons, pressure: event.pressure, pressureMin: 0.0, pressureMax: 1.0, - tilt: _computeHighestTilt(event), + tilt: tilt, ); } - return data; } List _expandEvents(html.PointerEvent event) { @@ -416,112 +515,211 @@ class PointerAdapter extends BaseAdapter { math.pi; } +typedef _TouchEventListener = dynamic Function(html.TouchEvent event); + /// Adapter to be used with browsers that support touch events. -class TouchAdapter extends BaseAdapter { - TouchAdapter( - PointerDataCallback callback, - DomRenderer domRenderer, +class _TouchAdapter extends _BaseAdapter { + _TouchAdapter( + _PointerDataCallback callback, + html.Element glassPaneElement, PointerDataConverter _pointerDataConverter - ) : super(callback, domRenderer, _pointerDataConverter); + ) : super(callback, glassPaneElement, _pointerDataConverter); + + final Set _pressedTouches = {}; + bool _isTouchPressed(int identifier) => _pressedTouches.contains(identifier); + void _pressTouch(int identifier) { _pressedTouches.add(identifier); } + void _unpressTouch(int identifier) { _pressedTouches.remove(identifier); } + + void _addTouchEventListener(String eventName, _TouchEventListener handler) { + addEventListener(eventName, (html.Event event) { + final html.TouchEvent touchEvent = event; + return handler(touchEvent); + }); + } @override - void _setup() { - _addEventListener('touchstart', (html.Event event) { - _updateButtonDownState( - _deviceFromHtmlEvent(event), _kPrimaryMouseButton, true); - _callback(_convertEventToPointerData(ui.PointerChange.down, event)); + void setup() { + _addTouchEventListener('touchstart', (html.TouchEvent event) { + final Duration timeStamp = _BaseAdapter._eventTimeStampToDuration(event.timeStamp); + final List pointerData = []; + for (html.Touch touch in event.changedTouches) { + final nowPressed = _isTouchPressed(touch.identifier); + if (!nowPressed) { + _pressTouch(touch.identifier); + _convertEventToPointerData( + data: pointerData, + change: ui.PointerChange.down, + touch: touch, + pressed: true, + timeStamp: timeStamp, + ); + } + } + _callback(pointerData); }); - _addEventListener('touchmove', (html.Event event) { + _addTouchEventListener('touchmove', (html.TouchEvent event) { event.preventDefault(); // Prevents standard overscroll on iOS/Webkit. - if (!_isButtonDown(_deviceFromHtmlEvent(event), _kPrimaryMouseButton)) { - return; + final Duration timeStamp = _BaseAdapter._eventTimeStampToDuration(event.timeStamp); + final List pointerData = []; + for (html.Touch touch in event.changedTouches) { + final nowPressed = _isTouchPressed(touch.identifier); + if (nowPressed) { + _convertEventToPointerData( + data: pointerData, + change: ui.PointerChange.move, + touch: touch, + pressed: true, + timeStamp: timeStamp, + ); + } } - _callback(_convertEventToPointerData(ui.PointerChange.move, event)); + _callback(pointerData); }); - _addEventListener('touchend', (html.Event event) { + _addTouchEventListener('touchend', (html.TouchEvent event) { // On Safari Mobile, the keyboard does not show unless this line is // added. event.preventDefault(); - _updateButtonDownState( - _deviceFromHtmlEvent(event), _kPrimaryMouseButton, false); - _callback(_convertEventToPointerData(ui.PointerChange.up, event)); + final Duration timeStamp = _BaseAdapter._eventTimeStampToDuration(event.timeStamp); + final List pointerData = []; + for (html.Touch touch in event.changedTouches) { + final nowPressed = _isTouchPressed(touch.identifier); + if (nowPressed) { + _unpressTouch(touch.identifier); + _convertEventToPointerData( + data: pointerData, + change: ui.PointerChange.up, + touch: touch, + pressed: false, + timeStamp: timeStamp, + ); + _convertEventToPointerData( + data: pointerData, + change: ui.PointerChange.remove, + touch: touch, + pressed: false, + timeStamp: timeStamp, + ); + } + } + _callback(pointerData); }); - _addEventListener('touchcancel', (html.Event event) { - _callback(_convertEventToPointerData(ui.PointerChange.cancel, event)); + _addTouchEventListener('touchcancel', (html.TouchEvent event) { + final Duration timeStamp = _BaseAdapter._eventTimeStampToDuration(event.timeStamp); + final List pointerData = []; + for (html.Touch touch in event.changedTouches) { + final nowPressed = _isTouchPressed(touch.identifier); + if (nowPressed) { + _unpressTouch(touch.identifier); + _convertEventToPointerData( + data: pointerData, + change: ui.PointerChange.cancel, + touch: touch, + pressed: false, + timeStamp: timeStamp, + ); + _convertEventToPointerData( + data: pointerData, + change: ui.PointerChange.remove, + touch: touch, + pressed: false, + timeStamp: timeStamp, + ); + } + } + _callback(pointerData); }); } - List _convertEventToPointerData( - ui.PointerChange change, - html.TouchEvent event, - ) { - final html.TouchList touches = event.changedTouches; - final List data = List(); - final int len = touches.length; - for (int i = 0; i < len; i++) { - final html.Touch touch = touches[i]; - _pointerDataConverter.convert( - data, - change: change, - timeStamp: _eventTimeStampToDuration(event.timeStamp), - kind: ui.PointerDeviceKind.touch, - signalKind: ui.PointerSignalKind.none, - device: touch.identifier, - physicalX: touch.client.x * ui.window.devicePixelRatio, - physicalY: touch.client.y * ui.window.devicePixelRatio, - pressure: 1.0, - pressureMin: 0.0, - pressureMax: 1.0, - ); - } - - return data; + void _convertEventToPointerData({ + @required List data, + @required ui.PointerChange change, + @required html.Touch touch, + @required bool pressed, + @required Duration timeStamp, + }) { + _pointerDataConverter.convert( + data, + change: change, + timeStamp: timeStamp, + kind: ui.PointerDeviceKind.touch, + signalKind: ui.PointerSignalKind.none, + device: touch.identifier, + physicalX: touch.client.x * ui.window.devicePixelRatio, + physicalY: touch.client.y * ui.window.devicePixelRatio, + buttons: pressed ? _kPrimaryMouseButton : 0, + pressure: 1.0, + pressureMin: 0.0, + pressureMax: 1.0, + ); } } -/// Intentionally set to -1 so it doesn't conflict with other device IDs. -const int _mouseDeviceId = -1; +typedef _MouseEventListener = dynamic Function(html.MouseEvent event); /// Adapter to be used with browsers that support mouse events. -class MouseAdapter extends BaseAdapter { - MouseAdapter( - PointerDataCallback callback, - DomRenderer domRenderer, +/// +/// The difference between MouseEvent and PointerEvent can be illustrated using +/// a scenario of changing buttons during a drag sequence: LMB down, RMB down, +/// move, LMB up, RMB up, hover. +/// +/// LMB down RMB down move LMB up RMB up hover +/// PntEvt type | pointerdown pointermove pointermove pointermove pointerup pointermove +/// button | 0 2 -1 0 2 -1 +/// buttons | 0x1 0x3 0x3 0x2 0x0 0x0 +/// MosEvt type | mousedown mousedown mousemove mouseup mouseup mousemove +/// button | 0 2 0 0 2 0 +/// buttons | 0x1 0x3 0x3 0x2 0x0 0x0 +/// +/// The major differences are: +/// +/// * The type of events for changing buttons during a drag sequence. +/// * The `button` for dragging or hovering. +class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin { + _MouseAdapter( + _PointerDataCallback callback, + html.Element glassPaneElement, PointerDataConverter _pointerDataConverter - ) : super(callback, domRenderer, _pointerDataConverter); + ) : super(callback, glassPaneElement, _pointerDataConverter); + + final _ButtonSanitizer _sanitizer = _ButtonSanitizer(); + + void _addMouseEventListener(String eventName, _MouseEventListener handler) { + addEventListener(eventName, (html.Event event) { + final html.MouseEvent mouseEvent = event; + return handler(mouseEvent); + }); + } @override - void _setup() { - _addEventListener('mousedown', (html.Event event) { - final int pointerButton = _pointerButtonFromHtmlEvent(event); - final int device = _deviceFromHtmlEvent(event); - if (_isButtonDown(device, pointerButton)) { - // TODO(flutter_web): Remove this temporary fix for right click - // on web platform once context guesture is implemented. - _callback(_convertEventToPointerData(ui.PointerChange.up, event)); - } - _updateButtonDownState(device, pointerButton, true); - _callback(_convertEventToPointerData(ui.PointerChange.down, event)); + void setup() { + _addMouseEventListener('mousedown', (html.MouseEvent event) { + final List pointerData = []; + final bool isStartOfDrag = event.buttons == convertButtonToButtons(event.button); + final List<_SanitizedDetails> sanitizedDetails = isStartOfDrag ? + _sanitizer.sanitizeDownEvent(buttons: event.buttons) : + _sanitizer.sanitizeMoveEvent(buttons: event.buttons); + _convertEventsToPointerData(data: pointerData, event: event, detailsList: sanitizedDetails); + _callback(pointerData); }); - _addEventListener('mousemove', (html.Event event) { - final int pointerButton = _pointerButtonFromHtmlEvent(event); - final int device = _deviceFromHtmlEvent(event); - final List data = _convertEventToPointerData( - _isButtonDown(device, pointerButton) - ? ui.PointerChange.move - : ui.PointerChange.hover, - event); - _callback(data); + _addMouseEventListener('mousemove', (html.MouseEvent event) { + final List pointerData = []; + final List<_SanitizedDetails> sanitizedDetails = _sanitizer.sanitizeMoveEvent(buttons: event.buttons); + _convertEventsToPointerData(data: pointerData, event: event, detailsList: sanitizedDetails); + _callback(pointerData); }); - _addEventListener('mouseup', (html.Event event) { - final int device = _deviceFromHtmlEvent(event); - _updateButtonDownState(device, _pointerButtonFromHtmlEvent(event), false); - _callback(_convertEventToPointerData(ui.PointerChange.up, event)); + _addMouseEventListener('mouseup', (html.MouseEvent event) { + final List pointerData = []; + final bool isEndOfDrag = event.buttons == 0; + final List<_SanitizedDetails> sanitizedDetails = isEndOfDrag ? + _sanitizer.sanitizeUpEvent() : + _sanitizer.sanitizeMoveEvent(buttons: event.buttons); + _convertEventsToPointerData(data: pointerData, event: event, detailsList: sanitizedDetails); + _callback(pointerData); }); _addWheelEventListener((html.Event event) { @@ -530,29 +728,37 @@ class MouseAdapter extends BaseAdapter { print(event.type); } _callback(_convertWheelEventToPointerData(event)); + // Prevent default so mouse wheel event doesn't get converted to + // a scroll event that semantic nodes would process. event.preventDefault(); }); } - List _convertEventToPointerData( - ui.PointerChange change, - html.MouseEvent event, - ) { - List data = []; - _pointerDataConverter.convert( - data, - change: change, - timeStamp: _eventTimeStampToDuration(event.timeStamp), - kind: ui.PointerDeviceKind.mouse, - signalKind: ui.PointerSignalKind.none, - device: _mouseDeviceId, - physicalX: event.client.x * ui.window.devicePixelRatio, - physicalY: event.client.y * ui.window.devicePixelRatio, - buttons: event.buttons, - pressure: 1.0, - pressureMin: 0.0, - pressureMax: 1.0, - ); - return data; + // For each event that is de-coalesced from `event` and described in + // `detailsList`, convert it to pointer data and store in `data`. + void _convertEventsToPointerData({ + @required List data, + @required html.MouseEvent event, + @required Iterable<_SanitizedDetails> detailsList, + }) { + assert(data != null); + assert(event != null); + assert(detailsList != null); + for (_SanitizedDetails details in detailsList) { + _pointerDataConverter.convert( + data, + change: details.change, + timeStamp: _BaseAdapter._eventTimeStampToDuration(event.timeStamp), + kind: ui.PointerDeviceKind.mouse, + signalKind: ui.PointerSignalKind.none, + device: _mouseDeviceId, + physicalX: event.client.x * ui.window.devicePixelRatio, + physicalY: event.client.y * ui.window.devicePixelRatio, + buttons: details.buttons, + pressure: 1.0, + pressureMin: 0.0, + pressureMax: 1.0, + ); + } } } diff --git a/lib/web_ui/lib/src/engine/pointer_converter.dart b/lib/web_ui/lib/src/engine/pointer_converter.dart index c6b081558913e..6bd4e7c8aaeb9 100644 --- a/lib/web_ui/lib/src/engine/pointer_converter.dart +++ b/lib/web_ui/lib/src/engine/pointer_converter.dart @@ -333,10 +333,11 @@ class PointerDataConverter { final _PointerState state = _ensureStateForPointer( device, physicalX, physicalY); assert(!state.down); + state.startNewPointer(); if (!alreadyAdded) { // Synthesizes an add pointer data. result.add( - _synthesizePointerData( + _synthesizePointerData( timeStamp: timeStamp, change: ui.PointerChange.add, kind: kind, @@ -364,7 +365,6 @@ class PointerDataConverter { ); } assert(!_locationHasChanged(device, physicalX, physicalY)); - state.startNewPointer(); state.down = true; result.add( _generateCompletePointerData( @@ -433,6 +433,15 @@ class PointerDataConverter { assert(_pointers.containsKey(device)); final _PointerState state = _pointers[device]; assert(state.down); + // Cancel events can have different coordinates due to various + // reasons (window lost focus which is accompanied by window + // movement, or PointerEvent simply always gives 0). Instead of + // caring about the coordinates, we want to cancel the pointers as + // soon as possible. + if (change == ui.PointerChange.cancel) { + physicalX = state.x; + physicalY = state.y; + } assert(!_locationHasChanged(device, physicalX, physicalY)); state.down = false; result.add( @@ -468,8 +477,6 @@ class PointerDataConverter { assert(_pointers.containsKey(device)); final _PointerState state = _pointers[device]; assert(!state.down); - assert(!_locationHasChanged(device, physicalX, physicalY)); - _pointers.remove(device); result.add( _generateCompletePointerData( timeStamp: timeStamp, @@ -477,8 +484,8 @@ class PointerDataConverter { kind: kind, signalKind: signalKind, device: device, - physicalX: physicalX, - physicalY: physicalY, + physicalX: state.x, + physicalY: state.y, buttons: buttons, obscured: obscured, pressure: pressure, @@ -498,6 +505,7 @@ class PointerDataConverter { scrollDeltaY: scrollDeltaY, ) ); + _pointers.remove(device); break; } } else { diff --git a/lib/web_ui/test/engine/pointer_binding_test.dart b/lib/web_ui/test/engine/pointer_binding_test.dart index cee6b8f16de14..6ba4bed7f1cc8 100644 --- a/lib/web_ui/test/engine/pointer_binding_test.dart +++ b/lib/web_ui/test/engine/pointer_binding_test.dart @@ -3,268 +3,469 @@ // found in the LICENSE file. import 'dart:html' as html; -import 'dart:typed_data'; +import 'dart:js_util' as js_util; import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; import 'package:test/test.dart'; +const int _kNoButtonChange = -1; +const PointerSupportDetector _defaultSupportDetector = PointerSupportDetector(); + +List _allPointerData(List packets) { + return packets.expand((ui.PointerDataPacket packet) => packet.data).toList(); +} + +typedef _ContextTestBody = void Function(T); + +void _testEach( + Iterable contexts, + String description, + _ContextTestBody body, +) { + for (T context in contexts) { + if (context.isSupported) { + test('${context.name} $description', () { + body(context); + }); + } + } +} + void main() { - group('Pointer Binding', () { - html.Element glassPane = domRenderer.glassPaneElement; + html.Element glassPane = domRenderer.glassPaneElement; - setUp(() { - // Touching domRenderer creates PointerBinding.instance. - domRenderer; + setUp(() { + // Touching domRenderer creates PointerBinding.instance. + domRenderer; - // Set a new detector to reset the state of the listeners. - PointerBinding.instance.debugOverrideDetector(TestPointerDetector()); + ui.window.onPointerDataPacket = null; + }); - ui.window.onPointerDataPacket = null; - }); + test('_PointerEventContext generates expected events', () { + if (!_PointerEventContext().isSupported) + return; - test('can receive pointer events on the glass pane', () { - ui.PointerDataPacket receivedPacket; - ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) { - receivedPacket = packet; - }; + html.PointerEvent expectCorrectType(html.Event e) { + expect(e.runtimeType, equals(html.PointerEvent)); + return e; + } + List expectCorrectTypes(List events) { + return events.map(expectCorrectType).toList(); + } - glassPane.dispatchEvent(html.PointerEvent('pointerdown', { - 'pointerId': 1, - 'button': 1, - })); + final _PointerEventContext context = _PointerEventContext(); + html.PointerEvent event; + List events; - expect(receivedPacket, isNotNull); - expect(receivedPacket.data[0].device, equals(1)); - }); + event = expectCorrectType(context.primaryDown(clientX: 100, clientY: 101)); + expect(event.type, equals('pointerdown')); + expect(event.pointerId, equals(1)); + expect(event.button, equals(0)); + expect(event.buttons, equals(1)); + expect(event.client.x, equals(100)); + expect(event.client.y, equals(101)); - test('synthesizes a pointerup event on two pointerdowns in a row', () { - List packets = []; - ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) { - packets.add(packet); - }; + event = expectCorrectType(context.mouseDown(clientX: 110, clientY: 111, button: 2, buttons: 2)); + expect(event.type, equals('pointerdown')); + expect(event.pointerId, equals(1)); + expect(event.button, equals(2)); + expect(event.buttons, equals(2)); + expect(event.client.x, equals(110)); + expect(event.client.y, equals(111)); - glassPane.dispatchEvent(html.PointerEvent('pointerdown', { - 'pointerId': 1, - 'button': 1, - })); + events = expectCorrectTypes(context.multiTouchDown(<_TouchDetails>[ + _TouchDetails(pointer: 100, clientX: 120, clientY: 121), + _TouchDetails(pointer: 101, clientX: 122, clientY: 123), + ])); + expect(events.length, equals(2)); + expect(events[0].type, equals('pointerdown')); + expect(events[0].pointerId, equals(100)); + expect(events[0].button, equals(0)); + expect(events[0].buttons, equals(1)); + expect(events[0].client.x, equals(120)); + expect(events[0].client.y, equals(121)); + expect(events[1].type, equals('pointerdown')); + expect(events[1].pointerId, equals(101)); + expect(events[1].button, equals(0)); + expect(events[1].buttons, equals(1)); + expect(events[1].client.x, equals(122)); + expect(events[1].client.y, equals(123)); - glassPane.dispatchEvent(html.PointerEvent('pointerdown', { - 'pointerId': 1, - 'button': 1, - })); + event = expectCorrectType(context.primaryMove(clientX: 200, clientY: 201)); + expect(event.type, equals('pointermove')); + expect(event.pointerId, equals(1)); + expect(event.button, equals(-1)); + expect(event.buttons, equals(1)); + expect(event.client.x, equals(200)); + expect(event.client.y, equals(201)); - expect(packets, hasLength(3)); - // An add will be synthesized. - 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[1].data[0].change, equals(ui.PointerChange.up)); - expect(packets[2].data[0].change, equals(ui.PointerChange.down)); - }); + event = expectCorrectType(context.mouseMove(clientX: 210, clientY: 211, button: _kNoButtonChange, buttons: 6)); + expect(event.type, equals('pointermove')); + expect(event.pointerId, equals(1)); + expect(event.button, equals(-1)); + expect(event.buttons, equals(6)); + expect(event.client.x, equals(210)); + expect(event.client.y, equals(211)); - test('does not synthesize pointer up if from different device', () { - List packets = []; + event = expectCorrectType(context.mouseMove(clientX: 212, clientY: 213, button: 2, buttons: 6)); + expect(event.type, equals('pointermove')); + expect(event.pointerId, equals(1)); + expect(event.button, equals(2)); + expect(event.buttons, equals(6)); + expect(event.client.x, equals(212)); + expect(event.client.y, equals(213)); + + event = expectCorrectType(context.mouseMove(clientX: 214, clientY: 215, button: 2, buttons: 1)); + expect(event.type, equals('pointermove')); + expect(event.pointerId, equals(1)); + expect(event.button, equals(2)); + expect(event.buttons, equals(1)); + expect(event.client.x, equals(214)); + expect(event.client.y, equals(215)); + + events = expectCorrectTypes(context.multiTouchMove(<_TouchDetails>[ + _TouchDetails(pointer: 102, clientX: 220, clientY: 221), + _TouchDetails(pointer: 103, clientX: 222, clientY: 223), + ])); + expect(events.length, equals(2)); + expect(events[0].type, equals('pointermove')); + expect(events[0].pointerId, equals(102)); + expect(events[0].button, equals(-1)); + expect(events[0].buttons, equals(1)); + expect(events[0].client.x, equals(220)); + expect(events[0].client.y, equals(221)); + expect(events[1].type, equals('pointermove')); + expect(events[1].pointerId, equals(103)); + expect(events[1].button, equals(-1)); + expect(events[1].buttons, equals(1)); + expect(events[1].client.x, equals(222)); + expect(events[1].client.y, equals(223)); + + event = expectCorrectType(context.primaryUp(clientX: 300, clientY: 301)); + expect(event.type, equals('pointerup')); + expect(event.pointerId, equals(1)); + expect(event.button, equals(0)); + expect(event.buttons, equals(0)); + expect(event.client.x, equals(300)); + expect(event.client.y, equals(301)); + + event = expectCorrectType(context.mouseUp(clientX: 310, clientY: 311, button: 2)); + expect(event.type, equals('pointerup')); + expect(event.pointerId, equals(1)); + expect(event.button, equals(2)); + expect(event.buttons, equals(0)); + expect(event.client.x, equals(310)); + expect(event.client.y, equals(311)); + + events = expectCorrectTypes(context.multiTouchUp(<_TouchDetails>[ + _TouchDetails(pointer: 104, clientX: 320, clientY: 321), + _TouchDetails(pointer: 105, clientX: 322, clientY: 323), + ])); + expect(events.length, equals(2)); + expect(events[0].type, equals('pointerup')); + expect(events[0].pointerId, equals(104)); + expect(events[0].button, equals(0)); + expect(events[0].buttons, equals(0)); + expect(events[0].client.x, equals(320)); + expect(events[0].client.y, equals(321)); + expect(events[1].type, equals('pointerup')); + expect(events[1].pointerId, equals(105)); + expect(events[1].button, equals(0)); + expect(events[1].buttons, equals(0)); + expect(events[1].client.x, equals(322)); + expect(events[1].client.y, equals(323)); + + event = expectCorrectType(context.hover(clientX: 400, clientY: 401)); + expect(event.type, equals('pointermove')); + expect(event.pointerId, equals(1)); + expect(event.button, equals(-1)); + expect(event.buttons, equals(0)); + expect(event.client.x, equals(400)); + expect(event.client.y, equals(401)); + + events = expectCorrectTypes(context.multiTouchCancel(<_TouchDetails>[ + _TouchDetails(pointer: 106, clientX: 500, clientY: 501), + _TouchDetails(pointer: 107, clientX: 502, clientY: 503), + ])); + expect(events.length, equals(2)); + expect(events[0].type, equals('pointercancel')); + expect(events[0].pointerId, equals(106)); + expect(events[0].button, equals(0)); + expect(events[0].buttons, equals(0)); + expect(events[0].client.x, equals(0)); + expect(events[0].client.y, equals(0)); + expect(events[1].type, equals('pointercancel')); + expect(events[1].pointerId, equals(107)); + expect(events[1].button, equals(0)); + expect(events[1].buttons, equals(0)); + expect(events[1].client.x, equals(0)); + expect(events[1].client.y, equals(0)); + }); + + test('_TouchEventContext generates expected events', () { + if (!_TouchEventContext().isSupported) + return; + + html.TouchEvent expectCorrectType(html.Event e) { + expect(e.runtimeType, equals(html.TouchEvent)); + return e; + } + List expectCorrectTypes(List events) { + return events.map(expectCorrectType).toList(); + } + + final _TouchEventContext context = _TouchEventContext(); + html.TouchEvent event; + List events; + + event = expectCorrectType(context.primaryDown(clientX: 100, clientY: 101)); + expect(event.type, equals('touchstart')); + expect(event.changedTouches.length, equals(1)); + expect(event.changedTouches[0].identifier, equals(1)); + expect(event.changedTouches[0].client.x, equals(100)); + expect(event.changedTouches[0].client.y, equals(101)); + + events = expectCorrectTypes(context.multiTouchDown(<_TouchDetails>[ + _TouchDetails(pointer: 100, clientX: 120, clientY: 121), + _TouchDetails(pointer: 101, clientX: 122, clientY: 123), + ])); + expect(events.length, equals(1)); + expect(events[0].type, equals('touchstart')); + expect(events[0].changedTouches.length, equals(2)); + expect(events[0].changedTouches[0].identifier, equals(100)); + expect(events[0].changedTouches[0].client.x, equals(120)); + expect(events[0].changedTouches[0].client.y, equals(121)); + expect(events[0].changedTouches[1].identifier, equals(101)); + expect(events[0].changedTouches[1].client.x, equals(122)); + expect(events[0].changedTouches[1].client.y, equals(123)); + + event = expectCorrectType(context.primaryMove(clientX: 200, clientY: 201)); + expect(event.type, equals('touchmove')); + expect(event.changedTouches.length, equals(1)); + expect(event.changedTouches[0].identifier, equals(1)); + expect(event.changedTouches[0].client.x, equals(200)); + expect(event.changedTouches[0].client.y, equals(201)); + + events = expectCorrectTypes(context.multiTouchMove(<_TouchDetails>[ + _TouchDetails(pointer: 102, clientX: 220, clientY: 221), + _TouchDetails(pointer: 103, clientX: 222, clientY: 223), + ])); + expect(events.length, equals(1)); + expect(events[0].type, equals('touchmove')); + expect(events[0].changedTouches.length, equals(2)); + expect(events[0].changedTouches[0].identifier, equals(102)); + expect(events[0].changedTouches[0].client.x, equals(220)); + expect(events[0].changedTouches[0].client.y, equals(221)); + expect(events[0].changedTouches[1].identifier, equals(103)); + expect(events[0].changedTouches[1].client.x, equals(222)); + expect(events[0].changedTouches[1].client.y, equals(223)); + + event = expectCorrectType(context.primaryUp(clientX: 300, clientY: 301)); + expect(event.type, equals('touchend')); + expect(event.changedTouches.length, equals(1)); + expect(event.changedTouches[0].identifier, equals(1)); + expect(event.changedTouches[0].client.x, equals(300)); + expect(event.changedTouches[0].client.y, equals(301)); + + events = expectCorrectTypes(context.multiTouchUp(<_TouchDetails>[ + _TouchDetails(pointer: 104, clientX: 320, clientY: 321), + _TouchDetails(pointer: 105, clientX: 322, clientY: 323), + ])); + expect(events.length, equals(1)); + expect(events[0].type, equals('touchend')); + expect(events[0].changedTouches.length, equals(2)); + expect(events[0].changedTouches[0].identifier, equals(104)); + expect(events[0].changedTouches[0].client.x, equals(320)); + expect(events[0].changedTouches[0].client.y, equals(321)); + expect(events[0].changedTouches[1].identifier, equals(105)); + expect(events[0].changedTouches[1].client.x, equals(322)); + expect(events[0].changedTouches[1].client.y, equals(323)); + + events = expectCorrectTypes(context.multiTouchCancel(<_TouchDetails>[ + _TouchDetails(pointer: 104, clientX: 320, clientY: 321), + _TouchDetails(pointer: 105, clientX: 322, clientY: 323), + ])); + expect(events.length, equals(1)); + expect(events[0].type, equals('touchcancel')); + expect(events[0].changedTouches.length, equals(2)); + expect(events[0].changedTouches[0].identifier, equals(104)); + expect(events[0].changedTouches[0].client.x, equals(320)); + expect(events[0].changedTouches[0].client.y, equals(321)); + expect(events[0].changedTouches[1].identifier, equals(105)); + expect(events[0].changedTouches[1].client.x, equals(322)); + expect(events[0].changedTouches[1].client.y, equals(323)); + }); + + test('_MouseEventContext generates expected events', () { + if (!_MouseEventContext().isSupported) + return; + + html.MouseEvent expectCorrectType(html.Event e) { + expect(e.runtimeType, equals(html.MouseEvent)); + return e; + } + + final _MouseEventContext context = _MouseEventContext(); + html.MouseEvent event; + + event = expectCorrectType(context.primaryDown(clientX: 100, clientY: 101)); + expect(event.type, equals('mousedown')); + expect(event.button, equals(0)); + expect(event.buttons, equals(1)); + expect(event.client.x, equals(100)); + expect(event.client.y, equals(101)); + + event = expectCorrectType(context.mouseDown(clientX: 110, clientY: 111, button: 2, buttons: 2)); + expect(event.type, equals('mousedown')); + expect(event.button, equals(2)); + expect(event.buttons, equals(2)); + expect(event.client.x, equals(110)); + expect(event.client.y, equals(111)); + + event = expectCorrectType(context.primaryMove(clientX: 200, clientY: 201)); + expect(event.type, equals('mousemove')); + expect(event.button, equals(0)); + expect(event.buttons, equals(1)); + expect(event.client.x, equals(200)); + expect(event.client.y, equals(201)); + + event = expectCorrectType(context.mouseMove(clientX: 210, clientY: 211, button: _kNoButtonChange, buttons: 6)); + expect(event.type, equals('mousemove')); + expect(event.button, equals(0)); + expect(event.buttons, equals(6)); + expect(event.client.x, equals(210)); + expect(event.client.y, equals(211)); + + event = expectCorrectType(context.mouseMove(clientX: 212, clientY: 213, button: 2, buttons: 6)); + expect(event.type, equals('mousedown')); + expect(event.button, equals(2)); + expect(event.buttons, equals(6)); + expect(event.client.x, equals(212)); + expect(event.client.y, equals(213)); + + event = expectCorrectType(context.mouseMove(clientX: 214, clientY: 215, button: 2, buttons: 1)); + expect(event.type, equals('mouseup')); + expect(event.button, equals(2)); + expect(event.buttons, equals(1)); + expect(event.client.x, equals(214)); + expect(event.client.y, equals(215)); + + event = expectCorrectType(context.primaryUp(clientX: 300, clientY: 301)); + expect(event.type, equals('mouseup')); + expect(event.button, equals(0)); + expect(event.buttons, equals(0)); + expect(event.client.x, equals(300)); + expect(event.client.y, equals(301)); + + event = expectCorrectType(context.mouseUp(clientX: 310, clientY: 311, button: 2)); + expect(event.type, equals('mouseup')); + expect(event.button, equals(2)); + expect(event.buttons, equals(0)); + expect(event.client.x, equals(310)); + expect(event.client.y, equals(311)); + + event = expectCorrectType(context.hover(clientX: 400, clientY: 401)); + expect(event.type, equals('mousemove')); + expect(event.button, equals(0)); + expect(event.buttons, equals(0)); + expect(event.client.x, equals(400)); + expect(event.client.y, equals(401)); + }); + + // ALL ADAPTERS + + _testEach( + [_PointerEventContext(), _MouseEventContext(), _TouchEventContext()], + 'can receive pointer events on the glass pane', + (_BasicEventContext context) { + PointerBinding.instance.debugOverrideDetector(context); + ui.PointerDataPacket receivedPacket; ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) { - packets.add(packet); + receivedPacket = packet; }; - glassPane.dispatchEvent(html.PointerEvent('pointerdown', { - 'pointerId': 1, - 'button': 1, - })); - - glassPane.dispatchEvent(html.PointerEvent('pointerdown', { - 'pointerId': 2, - 'button': 1, - })); + glassPane.dispatchEvent(context.primaryDown()); - expect(packets, hasLength(2)); - // An add will be synthesized. - 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[0].device, equals(1)); - expect(packets[0].data[1].change, equals(ui.PointerChange.down)); - expect(packets[0].data[1].device, equals(1)); - // An add will be synthesized. - expect(packets[1].data, hasLength(2)); - expect(packets[1].data[0].change, equals(ui.PointerChange.add)); - expect(packets[1].data[0].synthesized, equals(true)); - expect(packets[1].data[0].device, equals(2)); - expect(packets[1].data[1].change, equals(ui.PointerChange.down)); - expect(packets[1].data[1].device, equals(2)); - }); + expect(receivedPacket, isNotNull); + expect(receivedPacket.data[0].buttons, equals(1)); + }, + ); - test('creates an add event if the first pointer activity is a hover', () { + _testEach( + [_PointerEventContext(), _MouseEventContext(), _TouchEventContext()], + 'does create an add event if got a pointerdown', + (_BasicEventContext context) { + PointerBinding.instance.debugOverrideDetector(context); List packets = []; ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) { packets.add(packet); }; - glassPane.dispatchEvent(html.PointerEvent('pointermove', { - 'pointerId': 1, - 'button': 1, - })); + glassPane.dispatchEvent(context.primaryDown()); expect(packets, hasLength(1)); expect(packets.single.data, hasLength(2)); expect(packets.single.data[0].change, equals(ui.PointerChange.add)); - expect(packets.single.data[0].synthesized, equals(true)); - expect(packets.single.data[1].change, equals(ui.PointerChange.hover)); - }); + expect(packets.single.data[1].change, equals(ui.PointerChange.down)); + }, + ); - test('does create an add event if got a pointerdown', () { + // BUTTONED ADAPTERS + + _testEach<_ButtonedEventMixin>( + [_MouseEventContext(), _PointerEventContext()], + 'creates an add event if the first pointer activity is a hover', + (_ButtonedEventMixin context) { + PointerBinding.instance.debugOverrideDetector(context); List packets = []; ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) { packets.add(packet); }; - glassPane.dispatchEvent(html.PointerEvent('pointerdown', { - 'pointerId': 1, - 'button': 1, - })); + glassPane.dispatchEvent(context.hover()); expect(packets, hasLength(1)); expect(packets.single.data, hasLength(2)); expect(packets.single.data[0].change, equals(ui.PointerChange.add)); - expect(packets.single.data[1].change, equals(ui.PointerChange.down)); - }); + expect(packets.single.data[0].synthesized, equals(true)); + expect(packets.single.data[1].change, equals(ui.PointerChange.hover)); + }, + ); - test('does calculate delta and pointer identifier correctly', () { + _testEach<_ButtonedEventMixin>( + [_PointerEventContext(), _MouseEventContext()], + 'synthesizes a pointerup event on two pointerdowns in a row', + (_ButtonedEventMixin context) { + PointerBinding.instance.debugOverrideDetector(context); List packets = []; ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) { packets.add(packet); }; - glassPane.dispatchEvent(html.PointerEvent('pointermove', { - 'pointerId': 1, - 'button': 1, - 'clientX': 10.0, - 'clientY': 10.0, - })); - - glassPane.dispatchEvent(html.PointerEvent('pointermove', { - 'pointerId': 1, - 'button': 1, - 'clientX': 20.0, - 'clientY': 20.0, - })); - - glassPane.dispatchEvent(html.PointerEvent('pointerdown', { - 'pointerId': 1, - 'button': 1, - 'clientX': 20.0, - 'clientY': 20.0, - })); - - glassPane.dispatchEvent(html.PointerEvent('pointermove', { - 'pointerId': 1, - 'button': 1, - 'clientX': 40.0, - 'clientY': 30.0, - })); - - glassPane.dispatchEvent(html.PointerEvent('pointerup', { - 'pointerId': 1, - 'button': 1, - 'clientX': 40.0, - 'clientY': 30.0, - })); - - glassPane.dispatchEvent(html.PointerEvent('pointermove', { - 'pointerId': 1, - 'button': 1, - 'clientX': 20.0, - 'clientY': 10.0, - })); - - glassPane.dispatchEvent(html.PointerEvent('pointerdown', { - 'pointerId': 1, - 'button': 1, - 'clientX': 20.0, - 'clientY': 10.0, - })); - - expect(packets, hasLength(7)); + glassPane.dispatchEvent(context.primaryDown()); + + glassPane.dispatchEvent(context.primaryDown()); + expect(packets, hasLength(2)); + // An add will be synthesized. expect(packets[0].data, hasLength(2)); expect(packets[0].data[0].change, equals(ui.PointerChange.add)); - expect(packets[0].data[0].pointerIdentifier, equals(0)); expect(packets[0].data[0].synthesized, equals(true)); - expect(packets[0].data[0].physicalX, equals(10.0)); - expect(packets[0].data[0].physicalY, equals(10.0)); - expect(packets[0].data[0].physicalDeltaX, equals(0.0)); - expect(packets[0].data[0].physicalDeltaY, equals(0.0)); - - expect(packets[0].data[1].change, equals(ui.PointerChange.hover)); - expect(packets[0].data[1].pointerIdentifier, equals(0)); - expect(packets[0].data[1].synthesized, equals(false)); - expect(packets[0].data[1].physicalX, equals(10.0)); - expect(packets[0].data[1].physicalY, equals(10.0)); - expect(packets[0].data[1].physicalDeltaX, equals(0.0)); - expect(packets[0].data[1].physicalDeltaY, equals(0.0)); - - expect(packets[1].data, hasLength(1)); - expect(packets[1].data[0].change, equals(ui.PointerChange.hover)); - expect(packets[1].data[0].pointerIdentifier, equals(0)); - expect(packets[1].data[0].synthesized, equals(false)); - expect(packets[1].data[0].physicalX, equals(20.0)); - expect(packets[1].data[0].physicalY, equals(20.0)); - expect(packets[1].data[0].physicalDeltaX, equals(10.0)); - expect(packets[1].data[0].physicalDeltaY, equals(10.0)); - - expect(packets[2].data, hasLength(1)); - expect(packets[2].data[0].change, equals(ui.PointerChange.down)); - expect(packets[2].data[0].pointerIdentifier, equals(1)); - expect(packets[2].data[0].synthesized, equals(false)); - expect(packets[2].data[0].physicalX, equals(20.0)); - expect(packets[2].data[0].physicalY, equals(20.0)); - expect(packets[2].data[0].physicalDeltaX, equals(0.0)); - expect(packets[2].data[0].physicalDeltaY, equals(0.0)); - - expect(packets[3].data, hasLength(1)); - expect(packets[3].data[0].change, equals(ui.PointerChange.move)); - expect(packets[3].data[0].pointerIdentifier, equals(1)); - expect(packets[3].data[0].synthesized, equals(false)); - expect(packets[3].data[0].physicalX, equals(40.0)); - expect(packets[3].data[0].physicalY, equals(30.0)); - expect(packets[3].data[0].physicalDeltaX, equals(20.0)); - expect(packets[3].data[0].physicalDeltaY, equals(10.0)); - - expect(packets[4].data, hasLength(1)); - expect(packets[4].data[0].change, equals(ui.PointerChange.up)); - expect(packets[4].data[0].pointerIdentifier, equals(1)); - expect(packets[4].data[0].synthesized, equals(false)); - expect(packets[4].data[0].physicalX, equals(40.0)); - expect(packets[4].data[0].physicalY, equals(30.0)); - expect(packets[4].data[0].physicalDeltaX, equals(0.0)); - expect(packets[4].data[0].physicalDeltaY, equals(0.0)); - - expect(packets[5].data, hasLength(1)); - expect(packets[5].data[0].change, equals(ui.PointerChange.hover)); - expect(packets[5].data[0].pointerIdentifier, equals(1)); - expect(packets[5].data[0].synthesized, equals(false)); - expect(packets[5].data[0].physicalX, equals(20.0)); - expect(packets[5].data[0].physicalY, equals(10.0)); - expect(packets[5].data[0].physicalDeltaX, equals(-20.0)); - expect(packets[5].data[0].physicalDeltaY, equals(-20.0)); - - expect(packets[6].data, hasLength(1)); - expect(packets[6].data[0].change, equals(ui.PointerChange.down)); - expect(packets[6].data[0].pointerIdentifier, equals(2)); - expect(packets[6].data[0].synthesized, equals(false)); - expect(packets[6].data[0].physicalX, equals(20.0)); - expect(packets[6].data[0].physicalY, equals(10.0)); - expect(packets[6].data[0].physicalDeltaX, equals(0.0)); - expect(packets[6].data[0].physicalDeltaY, equals(0.0)); - }); + expect(packets[0].data[1].change, equals(ui.PointerChange.down)); + expect(packets[1].data[0].change, equals(ui.PointerChange.up)); + expect(packets[1].data[1].change, equals(ui.PointerChange.down)); + }, + ); - test('does synthesize add or hover or more for scroll', () { + _testEach<_ButtonedEventMixin>( + [_PointerEventContext(), _MouseEventContext()], + 'does synthesize add or hover or more for scroll', + (_ButtonedEventMixin context) { + PointerBinding.instance.debugOverrideDetector(context); List packets = []; ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) { packets.add(packet); @@ -286,12 +487,12 @@ void main() { deltaY: 10, )); - glassPane.dispatchEvent(html.PointerEvent('pointerdown', { - 'pointerId': -1, - 'button': 1, - 'clientX': 20.0, - 'clientY': 50.0, - })); + glassPane.dispatchEvent(context.mouseDown( + button: 0, + buttons: 1, + clientX: 20.0, + clientY: 50.0, + )); glassPane.dispatchEvent(html.WheelEvent('wheel', button: 1, @@ -344,7 +545,7 @@ void main() { // No synthetic pointer data for down event. expect(packets[2].data, hasLength(1)); expect(packets[2].data[0].change, equals(ui.PointerChange.down)); - expect(packets[2].data[0].signalKind, equals(null)); + expect(packets[2].data[0].signalKind, equals(ui.PointerSignalKind.none)); expect(packets[2].data[0].pointerIdentifier, equals(1)); expect(packets[2].data[0].synthesized, equals(false)); expect(packets[2].data[0].physicalX, equals(20.0)); @@ -370,17 +571,1294 @@ void main() { expect(packets[3].data[1].physicalY, equals(60.0)); expect(packets[3].data[1].physicalDeltaX, equals(0.0)); expect(packets[3].data[1].physicalDeltaY, equals(0.0)); + }, + ); + + _testEach<_ButtonedEventMixin>( + [_PointerEventContext(), _MouseEventContext()], + 'does calculate delta and pointer identifier correctly', + (_ButtonedEventMixin context) { + PointerBinding.instance.debugOverrideDetector(context); + List packets = []; + ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) { + packets.add(packet); + }; + + glassPane.dispatchEvent(context.hover( + clientX: 10.0, + clientY: 10.0, + )); + 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].pointerIdentifier, equals(0)); + expect(packets[0].data[0].synthesized, equals(true)); + expect(packets[0].data[0].physicalX, equals(10.0)); + expect(packets[0].data[0].physicalY, equals(10.0)); + expect(packets[0].data[0].physicalDeltaX, equals(0.0)); + expect(packets[0].data[0].physicalDeltaY, equals(0.0)); + + expect(packets[0].data[1].change, equals(ui.PointerChange.hover)); + expect(packets[0].data[1].pointerIdentifier, equals(0)); + expect(packets[0].data[1].synthesized, equals(false)); + expect(packets[0].data[1].physicalX, equals(10.0)); + expect(packets[0].data[1].physicalY, equals(10.0)); + expect(packets[0].data[1].physicalDeltaX, equals(0.0)); + expect(packets[0].data[1].physicalDeltaY, equals(0.0)); + packets.clear(); + + glassPane.dispatchEvent(context.hover( + clientX: 20.0, + clientY: 20.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.hover)); + expect(packets[0].data[0].pointerIdentifier, equals(0)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].physicalX, equals(20.0)); + expect(packets[0].data[0].physicalY, equals(20.0)); + expect(packets[0].data[0].physicalDeltaX, equals(10.0)); + expect(packets[0].data[0].physicalDeltaY, equals(10.0)); + packets.clear(); + + glassPane.dispatchEvent(context.primaryDown( + clientX: 20.0, + clientY: 20.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.down)); + expect(packets[0].data[0].pointerIdentifier, equals(1)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].physicalX, equals(20.0)); + expect(packets[0].data[0].physicalY, equals(20.0)); + expect(packets[0].data[0].physicalDeltaX, equals(0.0)); + expect(packets[0].data[0].physicalDeltaY, equals(0.0)); + packets.clear(); + + glassPane.dispatchEvent(context.primaryMove( + clientX: 40.0, + clientY: 30.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.move)); + expect(packets[0].data[0].pointerIdentifier, equals(1)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].physicalX, equals(40.0)); + expect(packets[0].data[0].physicalY, equals(30.0)); + expect(packets[0].data[0].physicalDeltaX, equals(20.0)); + expect(packets[0].data[0].physicalDeltaY, equals(10.0)); + packets.clear(); + + glassPane.dispatchEvent(context.primaryUp( + clientX: 40.0, + clientY: 30.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.up)); + expect(packets[0].data[0].pointerIdentifier, equals(1)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].physicalX, equals(40.0)); + expect(packets[0].data[0].physicalY, equals(30.0)); + expect(packets[0].data[0].physicalDeltaX, equals(0.0)); + expect(packets[0].data[0].physicalDeltaY, equals(0.0)); + packets.clear(); + + glassPane.dispatchEvent(context.hover( + clientX: 20.0, + clientY: 10.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.hover)); + expect(packets[0].data[0].pointerIdentifier, equals(1)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].physicalX, equals(20.0)); + expect(packets[0].data[0].physicalY, equals(10.0)); + expect(packets[0].data[0].physicalDeltaX, equals(-20.0)); + expect(packets[0].data[0].physicalDeltaY, equals(-20.0)); + packets.clear(); + + glassPane.dispatchEvent(context.primaryDown( + clientX: 20.0, + clientY: 10.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.down)); + expect(packets[0].data[0].pointerIdentifier, equals(2)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].physicalX, equals(20.0)); + expect(packets[0].data[0].physicalY, equals(10.0)); + expect(packets[0].data[0].physicalDeltaX, equals(0.0)); + expect(packets[0].data[0].physicalDeltaY, equals(0.0)); + packets.clear(); + }, + ); + + _testEach<_ButtonedEventMixin>( + [_PointerEventContext(), _MouseEventContext()], + 'correctly converts buttons of down, move and up events', + (_ButtonedEventMixin context) { + PointerBinding.instance.debugOverrideDetector(context); + List packets = []; + ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) { + packets.add(packet); + }; + + // Add and hover + + glassPane.dispatchEvent(context.hover( + clientX: 10, + clientY: 11, + )); + + 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[0].physicalX, equals(10)); + expect(packets[0].data[0].physicalY, equals(11)); + + expect(packets[0].data[1].change, equals(ui.PointerChange.hover)); + expect(packets[0].data[1].synthesized, equals(false)); + expect(packets[0].data[1].physicalX, equals(10)); + expect(packets[0].data[1].physicalY, equals(11)); + expect(packets[0].data[1].buttons, equals(0)); + packets.clear(); + + glassPane.dispatchEvent(context.mouseDown( + button: 0, + buttons: 1, + clientX: 10.0, + clientY: 11.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.down)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].physicalX, equals(10)); + expect(packets[0].data[0].physicalY, equals(11)); + expect(packets[0].data[0].buttons, equals(1)); + packets.clear(); + + glassPane.dispatchEvent(context.mouseMove( + button: _kNoButtonChange, + buttons: 1, + clientX: 20.0, + clientY: 21.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.move)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].physicalX, equals(20)); + expect(packets[0].data[0].physicalY, equals(21)); + expect(packets[0].data[0].buttons, equals(1)); + packets.clear(); + + glassPane.dispatchEvent(context.mouseUp( + button: 0, + clientX: 20.0, + clientY: 21.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.up)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].physicalX, equals(20)); + expect(packets[0].data[0].physicalY, equals(21)); + expect(packets[0].data[0].buttons, equals(0)); + packets.clear(); + + // Drag with secondary button + glassPane.dispatchEvent(context.mouseDown( + button: 2, + buttons: 2, + clientX: 20.0, + clientY: 21.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.down)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].physicalX, equals(20)); + expect(packets[0].data[0].physicalY, equals(21)); + expect(packets[0].data[0].buttons, equals(2)); + packets.clear(); + + glassPane.dispatchEvent(context.mouseMove( + button: _kNoButtonChange, + buttons: 2, + clientX: 30.0, + clientY: 31.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.move)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].physicalX, equals(30)); + expect(packets[0].data[0].physicalY, equals(31)); + expect(packets[0].data[0].buttons, equals(2)); + packets.clear(); + + glassPane.dispatchEvent(context.mouseUp( + button: 2, + clientX: 30.0, + clientY: 31.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.up)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].physicalX, equals(30)); + expect(packets[0].data[0].physicalY, equals(31)); + expect(packets[0].data[0].buttons, equals(0)); + packets.clear(); + + // Drag with middle button + glassPane.dispatchEvent(context.mouseDown( + button: 1, + buttons: 4, + clientX: 30.0, + clientY: 31.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.down)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].physicalX, equals(30)); + expect(packets[0].data[0].physicalY, equals(31)); + expect(packets[0].data[0].buttons, equals(4)); + packets.clear(); + + glassPane.dispatchEvent(context.mouseMove( + button: _kNoButtonChange, + buttons: 4, + clientX: 40.0, + clientY: 41.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.move)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].physicalX, equals(40)); + expect(packets[0].data[0].physicalY, equals(41)); + expect(packets[0].data[0].buttons, equals(4)); + packets.clear(); + + glassPane.dispatchEvent(context.mouseUp( + button: 1, + clientX: 40.0, + clientY: 41.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.up)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].physicalX, equals(40)); + expect(packets[0].data[0].physicalY, equals(41)); + expect(packets[0].data[0].buttons, equals(0)); + packets.clear(); + }, + ); + + _testEach<_ButtonedEventMixin>( + [_PointerEventContext(), _MouseEventContext()], + 'correctly handles button changes during a down sequence', + (_ButtonedEventMixin context) { + PointerBinding.instance.debugOverrideDetector(context); + List packets = []; + ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) { + packets.add(packet); + }; + + // Press LMB. + glassPane.dispatchEvent(context.mouseDown( + button: 0, + buttons: 1, + )); + 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(1)); + packets.clear(); + + // Press MMB. + glassPane.dispatchEvent(context.mouseMove( + button: 1, + buttons: 5, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.move)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].buttons, equals(5)); + packets.clear(); + + // Release LMB. + glassPane.dispatchEvent(context.mouseMove( + button: 0, + buttons: 4, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.move)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].buttons, equals(4)); + packets.clear(); + + // Release MMB. + glassPane.dispatchEvent(context.mouseUp( + button: 1, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + 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)); + packets.clear(); + }, + ); + + _testEach<_ButtonedEventMixin>( + [_PointerEventContext(), _MouseEventContext()], + 'synthesizes a pointerup event when pointermove comes before the up', + (_ButtonedEventMixin context) { + PointerBinding.instance.debugOverrideDetector(context); + // This can happen when the user pops up the context menu by right + // clicking, then dismisses it with a left click. + + List packets = []; + ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) { + packets.add(packet); + }; + + glassPane.dispatchEvent(context.mouseDown( + button: 2, + buttons: 2, + clientX: 10, + clientY: 11, + )); + + 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[0].physicalX, equals(10)); + expect(packets[0].data[0].physicalY, equals(11)); + + expect(packets[0].data[1].change, equals(ui.PointerChange.down)); + expect(packets[0].data[1].synthesized, equals(false)); + expect(packets[0].data[1].physicalX, equals(10)); + expect(packets[0].data[1].physicalY, equals(11)); + expect(packets[0].data[1].buttons, equals(2)); + packets.clear(); + + glassPane.dispatchEvent(context.mouseMove( + button: _kNoButtonChange, + buttons: 2, + clientX: 20.0, + clientY: 21.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.move)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].physicalX, equals(20)); + expect(packets[0].data[0].physicalY, equals(21)); + expect(packets[0].data[0].buttons, equals(2)); + packets.clear(); + + + glassPane.dispatchEvent(context.mouseMove( + button: _kNoButtonChange, + buttons: 2, + clientX: 20.0, + clientY: 21.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.move)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].physicalX, equals(20)); + expect(packets[0].data[0].physicalY, equals(21)); + expect(packets[0].data[0].buttons, equals(2)); + packets.clear(); + + + glassPane.dispatchEvent(context.mouseUp( + button: 2, + clientX: 20.0, + clientY: 21.0, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.up)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].physicalX, equals(20)); + expect(packets[0].data[0].physicalY, equals(21)); + expect(packets[0].data[0].buttons, equals(0)); + packets.clear(); + }, + ); + + _testEach<_ButtonedEventMixin>( + [_PointerEventContext(), _MouseEventContext()], + 'correctly handles uncontinuous button changes during a down sequence', + (_ButtonedEventMixin context) { + PointerBinding.instance.debugOverrideDetector(context); + // This can happen with the following gesture sequence: + // + // - Pops up the context menu by right clicking, but holds RMB; + // - Clicks LMB; + // - Releases RMB. + + List packets = []; + ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) { + packets.add(packet); + }; + + // Press RMB and hold, popping up the context menu. + 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(); + + // Press LMB. The event will have "button: -1" here, despite the change + // in "buttons", probably because the "press" gesture was absorbed by + // dismissing the context menu. + glassPane.dispatchEvent(context.mouseMove( + button: _kNoButtonChange, + buttons: 3, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.move)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].buttons, equals(3)); + packets.clear(); + + // Release LMB. + glassPane.dispatchEvent(context.mouseMove( + button: 0, + buttons: 2, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.move)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].buttons, equals(2)); + packets.clear(); + + // Release RMB. + glassPane.dispatchEvent(context.mouseUp( + button: 2, + )); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + 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)); + packets.clear(); + }, + ); + + // MULTIPOINTER ADAPTERS + + _testEach<_MultiPointerEventMixin>( + [_PointerEventContext(), _TouchEventContext()], + 'treats each pointer separately', + (_MultiPointerEventMixin context) { + PointerBinding.instance.debugOverrideDetector(context); + List packets = []; + List data; + ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) { + packets.add(packet); + }; + + // Two pointers down + context.multiTouchDown(<_TouchDetails>[ + _TouchDetails(pointer: 2, clientX: 100, clientY: 101), + _TouchDetails(pointer: 3, clientX: 200, clientY: 201), + ]).forEach(glassPane.dispatchEvent); + if (context.runtimeType == _PointerEventContext) { + expect(packets.length, 2); + expect(packets[0].data.length, 2); + expect(packets[1].data.length, 2); + } else if (context.runtimeType == _TouchEventContext) { + expect(packets.length, 1); + expect(packets[0].data.length, 4); + } else { + assert(false, 'Unexpected context type ${context.runtimeType}'); + } + + data = _allPointerData(packets); + expect(data, hasLength(4)); + expect(data[0].change, equals(ui.PointerChange.add)); + expect(data[0].synthesized, equals(true)); + expect(data[0].device, equals(2)); + expect(data[0].physicalX, equals(100)); + expect(data[0].physicalY, equals(101)); + + expect(data[1].change, equals(ui.PointerChange.down)); + expect(data[1].device, equals(2)); + expect(data[1].buttons, equals(1)); + expect(data[1].physicalX, equals(100)); + expect(data[1].physicalY, equals(101)); + expect(data[1].physicalDeltaX, equals(0)); + expect(data[1].physicalDeltaY, equals(0)); + + expect(data[2].change, equals(ui.PointerChange.add)); + expect(data[2].synthesized, equals(true)); + expect(data[2].device, equals(3)); + expect(data[2].physicalX, equals(200)); + expect(data[2].physicalY, equals(201)); + + expect(data[3].change, equals(ui.PointerChange.down)); + expect(data[3].device, equals(3)); + expect(data[3].buttons, equals(1)); + expect(data[3].physicalX, equals(200)); + expect(data[3].physicalY, equals(201)); + expect(data[3].physicalDeltaX, equals(0)); + expect(data[3].physicalDeltaY, equals(0)); + packets.clear(); + + // Two pointers move + context.multiTouchMove(<_TouchDetails>[ + _TouchDetails(pointer: 3, clientX: 300, clientY: 302), + _TouchDetails(pointer: 2, clientX: 400, clientY: 402), + ]).forEach(glassPane.dispatchEvent); + if (context.runtimeType == _PointerEventContext) { + expect(packets.length, 2); + expect(packets[0].data.length, 1); + expect(packets[1].data.length, 1); + } else if (context.runtimeType == _TouchEventContext) { + expect(packets.length, 1); + expect(packets[0].data.length, 2); + } else { + assert(false, 'Unexpected context type ${context.runtimeType}'); + } + + data = _allPointerData(packets); + expect(data, hasLength(2)); + expect(data[0].change, equals(ui.PointerChange.move)); + expect(data[0].device, equals(3)); + expect(data[0].buttons, equals(1)); + expect(data[0].physicalX, equals(300)); + expect(data[0].physicalY, equals(302)); + expect(data[0].physicalDeltaX, equals(100)); + expect(data[0].physicalDeltaY, equals(101)); + + expect(data[1].change, equals(ui.PointerChange.move)); + expect(data[1].device, equals(2)); + expect(data[1].buttons, equals(1)); + expect(data[1].physicalX, equals(400)); + expect(data[1].physicalY, equals(402)); + expect(data[1].physicalDeltaX, equals(300)); + expect(data[1].physicalDeltaY, equals(301)); + packets.clear(); + + // One pointer up + context.multiTouchUp(<_TouchDetails>[ + _TouchDetails(pointer: 3, clientX: 300, clientY: 302), + ]).forEach(glassPane.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(3)); + expect(packets[0].data[0].buttons, equals(0)); + expect(packets[0].data[0].physicalX, equals(300)); + expect(packets[0].data[0].physicalY, equals(302)); + 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(3)); + expect(packets[0].data[1].buttons, equals(0)); + expect(packets[0].data[1].physicalX, equals(300)); + expect(packets[0].data[1].physicalY, equals(302)); + expect(packets[0].data[1].physicalDeltaX, equals(0)); + expect(packets[0].data[1].physicalDeltaY, equals(0)); + packets.clear(); + + // Another pointer up + context.multiTouchUp(<_TouchDetails>[ + _TouchDetails(pointer: 2, clientX: 400, clientY: 402), + ]).forEach(glassPane.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(2)); + expect(packets[0].data[0].buttons, equals(0)); + expect(packets[0].data[0].physicalX, equals(400)); + expect(packets[0].data[0].physicalY, equals(402)); + 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(2)); + expect(packets[0].data[1].buttons, equals(0)); + expect(packets[0].data[1].physicalX, equals(400)); + expect(packets[0].data[1].physicalY, equals(402)); + expect(packets[0].data[1].physicalDeltaX, equals(0)); + expect(packets[0].data[1].physicalDeltaY, equals(0)); + packets.clear(); + + // Again two pointers down (reuse pointer ID) + context.multiTouchDown(<_TouchDetails>[ + _TouchDetails(pointer: 3, clientX: 500, clientY: 501), + _TouchDetails(pointer: 2, clientX: 600, clientY: 601), + ]).forEach(glassPane.dispatchEvent); + if (context.runtimeType == _PointerEventContext) { + expect(packets.length, 2); + expect(packets[0].data.length, 2); + expect(packets[1].data.length, 2); + } else if (context.runtimeType == _TouchEventContext) { + expect(packets.length, 1); + expect(packets[0].data.length, 4); + } else { + assert(false, 'Unexpected context type ${context.runtimeType}'); + } + + data = _allPointerData(packets); + expect(data, hasLength(4)); + expect(data[0].change, equals(ui.PointerChange.add)); + expect(data[0].synthesized, equals(true)); + expect(data[0].device, equals(3)); + expect(data[0].physicalX, equals(500)); + expect(data[0].physicalY, equals(501)); + + expect(data[1].change, equals(ui.PointerChange.down)); + expect(data[1].device, equals(3)); + expect(data[1].buttons, equals(1)); + expect(data[1].physicalX, equals(500)); + expect(data[1].physicalY, equals(501)); + expect(data[1].physicalDeltaX, equals(0)); + expect(data[1].physicalDeltaY, equals(0)); + + expect(data[2].change, equals(ui.PointerChange.add)); + expect(data[2].synthesized, equals(true)); + expect(data[2].device, equals(2)); + expect(data[2].physicalX, equals(600)); + expect(data[2].physicalY, equals(601)); + + expect(data[3].change, equals(ui.PointerChange.down)); + expect(data[3].device, equals(2)); + expect(data[3].buttons, equals(1)); + expect(data[3].physicalX, equals(600)); + expect(data[3].physicalY, equals(601)); + expect(data[3].physicalDeltaX, equals(0)); + expect(data[3].physicalDeltaY, equals(0)); + packets.clear(); + }, + ); + + _testEach<_MultiPointerEventMixin>( + [_PointerEventContext(), _TouchEventContext()], + 'correctly parses cancel event', + (_MultiPointerEventMixin context) { + PointerBinding.instance.debugOverrideDetector(context); + List packets = []; + ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) { + packets.add(packet); + }; + + // Two pointers down + context.multiTouchDown(<_TouchDetails>[ + _TouchDetails(pointer: 2, clientX: 100, clientY: 101), + _TouchDetails(pointer: 3, clientX: 200, clientY: 201), + ]).forEach(glassPane.dispatchEvent); + packets.clear(); // Down event is tested in other tests. + + // One pointer cancel + context.multiTouchCancel(<_TouchDetails>[ + _TouchDetails(pointer: 3, clientX: 300, clientY: 302), + ]).forEach(glassPane.dispatchEvent); + expect(packets.length, 1); + expect(packets[0].data.length, 2); + expect(packets[0].data[0].change, equals(ui.PointerChange.cancel)); + expect(packets[0].data[0].device, equals(3)); + expect(packets[0].data[0].buttons, equals(0)); + expect(packets[0].data[0].physicalX, equals(200)); + expect(packets[0].data[0].physicalY, equals(201)); + 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(3)); + expect(packets[0].data[1].buttons, equals(0)); + expect(packets[0].data[1].physicalX, equals(200)); + expect(packets[0].data[1].physicalY, equals(201)); + expect(packets[0].data[1].physicalDeltaX, equals(0)); + expect(packets[0].data[1].physicalDeltaY, equals(0)); + packets.clear(); + }, + ); + + // POINTER ADAPTER + + _testEach( + [_PointerEventContext()], + 'does not synthesize pointer up if from different device', + (_PointerEventContext context) { + PointerBinding.instance.debugOverrideDetector(context); + List packets = []; + ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) { + packets.add(packet); + }; + + context.multiTouchDown(<_TouchDetails>[ + _TouchDetails(pointer: 1, clientX: 100, clientY: 101), + ]).forEach(glassPane.dispatchEvent); + expect(packets, hasLength(1)); + // An add will be synthesized. + 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[0].device, equals(1)); + expect(packets[0].data[1].change, equals(ui.PointerChange.down)); + expect(packets[0].data[1].device, equals(1)); + packets.clear(); + + context.multiTouchDown(<_TouchDetails>[ + _TouchDetails(pointer: 2, clientX: 200, clientY: 202), + ]).forEach(glassPane.dispatchEvent); + // An add will be synthesized. + 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[0].device, equals(2)); + expect(packets[0].data[1].change, equals(ui.PointerChange.down)); + expect(packets[0].data[1].device, equals(2)); + packets.clear(); + }, + ); + + // TOUCH ADAPTER + + _testEach( + [_TouchEventContext()], + 'does calculate delta and pointer identifier correctly', + (_TouchEventContext context) { + // Mouse and Pointer are in another test since these tests can involve hovering + PointerBinding.instance.debugOverrideDetector(context); + List packets = []; + ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) { + packets.add(packet); + }; + + context.multiTouchDown(<_TouchDetails>[ + _TouchDetails(pointer: 1, clientX: 20, clientY: 20), + ]).forEach(glassPane.dispatchEvent); + 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].pointerIdentifier, equals(1)); + expect(packets[0].data[0].synthesized, equals(true)); + expect(packets[0].data[0].physicalX, equals(20.0)); + expect(packets[0].data[0].physicalY, equals(20.0)); + expect(packets[0].data[0].physicalDeltaX, equals(0.0)); + expect(packets[0].data[0].physicalDeltaY, equals(0.0)); + + expect(packets[0].data[1].change, equals(ui.PointerChange.down)); + expect(packets[0].data[1].pointerIdentifier, equals(1)); + expect(packets[0].data[1].synthesized, equals(false)); + expect(packets[0].data[1].physicalX, equals(20.0)); + expect(packets[0].data[1].physicalY, equals(20.0)); + expect(packets[0].data[1].physicalDeltaX, equals(0.0)); + expect(packets[0].data[1].physicalDeltaY, equals(0.0)); + packets.clear(); + + context.multiTouchMove(<_TouchDetails>[ + _TouchDetails(pointer: 1, clientX: 40, clientY: 30), + ]).forEach(glassPane.dispatchEvent); + expect(packets, hasLength(1)); + expect(packets[0].data, hasLength(1)); + expect(packets[0].data[0].change, equals(ui.PointerChange.move)); + expect(packets[0].data[0].pointerIdentifier, equals(1)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].physicalX, equals(40.0)); + expect(packets[0].data[0].physicalY, equals(30.0)); + expect(packets[0].data[0].physicalDeltaX, equals(20.0)); + expect(packets[0].data[0].physicalDeltaY, equals(10.0)); + packets.clear(); + + context.multiTouchUp(<_TouchDetails>[ + _TouchDetails(pointer: 1, clientX: 40, clientY: 30), + ]).forEach(glassPane.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].pointerIdentifier, equals(1)); + expect(packets[0].data[0].synthesized, equals(false)); + expect(packets[0].data[0].physicalX, equals(40.0)); + expect(packets[0].data[0].physicalY, equals(30.0)); + expect(packets[0].data[0].physicalDeltaX, equals(0.0)); + expect(packets[0].data[0].physicalDeltaY, equals(0.0)); + + expect(packets[0].data[1].change, equals(ui.PointerChange.remove)); + expect(packets[0].data[1].pointerIdentifier, equals(1)); + expect(packets[0].data[1].synthesized, equals(false)); + expect(packets[0].data[1].physicalX, equals(40.0)); + expect(packets[0].data[1].physicalY, equals(30.0)); + expect(packets[0].data[1].physicalDeltaX, equals(0.0)); + expect(packets[0].data[1].physicalDeltaY, equals(0.0)); + packets.clear(); + + context.multiTouchDown(<_TouchDetails>[ + _TouchDetails(pointer: 2, clientX: 20, clientY: 10), + ]).forEach(glassPane.dispatchEvent); + 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].pointerIdentifier, equals(2)); + expect(packets[0].data[0].synthesized, equals(true)); + expect(packets[0].data[0].physicalX, equals(20.0)); + expect(packets[0].data[0].physicalY, equals(10.0)); + expect(packets[0].data[0].physicalDeltaX, equals(0.0)); + expect(packets[0].data[0].physicalDeltaY, equals(0.0)); + + expect(packets[0].data[1].change, equals(ui.PointerChange.down)); + expect(packets[0].data[1].pointerIdentifier, equals(2)); + expect(packets[0].data[1].synthesized, equals(false)); + expect(packets[0].data[1].physicalX, equals(20.0)); + expect(packets[0].data[1].physicalY, equals(10.0)); + expect(packets[0].data[1].physicalDeltaX, equals(0.0)); + expect(packets[0].data[1].physicalDeltaY, equals(0.0)); + packets.clear(); + }, + ); +} + +abstract class _BasicEventContext implements PointerSupportDetector { + String get name; + + bool get isSupported; + + // Generate an event that is: + // + // * For mouse, a left click + // * For touch, a touch down + html.Event primaryDown({double clientX, double clientY}); + + + // Generate an event that is: + // + // * For mouse, a drag with LMB down + // * For touch, a touch drag + html.Event primaryMove({double clientX, double clientY}); + + + // Generate an event that is: + // + // * For mouse, release LMB + // * For touch, a touch up + html.Event primaryUp({double clientX, double clientY}); +} + +mixin _ButtonedEventMixin on _BasicEventContext { + // Generate an event that is a mouse down with the specific buttons. + html.Event mouseDown({double clientX, double clientY, int button, int buttons}); + + // Generate an event that is a mouse drag with the specific buttons, or button + // changes during the drag. + // + // If there is no button change, assign `button` with _kNoButtonChange. + html.Event mouseMove({double clientX, double clientY, int button, int buttons}); + + // Generate an event that releases all mouse buttons. + html.Event mouseUp({double clientX, double clientY, int button}); + + html.Event hover({double clientX, double clientY}) { + return mouseMove( + buttons: 0, + button: _kNoButtonChange, + clientX: clientX, + clientY: clientY, + ); + } + + @override + html.Event primaryDown({double clientX, double clientY}) { + return mouseDown( + buttons: 1, + button: 0, + clientX: clientX, + clientY: clientY, + ); + } + + + @override + html.Event primaryMove({double clientX, double clientY}) { + return mouseMove( + buttons: 1, + button: _kNoButtonChange, + clientX: clientX, + clientY: clientY, + ); + } + + @override + html.Event primaryUp({double clientX, double clientY}) { + return mouseUp( + button: 0, + clientX: clientX, + clientY: clientY, + ); + } +} + +class _TouchDetails { + const _TouchDetails({this.pointer, this.clientX, this.clientY}); + + final int pointer; + final double clientX; + final double clientY; +} + +mixin _MultiPointerEventMixin on _BasicEventContext { + List multiTouchDown(List<_TouchDetails> touches); + List multiTouchMove(List<_TouchDetails> touches); + List multiTouchUp(List<_TouchDetails> touches); + List multiTouchCancel(List<_TouchDetails> touches); + + @override + html.Event primaryDown({double clientX, double clientY}) { + return multiTouchDown(<_TouchDetails>[ + _TouchDetails( + pointer: 1, + clientX: clientX, + clientY: clientY, + ), + ])[0]; + } + + @override + html.Event primaryMove({double clientX, double clientY}) { + return multiTouchMove(<_TouchDetails>[ + _TouchDetails( + pointer: 1, + clientX: clientX, + clientY: clientY, + ), + ])[0]; + } + + @override + html.Event primaryUp({double clientX, double clientY}) { + return multiTouchUp(<_TouchDetails>[ + _TouchDetails( + pointer: 1, + clientX: clientX, + clientY: clientY, + ), + ])[0]; + } +} + +// A test context for `_TouchAdapter`, including its name, PointerSupportDetector +// to override, and how to generate events. +class _TouchEventContext extends _BasicEventContext with _MultiPointerEventMixin implements PointerSupportDetector { + _TouchEventContext() { + _target = html.document.createElement('div'); + } + + @override + String get name => 'TouchAdapter'; + + @override + bool get isSupported => _defaultSupportDetector.hasTouchEvents; + + @override + bool get hasPointerEvents => false; + + @override + bool get hasTouchEvents => true; + + @override + bool get hasMouseEvents => false; + + html.EventTarget _target; + + html.Touch _createTouch({ + int identifier, + double clientX, + double clientY, + }) { + return html.Touch({ + 'identifier': identifier, + 'clientX': clientX, + 'clientY': clientY, + 'target': _target, }); - }); + } + + html.TouchEvent _createTouchEvent(String eventType, List<_TouchDetails> touches) { + return html.TouchEvent(eventType, { + 'changedTouches': touches.map( + (_TouchDetails details) => + _createTouch( + identifier: details.pointer, + clientX: details.clientX, + clientY: details.clientY, + ), + ).toList(), + }, + ); + } + + @override + List multiTouchDown(List<_TouchDetails> touches) { + return [_createTouchEvent('touchstart', touches)]; + } + + @override + List multiTouchMove(List<_TouchDetails> touches) { + return [_createTouchEvent('touchmove', touches)]; + } + + @override + List multiTouchUp(List<_TouchDetails> touches) { + return [_createTouchEvent('touchend', touches)]; + } + + @override + List multiTouchCancel(List<_TouchDetails> touches) { + return [_createTouchEvent('touchcancel', touches)]; + } +} + +// A test context for `_MouseAdapter`, including its name, PointerSupportDetector +// to override, and how to generate events. +// +// For the difference between MouseEvent and PointerEvent, see _MouseAdapter. +class _MouseEventContext extends _BasicEventContext with _ButtonedEventMixin implements PointerSupportDetector { + @override + String get name => 'MouseAdapter'; + + @override + bool get isSupported => _defaultSupportDetector.hasMouseEvents; + + @override + bool get hasPointerEvents => false; + + @override + bool get hasTouchEvents => false; + + @override + bool get hasMouseEvents => true; + + @override + html.Event mouseDown({double clientX, double clientY, int button, int buttons}) { + return _createMouseEvent( + 'mousedown', + buttons: buttons, + button: button, + clientX: clientX, + clientY: clientY, + ); + } + + @override + html.Event mouseMove({double clientX, double clientY, int button, int buttons}) { + final bool hasButtonChange = button != _kNoButtonChange; + final bool changeIsButtonDown = hasButtonChange && (buttons & convertButtonToButtons(button)) != 0; + final String adjustedType = !hasButtonChange ? 'mousemove' : + changeIsButtonDown ? 'mousedown' : + 'mouseup'; + final int adjustedButton = hasButtonChange ? button : 0; + return _createMouseEvent( + adjustedType, + buttons: buttons, + button: adjustedButton, + clientX: clientX, + clientY: clientY, + ); + } + + @override + html.Event mouseUp({double clientX, double clientY, int button}) { + return _createMouseEvent( + 'mouseup', + buttons: 0, + button: button, + clientX: clientX, + clientY: clientY, + ); + } + + html.MouseEvent _createMouseEvent( + String type, { + int buttons, + int button, + double clientX, + double clientY, + }) { + final Function jsMouseEvent = js_util.getProperty(html.window, 'MouseEvent'); + final List eventArgs = [ + type, + { + 'buttons': buttons, + 'button': button, + 'clientX': clientX, + 'clientY': clientY, + } + ]; + return js_util.callConstructor(jsMouseEvent, js_util.jsify(eventArgs)); + } } -class TestPointerDetector extends PointerSupportDetector { +// A test context for `_PointerAdapter`, including its name, PointerSupportDetector +// to override, and how to generate events. +// +// For the difference between MouseEvent and PointerEvent, see _MouseAdapter. +class _PointerEventContext extends _BasicEventContext with _ButtonedEventMixin implements PointerSupportDetector, _MultiPointerEventMixin { + @override + String get name => 'PointerAdapter'; + + @override + bool get isSupported => _defaultSupportDetector.hasPointerEvents; + + @override + bool get hasPointerEvents => true; + + @override + bool get hasTouchEvents => false; + @override - final bool hasPointerEvents = true; + bool get hasMouseEvents => false; @override - final bool hasTouchEvents = false; + List multiTouchDown(List<_TouchDetails> touches) { + return touches.map((_TouchDetails details) => _downWithFullDetails( + pointer: details.pointer, + buttons: 1, + button: 0, + clientX: details.clientX, + clientY: details.clientY, + pointerType: 'touch', + )).toList(); + } + + @override + html.Event mouseDown({double clientX, double clientY, int button, int buttons}) { + return _downWithFullDetails( + pointer: 1, + buttons: buttons, + button: button, + clientX: clientX, + clientY: clientY, + pointerType: 'mouse', + ); + } + + html.Event _downWithFullDetails({double clientX, double clientY, int button, int buttons, int pointer, String pointerType}) { + return html.PointerEvent('pointerdown', { + 'pointerId': pointer, + 'button': button, + 'buttons': buttons, + 'clientX': clientX, + 'clientY': clientY, + 'pointerType': pointerType, + }); + } + + @override + List multiTouchMove(List<_TouchDetails> touches) { + return touches.map((_TouchDetails details) => _moveWithFullDetails( + pointer: details.pointer, + buttons: 1, + button: _kNoButtonChange, + clientX: details.clientX, + clientY: details.clientY, + pointerType: 'touch', + )).toList(); + } + + @override + html.Event mouseMove({double clientX, double clientY, int button, int buttons}) { + return _moveWithFullDetails( + pointer: 1, + buttons: buttons, + button: button, + clientX: clientX, + clientY: clientY, + pointerType: 'mouse', + ); + } + + html.Event _moveWithFullDetails({double clientX, double clientY, int button, int buttons, int pointer, String pointerType}) { + return html.PointerEvent('pointermove', { + 'pointerId': pointer, + 'button': button, + 'buttons': buttons, + 'clientX': clientX, + 'clientY': clientY, + 'pointerType': pointerType, + }); + } + + @override + List multiTouchUp(List<_TouchDetails> touches) { + return touches.map((_TouchDetails details) => _upWithFullDetails( + pointer: details.pointer, + button: 0, + clientX: details.clientX, + clientY: details.clientY, + pointerType: 'touch', + )).toList(); + } + + @override + html.Event mouseUp({double clientX, double clientY, int button}) { + return _upWithFullDetails( + pointer: 1, + button: button, + clientX: clientX, + clientY: clientY, + pointerType: 'mouse', + ); + } + + html.Event _upWithFullDetails({double clientX, double clientY, int button, int pointer, String pointerType}) { + return html.PointerEvent('pointerup', { + 'pointerId': pointer, + 'button': button, + 'buttons': 0, + 'clientX': clientX, + 'clientY': clientY, + 'pointerType': pointerType, + }); + } @override - final bool hasMouseEvents = false; + List multiTouchCancel(List<_TouchDetails> touches) { + return touches.map((_TouchDetails details) => html.PointerEvent('pointercancel', { + 'pointerId': details.pointer, + 'button': 0, + 'buttons': 0, + 'clientX': 0, + 'clientY': 0, + 'pointerType': 'touch', + })).toList(); + } }