diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 1c549bd6e61c39b7fad38b66d812795f27ef5910..299844a2a98a4ddbdae39bc25a8bc9c4b1d0c827 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -417,6 +417,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/picture.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_views.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/plugins.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/pointer_binding.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/pointer_converter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/recording_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/render_vertices.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/rrect_renderer.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index da577462e95dc90c8ab2b47a6b26eda3c8d6395b..d9f9dca215784ec3e2c3809bc2b946eb68a70433 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -62,6 +62,7 @@ part 'engine/picture.dart'; part 'engine/platform_views.dart'; part 'engine/plugins.dart'; part 'engine/pointer_binding.dart'; +part 'engine/pointer_converter.dart'; part 'engine/recording_canvas.dart'; part 'engine/render_vertices.dart'; part 'engine/rrect_renderer.dart'; diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index e70b9ad1f92b5256a6f69b3143f3eeb045d5dd32..46e0c2f751142aa32567e60fb7a54fdb5e950111 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -15,23 +15,17 @@ class PointerBinding { static PointerBinding get instance => _instance; static PointerBinding _instance; - // Set of pointerIds that are added before routing hover and mouse wheel - // events. - // - // The device needs to send a one time PointerChange.add before hover and - // wheel events. - Set _activePointerIds = {}; - PointerBinding(this.domRenderer) { if (_instance == null) { _instance = this; + _pointerDataConverter = PointerDataConverter(); _detector = const PointerSupportDetector(); _adapter = _createAdapter(); } assert(() { registerHotRestartListener(() { _adapter?.clearListeners(); - _activePointerIds.clear(); + _pointerDataConverter?.clearPointerState(); }); return true; }()); @@ -40,7 +34,7 @@ class PointerBinding { final DomRenderer domRenderer; PointerSupportDetector _detector; BaseAdapter _adapter; - + PointerDataConverter _pointerDataConverter; /// Should be used in tests to define custom detection of pointer support. /// /// ```dart @@ -62,22 +56,22 @@ class PointerBinding { newDetector ??= const PointerSupportDetector(); // When changing the detector, we need to swap the adapter. if (newDetector != _detector) { - _activePointerIds.clear(); _detector = newDetector; _adapter?.clearListeners(); _adapter = _createAdapter(); + _pointerDataConverter?.clearPointerState(); } } BaseAdapter _createAdapter() { if (_detector.hasPointerEvents) { - return PointerAdapter(_onPointerData, domRenderer); + return PointerAdapter(_onPointerData, domRenderer, _pointerDataConverter); } if (_detector.hasTouchEvents) { - return TouchAdapter(_onPointerData, domRenderer); + return TouchAdapter(_onPointerData, domRenderer, _pointerDataConverter); } if (_detector.hasMouseEvents) { - return MouseAdapter(_onPointerData, domRenderer); + return MouseAdapter(_onPointerData, domRenderer, _pointerDataConverter); } return null; } @@ -123,11 +117,19 @@ class _PressedButton { /// Common functionality that's shared among adapters. abstract class BaseAdapter { - static final Map _listeners = - {}; + BaseAdapter(this._callback, this.domRenderer, this._pointerDataConverter) { + _setup(); + } + /// Listeners that are registered through dart to js api. + static final Map _listeners = + {}; + /// Listeners that are registered through native javascript api. + static final Map _nativeListeners = + {}; final DomRenderer domRenderer; PointerDataCallback _callback; + PointerDataConverter _pointerDataConverter; // A set of the buttons that are currently being pressed. Set<_PressedButton> _pressedButtons = Set<_PressedButton>(); @@ -144,10 +146,6 @@ abstract class BaseAdapter { } } - BaseAdapter(this._callback, this.domRenderer) { - _setup(); - } - /// Each subclass is expected to override this method to attach its own event /// listeners and convert events into pointer events. void _setup(); @@ -156,9 +154,21 @@ abstract class BaseAdapter { void clearListeners() { final html.Element glassPane = domRenderer.glassPaneElement; _listeners.forEach((String eventName, html.EventListener listener) { - glassPane.removeEventListener(eventName, listener, true); + glassPane.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, + 'removeEventListener', [ + 'wheel', + listener, + ] + ); }); _listeners.clear(); + _nativeListeners.clear(); } void _addEventListener(String eventName, html.EventListener handler) { @@ -177,6 +187,75 @@ abstract class BaseAdapter { domRenderer.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) { + final int ms = milliseconds.toInt(); + final int micro = + ((milliseconds - ms) * Duration.microsecondsPerMillisecond).toInt(); + return Duration(milliseconds: ms, microseconds: micro); + } + + List _convertWheelEventToPointerData( + html.WheelEvent event, + ) { + const int domDeltaPixel = 0x00; + const int domDeltaLine = 0x01; + const int domDeltaPage = 0x02; + + // Flutter only supports pixel scroll delta. Convert deltaMode values + // to pixels. + double deltaX = event.deltaX; + double deltaY = event.deltaY; + switch (event.deltaMode) { + case domDeltaLine: + deltaX *= 32.0; + deltaY *= 32.0; + break; + case domDeltaPage: + deltaX *= ui.window.physicalSize.width; + deltaY *= ui.window.physicalSize.height; + break; + case domDeltaPixel: + default: + break; + } + final List data = []; + _pointerDataConverter.convert( + data, + change: ui.PointerChange.hover, + timeStamp: _eventTimeStampToDuration(event.timeStamp), + kind: ui.PointerDeviceKind.mouse, + signalKind: ui.PointerSignalKind.scroll, + 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, + scrollDeltaX: deltaX, + scrollDeltaY: deltaY, + ); + return data; + } + + 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; + js_util.setProperty(eventOptions, 'passive', false); + js_util.callMethod( + domRenderer.glassPaneElement, + 'addEventListener', [ + 'wheel', + jsHandler, + eventOptions + ] + ); + + } } const int _kPrimaryMouseButton = 0x1; @@ -207,16 +286,17 @@ int _deviceFromHtmlEvent(event) { /// Adapter class to be used with browsers that support native pointer events. class PointerAdapter extends BaseAdapter { - PointerAdapter(PointerDataCallback callback, DomRenderer domRenderer) - : super(callback, domRenderer); + PointerAdapter( + PointerDataCallback callback, + DomRenderer domRenderer, + PointerDataConverter _pointerDataConverter + ) : super(callback, domRenderer, _pointerDataConverter); @override void _setup() { _addEventListener('pointerdown', (html.Event event) { final int pointerButton = _pointerButtonFromHtmlEvent(event); final int device = _deviceFromHtmlEvent(event); - // The pointerdown event will cause an 'add' event on the framework side. - PointerBinding._instance._activePointerIds.add(device); if (_isButtonDown(device, pointerButton)) { // TODO(flutter_web): Remove this temporary fix for right click // on web platform once context guesture is implemented. @@ -239,13 +319,6 @@ class PointerAdapter extends BaseAdapter { ? ui.PointerChange.move : ui.PointerChange.hover, pointerEvent); - _ensureMouseDeviceAdded( - data, - pointerEvent.client.x, - pointerEvent.client.y, - pointerEvent.buttons, - pointerEvent.timeStamp, - pointerEvent.pointerId); _callback(data); }); @@ -270,7 +343,8 @@ class PointerAdapter extends BaseAdapter { _callback(_convertEventToPointerData(ui.PointerChange.cancel, event)); }); - _addWheelEventListener((html.WheelEvent event) { + _addWheelEventListener((html.Event event) { + assert(event is html.WheelEvent); if (_debugLogPointerEvents) { print(event.type); } @@ -289,7 +363,8 @@ class PointerAdapter extends BaseAdapter { final List data = []; for (int i = 0; i < allEvents.length; i++) { final html.PointerEvent event = allEvents[i]; - data.add(ui.PointerData( + _pointerDataConverter.convert( + data, change: change, timeStamp: _eventTimeStampToDuration(event.timeStamp), kind: _pointerTypeToDeviceKind(event.pointerType), @@ -301,7 +376,7 @@ class PointerAdapter extends BaseAdapter { pressureMin: 0.0, pressureMax: 1.0, tilt: _computeHighestTilt(event), - )); + ); } return data; } @@ -343,8 +418,11 @@ class PointerAdapter extends BaseAdapter { /// Adapter to be used with browsers that support touch events. class TouchAdapter extends BaseAdapter { - TouchAdapter(PointerDataCallback callback, DomRenderer domRenderer) - : super(callback, domRenderer); + TouchAdapter( + PointerDataCallback callback, + DomRenderer domRenderer, + PointerDataConverter _pointerDataConverter + ) : super(callback, domRenderer, _pointerDataConverter); @override void _setup() { @@ -381,11 +459,12 @@ class TouchAdapter extends BaseAdapter { html.TouchEvent event, ) { final html.TouchList touches = event.changedTouches; - final List data = List(touches.length); + final List data = List(); final int len = touches.length; for (int i = 0; i < len; i++) { final html.Touch touch = touches[i]; - data[i] = ui.PointerData( + _pointerDataConverter.convert( + data, change: change, timeStamp: _eventTimeStampToDuration(event.timeStamp), kind: ui.PointerDeviceKind.touch, @@ -408,8 +487,11 @@ const int _mouseDeviceId = -1; /// Adapter to be used with browsers that support mouse events. class MouseAdapter extends BaseAdapter { - MouseAdapter(PointerDataCallback callback, DomRenderer domRenderer) - : super(callback, domRenderer); + MouseAdapter( + PointerDataCallback callback, + DomRenderer domRenderer, + PointerDataConverter _pointerDataConverter + ) : super(callback, domRenderer, _pointerDataConverter); @override void _setup() { @@ -442,7 +524,8 @@ class MouseAdapter extends BaseAdapter { _callback(_convertEventToPointerData(ui.PointerChange.up, event)); }); - _addWheelEventListener((html.WheelEvent event) { + _addWheelEventListener((html.Event event) { + assert(event is html.WheelEvent); if (_debugLogPointerEvents) { print(event.type); } @@ -455,16 +538,9 @@ class MouseAdapter extends BaseAdapter { ui.PointerChange change, html.MouseEvent event, ) { - final List data = []; - // The mousedown event will cause an 'add' event on the framework side. - if (event.type == 'mousedown') { - PointerBinding._instance._activePointerIds.add(_mouseDeviceId); - } - if (event.type == 'mousemove') { - _ensureMouseDeviceAdded(data, event.client.x, event.client.y, - event.buttons, event.timeStamp, _mouseDeviceId); - } - data.add(ui.PointerData( + List data = []; + _pointerDataConverter.convert( + data, change: change, timeStamp: _eventTimeStampToDuration(event.timeStamp), kind: ui.PointerDeviceKind.mouse, @@ -476,101 +552,7 @@ class MouseAdapter extends BaseAdapter { pressure: 1.0, pressureMin: 0.0, pressureMax: 1.0, - )); + ); return data; } } - -/// Convert a floating number timestamp (in milliseconds) to a [Duration] by -/// splitting it into two integer components: milliseconds + microseconds. -Duration _eventTimeStampToDuration(num milliseconds) { - final int ms = milliseconds.toInt(); - final int micro = - ((milliseconds - ms) * Duration.microsecondsPerMillisecond).toInt(); - return Duration(milliseconds: ms, microseconds: micro); -} - -void _ensureMouseDeviceAdded(List data, double clientX, - double clientY, int buttons, double timeStamp, int deviceId) { - if (PointerBinding.instance._activePointerIds.contains(deviceId)) { - return; - } - PointerBinding.instance._activePointerIds.add(deviceId); - // Only send [PointerChange.add] the first time. - data.insert( - 0, - ui.PointerData( - change: ui.PointerChange.add, - timeStamp: _eventTimeStampToDuration(timeStamp), - kind: ui.PointerDeviceKind.mouse, - // In order for Flutter to actually add this pointer, we need to set the - // signal to none. - signalKind: ui.PointerSignalKind.none, - device: deviceId, - physicalX: clientX * ui.window.devicePixelRatio, - physicalY: clientY * ui.window.devicePixelRatio, - buttons: buttons, - pressure: 1.0, - pressureMin: 0.0, - pressureMax: 1.0, - scrollDeltaX: 0, - scrollDeltaY: 0, - )); -} - -List _convertWheelEventToPointerData( - html.WheelEvent event, -) { - const int domDeltaPixel = 0x00; - const int domDeltaLine = 0x01; - const int domDeltaPage = 0x02; - - // Flutter only supports pixel scroll delta. Convert deltaMode values - // to pixels. - double deltaX = event.deltaX; - double deltaY = event.deltaY; - switch (event.deltaMode) { - case domDeltaLine: - deltaX *= 32.0; - deltaY *= 32.0; - break; - case domDeltaPage: - deltaX *= ui.window.physicalSize.width; - deltaY *= ui.window.physicalSize.height; - break; - case domDeltaPixel: - default: - break; - } - - final List data = []; - _ensureMouseDeviceAdded(data, event.client.x, event.client.y, event.buttons, - event.timeStamp, _mouseDeviceId); - data.add(ui.PointerData( - change: ui.PointerChange.hover, - timeStamp: _eventTimeStampToDuration(event.timeStamp), - kind: ui.PointerDeviceKind.mouse, - signalKind: ui.PointerSignalKind.scroll, - 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, - scrollDeltaX: deltaX, - scrollDeltaY: deltaY, - )); - return data; -} - -void _addWheelEventListener(void listener(html.WheelEvent e)) { - final dynamic eventOptions = js_util.newObject(); - js_util.setProperty(eventOptions, 'passive', false); - js_util.callMethod(PointerBinding.instance.domRenderer.glassPaneElement, - 'addEventListener', [ - 'wheel', - js.allowInterop((html.WheelEvent event) => listener(event)), - eventOptions - ]); -} diff --git a/lib/web_ui/lib/src/engine/pointer_converter.dart b/lib/web_ui/lib/src/engine/pointer_converter.dart new file mode 100644 index 0000000000000000000000000000000000000000..c6b081558913e3b608821f2747154b32f818ed87 --- /dev/null +++ b/lib/web_ui/lib/src/engine/pointer_converter.dart @@ -0,0 +1,640 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of engine; + +class _PointerState { + _PointerState(this.x, this.y); + + /// The identifier used in framework hit test. + int get pointer => _pointer; + int _pointer; + static int _pointerCount = 0; + void startNewPointer() { + _pointerCount += 1; + _pointer = _pointerCount; + } + + bool down = false; + + double x; + double y; +} + +/// Converter to convert web pointer data into a form that framework can +/// understand. +/// +/// This converter calculates pointer location delta and pointer identifier for +/// each pointer. Both are required by framework to correctly trigger gesture +/// activity. It also attempts to sanitize pointer data input sequence by always +/// synthesizing an add pointer data prior to hover or down if it the pointer is +/// not previously added. +/// +/// For example: +/// before: +/// hover -> down -> move -> up +/// after: +/// add(synthesize) -> hover -> down -> move -> up +/// +/// before: +/// down -> move -> up +/// after: +/// add(synthesize) -> down -> move -> up +class PointerDataConverter { + PointerDataConverter(); + + // Map from browser pointer identifiers to PointerEvent pointer identifiers. + final Map _pointers = {}; + + /// Clears the existing pointer states. + /// + /// This method is invoked during hot reload to make sure we have a clean + /// converter after hot reload. + void clearPointerState() { + _pointers.clear(); + _PointerState._pointerCount = 0; + } + + _PointerState _ensureStateForPointer(int device, double x, double y) { + return _pointers.putIfAbsent( + device, + () => _PointerState(x, y), + ); + } + + ui.PointerData _generateCompletePointerData({ + Duration timeStamp, + ui.PointerChange change, + ui.PointerDeviceKind kind, + ui.PointerSignalKind signalKind, + int device, + double physicalX, + double physicalY, + int buttons, + bool obscured, + double pressure, + double pressureMin, + double pressureMax, + double distance, + double distanceMax, + double size, + double radiusMajor, + double radiusMinor, + double radiusMin, + double radiusMax, + double orientation, + double tilt, + int platformData, + double scrollDeltaX, + double scrollDeltaY, + }) { + assert(_pointers.containsKey(device)); + final _PointerState state = _pointers[device]; + final double deltaX = physicalX - state.x; + final double deltaY = physicalY - state.y; + state.x = physicalX; + state.y = physicalY; + return ui.PointerData( + timeStamp: timeStamp, + change: change, + kind: kind, + signalKind: signalKind, + device: device, + pointerIdentifier: state.pointer ?? 0, + physicalX: physicalX, + physicalY: physicalY, + physicalDeltaX: deltaX, + physicalDeltaY: deltaY, + buttons: buttons, + obscured: obscured, + pressure: pressure, + pressureMin: pressureMin, + pressureMax: pressureMax, + distance: distance, + distanceMax: distanceMax, + size: size, + radiusMajor: radiusMajor, + radiusMinor: radiusMinor, + radiusMin: radiusMin, + radiusMax: radiusMax, + orientation: orientation, + tilt: tilt, + platformData: platformData, + scrollDeltaX: scrollDeltaX, + scrollDeltaY: scrollDeltaY, + ); + } + + bool _locationHasChanged(int device, double physicalX, double physicalY) { + assert(_pointers.containsKey(device)); + final _PointerState state = _pointers[device]; + return state.x != physicalX || state.y != physicalY; + } + + ui.PointerData _synthesizePointerData({ + Duration timeStamp, + ui.PointerChange change, + ui.PointerDeviceKind kind, + int device, + double physicalX, + double physicalY, + int buttons, + bool obscured, + double pressure, + double pressureMin, + double pressureMax, + double distance, + double distanceMax, + double size, + double radiusMajor, + double radiusMinor, + double radiusMin, + double radiusMax, + double orientation, + double tilt, + int platformData, + double scrollDeltaX, + double scrollDeltaY, + }) { + assert(_pointers.containsKey(device)); + final _PointerState state = _pointers[device]; + final double deltaX = physicalX - state.x; + final double deltaY = physicalY - state.y; + state.x = physicalX; + state.y = physicalY; + return ui.PointerData( + timeStamp: timeStamp, + change: change, + kind: kind, + // All the pointer data except scroll should not have a signal kind, and + // there is no use case for synthetic scroll event. We should be + // safe to default it to ui.PointerSignalKind.none. + signalKind: ui.PointerSignalKind.none, + device: device, + pointerIdentifier: state.pointer ?? 0, + physicalX: physicalX, + physicalY: physicalY, + physicalDeltaX: deltaX, + physicalDeltaY: deltaY, + buttons: buttons, + obscured: obscured, + synthesized: true, + pressure: pressure, + pressureMin: pressureMin, + pressureMax: pressureMax, + distance: distance, + distanceMax: distanceMax, + size: size, + radiusMajor: radiusMajor, + radiusMinor: radiusMinor, + radiusMin: radiusMin, + radiusMax: radiusMax, + orientation: orientation, + tilt: tilt, + platformData: platformData, + scrollDeltaX: scrollDeltaX, + scrollDeltaY: scrollDeltaY, + ); + } + + /// Converts the given html pointer event metrics into a sequence of framework-compatible + /// pointer data and stores it into [result] + void convert( + List result, { + Duration timeStamp = Duration.zero, + ui.PointerChange change = ui.PointerChange.cancel, + ui.PointerDeviceKind kind = ui.PointerDeviceKind.touch, + ui.PointerSignalKind signalKind, + int device = 0, + double physicalX = 0.0, + double physicalY = 0.0, + int buttons = 0, + bool obscured = false, + double pressure = 0.0, + double pressureMin = 0.0, + double pressureMax = 0.0, + double distance = 0.0, + double distanceMax = 0.0, + double size = 0.0, + double radiusMajor = 0.0, + double radiusMinor = 0.0, + double radiusMin = 0.0, + double radiusMax = 0.0, + double orientation = 0.0, + double tilt = 0.0, + int platformData = 0, + double scrollDeltaX = 0.0, + double scrollDeltaY = 0.0, + }) { + assert(change != null); + if (signalKind == null || + signalKind == ui.PointerSignalKind.none) { + switch (change) { + case ui.PointerChange.add: + assert(!_pointers.containsKey(device)); + _ensureStateForPointer(device, physicalX, physicalY); + assert(!_locationHasChanged(device, physicalX, physicalY)); + result.add( + _generateCompletePointerData( + timeStamp: timeStamp, + change: change, + kind: kind, + signalKind: signalKind, + device: device, + physicalX: physicalX, + physicalY: physicalY, + buttons: buttons, + obscured: obscured, + pressure: pressure, + pressureMin: pressureMin, + pressureMax: pressureMax, + distance: distance, + distanceMax: distanceMax, + size: size, + radiusMajor: radiusMajor, + radiusMinor: radiusMinor, + radiusMin: radiusMin, + radiusMax: radiusMax, + orientation: orientation, + tilt: tilt, + platformData: platformData, + scrollDeltaX: scrollDeltaX, + scrollDeltaY: scrollDeltaY, + ) + ); + break; + case ui.PointerChange.hover: + final bool alreadyAdded = _pointers.containsKey(device); + final _PointerState state = _ensureStateForPointer( + device, physicalX, physicalY); + assert(!state.down); + if (!alreadyAdded) { + // Synthesizes an add pointer data. + result.add( + _synthesizePointerData( + timeStamp: timeStamp, + change: ui.PointerChange.add, + kind: kind, + device: device, + physicalX: physicalX, + physicalY: physicalY, + buttons: buttons, + obscured: obscured, + pressure: pressure, + pressureMin: pressureMin, + pressureMax: pressureMax, + distance: distance, + distanceMax: distanceMax, + size: size, + radiusMajor: radiusMajor, + radiusMinor: radiusMinor, + radiusMin: radiusMin, + radiusMax: radiusMax, + orientation: orientation, + tilt: tilt, + platformData: platformData, + scrollDeltaX: scrollDeltaX, + scrollDeltaY: scrollDeltaY, + ) + ); + } + result.add( + _generateCompletePointerData( + timeStamp: timeStamp, + change: change, + kind: kind, + signalKind: signalKind, + device: device, + physicalX: physicalX, + physicalY: physicalY, + buttons: buttons, + obscured: obscured, + pressure: pressure, + pressureMin: pressureMin, + pressureMax: pressureMax, + distance: distance, + distanceMax: distanceMax, + size: size, + radiusMajor: radiusMajor, + radiusMinor: radiusMinor, + radiusMin: radiusMin, + radiusMax: radiusMax, + orientation: orientation, + tilt: tilt, + platformData: platformData, + scrollDeltaX: scrollDeltaX, + scrollDeltaY: scrollDeltaY, + ) + ); + break; + case ui.PointerChange.down: + final bool alreadyAdded = _pointers.containsKey(device); + final _PointerState state = _ensureStateForPointer( + device, physicalX, physicalY); + assert(!state.down); + if (!alreadyAdded) { + // Synthesizes an add pointer data. + result.add( + _synthesizePointerData( + timeStamp: timeStamp, + change: ui.PointerChange.add, + kind: kind, + device: device, + physicalX: physicalX, + physicalY: physicalY, + buttons: buttons, + obscured: obscured, + pressure: pressure, + pressureMin: pressureMin, + pressureMax: pressureMax, + distance: distance, + distanceMax: distanceMax, + size: size, + radiusMajor: radiusMajor, + radiusMinor: radiusMinor, + radiusMin: radiusMin, + radiusMax: radiusMax, + orientation: orientation, + tilt: tilt, + platformData: platformData, + scrollDeltaX: scrollDeltaX, + scrollDeltaY: scrollDeltaY, + ) + ); + } + assert(!_locationHasChanged(device, physicalX, physicalY)); + state.startNewPointer(); + state.down = true; + result.add( + _generateCompletePointerData( + timeStamp: timeStamp, + change: change, + kind: kind, + signalKind: signalKind, + device: device, + physicalX: physicalX, + physicalY: physicalY, + buttons: buttons, + obscured: obscured, + pressure: pressure, + pressureMin: pressureMin, + pressureMax: pressureMax, + distance: distance, + distanceMax: distanceMax, + size: size, + radiusMajor: radiusMajor, + radiusMinor: radiusMinor, + radiusMin: radiusMin, + radiusMax: radiusMax, + orientation: orientation, + tilt: tilt, + platformData: platformData, + scrollDeltaX: scrollDeltaX, + scrollDeltaY: scrollDeltaY, + ) + ); + break; + case ui.PointerChange.move: + assert(_pointers.containsKey(device)); + final _PointerState state = _pointers[device]; + assert(state.down); + result.add( + _generateCompletePointerData( + timeStamp: timeStamp, + change: change, + kind: kind, + signalKind: signalKind, + device: device, + physicalX: physicalX, + physicalY: physicalY, + buttons: buttons, + obscured: obscured, + pressure: pressure, + pressureMin: pressureMin, + pressureMax: pressureMax, + distance: distance, + distanceMax: distanceMax, + size: size, + radiusMajor: radiusMajor, + radiusMinor: radiusMinor, + radiusMin: radiusMin, + radiusMax: radiusMax, + orientation: orientation, + tilt: tilt, + platformData: platformData, + scrollDeltaX: scrollDeltaX, + scrollDeltaY: scrollDeltaY, + ) + ); + break; + case ui.PointerChange.up: + case ui.PointerChange.cancel: + assert(_pointers.containsKey(device)); + final _PointerState state = _pointers[device]; + assert(state.down); + assert(!_locationHasChanged(device, physicalX, physicalY)); + state.down = false; + result.add( + _generateCompletePointerData( + timeStamp: timeStamp, + change: change, + kind: kind, + signalKind: signalKind, + device: device, + physicalX: physicalX, + physicalY: physicalY, + buttons: buttons, + obscured: obscured, + pressure: pressure, + pressureMin: pressureMin, + pressureMax: pressureMax, + distance: distance, + distanceMax: distanceMax, + size: size, + radiusMajor: radiusMajor, + radiusMinor: radiusMinor, + radiusMin: radiusMin, + radiusMax: radiusMax, + orientation: orientation, + tilt: tilt, + platformData: platformData, + scrollDeltaX: scrollDeltaX, + scrollDeltaY: scrollDeltaY, + ) + ); + break; + case ui.PointerChange.remove: + 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, + change: change, + kind: kind, + signalKind: signalKind, + device: device, + physicalX: physicalX, + physicalY: physicalY, + buttons: buttons, + obscured: obscured, + pressure: pressure, + pressureMin: pressureMin, + pressureMax: pressureMax, + distance: distance, + distanceMax: distanceMax, + size: size, + radiusMajor: radiusMajor, + radiusMinor: radiusMinor, + radiusMin: radiusMin, + radiusMax: radiusMax, + orientation: orientation, + tilt: tilt, + platformData: platformData, + scrollDeltaX: scrollDeltaX, + scrollDeltaY: scrollDeltaY, + ) + ); + break; + } + } else { + switch (signalKind) { + case ui.PointerSignalKind.scroll: + final bool alreadyAdded = _pointers.containsKey(device); + final _PointerState state = _ensureStateForPointer( + device, physicalX, physicalY); + if (!alreadyAdded) { + // Synthesizes an add pointer data. + result.add( + _synthesizePointerData( + timeStamp: timeStamp, + change: ui.PointerChange.add, + kind: kind, + device: device, + physicalX: physicalX, + physicalY: physicalY, + buttons: buttons, + obscured: obscured, + pressure: pressure, + pressureMin: pressureMin, + pressureMax: pressureMax, + distance: distance, + distanceMax: distanceMax, + size: size, + radiusMajor: radiusMajor, + radiusMinor: radiusMinor, + radiusMin: radiusMin, + radiusMax: radiusMax, + orientation: orientation, + tilt: tilt, + platformData: platformData, + scrollDeltaX: scrollDeltaX, + scrollDeltaY: scrollDeltaY, + ) + ); + } + if (_locationHasChanged(device, physicalX, physicalY)) { + // Synthesize a hover/move of the pointer to the scroll location + // before sending the scroll event, if necessary, so that clients + // don't have to worry about native ordering of hover and scroll + // events. + if (state.down) { + result.add( + _synthesizePointerData( + timeStamp: timeStamp, + change: ui.PointerChange.move, + kind: kind, + device: device, + physicalX: physicalX, + physicalY: physicalY, + buttons: buttons, + obscured: obscured, + pressure: pressure, + pressureMin: pressureMin, + pressureMax: pressureMax, + distance: distance, + distanceMax: distanceMax, + size: size, + radiusMajor: radiusMajor, + radiusMinor: radiusMinor, + radiusMin: radiusMin, + radiusMax: radiusMax, + orientation: orientation, + tilt: tilt, + platformData: platformData, + scrollDeltaX: scrollDeltaX, + scrollDeltaY: scrollDeltaY, + ) + ); + } else { + result.add( + _synthesizePointerData( + timeStamp: timeStamp, + change: ui.PointerChange.hover, + kind: kind, + device: device, + physicalX: physicalX, + physicalY: physicalY, + buttons: buttons, + obscured: obscured, + pressure: pressure, + pressureMin: pressureMin, + pressureMax: pressureMax, + distance: distance, + distanceMax: distanceMax, + size: size, + radiusMajor: radiusMajor, + radiusMinor: radiusMinor, + radiusMin: radiusMin, + radiusMax: radiusMax, + orientation: orientation, + tilt: tilt, + platformData: platformData, + scrollDeltaX: scrollDeltaX, + scrollDeltaY: scrollDeltaY, + ) + ); + } + } + result.add( + _generateCompletePointerData( + timeStamp: timeStamp, + change: change, + kind: kind, + signalKind: signalKind, + device: device, + physicalX: physicalX, + physicalY: physicalY, + buttons: buttons, + obscured: obscured, + pressure: pressure, + pressureMin: pressureMin, + pressureMax: pressureMax, + distance: distance, + distanceMax: distanceMax, + size: size, + radiusMajor: radiusMajor, + radiusMinor: radiusMinor, + radiusMin: radiusMin, + radiusMax: radiusMax, + orientation: orientation, + tilt: tilt, + platformData: platformData, + scrollDeltaX: scrollDeltaX, + scrollDeltaY: scrollDeltaY, + ) + ); + break; + case ui.PointerSignalKind.none: + assert(false); // This branch should already have 'none' filtered out. + break; + case ui.PointerSignalKind.unknown: + // Ignore unknown signals. + break; + } + } + } +} diff --git a/lib/web_ui/test/engine/pointer_binding_test.dart b/lib/web_ui/test/engine/pointer_binding_test.dart index 3638376cc6b8b7f45289b87fe59fec00476ee19f..cee6b8f16de141b70eff9e8497ebbc4c23684d72 100644 --- a/lib/web_ui/test/engine/pointer_binding_test.dart +++ b/lib/web_ui/test/engine/pointer_binding_test.dart @@ -56,7 +56,11 @@ void main() { })); expect(packets, hasLength(3)); - expect(packets[0].data[0].change, equals(ui.PointerChange.down)); + // 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)); }); @@ -78,10 +82,20 @@ void main() { })); expect(packets, hasLength(2)); - expect(packets[0].data[0].change, equals(ui.PointerChange.down)); + // 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[1].data[0].change, equals(ui.PointerChange.down)); + 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)); }); test('creates an add event if the first pointer activity is a hover', () { @@ -99,10 +113,11 @@ void main() { 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)); }); - test('does not create an add event if got a pointerdown', () { + test('does create an add event if got a pointerdown', () { List packets = []; ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) { packets.add(packet); @@ -114,9 +129,247 @@ void main() { })); expect(packets, hasLength(1)); - expect(packets.single.data, 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)); + }); + + test('does calculate delta and pointer identifier correctly', () { + 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)); + + 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)); + }); + + test('does synthesize add or hover or more for scroll', () { + List packets = []; + ui.window.onPointerDataPacket = (ui.PointerDataPacket packet) { + packets.add(packet); + }; + + glassPane.dispatchEvent(html.WheelEvent('wheel', + button: 1, + clientX: 10, + clientY: 10, + deltaX: 10, + deltaY: 10, + )); + + glassPane.dispatchEvent(html.WheelEvent('wheel', + button: 1, + clientX: 20, + clientY: 50, + deltaX: 10, + deltaY: 10, + )); + + glassPane.dispatchEvent(html.PointerEvent('pointerdown', { + 'pointerId': -1, + 'button': 1, + 'clientX': 20.0, + 'clientY': 50.0, + })); + + glassPane.dispatchEvent(html.WheelEvent('wheel', + button: 1, + clientX: 30, + clientY: 60, + deltaX: 10, + deltaY: 10, + )); + + expect(packets, hasLength(4)); + + // 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].signalKind, equals(ui.PointerSignalKind.scroll)); + 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)); + + // A hover will be synthesized. + expect(packets[1].data, hasLength(2)); + 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(true)); + expect(packets[1].data[0].physicalX, equals(20.0)); + expect(packets[1].data[0].physicalY, equals(50.0)); + expect(packets[1].data[0].physicalDeltaX, equals(10.0)); + expect(packets[1].data[0].physicalDeltaY, equals(40.0)); + + expect(packets[1].data[1].change, equals(ui.PointerChange.hover)); + expect(packets[1].data[1].signalKind, equals(ui.PointerSignalKind.scroll)); + expect(packets[1].data[1].pointerIdentifier, equals(0)); + expect(packets[1].data[1].synthesized, equals(false)); + expect(packets[1].data[1].physicalX, equals(20.0)); + expect(packets[1].data[1].physicalY, equals(50.0)); + expect(packets[1].data[1].physicalDeltaX, equals(0.0)); + expect(packets[1].data[1].physicalDeltaY, equals(0.0)); + + // 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].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(50.0)); + expect(packets[2].data[0].physicalDeltaX, equals(0.0)); + expect(packets[2].data[0].physicalDeltaY, equals(0.0)); + + // A move will be synthesized instead of hover because the button is currently down. + expect(packets[3].data, hasLength(2)); + 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(true)); + expect(packets[3].data[0].physicalX, equals(30.0)); + expect(packets[3].data[0].physicalY, equals(60.0)); + expect(packets[3].data[0].physicalDeltaX, equals(10.0)); + expect(packets[3].data[0].physicalDeltaY, equals(10.0)); - expect(packets.single.data[0].change, equals(ui.PointerChange.down)); + expect(packets[3].data[1].change, equals(ui.PointerChange.hover)); + expect(packets[3].data[1].signalKind, equals(ui.PointerSignalKind.scroll)); + expect(packets[3].data[1].pointerIdentifier, equals(1)); + expect(packets[3].data[1].synthesized, equals(false)); + expect(packets[3].data[1].physicalX, equals(30.0)); + 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)); }); }); }