From 728143665ebb6e58b3642862d3b109bc4a12d459 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Thu, 16 Nov 2023 10:05:54 -0500 Subject: [PATCH 1/5] [web] PointerBinding per view --- lib/web_ui/lib/src/engine/embedder.dart | 10 - lib/web_ui/lib/src/engine/initialization.dart | 2 + .../lib/src/engine/keyboard_binding.dart | 5 + .../lib/src/engine/platform_dispatcher.dart | 2 +- .../lib/src/engine/pointer_binding.dart | 168 ++++++++------- .../event_position_helper.dart | 10 +- .../lib/src/engine/pointer_converter.dart | 150 ++++++++----- .../lib/src/engine/semantics/tappable.dart | 2 +- lib/web_ui/lib/src/engine/window.dart | 5 + .../initialization/services_vs_ui_test.dart | 6 +- .../event_position_helper_test.dart | 27 ++- .../test/engine/pointer_binding_test.dart | 199 +++++++++--------- 12 files changed, 318 insertions(+), 268 deletions(-) diff --git a/lib/web_ui/lib/src/engine/embedder.dart b/lib/web_ui/lib/src/engine/embedder.dart index 039758ec0c835..04455eff9611d 100644 --- a/lib/web_ui/lib/src/engine/embedder.dart +++ b/lib/web_ui/lib/src/engine/embedder.dart @@ -8,9 +8,7 @@ import '../engine.dart' show buildMode, renderer; import 'browser_detection.dart'; import 'configuration.dart'; import 'dom.dart'; -import 'keyboard_binding.dart'; import 'platform_dispatcher.dart'; -import 'pointer_binding.dart'; import 'text_editing/text_editing.dart'; import 'view_embedder/style_manager.dart'; import 'window.dart'; @@ -67,14 +65,6 @@ class FlutterViewEmbedder { renderer.reset(this); - // TODO(mdebbar): Move these to `engine/initialization.dart`. - - KeyboardBinding.initInstance(); - PointerBinding.initInstance( - _flutterViewElement, - KeyboardBinding.instance!.converter, - ); - window.onResize.listen(_metricsDidChange); } diff --git a/lib/web_ui/lib/src/engine/initialization.dart b/lib/web_ui/lib/src/engine/initialization.dart index c8badf914ccf5..ea62ad16ebbc0 100644 --- a/lib/web_ui/lib/src/engine/initialization.dart +++ b/lib/web_ui/lib/src/engine/initialization.dart @@ -218,6 +218,8 @@ Future initializeEngineUi() async { _initializationState = DebugEngineInitializationState.initializingUi; RawKeyboard.initialize(onMacOs: operatingSystem == OperatingSystem.macOs); + KeyboardBinding.initInstance(); + if (!configuration.multiViewEnabled) { ensureImplicitViewInitialized(hostElement: configuration.hostElement); ensureFlutterViewEmbedderInitialized(); diff --git a/lib/web_ui/lib/src/engine/keyboard_binding.dart b/lib/web_ui/lib/src/engine/keyboard_binding.dart index c12d1a36a9f8b..6a5ea194b5255 100644 --- a/lib/web_ui/lib/src/engine/keyboard_binding.dart +++ b/lib/web_ui/lib/src/engine/keyboard_binding.dart @@ -258,6 +258,7 @@ class KeyboardConverter { bool _disposed = false; void dispose() { _disposed = true; + clearPressedKeys(); } // On macOS, CapsLock behaves differently in that, a keydown event occurs when @@ -699,4 +700,8 @@ class KeyboardConverter { bool keyIsPressed(int physical) { return _pressingRecords.containsKey(physical); } + + void clearPressedKeys() { + _pressingRecords.clear(); + } } diff --git a/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/lib/web_ui/lib/src/engine/platform_dispatcher.dart index ff0951cf1c449..b6167e32dc791 100644 --- a/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -634,7 +634,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { const StandardMessageCodec codec = StandardMessageCodec(); // TODO(yjbanov): Dispatch the announcement to the correct view? // https://github.com/flutter/flutter/issues/137445 - implicitView!.accessibilityAnnouncements.handleMessage(codec, data); + implicitView?.accessibilityAnnouncements.handleMessage(codec, data); replyToPlatformMessage(callback, codec.encodeMessage(true)); return; diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index bb2f1421d7b8b..082f36a028523 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -18,6 +18,7 @@ import 'pointer_binding/event_position_helper.dart'; import 'pointer_converter.dart'; import 'safe_browser_api.dart'; import 'semantics.dart'; +import 'window.dart'; /// Set this flag to true to log all the browser events. const bool _debugLogPointerEvents = false; @@ -73,72 +74,72 @@ int convertButtonToButtons(int button) { /// Wrapping the Safari iOS workaround that adds a dummy event listener /// More info about the issue and workaround: https://github.com/flutter/flutter/issues/70858 class SafariPointerEventWorkaround { - static SafariPointerEventWorkaround instance = SafariPointerEventWorkaround(); + DomEventListener? _listener; void workAroundMissingPointerEvents() { - domDocument.addEventListener('touchstart', createDomEventListener((DomEvent event) {})); + _listener = createDomEventListener((_) {}); + domDocument.addEventListener('touchstart', _listener); + } + + void dispose() { + if (_listener != null) { + domDocument.removeEventListener('touchstart', _listener); + } } } class PointerBinding { PointerBinding( - this.flutterViewElement, - this._keyboardConverter, [ - this._detector = const PointerSupportDetector(), - ]) : _pointerDataConverter = PointerDataConverter() { + this.view, { + PointerSupportDetector detector = const PointerSupportDetector(), + SafariPointerEventWorkaround? safariWorkaround, + }) : _pointerDataConverter = PointerDataConverter(), + _detector = detector { if (isIosSafari) { - SafariPointerEventWorkaround.instance.workAroundMissingPointerEvents(); + _safariWorkaround = safariWorkaround ?? SafariPointerEventWorkaround(); + _safariWorkaround!.workAroundMissingPointerEvents(); } _adapter = _createAdapter(); + assert(() { + registerHotRestartListener(dispose); + return true; + }()); } - /// The singleton instance of this object. - static PointerBinding? get instance => _instance; - static PointerBinding? _instance; - - static void initInstance(DomElement flutterViewElement, KeyboardConverter keyboardConverter) { - if (_instance == null) { - _instance = PointerBinding(flutterViewElement, keyboardConverter); - assert(() { - registerHotRestartListener(_instance!.dispose); - return true; - }()); - } + static final ClickDebouncer clickDebouncer = ClickDebouncer(); + + /// Resets global pointer state that's not tied to any single [PointerBinding] + /// instance. + static void resetGlobalState() { + clickDebouncer.reset(); + PointerDataConverter.globalPointerState.reset(); } - final ClickDebouncer clickDebouncer = ClickDebouncer(); + SafariPointerEventWorkaround? _safariWorkaround; /// Performs necessary clean up for PointerBinding including removing event listeners /// and clearing the existing pointer state void dispose() { - _adapter.clearListeners(); - _pointerDataConverter.clearPointerState(); - clickDebouncer.reset(); + _adapter.dispose(); + _safariWorkaround?.dispose(); } - final DomElement flutterViewElement; + final EngineFlutterView view; + DomElement get rootElement => view.dom.rootElement; final PointerSupportDetector _detector; final PointerDataConverter _pointerDataConverter; - KeyboardConverter _keyboardConverter; + KeyboardConverter? _keyboardConverter = KeyboardBinding.instance?.converter; late _BaseAdapter _adapter; @visibleForTesting - void debugReset() { - _adapter.clearListeners(); - _adapter = _createAdapter(); - _pointerDataConverter.clearPointerState(); - } - - @visibleForTesting - void debugOverrideKeyboardConverter(KeyboardConverter keyboardConverter) { + void debugOverrideKeyboardConverter(KeyboardConverter? keyboardConverter) { _keyboardConverter = keyboardConverter; - debugReset(); } _BaseAdapter _createAdapter() { if (_detector.hasPointerEvents) { - return _PointerAdapter(clickDebouncer.onPointerData, flutterViewElement, _pointerDataConverter, _keyboardConverter); + return _PointerAdapter(this); } throw UnsupportedError( 'This browser does not support pointer events which ' @@ -183,6 +184,13 @@ typedef DebounceState = ({ /// /// This mechanism is in place to deal with https://github.com/flutter/flutter/issues/130162. class ClickDebouncer { + ClickDebouncer() { + assert(() { + registerHotRestartListener(reset); + return true; + }()); + } + DebounceState? _state; @visibleForTesting @@ -468,29 +476,39 @@ class _Listener { /// Common functionality that's shared among adapters. abstract class _BaseAdapter { - _BaseAdapter( - this._callback, - this.flutterViewElement, - this._pointerDataConverter, - this._keyboardConverter, - ) { + _BaseAdapter(this._owner) { setup(); } + final PointerBinding _owner; + + EngineFlutterView get _view => _owner.view; + _PointerDataCallback get _callback => PointerBinding.clickDebouncer.onPointerData; + PointerDataConverter get _pointerDataConverter => _owner._pointerDataConverter; + KeyboardConverter? get _keyboardConverter => _owner._keyboardConverter; + final List<_Listener> _listeners = <_Listener>[]; - final DomElement flutterViewElement; - final _PointerDataCallback _callback; - final PointerDataConverter _pointerDataConverter; - final KeyboardConverter _keyboardConverter; DomWheelEvent? _lastWheelEvent; bool _lastWheelEventWasTrackpad = false; + DomElement get _viewTarget => _view.dom.rootElement; + DomEventTarget get _globalTarget { + // When the Flutter app owns the full page, we want to listen on window for + // some events. Otherwise, we just want to listen on the root element of the + // view. + // TODO(mdebbar): Is there a better way of doing this? + if (_view == EnginePlatformDispatcher.instance.implicitView) { + return domWindow; + } + return _viewTarget; + } + /// Each subclass is expected to override this method to attach its own event /// listeners and convert events into pointer events. void setup(); - /// Remove all active event listeners. - void clearListeners() { + /// Cleans up all event listeners attached by this adapter. + void dispose() { for (final _Listener listener in _listeners) { listener.unregister(); } @@ -499,7 +517,7 @@ abstract class _BaseAdapter { /// Adds a listener for the given [eventName] to [target]. /// - /// Generally speaking, down and leave events should use [flutterViewElement] + /// Generally speaking, down and leave events should use [_rootElement] /// as the [target], while move and up events should use [domWindow] /// instead, because the browser doesn't fire the latter two for DOM elements /// when the pointer is outside the window. @@ -512,7 +530,7 @@ abstract class _BaseAdapter { if (_debugLogPointerEvents) { if (domInstanceOfString(event, 'PointerEvent')) { final DomPointerEvent pointerEvent = event as DomPointerEvent; - final ui.Offset offset = computeEventOffsetToTarget(event, flutterViewElement); + final ui.Offset offset = computeEventOffsetToTarget(event, _view); print('${pointerEvent.type} ' '${offset.dx.toStringAsFixed(1)},' '${offset.dy.toStringAsFixed(1)}'); @@ -636,36 +654,37 @@ mixin _WheelEventListenerMixin on _BaseAdapter { deltaX *= _defaultScrollLineHeight!; deltaY *= _defaultScrollLineHeight!; case domDeltaPage: - deltaX *= ui.window.physicalSize.width; - deltaY *= ui.window.physicalSize.height; + deltaX *= _view.physicalSize.width; + deltaY *= _view.physicalSize.height; case domDeltaPixel: if (operatingSystem == OperatingSystem.macOs && (isSafari || isFirefox)) { // Safari and Firefox seem to report delta in logical pixels while // Chrome uses physical pixels. - deltaX *= ui.window.devicePixelRatio; - deltaY *= ui.window.devicePixelRatio; + deltaX *= _view.devicePixelRatio; + deltaY *= _view.devicePixelRatio; } default: break; } final List data = []; - final ui.Offset offset = computeEventOffsetToTarget(event, flutterViewElement); + final ui.Offset offset = computeEventOffsetToTarget(event, _view); bool ignoreCtrlKey = false; if (operatingSystem == OperatingSystem.macOs) { - ignoreCtrlKey = (KeyboardBinding.instance?.converter.keyIsPressed(kPhysicalControlLeft) ?? false) || - (KeyboardBinding.instance?.converter.keyIsPressed(kPhysicalControlRight) ?? false); + ignoreCtrlKey = (_keyboardConverter?.keyIsPressed(kPhysicalControlLeft) ?? false) || + (_keyboardConverter?.keyIsPressed(kPhysicalControlRight) ?? false); } if (event.ctrlKey && !ignoreCtrlKey) { _pointerDataConverter.convert( data, + viewId: _view.viewId, change: ui.PointerChange.hover, timeStamp: _BaseAdapter._eventTimeStampToDuration(event.timeStamp!), kind: kind, signalKind: ui.PointerSignalKind.scale, device: deviceId, - physicalX: offset.dx * ui.window.devicePixelRatio, - physicalY: offset.dy * ui.window.devicePixelRatio, + physicalX: offset.dx * _view.devicePixelRatio, + physicalY: offset.dy * _view.devicePixelRatio, buttons: event.buttons!.toInt(), pressure: 1.0, pressureMax: 1.0, @@ -674,13 +693,14 @@ mixin _WheelEventListenerMixin on _BaseAdapter { } else { _pointerDataConverter.convert( data, + viewId: _view.viewId, change: ui.PointerChange.hover, timeStamp: _BaseAdapter._eventTimeStampToDuration(event.timeStamp!), kind: kind, signalKind: ui.PointerSignalKind.scroll, device: deviceId, - physicalX: offset.dx * ui.window.devicePixelRatio, - physicalY: offset.dy * ui.window.devicePixelRatio, + physicalX: offset.dx * _view.devicePixelRatio, + physicalY: offset.dy * _view.devicePixelRatio, buttons: event.buttons!.toInt(), pressure: 1.0, pressureMax: 1.0, @@ -696,7 +716,7 @@ mixin _WheelEventListenerMixin on _BaseAdapter { void _addWheelEventListener(DartDomEventListener handler) { _listeners.add(_Listener.register( event: 'wheel', - target: flutterViewElement, + target: _viewTarget, handler: handler, passive: false, )); @@ -884,12 +904,7 @@ typedef _PointerEventListener = dynamic Function(DomPointerEvent event); /// /// For the difference between MouseEvent and PointerEvent, see _MouseAdapter. class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { - _PointerAdapter( - super.callback, - super.flutterViewElement, - super.pointerDataConverter, - super.keyboardConverter, - ); + _PointerAdapter(super.owner); final Map _sanitizers = {}; @@ -931,7 +946,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { } void _checkModifiersState(DomPointerEvent event) { - _keyboardConverter.synthesizeModifiersIfNeeded( + _keyboardConverter?.synthesizeModifiersIfNeeded( event.getModifierState('Alt'), event.getModifierState('Control'), event.getModifierState('Meta'), @@ -942,7 +957,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { @override void setup() { - _addPointerEventListener(flutterViewElement, 'pointerdown', (DomPointerEvent event) { + _addPointerEventListener(_viewTarget, 'pointerdown', (DomPointerEvent event) { final int device = _getPointerId(event); final List pointerData = []; final _ButtonSanitizer sanitizer = _ensureSanitizer(device); @@ -961,7 +976,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { }); // Why `domWindow` you ask? See this fiddle: https://jsfiddle.net/ditman/7towxaqp - _addPointerEventListener(domWindow, 'pointermove', (DomPointerEvent event) { + _addPointerEventListener(_globalTarget, 'pointermove', (DomPointerEvent event) { final int device = _getPointerId(event); final _ButtonSanitizer sanitizer = _ensureSanitizer(device); final List pointerData = []; @@ -977,7 +992,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { _callback(event, pointerData); }); - _addPointerEventListener(flutterViewElement, 'pointerleave', (DomPointerEvent event) { + _addPointerEventListener(_viewTarget, 'pointerleave', (DomPointerEvent event) { final int device = _getPointerId(event); final _ButtonSanitizer sanitizer = _ensureSanitizer(device); final List pointerData = []; @@ -989,7 +1004,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { }, checkModifiers: false); // TODO(dit): This must happen in the flutterViewElement, https://github.com/flutter/flutter/issues/116561 - _addPointerEventListener(domWindow, 'pointerup', (DomPointerEvent event) { + _addPointerEventListener(_globalTarget, 'pointerup', (DomPointerEvent event) { final int device = _getPointerId(event); if (_hasSanitizer(device)) { final List pointerData = []; @@ -1006,7 +1021,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { // A browser fires cancel event if it concludes the pointer will no longer // be able to generate events (example: device is deactivated) - _addPointerEventListener(flutterViewElement, 'pointercancel', (DomPointerEvent event) { + _addPointerEventListener(_viewTarget, 'pointercancel', (DomPointerEvent event) { final int device = _getPointerId(event); if (_hasSanitizer(device)) { final List pointerData = []; @@ -1033,16 +1048,17 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { final double tilt = _computeHighestTilt(event); final Duration timeStamp = _BaseAdapter._eventTimeStampToDuration(event.timeStamp!); final num? pressure = event.pressure; - final ui.Offset offset = computeEventOffsetToTarget(event, flutterViewElement); + final ui.Offset offset = computeEventOffsetToTarget(event, _view); _pointerDataConverter.convert( data, + viewId: _view.viewId, change: details.change, timeStamp: timeStamp, kind: kind, signalKind: ui.PointerSignalKind.none, device: _getPointerId(event), - physicalX: offset.dx * ui.window.devicePixelRatio, - physicalY: offset.dy * ui.window.devicePixelRatio, + physicalX: offset.dx * _view.devicePixelRatio, + physicalY: offset.dy * _view.devicePixelRatio, buttons: details.buttons, pressure: pressure == null ? 0.0 : pressure.toDouble(), pressureMax: 1.0, diff --git a/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart b/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart index 2331a05b09c3f..3770ad23a687e 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart @@ -7,10 +7,10 @@ import 'dart:typed_data'; import 'package:ui/ui.dart' as ui show Offset; import '../dom.dart'; -import '../platform_dispatcher.dart'; import '../semantics.dart' show EngineSemanticsOwner; import '../text_editing/text_editing.dart'; import '../vector_math.dart'; +import '../window.dart'; /// Returns an [ui.Offset] of the position of [event], relative to the position of [actualTarget]. /// @@ -23,17 +23,15 @@ import '../vector_math.dart'; /// /// It also takes into account semantics being enabled to fix the case where /// offsetX, offsetY == 0 (TalkBack events). -ui.Offset computeEventOffsetToTarget(DomMouseEvent event, DomElement actualTarget) { +ui.Offset computeEventOffsetToTarget(DomMouseEvent event, EngineFlutterView view) { + final DomElement actualTarget = view.dom.rootElement; // On a TalkBack event if (EngineSemanticsOwner.instance.semanticsEnabled && event.offsetX == 0 && event.offsetY == 0) { return _computeOffsetForTalkbackEvent(event, actualTarget); } // On one of our text-editing nodes - // TODO(mdebbar): There could be multiple views with multiple text editing hosts. - // https://github.com/flutter/flutter/issues/137344 - final DomElement textEditingHost = EnginePlatformDispatcher.instance.implicitView!.dom.textEditingHost; - final bool isInput = textEditingHost.contains(event.target! as DomNode); + final bool isInput = view.dom.textEditingHost.contains(event.target! as DomNode); if (isInput) { final EditableTextGeometry? inputGeometry = textEditing.strategy.geometry; if (inputGeometry != null) { diff --git a/lib/web_ui/lib/src/engine/pointer_converter.dart b/lib/web_ui/lib/src/engine/pointer_converter.dart index 5a04c23fa936e..54fdef946b11f 100644 --- a/lib/web_ui/lib/src/engine/pointer_converter.dart +++ b/lib/web_ui/lib/src/engine/pointer_converter.dart @@ -4,10 +4,13 @@ import 'package:ui/ui.dart' as ui; +import 'initialization.dart'; + const bool _debugLogPointerConverter = false; -class _PointerState { - _PointerState(this.x, this.y); +/// The state of the pointer of a specific device (e.g. finger, mouse). +class _PointerDeviceState { + _PointerDeviceState(this.x, this.y); /// The identifier used in framework hit test. int? get pointer => _pointer; @@ -22,6 +25,43 @@ class _PointerState { double y; } +class _GlobalPointerState { + _GlobalPointerState() { + assert(() { + registerHotRestartListener(reset); + return true; + }()); + } + + // Map from browser pointer identifiers to PointerEvent pointer identifiers. + final Map pointers = {}; + + /// This field is used to keep track of button state. + /// + /// To normalize pointer events, when we receive pointer down followed by + /// pointer up, we synthesize a move event. To make sure that button state + /// is correct for move regardless of button state at the time of up event + /// we store it on down,hover and move events. + int activeButtons = 0; + + _PointerDeviceState ensurePointerDeviceState(int device, double x, double y) { + return pointers.putIfAbsent( + device, + () => _PointerDeviceState(x, y), + ); + } + + /// Resets all pointer states. + /// + /// This method is invoked during hot reload to make sure we have a clean + /// converter after hot reload. + void reset() { + pointers.clear(); + _PointerDeviceState._pointerCount = 0; + activeButtons = 0; + } +} + /// Converter to convert web pointer data into a form that framework can /// understand. /// @@ -44,35 +84,14 @@ class _PointerState { class PointerDataConverter { PointerDataConverter(); - // Map from browser pointer identifiers to PointerEvent pointer identifiers. - final Map _pointers = {}; - - /// This field is used to keep track of button state. - /// - /// To normalize pointer events, when we receive pointer down followed by - /// pointer up, we synthesize a move event. To make sure that button state - /// is correct for move regardless of button state at the time of up event - /// we store it on down,hover and move events. - int _activeButtons = 0; - - /// 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; - _activeButtons = 0; - } - - _PointerState _ensureStateForPointer(int device, double x, double y) { - return _pointers.putIfAbsent( - device, - () => _PointerState(x, y), - ); - } + // This is made static because the state of pointer devices is global. This + // matches how the framework currently handles the state of pointer devices. + // + // See: https://github.com/flutter/flutter/blob/023e5addaa6e8e294a200cf754afaa1656f14aa6/packages/flutter/lib/src/rendering/binding.dart#L47-L47 + static final _GlobalPointerState globalPointerState = _GlobalPointerState(); ui.PointerData _generateCompletePointerData({ + required int viewId, required Duration timeStamp, required ui.PointerChange change, required ui.PointerDeviceKind kind, @@ -99,13 +118,14 @@ class PointerDataConverter { required double scrollDeltaY, required double scale, }) { - assert(_pointers.containsKey(device)); - final _PointerState state = _pointers[device]!; + assert(globalPointerState.pointers.containsKey(device)); + final _PointerDeviceState state = globalPointerState.pointers[device]!; final double deltaX = physicalX - state.x; final double deltaY = physicalY - state.y; state.x = physicalX; state.y = physicalY; return ui.PointerData( + viewId: viewId, timeStamp: timeStamp, change: change, kind: kind, @@ -138,12 +158,13 @@ class PointerDataConverter { } bool _locationHasChanged(int device, double physicalX, double physicalY) { - assert(_pointers.containsKey(device)); - final _PointerState state = _pointers[device]!; + assert(globalPointerState.pointers.containsKey(device)); + final _PointerDeviceState state = globalPointerState.pointers[device]!; return state.x != physicalX || state.y != physicalY; } ui.PointerData _synthesizePointerData({ + required int viewId, required Duration timeStamp, required ui.PointerChange change, required ui.PointerDeviceKind kind, @@ -169,13 +190,14 @@ class PointerDataConverter { required double scrollDeltaY, required double scale, }) { - assert(_pointers.containsKey(device)); - final _PointerState state = _pointers[device]!; + assert(globalPointerState.pointers.containsKey(device)); + final _PointerDeviceState state = globalPointerState.pointers[device]!; final double deltaX = physicalX - state.x; final double deltaY = physicalY - state.y; state.x = physicalX; state.y = physicalY; return ui.PointerData( + viewId: viewId, timeStamp: timeStamp, change: change, kind: kind, @@ -215,6 +237,7 @@ class PointerDataConverter { /// pointer data and stores it into [result] void convert( List result, { + required int viewId, Duration timeStamp = Duration.zero, ui.PointerChange change = ui.PointerChange.cancel, ui.PointerDeviceKind kind = ui.PointerDeviceKind.touch, @@ -242,18 +265,19 @@ class PointerDataConverter { double scale = 1.0, }) { if (_debugLogPointerConverter) { - print('>> device=$device change=$change buttons=$buttons'); + print('>> view=$viewId device=$device change=$change buttons=$buttons'); } final bool isDown = buttons != 0; if (signalKind == null || signalKind == ui.PointerSignalKind.none) { switch (change) { case ui.PointerChange.add: - assert(!_pointers.containsKey(device)); - _ensureStateForPointer(device, physicalX, physicalY); + assert(!globalPointerState.pointers.containsKey(device)); + globalPointerState.ensurePointerDeviceState(device, physicalX, physicalY); assert(!_locationHasChanged(device, physicalX, physicalY)); result.add( _generateCompletePointerData( + viewId: viewId, timeStamp: timeStamp, change: change, kind: kind, @@ -282,13 +306,14 @@ class PointerDataConverter { ) ); case ui.PointerChange.hover: - final bool alreadyAdded = _pointers.containsKey(device); - _ensureStateForPointer(device, physicalX, physicalY); + final bool alreadyAdded = globalPointerState.pointers.containsKey(device); + globalPointerState.ensurePointerDeviceState(device, physicalX, physicalY); assert(!isDown); if (!alreadyAdded) { // Synthesizes an add pointer data. result.add( _synthesizePointerData( + viewId: viewId, timeStamp: timeStamp, change: ui.PointerChange.add, kind: kind, @@ -318,6 +343,7 @@ class PointerDataConverter { } result.add( _generateCompletePointerData( + viewId: viewId, timeStamp: timeStamp, change: change, kind: kind, @@ -345,17 +371,18 @@ class PointerDataConverter { scale: scale, ) ); - _activeButtons = buttons; + globalPointerState.activeButtons = buttons; case ui.PointerChange.down: - final bool alreadyAdded = _pointers.containsKey(device); - final _PointerState state = _ensureStateForPointer( - device, physicalX, physicalY); + final bool alreadyAdded = globalPointerState.pointers.containsKey(device); + final _PointerDeviceState state = globalPointerState.ensurePointerDeviceState( + device, physicalX, physicalY); assert(isDown); state.startNewPointer(); if (!alreadyAdded) { // Synthesizes an add pointer data. result.add( _synthesizePointerData( + viewId: viewId, timeStamp: timeStamp, change: ui.PointerChange.add, kind: kind, @@ -389,6 +416,7 @@ class PointerDataConverter { // sending the down event, if necessary. result.add( _synthesizePointerData( + viewId: viewId, timeStamp: timeStamp, change: ui.PointerChange.hover, kind: kind, @@ -418,6 +446,7 @@ class PointerDataConverter { } result.add( _generateCompletePointerData( + viewId: viewId, timeStamp: timeStamp, change: change, kind: kind, @@ -445,12 +474,13 @@ class PointerDataConverter { scale: scale, ) ); - _activeButtons = buttons; + globalPointerState.activeButtons = buttons; case ui.PointerChange.move: - assert(_pointers.containsKey(device)); + assert(globalPointerState.pointers.containsKey(device)); assert(isDown); result.add( _generateCompletePointerData( + viewId: viewId, timeStamp: timeStamp, change: change, kind: kind, @@ -478,11 +508,11 @@ class PointerDataConverter { scale: scale, ) ); - _activeButtons = buttons; + globalPointerState.activeButtons = buttons; case ui.PointerChange.up: case ui.PointerChange.cancel: - assert(_pointers.containsKey(device)); - final _PointerState state = _pointers[device]!; + assert(globalPointerState.pointers.containsKey(device)); + final _PointerDeviceState state = globalPointerState.pointers[device]!; assert(!isDown); // Cancel events can have different coordinates due to various // reasons (window lost focus which is accompanied by window @@ -498,13 +528,14 @@ class PointerDataConverter { // sending the up event, if necessary. result.add( _synthesizePointerData( + viewId: viewId, timeStamp: timeStamp, change: ui.PointerChange.move, kind: kind, device: device, physicalX: physicalX, physicalY: physicalY, - buttons: _activeButtons, + buttons: globalPointerState.activeButtons, obscured: obscured, pressure: pressure, pressureMin: pressureMin, @@ -527,6 +558,7 @@ class PointerDataConverter { } result.add( _generateCompletePointerData( + viewId: viewId, timeStamp: timeStamp, change: change, kind: kind, @@ -560,6 +592,7 @@ class PointerDataConverter { // over (i.e. when "up" or "cancel" is received). result.add( _synthesizePointerData( + viewId: viewId, timeStamp: timeStamp, change: ui.PointerChange.remove, kind: kind, @@ -586,14 +619,15 @@ class PointerDataConverter { scale: scale, ) ); - _pointers.remove(device); + globalPointerState.pointers.remove(device); } case ui.PointerChange.remove: - assert(_pointers.containsKey(device)); - final _PointerState state = _pointers[device]!; + assert(globalPointerState.pointers.containsKey(device)); + final _PointerDeviceState state = globalPointerState.pointers[device]!; assert(!isDown); result.add( _generateCompletePointerData( + viewId: viewId, timeStamp: timeStamp, change: change, kind: kind, @@ -621,7 +655,7 @@ class PointerDataConverter { scale: scale, ) ); - _pointers.remove(device); + globalPointerState.pointers.remove(device); case ui.PointerChange.panZoomStart: case ui.PointerChange.panZoomUpdate: case ui.PointerChange.panZoomEnd: @@ -633,12 +667,13 @@ class PointerDataConverter { case ui.PointerSignalKind.scroll: case ui.PointerSignalKind.scrollInertiaCancel: case ui.PointerSignalKind.scale: - final bool alreadyAdded = _pointers.containsKey(device); - _ensureStateForPointer(device, physicalX, physicalY); + final bool alreadyAdded = globalPointerState.pointers.containsKey(device); + globalPointerState.ensurePointerDeviceState(device, physicalX, physicalY); if (!alreadyAdded) { // Synthesizes an add pointer data. result.add( _synthesizePointerData( + viewId: viewId, timeStamp: timeStamp, change: ui.PointerChange.add, kind: kind, @@ -674,6 +709,7 @@ class PointerDataConverter { if (isDown) { result.add( _synthesizePointerData( + viewId: viewId, timeStamp: timeStamp, change: ui.PointerChange.move, kind: kind, @@ -703,6 +739,7 @@ class PointerDataConverter { } else { result.add( _synthesizePointerData( + viewId: viewId, timeStamp: timeStamp, change: ui.PointerChange.hover, kind: kind, @@ -733,6 +770,7 @@ class PointerDataConverter { } result.add( _generateCompletePointerData( + viewId: viewId, timeStamp: timeStamp, change: change, kind: kind, diff --git a/lib/web_ui/lib/src/engine/semantics/tappable.dart b/lib/web_ui/lib/src/engine/semantics/tappable.dart index d3612ae80348c..e0cdfbb2ebf37 100644 --- a/lib/web_ui/lib/src/engine/semantics/tappable.dart +++ b/lib/web_ui/lib/src/engine/semantics/tappable.dart @@ -33,7 +33,7 @@ class Tappable extends RoleManager { Tappable(SemanticsObject semanticsObject, PrimaryRoleManager owner) : super(Role.tappable, semanticsObject, owner) { _clickListener = createDomEventListener((DomEvent click) { - PointerBinding.instance!.clickDebouncer.onClick( + PointerBinding.clickDebouncer.onClick( click, semanticsObject.id, _isListening, diff --git a/lib/web_ui/lib/src/engine/window.dart b/lib/web_ui/lib/src/engine/window.dart index 9b8d3f1037a1c..b57bfee3073d6 100644 --- a/lib/web_ui/lib/src/engine/window.dart +++ b/lib/web_ui/lib/src/engine/window.dart @@ -17,6 +17,7 @@ import 'mouse/cursor.dart'; import 'navigation/history.dart'; import 'platform_dispatcher.dart'; import 'platform_views/message_handler.dart'; +import 'pointer_binding.dart'; import 'semantics/accessibility.dart'; import 'services.dart'; import 'util.dart'; @@ -56,6 +57,7 @@ base class EngineFlutterView implements ui.FlutterView { DomElement? hostElement, ) : embeddingStrategy = EmbeddingStrategy.create(hostElement: hostElement), dimensionsProvider = DimensionsProvider.create(hostElement: hostElement) { + pointerBinding = PointerBinding(this); // The embeddingStrategy will take care of cleaning up the rootElement on // hot restart. embeddingStrategy.attachGlassPane(dom.rootElement); @@ -90,6 +92,7 @@ base class EngineFlutterView implements ui.FlutterView { } isDisposed = true; dimensionsProvider.close(); + pointerBinding.dispose(); dom.rootElement.remove(); // TODO(harryterkelsen): What should we do about this in multi-view? renderer.clearFragmentProgramCache(); @@ -122,6 +125,8 @@ base class EngineFlutterView implements ui.FlutterView { late final PlatformViewMessageHandler platformViewMessageHandler = PlatformViewMessageHandler(platformViewsContainer: dom.platformViewsHost); + late final PointerBinding pointerBinding; + // TODO(goderbauer): Provide API to configure constraints. See also TODO in "render". @override ViewConstraints get physicalConstraints => ViewConstraints.tight(physicalSize); diff --git a/lib/web_ui/test/canvaskit/initialization/services_vs_ui_test.dart b/lib/web_ui/test/canvaskit/initialization/services_vs_ui_test.dart index 49236eacfe084..0cfd55a178a11 100644 --- a/lib/web_ui/test/canvaskit/initialization/services_vs_ui_test.dart +++ b/lib/web_ui/test/canvaskit/initialization/services_vs_ui_test.dart @@ -21,7 +21,7 @@ void testMain() { expect(findGlassPane(), isNull); expect(RawKeyboard.instance, isNull); expect(KeyboardBinding.instance, isNull); - expect(PointerBinding.instance, isNull); + expect(EnginePlatformDispatcher.instance.implicitView, isNull); // After initializing services the UI should remain intact. await initializeEngineServices(jsConfiguration: config); @@ -31,14 +31,14 @@ void testMain() { expect(findGlassPane(), isNull); expect(RawKeyboard.instance, isNull); expect(KeyboardBinding.instance, isNull); - expect(PointerBinding.instance, isNull); + expect(EnginePlatformDispatcher.instance.implicitView, isNull); // Now UI should be taken over by Flutter. await initializeEngineUi(); expect(findGlassPane(), isNotNull); expect(RawKeyboard.instance, isNotNull); expect(KeyboardBinding.instance, isNotNull); - expect(PointerBinding.instance, isNotNull); + expect(EnginePlatformDispatcher.instance.implicitView, isNotNull); }); } diff --git a/lib/web_ui/test/engine/pointer_binding/event_position_helper_test.dart b/lib/web_ui/test/engine/pointer_binding/event_position_helper_test.dart index 3640cb78871a8..eca204cd6e613 100644 --- a/lib/web_ui/test/engine/pointer_binding/event_position_helper_test.dart +++ b/lib/web_ui/test/engine/pointer_binding/event_position_helper_test.dart @@ -9,9 +9,7 @@ import 'dart:async'; import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; -import 'package:ui/src/engine/dom.dart'; -import 'package:ui/src/engine/embedder.dart'; -import 'package:ui/src/engine/pointer_binding/event_position_helper.dart'; +import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui show Offset; void main() { @@ -19,9 +17,8 @@ void main() { } void doTests() { - ensureFlutterViewEmbedderInitialized(); - - late DomElement target; + late EngineFlutterView view; + late DomElement rootElement; late DomElement eventSource; final StreamController events = StreamController.broadcast(); @@ -34,14 +31,14 @@ void doTests() { group('computeEventOffsetToTarget', () { setUp(() { - target = createDomElement('div-target'); + view = EngineFlutterView(EnginePlatformDispatcher.instance, domDocument.body!); + rootElement = view.dom.rootElement; eventSource = createDomElement('div-event-source'); - target.append(eventSource); - domDocument.body!.append(target); + rootElement.append(eventSource); // make containers known fixed sizes, absolutely positioned elements, so // we can reason about screen coordinates relatively easily later! - target.style + rootElement.style ..position = 'absolute' ..width = '320px' ..height = '240px' @@ -55,18 +52,18 @@ void doTests() { ..top = '100px' ..left = '120px'; - target.addEventListener('click', createDomEventListener((DomEvent e) { + rootElement.addEventListener('click', createDomEventListener((DomEvent e) { events.add(e); })); }); tearDown(() { - target.remove(); + view.dispose(); }); test('Event dispatched by target returns offsetX, offsetY', () async { // Fire an event contained within target... - final DomMouseEvent event = await dispatchAndCatch(target, createDomPointerEvent( + final DomMouseEvent event = await dispatchAndCatch(rootElement, createDomPointerEvent( 'click', { 'bubbles': true, @@ -78,7 +75,7 @@ void doTests() { expect(event.offsetX, 10); expect(event.offsetY, 20); - final ui.Offset offset = computeEventOffsetToTarget(event, target); + final ui.Offset offset = computeEventOffsetToTarget(event, view); expect(offset.dx, event.offsetX); expect(offset.dy, event.offsetY); @@ -98,7 +95,7 @@ void doTests() { expect(event.offsetX, 20); expect(event.offsetY, 10); - final ui.Offset offset = computeEventOffsetToTarget(event, target); + final ui.Offset offset = computeEventOffsetToTarget(event, view); expect(offset.dx, 140); expect(offset.dy, 110); diff --git a/lib/web_ui/test/engine/pointer_binding_test.dart b/lib/web_ui/test/engine/pointer_binding_test.dart index f5477541fbc94..c03854b2cbfb5 100644 --- a/lib/web_ui/test/engine/pointer_binding_test.dart +++ b/lib/web_ui/test/engine/pointer_binding_test.dart @@ -21,21 +21,27 @@ void main() { internalBootstrapBrowserTest(() => testMain); } +late EngineFlutterView view; +DomElement get rootElement => view.dom.rootElement; + void testMain() { - ensureFlutterViewEmbedderInitialized(); - final DomElement rootElement = - EnginePlatformDispatcher.instance.implicitView!.dom.rootElement; - late double dpi; + final DomElement hostElement = createDomHTMLDivElement(); - setUp(() { - ui.PlatformDispatcher.instance.onPointerDataPacket = null; - dpi = EngineFlutterDisplay.instance.devicePixelRatio; + setUpAll(() { + domDocument.body!.append(hostElement); + // Remove margins to avoid messing up with all the test coordinates. + domDocument.body!.style.margin = '0'; }); - - tearDown(() { - PointerBinding.instance?.debugReset(); + tearDownAll(() { + hostElement.remove(); }); + late List keyDataList; + late KeyboardConverter keyboardConverter; + late PointerBinding instance; + late double dpi; + + KeyboardConverter createKeyboardConverter(List keyDataList) { return KeyboardConverter((ui.KeyData key) { keyDataList.add(key); @@ -43,19 +49,37 @@ void testMain() { }, OperatingSystem.linux); } + setUp(() { + keyDataList = []; + keyboardConverter = createKeyboardConverter(keyDataList); + + view = EngineFlutterView(EnginePlatformDispatcher.instance, hostElement); + instance = view.pointerBinding; + instance.debugOverrideKeyboardConverter(keyboardConverter); + + ui.PlatformDispatcher.instance.onPointerDataPacket = null; + dpi = EngineFlutterDisplay.instance.devicePixelRatio; + }); + + tearDown(() { + keyboardConverter.dispose(); + view.dispose(); + PointerBinding.resetGlobalState(); + }); + test('ios workaround', () { debugEmulateIosSafari = true; addTearDown(() { debugEmulateIosSafari = false; }); - final MockSafariPointerEventWorkaround mockSafariPointer = + final MockSafariPointerEventWorkaround mockSafariWorkaround = MockSafariPointerEventWorkaround(); - SafariPointerEventWorkaround.instance = mockSafariPointer; - final List keyDataList = []; - final KeyboardConverter keyboardConverter = createKeyboardConverter(keyDataList); - final PointerBinding instance = PointerBinding(createDomHTMLDivElement(), keyboardConverter); - expect(mockSafariPointer.workAroundInvoked, isIosSafari); + final PointerBinding instance = PointerBinding( + view, + safariWorkaround: mockSafariWorkaround, + ); + expect(mockSafariWorkaround.workAroundInvoked, isIosSafari); instance.dispose(); }, skip: !isSafari); @@ -328,10 +352,6 @@ void testMain() { final int physicalRight = kWebToPhysicalKey['${key}Right']!; final int logicalLeft = kWebLogicalLocationMap[key]![kLocationLeft]!; - final List keyDataList = []; - final KeyboardConverter keyboardConverter = createKeyboardConverter(keyDataList); - PointerBinding.instance!.debugOverrideKeyboardConverter(keyboardConverter); - expect(keyboardConverter.keyIsPressed(physicalLeft), false); expect(keyboardConverter.keyIsPressed(physicalRight), false); rootElement.dispatchEvent(context.primaryDown()); @@ -344,6 +364,8 @@ void testMain() { character: null, synthesized: true, ); + keyDataList.clear(); + keyboardConverter.clearPressedKeys(); } context.altPressed = true; @@ -373,10 +395,6 @@ void testMain() { final int physicalLeft = kWebToPhysicalKey['${key}Left']!; final int physicalRight = kWebToPhysicalKey['${key}Right']!; - final List keyDataList = []; - final KeyboardConverter keyboardConverter = createKeyboardConverter(keyDataList); - PointerBinding.instance!.debugOverrideKeyboardConverter(keyboardConverter); - keyboardConverter.handleEvent(keyDownEvent('${key}Left', key, modifiers, kLocationLeft)); expect(keyboardConverter.keyIsPressed(physicalLeft), true); expect(keyboardConverter.keyIsPressed(physicalRight), false); @@ -384,6 +402,7 @@ void testMain() { rootElement.dispatchEvent(context.primaryDown()); expect(keyDataList.length, 0); + keyboardConverter.clearPressedKeys(); } // Should not synthesize a modifier down event when DOM event indicates @@ -393,10 +412,6 @@ void testMain() { final int physicalLeft = kWebToPhysicalKey['${key}Left']!; final int physicalRight = kWebToPhysicalKey['${key}Right']!; - final List keyDataList = []; - final KeyboardConverter keyboardConverter = createKeyboardConverter(keyDataList); - PointerBinding.instance!.debugOverrideKeyboardConverter(keyboardConverter); - keyboardConverter.handleEvent(keyDownEvent('${key}Right', key, modifiers, kLocationRight)); expect(keyboardConverter.keyIsPressed(physicalLeft), false); expect(keyboardConverter.keyIsPressed(physicalRight), true); @@ -404,6 +419,7 @@ void testMain() { rootElement.dispatchEvent(context.primaryDown()); expect(keyDataList.length, 0); + keyboardConverter.clearPressedKeys(); } context.altPressed = true; @@ -438,10 +454,6 @@ void testMain() { final int physicalRight = kWebToPhysicalKey['${key}Right']!; final int logicalLeft = kWebLogicalLocationMap[key]![kLocationLeft]!; - final List keyDataList = []; - final KeyboardConverter keyboardConverter = createKeyboardConverter(keyDataList); - PointerBinding.instance!.debugOverrideKeyboardConverter(keyboardConverter); - keyboardConverter.handleEvent(keyDownEvent('${key}Left', key, modifiers, kLocationLeft)); expect(keyboardConverter.keyIsPressed(physicalLeft), true); expect(keyboardConverter.keyIsPressed(physicalRight), false); @@ -458,6 +470,7 @@ void testMain() { synthesized: true, ); expect(keyboardConverter.keyIsPressed(physicalLeft), false); + keyboardConverter.clearPressedKeys(); } // Should synthesize a modifier right key up event when DOM event indicates @@ -468,10 +481,6 @@ void testMain() { final int physicalRight = kWebToPhysicalKey['${key}Right']!; final int logicalRight = kWebLogicalLocationMap[key]![kLocationRight]!; - final List keyDataList = []; - final KeyboardConverter keyboardConverter = createKeyboardConverter(keyDataList); - PointerBinding.instance!.debugOverrideKeyboardConverter(keyboardConverter); - keyboardConverter.handleEvent(keyDownEvent('${key}Right', key, modifiers, kLocationRight)); expect(keyboardConverter.keyIsPressed(physicalLeft), false); expect(keyboardConverter.keyIsPressed(physicalRight), true); @@ -488,6 +497,7 @@ void testMain() { synthesized: true, ); expect(keyboardConverter.keyIsPressed(physicalRight), false); + keyboardConverter.clearPressedKeys(); } context.altPressed = false; @@ -517,16 +527,13 @@ void testMain() { final int physicalLeft = kWebToPhysicalKey['${key}Left']!; final int physicalRight = kWebToPhysicalKey['${key}Right']!; - final List keyDataList = []; - final KeyboardConverter keyboardConverter = createKeyboardConverter(keyDataList); - PointerBinding.instance!.debugOverrideKeyboardConverter(keyboardConverter); - expect(keyboardConverter.keyIsPressed(physicalLeft), false); expect(keyboardConverter.keyIsPressed(physicalRight), false); keyDataList.clear(); // Remove key data generated by handleEvent rootElement.dispatchEvent(context.primaryDown()); expect(keyDataList.length, 0); + keyboardConverter.clearPressedKeys(); } context.altPressed = false; @@ -545,10 +552,6 @@ void testMain() { () { final _BasicEventContext context = _PointerEventContext(); - final List keyDataList = []; - final KeyboardConverter keyboardConverter = createKeyboardConverter(keyDataList); - PointerBinding.instance!.debugOverrideKeyboardConverter(keyboardConverter); - final int physicalAltRight = kWebToPhysicalKey['AltRight']!; final int logicalAltGraph = kWebLogicalLocationMap['AltGraph']![0]!; @@ -568,6 +571,7 @@ void testMain() { synthesized: true, ); expect(keyboardConverter.keyIsPressed(physicalAltRight), false); + keyDataList.clear(); }, ); @@ -1151,6 +1155,8 @@ void testMain() { packets.add(packet); }; + debugOperatingSystemOverride = OperatingSystem.macOs; + rootElement.dispatchEvent(context.wheel( buttons: 0, clientX: 10, @@ -1168,8 +1174,7 @@ void testMain() { ctrlKey: true, )); - debugOperatingSystemOverride = OperatingSystem.macOs; - KeyboardBinding.instance?.converter.handleEvent(keyDownEvent('ControlLeft', 'Control', kCtrl)); + keyboardConverter.handleEvent(keyDownEvent('ControlLeft', 'Control', kCtrl)); rootElement.dispatchEvent(context.wheel( buttons: 0, @@ -1180,7 +1185,7 @@ void testMain() { ctrlKey: true, )); - KeyboardBinding.instance?.converter.handleEvent(keyUpEvent('ControlLeft', 'Control', kCtrl)); + keyboardConverter.handleEvent(keyUpEvent('ControlLeft', 'Control', kCtrl)); expect(packets, hasLength(3)); @@ -2638,17 +2643,13 @@ void testMain() { test('throws if browser does not support pointer events', () { expect( - () => PointerBinding( - createDomHTMLDivElement(), - createKeyboardConverter([]), - MockPointerSupportDetector(false), - ), + () => PointerBinding(view, detector: MockPointerSupportDetector(false)), throwsUnsupportedError, ); }); group('ClickDebouncer', () { - _testClickDebouncer(); + _testClickDebouncer(getBinding: () => instance); }); } @@ -2657,7 +2658,7 @@ typedef CapturedSemanticsEvent = ({ int nodeId, }); -void _testClickDebouncer() { +void _testClickDebouncer({required PointerBinding Function() getBinding}) { final DateTime testTime = DateTime(2018, 12, 17); late List pointerPackets; late List semanticsActions; @@ -2692,39 +2693,34 @@ void _testClickDebouncer() { EnginePlatformDispatcher.instance.onSemanticsActionEvent = (ui.SemanticsActionEvent event) { semanticsActions.add((type: event.type, nodeId: event.nodeId)); }; - binding = PointerBinding.instance!; - binding.clickDebouncer.reset(); - }); - - tearDown(() { - binding.clickDebouncer.reset(); + binding = getBinding(); }); test('Forwards to framework when semantics is off', () { expect(EnginePlatformDispatcher.instance.semanticsEnabled, false); - expect(binding.clickDebouncer.isDebouncing, false); - binding.flutterViewElement.dispatchEvent(context.primaryDown()); + expect(PointerBinding.clickDebouncer.isDebouncing, false); + binding.rootElement.dispatchEvent(context.primaryDown()); expect(pointerPackets, [ ui.PointerChange.add, ui.PointerChange.down, ]); - expect(binding.clickDebouncer.isDebouncing, false); + expect(PointerBinding.clickDebouncer.isDebouncing, false); expect(semanticsActions, isEmpty); }); testWithSemantics('Forwards to framework when not debouncing', () async { expect(EnginePlatformDispatcher.instance.semanticsEnabled, true); - expect(binding.clickDebouncer.isDebouncing, false); + expect(PointerBinding.clickDebouncer.isDebouncing, false); // This test DOM element is missing the `flt-tappable` attribute on purpose // so that the debouncer does not debounce events and simply lets // everything through. final DomElement testElement = createDomElement('flt-semantics'); - EnginePlatformDispatcher.instance.implicitView!.dom.semanticsHost.appendChild(testElement); + view.dom.semanticsHost.appendChild(testElement); testElement.dispatchEvent(context.primaryDown()); testElement.dispatchEvent(context.primaryUp()); - expect(binding.clickDebouncer.isDebouncing, false); + expect(PointerBinding.clickDebouncer.isDebouncing, false); expect(pointerPackets, [ ui.PointerChange.add, @@ -2736,23 +2732,23 @@ void _testClickDebouncer() { testWithSemantics('Accumulates pointer events starting from pointerdown', () async { expect(EnginePlatformDispatcher.instance.semanticsEnabled, true); - expect(binding.clickDebouncer.isDebouncing, false); + expect(PointerBinding.clickDebouncer.isDebouncing, false); final DomElement testElement = createDomElement('flt-semantics'); testElement.setAttribute('flt-tappable', ''); - EnginePlatformDispatcher.instance.implicitView!.dom.semanticsHost.appendChild(testElement); + view.dom.semanticsHost.appendChild(testElement); testElement.dispatchEvent(context.primaryDown()); expect( reason: 'Should start debouncing at first pointerdown', - binding.clickDebouncer.isDebouncing, + PointerBinding.clickDebouncer.isDebouncing, true, ); testElement.dispatchEvent(context.primaryUp()); expect( reason: 'Should still be debouncing after pointerup', - binding.clickDebouncer.isDebouncing, + PointerBinding.clickDebouncer.isDebouncing, true, ); @@ -2762,22 +2758,22 @@ void _testClickDebouncer() { [], ); expect( - binding.clickDebouncer.debugState!.target, + PointerBinding.clickDebouncer.debugState!.target, testElement, ); expect( - binding.clickDebouncer.debugState!.timer.isActive, + PointerBinding.clickDebouncer.debugState!.timer.isActive, isTrue, ); expect( - binding.clickDebouncer.debugState!.queue.map((QueuedEvent e) => e.event.type), + PointerBinding.clickDebouncer.debugState!.queue.map((QueuedEvent e) => e.event.type), ['pointerdown', 'pointerup'], ); await Future.delayed(const Duration(milliseconds: 250)); expect( reason: 'Should stop debouncing after timer expires.', - binding.clickDebouncer.isDebouncing, + PointerBinding.clickDebouncer.isDebouncing, false, ); expect( @@ -2794,32 +2790,32 @@ void _testClickDebouncer() { testWithSemantics('Flushes events to framework when target changes', () async { expect(EnginePlatformDispatcher.instance.semanticsEnabled, true); - expect(binding.clickDebouncer.isDebouncing, false); + expect(PointerBinding.clickDebouncer.isDebouncing, false); final DomElement testElement = createDomElement('flt-semantics'); testElement.setAttribute('flt-tappable', ''); - EnginePlatformDispatcher.instance.implicitView!.dom.semanticsHost.appendChild(testElement); + view.dom.semanticsHost.appendChild(testElement); testElement.dispatchEvent(context.primaryDown()); expect( reason: 'Should start debouncing at first pointerdown', - binding.clickDebouncer.isDebouncing, + PointerBinding.clickDebouncer.isDebouncing, true, ); final DomElement newTarget = createDomElement('flt-semantics'); newTarget.setAttribute('flt-tappable', ''); - EnginePlatformDispatcher.instance.implicitView!.dom.semanticsHost.appendChild(newTarget); + view.dom.semanticsHost.appendChild(newTarget); newTarget.dispatchEvent(context.primaryUp()); expect( reason: 'Should stop debouncing when target changes.', - binding.clickDebouncer.isDebouncing, + PointerBinding.clickDebouncer.isDebouncing, false, ); expect( reason: 'The state should be cleaned up after stopping debouncing.', - binding.clickDebouncer.debugState, + PointerBinding.clickDebouncer.debugState, isNull, ); expect( @@ -2835,11 +2831,11 @@ void _testClickDebouncer() { }); testWithSemantics('Forwards click to framework when not debouncing but listening', () async { - expect(binding.clickDebouncer.isDebouncing, false); + expect(PointerBinding.clickDebouncer.isDebouncing, false); final DomElement testElement = createDomElement('flt-semantics'); testElement.setAttribute('flt-tappable', ''); - EnginePlatformDispatcher.instance.implicitView!.dom.semanticsHost.appendChild(testElement); + view.dom.semanticsHost.appendChild(testElement); final DomEvent click = createDomMouseEvent( 'click', @@ -2849,8 +2845,8 @@ void _testClickDebouncer() { } ); - binding.clickDebouncer.onClick(click, 42, true); - expect(binding.clickDebouncer.isDebouncing, false); + PointerBinding.clickDebouncer.onClick(click, 42, true); + expect(PointerBinding.clickDebouncer.isDebouncing, false); expect(pointerPackets, isEmpty); expect(semanticsActions, [ (type: ui.SemanticsAction.tap, nodeId: 42) @@ -2858,13 +2854,13 @@ void _testClickDebouncer() { }); testWithSemantics('Forwards click to framework when debouncing and listening', () async { - expect(binding.clickDebouncer.isDebouncing, false); + expect(PointerBinding.clickDebouncer.isDebouncing, false); final DomElement testElement = createDomElement('flt-semantics'); testElement.setAttribute('flt-tappable', ''); - EnginePlatformDispatcher.instance.implicitView!.dom.semanticsHost.appendChild(testElement); + view.dom.semanticsHost.appendChild(testElement); testElement.dispatchEvent(context.primaryDown()); - expect(binding.clickDebouncer.isDebouncing, true); + expect(PointerBinding.clickDebouncer.isDebouncing, true); final DomEvent click = createDomMouseEvent( 'click', @@ -2874,7 +2870,7 @@ void _testClickDebouncer() { } ); - binding.clickDebouncer.onClick(click, 42, true); + PointerBinding.clickDebouncer.onClick(click, 42, true); expect(pointerPackets, isEmpty); expect(semanticsActions, [ (type: ui.SemanticsAction.tap, nodeId: 42) @@ -2882,13 +2878,13 @@ void _testClickDebouncer() { }); testWithSemantics('Dedupes click if debouncing but not listening', () async { - expect(binding.clickDebouncer.isDebouncing, false); + expect(PointerBinding.clickDebouncer.isDebouncing, false); final DomElement testElement = createDomElement('flt-semantics'); testElement.setAttribute('flt-tappable', ''); - EnginePlatformDispatcher.instance.implicitView!.dom.semanticsHost.appendChild(testElement); + view.dom.semanticsHost.appendChild(testElement); testElement.dispatchEvent(context.primaryDown()); - expect(binding.clickDebouncer.isDebouncing, true); + expect(PointerBinding.clickDebouncer.isDebouncing, true); final DomEvent click = createDomMouseEvent( 'click', @@ -2898,7 +2894,7 @@ void _testClickDebouncer() { } ); - binding.clickDebouncer.onClick(click, 42, false); + PointerBinding.clickDebouncer.onClick(click, 42, false); expect( reason: 'When tappable declares that it is not listening to click events ' 'the debouncer flushes the pointer events to the framework and ' @@ -2914,11 +2910,11 @@ void _testClickDebouncer() { testWithSemantics('Dedupes click if pointer down/up flushed recently', () async { expect(EnginePlatformDispatcher.instance.semanticsEnabled, true); - expect(binding.clickDebouncer.isDebouncing, false); + expect(PointerBinding.clickDebouncer.isDebouncing, false); final DomElement testElement = createDomElement('flt-semantics'); testElement.setAttribute('flt-tappable', ''); - EnginePlatformDispatcher.instance.implicitView!.dom.semanticsHost.appendChild(testElement); + view.dom.semanticsHost.appendChild(testElement); testElement.dispatchEvent(context.primaryDown()); @@ -2930,7 +2926,7 @@ void _testClickDebouncer() { await Future.delayed(const Duration(milliseconds: 190)); testElement.dispatchEvent(context.primaryUp()); - expect(binding.clickDebouncer.isDebouncing, true); + expect(PointerBinding.clickDebouncer.isDebouncing, true); expect( reason: 'Timer has not expired yet', pointerPackets, isEmpty, @@ -2957,7 +2953,7 @@ void _testClickDebouncer() { 'clientY': testElement.getBoundingClientRect().y, } ); - binding.clickDebouncer.onClick(click, 42, true); + PointerBinding.clickDebouncer.onClick(click, 42, true); expect( reason: 'Because the DOM click event was deduped.', @@ -2969,11 +2965,11 @@ void _testClickDebouncer() { testWithSemantics('Forwards click if enough time passed after the last flushed pointerup', () async { expect(EnginePlatformDispatcher.instance.semanticsEnabled, true); - expect(binding.clickDebouncer.isDebouncing, false); + expect(PointerBinding.clickDebouncer.isDebouncing, false); final DomElement testElement = createDomElement('flt-semantics'); testElement.setAttribute('flt-tappable', ''); - EnginePlatformDispatcher.instance.implicitView!.dom.semanticsHost.appendChild(testElement); + view.dom.semanticsHost.appendChild(testElement); testElement.dispatchEvent(context.primaryDown()); @@ -2985,7 +2981,7 @@ void _testClickDebouncer() { await Future.delayed(const Duration(milliseconds: 190)); testElement.dispatchEvent(context.primaryUp()); - expect(binding.clickDebouncer.isDebouncing, true); + expect(PointerBinding.clickDebouncer.isDebouncing, true); expect( reason: 'Timer has not expired yet', pointerPackets, isEmpty, @@ -3012,7 +3008,7 @@ void _testClickDebouncer() { 'clientY': testElement.getBoundingClientRect().y, } ); - binding.clickDebouncer.onClick(click, 42, true); + PointerBinding.clickDebouncer.onClick(click, 42, true); expect( reason: 'The DOM click should still be sent to the framework because it ' @@ -3033,6 +3029,9 @@ class MockSafariPointerEventWorkaround implements SafariPointerEventWorkaround { void workAroundMissingPointerEvents() { workAroundInvoked = true; } + + @override + void dispose() {} } abstract class _BasicEventContext { From 5845957b0476076096e9b4531942432524c0431f Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Mon, 11 Dec 2023 11:23:34 -0500 Subject: [PATCH 2/5] safari workaround attach listener once --- lib/web_ui/lib/src/engine/pointer_binding.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index 0dcbafb7ae7a1..d33b480fad694 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -74,16 +74,22 @@ int convertButtonToButtons(int button) { /// Wrapping the Safari iOS workaround that adds a dummy event listener /// More info about the issue and workaround: https://github.com/flutter/flutter/issues/70858 class SafariPointerEventWorkaround { + SafariPointerEventWorkaround._(); + DomEventListener? _listener; void workAroundMissingPointerEvents() { - _listener = createDomEventListener((_) {}); - domDocument.addEventListener('touchstart', _listener); + // We only need to attach the listener once. + if (_listener == null) { + _listener = createDomEventListener((_) {}); + domDocument.addEventListener('touchstart', _listener); + } } void dispose() { if (_listener != null) { domDocument.removeEventListener('touchstart', _listener); + _listener = null; } } } @@ -96,7 +102,7 @@ class PointerBinding { }) : _pointerDataConverter = PointerDataConverter(), _detector = detector { if (isIosSafari) { - _safariWorkaround = safariWorkaround ?? SafariPointerEventWorkaround(); + _safariWorkaround = safariWorkaround ?? _defaultSafariWorkaround; _safariWorkaround!.workAroundMissingPointerEvents(); } _adapter = _createAdapter(); @@ -106,6 +112,7 @@ class PointerBinding { }()); } + static final SafariPointerEventWorkaround _defaultSafariWorkaround = SafariPointerEventWorkaround._(); static final ClickDebouncer clickDebouncer = ClickDebouncer(); /// Resets global pointer state that's not tied to any single [PointerBinding] From 903d7658870ab15fc25a6d9efd9b1dc3f945851a Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Mon, 11 Dec 2023 11:23:50 -0500 Subject: [PATCH 3/5] debugResetGlobalState --- lib/web_ui/lib/src/engine/pointer_binding.dart | 3 ++- lib/web_ui/test/engine/pointer_binding_test.dart | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index d33b480fad694..d0c2c560fcda6 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -117,7 +117,8 @@ class PointerBinding { /// Resets global pointer state that's not tied to any single [PointerBinding] /// instance. - static void resetGlobalState() { + @visibleForTesting + static void debugResetGlobalState() { clickDebouncer.reset(); PointerDataConverter.globalPointerState.reset(); } diff --git a/lib/web_ui/test/engine/pointer_binding_test.dart b/lib/web_ui/test/engine/pointer_binding_test.dart index 6ef6792604f3a..0e27270917244 100644 --- a/lib/web_ui/test/engine/pointer_binding_test.dart +++ b/lib/web_ui/test/engine/pointer_binding_test.dart @@ -64,7 +64,7 @@ void testMain() { tearDown(() { keyboardConverter.dispose(); view.dispose(); - PointerBinding.resetGlobalState(); + PointerBinding.debugResetGlobalState(); }); test('ios workaround', () { From 1de6ab983c0c1f3442d06348241c3e992b928e5f Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Mon, 11 Dec 2023 11:37:05 -0500 Subject: [PATCH 4/5] embedding strategy global target --- lib/web_ui/lib/src/engine/pointer_binding.dart | 13 ++----------- .../custom_element_embedding_strategy.dart | 17 ++++++++++++----- .../embedding_strategy/embedding_strategy.dart | 7 +++++-- .../full_page_embedding_strategy.dart | 13 ++++++++----- lib/web_ui/lib/src/engine/window.dart | 2 +- .../custom_element_embedding_strategy_test.dart | 6 +++--- .../full_page_embedding_strategy_test.dart | 6 +++--- 7 files changed, 34 insertions(+), 30 deletions(-) diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index d0c2c560fcda6..d4cb96616ef72 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -499,17 +499,8 @@ abstract class _BaseAdapter { DomWheelEvent? _lastWheelEvent; bool _lastWheelEventWasTrackpad = false; - DomElement get _viewTarget => _view.dom.rootElement; - DomEventTarget get _globalTarget { - // When the Flutter app owns the full page, we want to listen on window for - // some events. Otherwise, we just want to listen on the root element of the - // view. - // TODO(mdebbar): Is there a better way of doing this? - if (_view == EnginePlatformDispatcher.instance.implicitView) { - return domWindow; - } - return _viewTarget; - } + DomEventTarget get _viewTarget => _view.dom.rootElement; + DomEventTarget get _globalTarget => _view.embeddingStrategy.globalEventTarget; /// Each subclass is expected to override this method to attach its own event /// listeners and convert events into pointer events. diff --git a/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart b/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart index 94a211b23a21c..e835b0c3ba409 100644 --- a/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart +++ b/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart @@ -17,9 +17,15 @@ class CustomElementEmbeddingStrategy implements EmbeddingStrategy { _hostElement.clearChildren(); } - /// The target element in which this strategy will embedd Flutter. + @override + DomEventTarget get globalEventTarget => _rootElement; + + /// The target element in which this strategy will embed the Flutter view. final DomElement _hostElement; + /// The root element of the Flutter view. + late final DomElement _rootElement; + @override void initialize({ Map? hostElementAttributes, @@ -32,17 +38,18 @@ class CustomElementEmbeddingStrategy implements EmbeddingStrategy { } @override - void attachGlassPane(DomElement glassPaneElement) { - glassPaneElement + void attachViewRoot(DomElement rootElement) { + rootElement ..style.width = '100%' ..style.height = '100%' ..style.display = 'block' ..style.overflow = 'hidden' ..style.position = 'relative'; - _hostElement.appendChild(glassPaneElement); + _hostElement.appendChild(rootElement); - registerElementForCleanup(glassPaneElement); + registerElementForCleanup(rootElement); + _rootElement = rootElement; } @override diff --git a/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/embedding_strategy.dart b/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/embedding_strategy.dart index f9f358be21518..aa5a8592afc2f 100644 --- a/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/embedding_strategy.dart +++ b/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/embedding_strategy.dart @@ -26,12 +26,15 @@ abstract class EmbeddingStrategy { } } + /// The global event target for the Flutter view. + DomEventTarget get globalEventTarget; + void initialize({ Map? hostElementAttributes, }); - /// Attaches the glassPane element into the hostElement. - void attachGlassPane(DomElement glassPaneElement); + /// Attaches the view root element into the hostElement. + void attachViewRoot(DomElement rootElement); /// Attaches the resourceHost element into the hostElement. void attachResourcesHost(DomElement resourceHost, {DomElement? nextTo}); diff --git a/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/full_page_embedding_strategy.dart b/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/full_page_embedding_strategy.dart index fdb7028fe991a..1a4d7d1d15879 100644 --- a/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/full_page_embedding_strategy.dart +++ b/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/full_page_embedding_strategy.dart @@ -13,6 +13,9 @@ import 'embedding_strategy.dart'; /// This strategy takes over the element, modifies the viewport meta-tag, /// and ensures that the root Flutter view covers the whole screen. class FullPageEmbeddingStrategy implements EmbeddingStrategy { + @override + DomEventTarget get globalEventTarget => domWindow; + @override void initialize({ Map? hostElementAttributes, @@ -28,18 +31,18 @@ class FullPageEmbeddingStrategy implements EmbeddingStrategy { } @override - void attachGlassPane(DomElement glassPaneElement) { - /// Tweaks style so the glassPane works well with the hostElement. - glassPaneElement.style + void attachViewRoot(DomElement rootElement) { + /// Tweaks style so the rootElement works well with the hostElement. + rootElement.style ..position = 'absolute' ..top = '0' ..right = '0' ..bottom = '0' ..left = '0'; - domDocument.body!.append(glassPaneElement); + domDocument.body!.append(rootElement); - registerElementForCleanup(glassPaneElement); + registerElementForCleanup(rootElement); } @override diff --git a/lib/web_ui/lib/src/engine/window.dart b/lib/web_ui/lib/src/engine/window.dart index 18191f7ed74b0..07d4218d2c828 100644 --- a/lib/web_ui/lib/src/engine/window.dart +++ b/lib/web_ui/lib/src/engine/window.dart @@ -60,7 +60,7 @@ base class EngineFlutterView implements ui.FlutterView { pointerBinding = PointerBinding(this); // The embeddingStrategy will take care of cleaning up the rootElement on // hot restart. - embeddingStrategy.attachGlassPane(dom.rootElement); + embeddingStrategy.attachViewRoot(dom.rootElement); registerHotRestartListener(dispose); } diff --git a/lib/web_ui/test/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy_test.dart b/lib/web_ui/test/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy_test.dart index b28fbc3d7fe1c..44a3deaf34445 100644 --- a/lib/web_ui/test/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy_test.dart +++ b/lib/web_ui/test/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy_test.dart @@ -45,7 +45,7 @@ void doTests() { }); }); - group('attachGlassPane', () { + group('attachViewRoot', () { setUp(() { target = createDomElement('this-is-the-target'); domDocument.body!.append(target); @@ -66,7 +66,7 @@ void doTests() { reason: 'Should not have any specific position.'); expect(style.width, '', reason: 'Should not have any size set.'); - strategy.attachGlassPane(glassPane); + strategy.attachViewRoot(glassPane); // Assert injection into expect(glassPane.isConnected, isTrue, @@ -98,7 +98,7 @@ void doTests() { domDocument.body!.append(target); strategy = CustomElementEmbeddingStrategy(target); strategy.initialize(); - strategy.attachGlassPane(glassPane); + strategy.attachViewRoot(glassPane); }); tearDown(() { diff --git a/lib/web_ui/test/engine/view_embedder/embedding_strategy/full_page_embedding_strategy_test.dart b/lib/web_ui/test/engine/view_embedder/embedding_strategy/full_page_embedding_strategy_test.dart index 5bf979105e142..476aceec430f9 100644 --- a/lib/web_ui/test/engine/view_embedder/embedding_strategy/full_page_embedding_strategy_test.dart +++ b/lib/web_ui/test/engine/view_embedder/embedding_strategy/full_page_embedding_strategy_test.dart @@ -64,7 +64,7 @@ void doTests() { }); }); - group('attachGlassPane', () { + group('attachViewRoot', () { setUp(() { strategy = FullPageEmbeddingStrategy(); strategy.initialize(); @@ -81,7 +81,7 @@ void doTests() { reason: 'Should not have any top/right/bottom/left positioning/inset.'); - strategy.attachGlassPane(glassPane); + strategy.attachViewRoot(glassPane); // Assert injection into expect(glassPane.isConnected, isTrue, @@ -110,7 +110,7 @@ void doTests() { glassPane = createDomElement('some-tag-for-tests'); strategy = FullPageEmbeddingStrategy(); strategy.initialize(); - strategy.attachGlassPane(glassPane); + strategy.attachViewRoot(glassPane); }); test( From e36557bcbdddd94cd7bcc0d35e7f132a5ba6db51 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Mon, 11 Dec 2023 14:13:28 -0500 Subject: [PATCH 5/5] fix initialization order --- lib/web_ui/lib/src/engine/window.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/window.dart b/lib/web_ui/lib/src/engine/window.dart index 07d4218d2c828..f91dc68395063 100644 --- a/lib/web_ui/lib/src/engine/window.dart +++ b/lib/web_ui/lib/src/engine/window.dart @@ -57,10 +57,10 @@ base class EngineFlutterView implements ui.FlutterView { DomElement? hostElement, ) : embeddingStrategy = EmbeddingStrategy.create(hostElement: hostElement), dimensionsProvider = DimensionsProvider.create(hostElement: hostElement) { - pointerBinding = PointerBinding(this); // The embeddingStrategy will take care of cleaning up the rootElement on // hot restart. embeddingStrategy.attachViewRoot(dom.rootElement); + pointerBinding = PointerBinding(this); registerHotRestartListener(dispose); }