From 3b115f0334421e6559e5c7e77e60f5f04b1daa72 Mon Sep 17 00:00:00 2001 From: David Iglesias Date: Thu, 22 Dec 2022 13:02:34 -0800 Subject: [PATCH] Revert "[web] Render in custom target (#37738)" (#38469) This reverts commit 23582159af46fe219bcf4da7ad894865ff7b3f91. --- ci/licenses_golden/licenses_flutter | 16 - lib/web_ui/lib/src/engine.dart | 8 - lib/web_ui/lib/src/engine/dom.dart | 86 +--- lib/web_ui/lib/src/engine/embedder.dart | 453 ++++++++++++++---- lib/web_ui/lib/src/engine/host_node.dart | 205 +------- lib/web_ui/lib/src/engine/html/scene.dart | 11 +- .../lib/src/engine/platform_dispatcher.dart | 25 - .../lib/src/engine/pointer_binding.dart | 45 +- .../event_position_helper.dart | 113 ----- .../custom_element_dimensions_provider.dart | 88 ---- .../dimensions_provider.dart | 65 --- .../full_page_dimensions_provider.dart | 123 ----- .../custom_element_embedding_strategy.dart | 57 --- .../embedding_strategy.dart | 58 --- .../full_page_embedding_strategy.dart | 108 ----- .../hot_restart_cache_handler.dart | 57 --- lib/web_ui/lib/src/engine/window.dart | 84 +++- lib/web_ui/test/engine/host_node_test.dart | 102 +--- .../test/engine/pointer_binding_test.dart | 17 +- ...stom_element_dimensions_provider_test.dart | 170 ------- .../dimensions_provider_test.dart | 48 -- .../full_page_dimensions_provider_test.dart | 107 ----- ...ustom_element_embedding_strategy_test.dart | 124 ----- .../embedding_strategy_test.dart | 58 --- .../full_page_embedding_strategy_test.dart | 132 ----- .../hot_restart_cache_handler_test.dart | 82 ---- 26 files changed, 491 insertions(+), 1951 deletions(-) delete mode 100644 lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart delete mode 100644 lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart delete mode 100644 lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart delete mode 100644 lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart delete mode 100644 lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart delete mode 100644 lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/embedding_strategy.dart delete mode 100644 lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/full_page_embedding_strategy.dart delete mode 100644 lib/web_ui/lib/src/engine/view_embedder/hot_restart_cache_handler.dart delete mode 100644 lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart delete mode 100644 lib/web_ui/test/engine/view_embedder/dimensions_provider/dimensions_provider_test.dart delete mode 100644 lib/web_ui/test/engine/view_embedder/dimensions_provider/full_page_dimensions_provider_test.dart delete mode 100644 lib/web_ui/test/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy_test.dart delete mode 100644 lib/web_ui/test/engine/view_embedder/embedding_strategy/embedding_strategy_test.dart delete mode 100644 lib/web_ui/test/engine/view_embedder/embedding_strategy/full_page_embedding_strategy_test.dart delete mode 100644 lib/web_ui/test/engine/view_embedder/hot_restart_cache_handler_test.dart diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index c3da2a5a83479..eece0b9e72c77 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1962,7 +1962,6 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/platform_views/message_handle ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/platform_views/slots.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/plugins.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/pointer_binding.dart + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/pointer_converter.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/profiler.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/raw_keyboard.dart + ../../../flutter/LICENSE @@ -2018,13 +2017,6 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/ulps.dart + ../../../flutter/ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/util.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/validators.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/vector_math.dart + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/embedding_strategy.dart + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/full_page_embedding_strategy.dart + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/hot_restart_cache_handler.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/window.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/text.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/tile_mode.dart + ../../../flutter/LICENSE @@ -4421,7 +4413,6 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_views/message_handler. FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_views/slots.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_binding/event_position_helper.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/pointer_converter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/profiler.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/raw_keyboard.dart @@ -4477,13 +4468,6 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/ulps.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/util.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/validators.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/vector_math.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/embedding_strategy.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/full_page_embedding_strategy.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/hot_restart_cache_handler.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/window.dart FILE: ../../../flutter/lib/web_ui/lib/text.dart FILE: ../../../flutter/lib/web_ui/lib/tile_mode.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index e86ffb97942ff..f83e43ce5245d 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -122,7 +122,6 @@ export 'engine/platform_views/message_handler.dart'; export 'engine/platform_views/slots.dart'; export 'engine/plugins.dart'; export 'engine/pointer_binding.dart'; -export 'engine/pointer_binding/event_position_helper.dart'; export 'engine/pointer_converter.dart'; export 'engine/profiler.dart'; export 'engine/raw_keyboard.dart'; @@ -171,11 +170,4 @@ export 'engine/text_editing/text_editing.dart'; export 'engine/util.dart'; export 'engine/validators.dart'; export 'engine/vector_math.dart'; -export 'engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart'; -export 'engine/view_embedder/dimensions_provider/dimensions_provider.dart'; -export 'engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart'; -export 'engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart'; -export 'engine/view_embedder/embedding_strategy/embedding_strategy.dart'; -export 'engine/view_embedder/embedding_strategy/full_page_embedding_strategy.dart'; -export 'engine/view_embedder/hot_restart_cache_handler.dart'; export 'engine/window.dart'; diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index e982f04707538..61ff5532e68bb 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -174,7 +174,6 @@ class DomEvent {} extension DomEventExtension on DomEvent { external DomEventTarget? get target; - external DomEventTarget? get currentTarget; external double? get timeStamp; external String get type; external void preventDefault(); @@ -462,9 +461,6 @@ class DomHTMLElement extends DomElement {} extension DomHTMLElementExtension on DomHTMLElement { external double get offsetWidth; - external double get offsetLeft; - external double get offsetTop; - external DomHTMLElement? get offsetParent; } @JS() @@ -1093,8 +1089,6 @@ extension DomMouseEventExtension on DomMouseEvent { external double get clientY; external double get offsetX; external double get offsetY; - external double get pageX; - external double get pageY; DomPoint get client => DomPoint(clientX, clientY); DomPoint get offset => DomPoint(offsetX, offsetY); external double get button; @@ -1318,10 +1312,7 @@ class DomStyleSheet {} class DomCSSStyleSheet extends DomStyleSheet {} extension DomCSSStyleSheetExtension on DomCSSStyleSheet { - Iterable get cssRules => - createDomListWrapper(js_util - .getProperty<_DomList>(this, 'cssRules')); - + external DomCSSRuleList get cssRules; double insertRule(String rule, [int? index]) => js_util .callMethod( this, 'insertRule', @@ -1332,12 +1323,6 @@ extension DomCSSStyleSheetExtension on DomCSSStyleSheet { @staticInterop class DomCSSRule {} -@JS() -@staticInterop -extension DomCSSRuleExtension on DomCSSRule { - external String get cssText; -} - @JS() @staticInterop class DomScreen {} @@ -1435,75 +1420,12 @@ extension DomMessageChannelExtension on DomMessageChannel { external DomMessagePort get port2; } -/// ResizeObserver JS binding. -/// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver @JS() @staticInterop -abstract class DomResizeObserver {} +class DomCSSRuleList {} -/// Creates a DomResizeObserver with a callback. -/// -/// Internally converts the `List` of entries into the expected -/// `List` -DomResizeObserver? createDomResizeObserver(DomResizeObserverCallbackFn fn) { - return domCallConstructorString('ResizeObserver', [ - allowInterop( - (List entries, DomResizeObserver observer) { - fn(entries.cast(), observer); - } - ), - ]) as DomResizeObserver?; -} - -/// ResizeObserver instance methods. -/// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#instance_methods -extension DomResizeObserverExtension on DomResizeObserver { - external void disconnect(); - external void observe(DomElement target, [DomResizeObserverObserveOptions options]); - external void unobserve(DomElement target); -} - -/// Options object passed to the `observe` method of a [DomResizeObserver]. -/// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/observe#parameters -@JS() -@staticInterop -@anonymous -abstract class DomResizeObserverObserveOptions { - external factory DomResizeObserverObserveOptions({ - String box, - }); -} - -/// Type of the function used to create a Resize Observer. -typedef DomResizeObserverCallbackFn = void Function(List entries, DomResizeObserver observer); - -/// The object passed to the [DomResizeObserverCallbackFn], which allows access to the new dimensions of the observed element. -/// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry -@JS() -@staticInterop -abstract class DomResizeObserverEntry {} - -/// ResizeObserverEntry instance properties. -/// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry#instance_properties -extension DomResizeObserverEntryExtension on DomResizeObserverEntry { - /// A DOMRectReadOnly object containing the new size of the observed element when the callback is run. - /// - /// Note that this is better supported than the above two properties, but it - /// is left over from an earlier implementation of the Resize Observer API, is - /// still included in the spec for web compat reasons, and may be deprecated - /// in future versions. - external DomRectReadOnly get contentRect; - external DomElement get target; - // Some more future getters: - // - // borderBoxSize - // contentBoxSize - // devicePixelContentBoxSize +extension DomCSSRuleListExtension on DomCSSRuleList { + external double get length; } /// A factory to create `TrustedTypePolicy` objects. diff --git a/lib/web_ui/lib/src/engine/embedder.dart b/lib/web_ui/lib/src/engine/embedder.dart index db28eac7672c9..0c98a3d259d86 100644 --- a/lib/web_ui/lib/src/engine/embedder.dart +++ b/lib/web_ui/lib/src/engine/embedder.dart @@ -6,7 +6,7 @@ import 'dart:async'; import 'package:ui/ui.dart' as ui; -import '../engine.dart' show buildMode, renderer, window; +import '../engine.dart' show buildMode, registerHotRestartListener, renderer; import 'browser_detection.dart'; import 'configuration.dart'; import 'dom.dart'; @@ -14,10 +14,11 @@ import 'host_node.dart'; import 'keyboard_binding.dart'; import 'platform_dispatcher.dart'; import 'pointer_binding.dart'; +import 'safe_browser_api.dart'; import 'semantics.dart'; import 'text_editing/text_editing.dart'; -import 'view_embedder/dimensions_provider/dimensions_provider.dart'; -import 'view_embedder/embedding_strategy/embedding_strategy.dart'; +import 'util.dart'; +import 'window.dart'; /// Controls the placement and lifecycle of a Flutter view on the web page. /// @@ -33,37 +34,34 @@ import 'view_embedder/embedding_strategy/embedding_strategy.dart'; /// - [sceneHostElement], the anchor that provides a stable location in the DOM /// tree for the [sceneElement]. /// - [semanticsHostElement], hosts the ARIA-annotated semantics tree. -/// -/// This class is currently a singleton, but it'll possibly need to morph to have -/// multiple instances in a multi-view scenario. (One ViewEmbedder per FlutterView). class FlutterViewEmbedder { - /// Creates a FlutterViewEmbedder. - /// - /// The incoming [hostElement] parameter specifies the root element in the DOM - /// into which Flutter will be rendered. - /// - /// The hostElement is abstracted by an [EmbeddingStrategy] instance, which has - /// different behavior depending on the `hostElement` value: - /// - /// - A `null` `hostElement` will cause Flutter to take over the whole page. - /// - A non-`null` `hostElement` will render flutter inside that element. - FlutterViewEmbedder({DomElement? hostElement}) - : _embeddingStrategy = - EmbeddingStrategy.create(hostElement: hostElement) { - // Configure the EngineWindow so it knows how to measure itself. - // TODO(dit): Refactor ownership according to new design, https://github.com/flutter/flutter/issues/117098 - window.configureDimensionsProvider(DimensionsProvider.create( - hostElement: hostElement, - )); - + FlutterViewEmbedder() { + assert(() { + _setupHotRestart(); + return true; + }()); reset(); + assert(() { + _registerHotRestartCleanUp(); + return true; + }()); } - /// Abstracts all the DOM manipulations required to embed a Flutter app in an user-supplied `hostElement`. - final EmbeddingStrategy _embeddingStrategy; - // The tag name for the root view of the flutter app (glass-pane) - static const String glassPaneTagName = 'flt-glass-pane'; + static const String _glassPaneTagName = 'flt-glass-pane'; + + /// Listens to window resize events + DomSubscription? _resizeSubscription; + + /// Listens to window locale events. + DomSubscription? _localeSubscription; + + /// Contains Flutter-specific CSS rules, such as default margins and + /// paddings. + DomHTMLStyleElement? _styleElement; + + /// Configures the screen, such as scaling. + DomHTMLMetaElement? _viewportMeta; /// The element that contains the [sceneElement]. /// @@ -99,6 +97,50 @@ class FlutterViewEmbedder { DomElement? get sceneElement => _sceneElement; DomElement? _sceneElement; + /// This is state persistent across hot restarts that indicates what + /// to clear. Delay removal of old visible state to make the + /// transition appear smooth. + static const String _staleHotRestartStore = '__flutter_state'; + List? _staleHotRestartState; + + /// Creates a container for DOM elements that need to be cleaned up between + /// hot restarts. + /// + /// If a contains already exists, reuses the existing one. + void _setupHotRestart() { + // This persists across hot restarts to clear stale DOM. + _staleHotRestartState = getJsProperty?>(domWindow, _staleHotRestartStore); + if (_staleHotRestartState == null) { + _staleHotRestartState = []; + setJsProperty( + domWindow, _staleHotRestartStore, _staleHotRestartState); + } + } + + /// Registers DOM elements that need to be cleaned up before hot restarting. + /// + /// [_setupHotRestart] must have been called prior to calling this method. + void _registerHotRestartCleanUp() { + registerHotRestartListener(() { + _resizeSubscription?.cancel(); + _localeSubscription?.cancel(); + _staleHotRestartState!.addAll([ + _glassPaneElement, + _styleElement, + _viewportMeta, + ]); + }); + } + + void _clearOnHotRestart() { + if (_staleHotRestartState!.isNotEmpty) { + for (final DomElement? element in _staleHotRestartState!) { + element?.remove(); + } + _staleHotRestartState!.clear(); + } + } + /// Don't unnecessarily move DOM nodes around. If a DOM node is /// already in the right place, skip DOM mutation. This is both faster and /// more correct, because moving DOM nodes loses internal state, such as @@ -109,6 +151,10 @@ class FlutterViewEmbedder { _sceneElement = sceneElement; _sceneHostElement!.append(sceneElement!); } + assert(() { + _clearOnHotRestart(); + return true; + }()); } /// The element that captures input events, such as pointer events. @@ -124,6 +170,8 @@ class FlutterViewEmbedder { HostNode? get glassPaneShadow => _glassPaneShadow; HostNode? _glassPaneShadow; + final DomElement rootElement = domDocument.body!; + static const String defaultFontStyle = 'normal'; static const String defaultFontWeight = 'normal'; static const double defaultFontSize = 14; @@ -132,42 +180,106 @@ class FlutterViewEmbedder { '$defaultFontStyle $defaultFontWeight ${defaultFontSize}px $defaultFontFamily'; void reset() { - // How was the current renderer selected? - const String rendererSelection = FlutterConfiguration.flutterWebAutoDetect - ? 'auto-selected' - : 'requested explicitly'; - - // Initializes the embeddingStrategy so it can host a single-view Flutter app. - _embeddingStrategy.initialize( - hostElementAttributes: { - 'flt-renderer': '${renderer.rendererTag} ($rendererSelection)', - 'flt-build-mode': buildMode, - // TODO(mdebbar): Disable spellcheck until changes in the framework and - // engine are complete. - 'spellcheck': 'false', - }, + final bool isWebKit = browserEngine == BrowserEngine.webkit; + + _styleElement?.remove(); + _styleElement = createDomHTMLStyleElement(); + _resourcesHost?.remove(); + _resourcesHost = null; + domDocument.head!.append(_styleElement!); + final DomCSSStyleSheet sheet = _styleElement!.sheet! as DomCSSStyleSheet; + applyGlobalCssRulesToSheet( + sheet, + browserEngine: browserEngine, + hasAutofillOverlay: browserHasAutofillOverlay(), + ); + + final DomHTMLBodyElement bodyElement = domDocument.body!; + + bodyElement.setAttribute( + 'flt-renderer', + '${renderer.rendererTag} (${FlutterConfiguration.flutterWebAutoDetect ? 'auto-selected' : 'requested explicitly'})', ); + bodyElement.setAttribute('flt-build-mode', buildMode); + + setElementStyle(bodyElement, 'position', 'fixed'); + setElementStyle(bodyElement, 'top', '0'); + setElementStyle(bodyElement, 'right', '0'); + setElementStyle(bodyElement, 'bottom', '0'); + setElementStyle(bodyElement, 'left', '0'); + setElementStyle(bodyElement, 'overflow', 'hidden'); + setElementStyle(bodyElement, 'padding', '0'); + setElementStyle(bodyElement, 'margin', '0'); + + // TODO(yjbanov): fix this when KVM I/O support is added. Currently scroll + // using drag, and text selection interferes. + setElementStyle(bodyElement, 'user-select', 'none'); + setElementStyle(bodyElement, '-webkit-user-select', 'none'); + setElementStyle(bodyElement, '-ms-user-select', 'none'); + setElementStyle(bodyElement, '-moz-user-select', 'none'); + + // This is required to prevent the browser from doing any native touch + // handling. If this is not done, the browser doesn't report 'pointermove' + // events properly. + setElementStyle(bodyElement, 'touch-action', 'none'); + + // These are intentionally outrageous font parameters to make sure that the + // apps fully specify their text styles. + setElementStyle(bodyElement, 'font', defaultCssFont); + setElementStyle(bodyElement, 'color', 'red'); + + // TODO(mdebbar): Disable spellcheck until changes in the framework and + // engine are complete. + bodyElement.spellcheck = false; + + for (final DomElement viewportMeta + in domDocument.head!.querySelectorAll('meta[name="viewport"]')) { + if (assertionsEnabled) { + // Filter out the meta tag that the engine placed on the page. This is + // to avoid UI flicker during hot restart. Hot restart will clean up the + // old meta tag synchronously with the first post-restart frame. + if (!viewportMeta.hasAttribute('flt-viewport')) { + print( + 'WARNING: found an existing tag. Flutter ' + 'Web uses its own viewport configuration for better compatibility ' + 'with Flutter. This tag will be replaced.', + ); + } + } + viewportMeta.remove(); + } - // Create and inject the [_glassPaneElement]. - final DomElement glassPaneElement = - domDocument.createElement(glassPaneTagName); + // This removes a previously created meta tag. Note, however, that this does + // not remove the meta tag during hot restart. Hot restart resets all static + // variables, so this will be null upon hot restart. Instead, this tag is + // removed by _clearOnHotRestart. + _viewportMeta?.remove(); + _viewportMeta = createDomHTMLMetaElement() + ..setAttribute('flt-viewport', '') + ..name = 'viewport' + ..content = 'width=device-width, initial-scale=1.0, ' + 'maximum-scale=1.0, user-scalable=no'; + domDocument.head!.append(_viewportMeta!); + + // IMPORTANT: the glass pane element must come after the scene element in the DOM node list so + // it can intercept input events. + _glassPaneElement?.remove(); + final DomElement glassPaneElement = domDocument.createElement(_glassPaneTagName); _glassPaneElement = glassPaneElement; + glassPaneElement.style + ..position = 'absolute' + ..top = '0' + ..right = '0' + ..bottom = '0' + ..left = '0'; - // This must be attached to the DOM now, so the engine can create a host - // node (ShadowDOM or a fallback) next. - // - // The embeddingStrategy will take care of cleaning up the glassPane on hot - // restart. - _embeddingStrategy.attachGlassPane(glassPaneElement); + // This must be appended to the body, so the engine can create a host node + // properly. + bodyElement.append(glassPaneElement); // Create a [HostNode] under the glass pane element, and attach everything // there, instead of directly underneath the glass panel. - // - // TODO(dit): clean HostNode, https://github.com/flutter/flutter/issues/116204 - final HostNode glassPaneElementHostNode = HostNode.create( - glassPaneElement, - defaultCssFont, - ); + final HostNode glassPaneElementHostNode = _createHostNode(glassPaneElement); _glassPaneShadow = glassPaneElementHostNode; // Don't allow the scene to receive pointer events. @@ -212,20 +324,67 @@ class FlutterViewEmbedder { } KeyboardBinding.initInstance(); - PointerBinding.initInstance( - glassPaneElement, - KeyboardBinding.instance!.converter, - ); + PointerBinding.initInstance(glassPaneElement, KeyboardBinding.instance!.converter); + + if (domWindow.visualViewport == null && isWebKit) { + // Older Safari versions sometimes give us bogus innerWidth/innerHeight + // values when the page loads. When it changes the values to correct ones + // it does not notify of the change via `onResize`. As a workaround, we + // set up a temporary periodic timer that polls innerWidth and triggers + // the resizeListener so that the framework can react to the change. + // + // Safari 13 has implemented visualViewport API so it doesn't need this + // timer. + // + // VisualViewport API is not enabled in Firefox as well. On the other hand + // Firefox returns correct values for innerHeight, innerWidth. + // Firefox also triggers domWindow.onResize therefore this timer does + // not need to be set up for Firefox. + final int initialInnerWidth = domWindow.innerWidth!.toInt(); + // Counts how many times screen size was checked. It is checked up to 5 + // times. + int checkCount = 0; + Timer.periodic(const Duration(milliseconds: 100), (Timer t) { + checkCount += 1; + if (initialInnerWidth != domWindow.innerWidth) { + // Window size changed. Notify. + t.cancel(); + _metricsDidChange(null); + } else if (checkCount > 5) { + // Checked enough times. Stop. + t.cancel(); + } + }); + } - window.onResize.listen(_metricsDidChange); + if (domWindow.visualViewport != null) { + _resizeSubscription = DomSubscription(domWindow.visualViewport!, 'resize', + allowInterop(_metricsDidChange)); + } else { + _resizeSubscription = DomSubscription(domWindow, 'resize', + allowInterop(_metricsDidChange)); + } + _localeSubscription = DomSubscription(domWindow, 'languagechange', + allowInterop(_languageDidChange)); + EnginePlatformDispatcher.instance.updateLocales(); + } + + // Creates a [HostNode] into a `root` [DomElement]. + HostNode _createHostNode(DomElement root) { + if (getJsProperty(root, 'attachShadow') != null) { + return ShadowDomHostNode(root); + } else { + // attachShadow not available, fall back to ElementHostNode. + return ElementHostNode(root); + } } /// The framework specifies semantics in physical pixels, but CSS uses /// logical pixels. To compensate, an inverse scale is injected at the root /// level. void updateSemanticsScreenProperties() { - _semanticsHostElement!.style - .setProperty('transform', 'scale(${1 / window.devicePixelRatio})'); + _semanticsHostElement!.style.setProperty('transform', + 'scale(${1 / domWindow.devicePixelRatio})'); } /// Called immediately after browser window metrics change. @@ -237,9 +396,8 @@ class FlutterViewEmbedder { /// /// Note: always check for rotations for a mobile device. Update the physical /// size if the change is caused by a rotation. - void _metricsDidChange(ui.Size? newSize) { + void _metricsDidChange(DomEvent? event) { updateSemanticsScreenProperties(); - // TODO(dit): Do not computePhysicalSize twice, https://github.com/flutter/flutter/issues/117036 if (isMobile && !window.isRotation() && textEditing.isEditing) { window.computeOnScreenKeyboardInsets(true); EnginePlatformDispatcher.instance.invokeOnMetricsChanged(); @@ -251,6 +409,12 @@ class FlutterViewEmbedder { } } + /// Called immediately after browser window language change. + void _languageDidChange(DomEvent event) { + EnginePlatformDispatcher.instance.updateLocales(); + ui.window.onLocaleChanged?.call(); + } + static const String orientationLockTypeAny = 'any'; static const String orientationLockTypeNatural = 'natural'; static const String orientationLockTypeLandscape = 'landscape'; @@ -323,6 +487,17 @@ class FlutterViewEmbedder { } } + /// The element corresponding to the only child of the root surface. + DomElement? get _rootApplicationElement { + final DomElement lastElement = rootElement.children.last; + for (final DomElement child in lastElement.children) { + if (child.tagName == 'FLT-SCENE') { + return child; + } + } + return null; + } + /// Add an element as a global resource to be referenced by CSS. /// /// This call create a global resource host element on demand and either @@ -332,18 +507,15 @@ class FlutterViewEmbedder { void addResource(DomElement element) { final bool isWebKit = browserEngine == BrowserEngine.webkit; if (_resourcesHost == null) { - final DomElement resourcesHost = domDocument - .createElement('flt-svg-filters') + _resourcesHost = createDomHTMLDivElement() ..style.visibility = 'hidden'; if (isWebKit) { - // The resourcesHost *must* be a sibling of the glassPaneElement. - _embeddingStrategy.attachResourcesHost(resourcesHost, - nextTo: glassPaneElement); + final DomNode bodyNode = domDocument.body!; + bodyNode.insertBefore(_resourcesHost!, bodyNode.firstChild); } else { - glassPaneShadow!.node - .insertBefore(resourcesHost, glassPaneShadow!.node.firstChild); + _glassPaneShadow!.node.insertBefore( + _resourcesHost!, _glassPaneShadow!.node.firstChild); } - _resourcesHost = resourcesHost; } _resourcesHost!.append(element); } @@ -356,6 +528,127 @@ class FlutterViewEmbedder { assert(element.parentNode == _resourcesHost); element.remove(); } + + String get currentHtml => _rootApplicationElement?.outerHTML ?? ''; +} + +// Applies the required global CSS to an incoming [DomCSSStyleSheet] `sheet`. +void applyGlobalCssRulesToSheet( + DomCSSStyleSheet sheet, { + required BrowserEngine browserEngine, + required bool hasAutofillOverlay, + String glassPaneTagName = FlutterViewEmbedder._glassPaneTagName, +}) { + final bool isWebKit = browserEngine == BrowserEngine.webkit; + final bool isFirefox = browserEngine == BrowserEngine.firefox; + // TODO(web): use more efficient CSS selectors; descendant selectors are slow. + // More info: https://csswizardry.com/2011/09/writing-efficient-css-selectors + + if (isFirefox) { + // For firefox set line-height, otherwise textx at same font-size will + // measure differently in ruler. + // + // - See: https://github.com/flutter/flutter/issues/44803 + sheet.insertRule( + 'flt-paragraph, flt-span {line-height: 100%;}', + sheet.cssRules.length.toInt(), + ); + } + + // This undoes browser's default painting and layout attributes of range + // input, which is used in semantics. + sheet.insertRule( + ''' + flt-semantics input[type=range] { + appearance: none; + -webkit-appearance: none; + width: 100%; + position: absolute; + border: none; + top: 0; + right: 0; + bottom: 0; + left: 0; + } + ''', + sheet.cssRules.length.toInt(), + ); + + if (isWebKit) { + sheet.insertRule( + 'flt-semantics input[type=range]::-webkit-slider-thumb {' + ' -webkit-appearance: none;' + '}', + sheet.cssRules.length.toInt()); + } + + if (isFirefox) { + sheet.insertRule( + 'input::-moz-selection {' + ' background-color: transparent;' + '}', + sheet.cssRules.length.toInt()); + sheet.insertRule( + 'textarea::-moz-selection {' + ' background-color: transparent;' + '}', + sheet.cssRules.length.toInt()); + } else { + // On iOS, the invisible semantic text field has a visible cursor and + // selection highlight. The following 2 CSS rules force everything to be + // transparent. + sheet.insertRule( + 'input::selection {' + ' background-color: transparent;' + '}', + sheet.cssRules.length.toInt()); + sheet.insertRule( + 'textarea::selection {' + ' background-color: transparent;' + '}', + sheet.cssRules.length.toInt()); + } + sheet.insertRule(''' + flt-semantics input, + flt-semantics textarea, + flt-semantics [contentEditable="true"] { + caret-color: transparent; + } + ''', sheet.cssRules.length.toInt()); + + // By default on iOS, Safari would highlight the element that's being tapped + // on using gray background. This CSS rule disables that. + if (isWebKit) { + sheet.insertRule(''' + $glassPaneTagName * { + -webkit-tap-highlight-color: transparent; + } + ''', sheet.cssRules.length.toInt()); + } + + // Hide placeholder text + sheet.insertRule( + ''' + .flt-text-editing::placeholder { + opacity: 0; + } + ''', + sheet.cssRules.length.toInt(), + ); + + // This css prevents an autofill overlay brought by the browser during + // text field autofill by delaying the transition effect. + // See: https://github.com/flutter/flutter/issues/61132. + if (browserHasAutofillOverlay()) { + sheet.insertRule(''' + .transparentTextEditing:-webkit-autofill, + .transparentTextEditing:-webkit-autofill:hover, + .transparentTextEditing:-webkit-autofill:focus, + .transparentTextEditing:-webkit-autofill:active { + -webkit-transition-delay: 99999s; + } + ''', sheet.cssRules.length.toInt()); + } } /// The embedder singleton. @@ -367,17 +660,15 @@ FlutterViewEmbedder get flutterViewEmbedder { assert(() { if (embedder == null) { throw StateError( - 'FlutterViewEmbedder not initialized. Call `ensureFlutterViewEmbedderInitialized()` ' - 'prior to calling the `flutterViewEmbedder` getter.'); + 'FlutterViewEmbedder not initialized. Call `ensureFlutterViewEmbedderInitialized()` ' + 'prior to calling the `flutterViewEmbedder` getter.' + ); } return true; }()); return embedder!; } - FlutterViewEmbedder? _flutterViewEmbedder; /// Initializes the [FlutterViewEmbedder], if it's not already initialized. -FlutterViewEmbedder ensureFlutterViewEmbedderInitialized() => - _flutterViewEmbedder ??= - FlutterViewEmbedder(hostElement: configuration.hostElement); +FlutterViewEmbedder ensureFlutterViewEmbedderInitialized() => _flutterViewEmbedder ??= FlutterViewEmbedder(); diff --git a/lib/web_ui/lib/src/engine/host_node.dart b/lib/web_ui/lib/src/engine/host_node.dart index f2a6c74cb39f5..9421ec2e018e4 100644 --- a/lib/web_ui/lib/src/engine/host_node.dart +++ b/lib/web_ui/lib/src/engine/host_node.dart @@ -5,7 +5,6 @@ import 'browser_detection.dart'; import 'dom.dart'; import 'embedder.dart'; -import 'safe_browser_api.dart'; import 'text_editing/text_editing.dart'; /// The interface required to host a flutter app in the DOM, and its tests. @@ -14,25 +13,7 @@ import 'text_editing/text_editing.dart'; /// (preferred Flutter rendering method) and [DomDocument] (fallback). /// /// Not to be confused with [DomDocumentOrShadowRoot]. -/// -/// This also handles the stylesheet that is applied to the different types of -/// HostNodes; for ShadowDOM there's not much to do, but for ElementNodes, the -/// stylesheet is "namespaced" by the `flt-glass-pane` prefix, so it "only" -/// affects things that Flutter web owns. abstract class HostNode { - /// Returns an appropriate HostNode for the given [root]. - /// - /// If `attachShadow` is supported, this returns a [ShadowDomHostNode], else - /// this will fall-back to an [ElementHostNode]. - factory HostNode.create(DomElement root, String defaultFont) { - if (getJsProperty(root, 'attachShadow') != null) { - return ShadowDomHostNode(root, defaultFont); - } else { - // attachShadow not available, fall back to ElementHostNode. - return ElementHostNode(root, defaultFont); - } - } - /// Retrieves the [DomElement] that currently has focus. /// /// See: @@ -107,12 +88,11 @@ abstract class HostNode { class ShadowDomHostNode implements HostNode { /// Build a HostNode by attaching a [DomShadowRoot] to the `root` element. /// - /// This also calls [applyGlobalCssRulesToSheet], with the [defaultFont] - /// to be used as the default font definition. - ShadowDomHostNode(DomElement root, String defaultFont) - : assert( + /// This also calls [applyGlobalCssRulesToSheet], defined in dom_renderer. + ShadowDomHostNode(DomElement root) : + assert( root.isConnected ?? true, - 'The `root` of a ShadowDomHostNode must be connected to the Document object or a ShadowRoot.' + 'The `root` of a ShadowDomHostNode must be connected to the Document object or a ShadowRoot.', ) { _shadow = root.attachShadow({ 'mode': 'open', @@ -121,16 +101,29 @@ class ShadowDomHostNode implements HostNode { 'delegatesFocus': false, }); - final DomHTMLStyleElement shadowRootStyleElement = - createDomHTMLStyleElement(); - shadowRootStyleElement.id = 'flt-internals-stylesheet'; + final DomHTMLStyleElement shadowRootStyleElement = createDomHTMLStyleElement(); // The shadowRootStyleElement must be appended to the DOM, or its `sheet` will be null later. _shadow.appendChild(shadowRootStyleElement); + + // TODO(dit): Apply only rules for the shadow root applyGlobalCssRulesToSheet( shadowRootStyleElement.sheet! as DomCSSStyleSheet, + browserEngine: browserEngine, hasAutofillOverlay: browserHasAutofillOverlay(), - defaultCssFont: defaultFont, ); + + // Removes password reveal icon for text inputs in Edge browsers. + // Style tag needs to be injected into DOM because non-Edge + // browsers will crash trying to parse -ms-reveal CSS selectors if added via + // sheet.insertRule(). + // See: https://github.com/flutter/flutter/issues/83695 + if (isEdge) { + final DomHTMLStyleElement edgeStyleElement = createDomHTMLStyleElement(); + + edgeStyleElement.id = 'ms-reveal'; + edgeStyleElement.innerText = 'input::-ms-reveal {display: none;}'; + _shadow.appendChild(edgeStyleElement); + } } late DomShadowRoot _shadow; @@ -171,20 +164,7 @@ class ShadowDomHostNode implements HostNode { /// being constructed. class ElementHostNode implements HostNode { /// Build a HostNode by attaching a child [DomElement] to the `root` element. - ElementHostNode(DomElement root, String defaultFont) { - // Append the stylesheet here, so this class is completely symmetric to the - // ShadowDOM version. - final DomHTMLStyleElement styleElement = createDomHTMLStyleElement(); - styleElement.id = 'flt-internals-stylesheet'; - // The styleElement must be appended to the DOM, or its `sheet` will be null later. - root.appendChild(styleElement); - applyGlobalCssRulesToSheet( - styleElement.sheet! as DomCSSStyleSheet, - hasAutofillOverlay: browserHasAutofillOverlay(), - cssSelectorPrefix: FlutterViewEmbedder.glassPaneTagName, - defaultCssFont: defaultFont, - ); - + ElementHostNode(DomElement root) { _element = domDocument.createElement('flt-element-host-node'); root.appendChild(_element); } @@ -220,144 +200,3 @@ class ElementHostNode implements HostNode { @override void appendAll(Iterable nodes) => nodes.forEach(append); } - -// Applies the required global CSS to an incoming [DomCSSStyleSheet] `sheet`. -void applyGlobalCssRulesToSheet( - DomCSSStyleSheet sheet, { - required bool hasAutofillOverlay, - String cssSelectorPrefix = '', - required String defaultCssFont, -}) { - // TODO(web): use more efficient CSS selectors; descendant selectors are slow. - // More info: https://csswizardry.com/2011/09/writing-efficient-css-selectors - - // These are intentionally outrageous font parameters to make sure that the - // apps fully specify their text styles. - // - // Fixes #115216 by ensuring that our parameters only affect the flt-scene-host children. - sheet.insertRule(''' - $cssSelectorPrefix flt-scene-host { - color: red; - font: $defaultCssFont; - } - ''', sheet.cssRules.length); - - // By default on iOS, Safari would highlight the element that's being tapped - // on using gray background. This CSS rule disables that. - if (isSafari) { - sheet.insertRule(''' - $cssSelectorPrefix * { - -webkit-tap-highlight-color: transparent; - } - ''', sheet.cssRules.length); - } - - if (isFirefox) { - // For firefox set line-height, otherwise text at same font-size will - // measure differently in ruler. - // - // - See: https://github.com/flutter/flutter/issues/44803 - sheet.insertRule(''' - $cssSelectorPrefix flt-paragraph, - $cssSelectorPrefix flt-span { - line-height: 100%; - } - ''', sheet.cssRules.length); - } - - // This undoes browser's default painting and layout attributes of range - // input, which is used in semantics. - sheet.insertRule(''' - $cssSelectorPrefix flt-semantics input[type=range] { - appearance: none; - -webkit-appearance: none; - width: 100%; - position: absolute; - border: none; - top: 0; - right: 0; - bottom: 0; - left: 0; - } - ''', sheet.cssRules.length); - - if (isSafari) { - sheet.insertRule(''' - $cssSelectorPrefix flt-semantics input[type=range]::-webkit-slider-thumb { - -webkit-appearance: none; - } - ''', sheet.cssRules.length); - } - - // The invisible semantic text field may have a visible cursor and selection - // highlight. The following 2 CSS rules force everything to be transparent. - sheet.insertRule(''' - $cssSelectorPrefix input::selection { - background-color: transparent; - } - ''', sheet.cssRules.length); - sheet.insertRule(''' - $cssSelectorPrefix textarea::selection { - background-color: transparent; - } - ''', sheet.cssRules.length); - - sheet.insertRule(''' - $cssSelectorPrefix flt-semantics input, - $cssSelectorPrefix flt-semantics textarea, - $cssSelectorPrefix flt-semantics [contentEditable="true"] { - caret-color: transparent; - } - ''', sheet.cssRules.length); - - // Hide placeholder text - sheet.insertRule(''' - $cssSelectorPrefix .flt-text-editing::placeholder { - opacity: 0; - } - ''', sheet.cssRules.length); - - // This css prevents an autofill overlay brought by the browser during - // text field autofill by delaying the transition effect. - // See: https://github.com/flutter/flutter/issues/61132. - if (browserHasAutofillOverlay()) { - sheet.insertRule(''' - $cssSelectorPrefix .transparentTextEditing:-webkit-autofill, - $cssSelectorPrefix .transparentTextEditing:-webkit-autofill:hover, - $cssSelectorPrefix .transparentTextEditing:-webkit-autofill:focus, - $cssSelectorPrefix .transparentTextEditing:-webkit-autofill:active { - -webkit-transition-delay: 99999s; - } - ''', sheet.cssRules.length); - } - - // Removes password reveal icon for text inputs in Edge browsers. - // Non-Edge browsers will crash trying to parse -ms-reveal CSS selector, - // so we guard it behind an isEdge check. - // Fixes: https://github.com/flutter/flutter/issues/83695 - if (isEdge) { - // We try-catch this, because in testing, we fake Edge via the UserAgent, - // so the below will throw an exception (because only real Edge understands - // the ::-ms-reveal pseudo-selector). - try { - sheet.insertRule(''' - $cssSelectorPrefix input::-ms-reveal { - display: none; - } - ''', sheet.cssRules.length); - } on DomException catch (e) { - // Browsers that don't understand ::-ms-reveal throw a DOMException - // of type SyntaxError. - domWindow.console.warn(e); - // Add a fake rule if our code failed because we're under testing - assert(() { - sheet.insertRule(''' - $cssSelectorPrefix input.fallback-for-fakey-browser-in-ci { - display: none; - } - ''', sheet.cssRules.length); - return true; - }()); - } - } -} diff --git a/lib/web_ui/lib/src/engine/html/scene.dart b/lib/web_ui/lib/src/engine/html/scene.dart index 564ad97d00fce..9beba6a4a6537 100644 --- a/lib/web_ui/lib/src/engine/html/scene.dart +++ b/lib/web_ui/lib/src/engine/html/scene.dart @@ -6,7 +6,6 @@ import 'package:ui/ui.dart' as ui; import '../dom.dart'; import '../vector_math.dart'; -import '../window.dart'; import 'surface.dart'; class SurfaceScene implements ui.Scene { @@ -46,10 +45,12 @@ class PersistedScene extends PersistedContainerSurface { @override void recomputeTransformAndClip() { // The scene clip is the size of the entire window. - final ui.Size screen = window.physicalSize / window.devicePixelRatio; - // Question: why is the above a logical size, rather than a physical size - // like everywhere else in the metrics? - localClipBounds = ui.Rect.fromLTRB(0, 0, screen.width, screen.height); + // TODO(yjbanov): in the add2app scenario where we might be hosted inside + // a custom element, this will be different. We will need to + // update this code when we add add2app support. + final double screenWidth = domWindow.innerWidth!; + final double screenHeight = domWindow.innerHeight!; + localClipBounds = ui.Rect.fromLTRB(0, 0, screenWidth, screenHeight); projectedClip = null; } diff --git a/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/lib/web_ui/lib/src/engine/platform_dispatcher.dart index 480b367a2be3c..e003513953583 100644 --- a/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -84,7 +84,6 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { _addBrightnessMediaQueryListener(); HighContrastSupport.instance.addListener(_updateHighContrast); _addFontSizeObserver(); - _addLocaleChangedListener(); registerHotRestartListener(dispose); } @@ -113,7 +112,6 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { void dispose() { _removeBrightnessMediaQueryListener(); _disconnectFontSizeObserver(); - _removeLocaleChangedListener(); HighContrastSupport.instance.removeListener(_updateHighContrast); } @@ -745,29 +743,6 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { @override List get locales => configuration.locales; - // A subscription to the 'languagechange' event of 'window'. - DomSubscription? _onLocaleChangedSubscription; - - /// Configures the [_onLocaleChangedSubscription]. - void _addLocaleChangedListener() { - if (_onLocaleChangedSubscription != null) { - return; - } - updateLocales(); // First time, for good measure. - _onLocaleChangedSubscription = - DomSubscription(domWindow, 'languagechange', allowInterop((DomEvent _) { - // Update internal config, then propagate the changes. - updateLocales(); - invokeOnLocaleChanged(); - })); - } - - /// Removes the [_onLocaleChangedSubscription]. - void _removeLocaleChangedListener() { - _onLocaleChangedSubscription?.cancel(); - _onLocaleChangedSubscription = null; - } - /// Performs the platform-native locale resolution. /// /// Each platform may return different results. diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index d53a5c8cefd75..1c3468597bc47 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -12,17 +12,13 @@ import '../engine.dart' show registerHotRestartListener; import 'browser_detection.dart'; import 'dom.dart'; import 'platform_dispatcher.dart'; -import 'pointer_binding/event_position_helper.dart'; import 'pointer_converter.dart'; import 'safe_browser_api.dart'; import 'semantics.dart'; -/// Set this flag to true to log all the browser events. +/// Set this flag to true to see all the fired events in the console. const bool _debugLogPointerEvents = false; -/// Set this to true to log all the events sent to the Flutter framework. -const bool _debugLogFlutterEvents = false; - /// The signature of a callback that handles pointer events. typedef _PointerDataCallback = void Function(Iterable); @@ -151,16 +147,13 @@ class PointerBinding { _pointerDataConverter.clearPointerState(); } - // TODO(dit): remove old API fallbacks, https://github.com/flutter/flutter/issues/116141 _BaseAdapter _createAdapter() { if (_detector.hasPointerEvents) { return _PointerAdapter(_onPointerData, glassPaneElement, _pointerDataConverter, _keyboardConverter); } - // Fallback for Safari Mobile < 13. To be removed. if (_detector.hasTouchEvents) { return _TouchAdapter(_onPointerData, glassPaneElement, _pointerDataConverter, _keyboardConverter); } - // Fallback for Safari Desktop < 13. To be removed. if (_detector.hasMouseEvents) { return _MouseAdapter(_onPointerData, glassPaneElement, _pointerDataConverter, _keyboardConverter); } @@ -169,11 +162,6 @@ class PointerBinding { void _onPointerData(Iterable data) { final ui.PointerDataPacket packet = ui.PointerDataPacket(data: data.toList()); - if (_debugLogFlutterEvents) { - for(final ui.PointerData datum in data) { - print('fw:${datum.change} ${datum.physicalX},${datum.physicalY}'); - } - } EnginePlatformDispatcher.instance.invokeOnPointerDataPacket(packet); } } @@ -312,10 +300,9 @@ abstract class _BaseAdapter { if (_debugLogPointerEvents) { if (domInstanceOfString(event, 'PointerEvent')) { final DomPointerEvent pointerEvent = event as DomPointerEvent; - final ui.Offset offset = computeEventOffsetToTarget(event, glassPaneElement); print('${pointerEvent.type} ' - '${offset.dx.toStringAsFixed(1)},' - '${offset.dy.toStringAsFixed(1)}'); + '${pointerEvent.clientX.toStringAsFixed(1)},' + '${pointerEvent.clientY.toStringAsFixed(1)}'); } else { print(event.type); } @@ -452,7 +439,6 @@ mixin _WheelEventListenerMixin on _BaseAdapter { } final List data = []; - final ui.Offset offset = computeEventOffsetToTarget(event, glassPaneElement); _pointerDataConverter.convert( data, change: ui.PointerChange.hover, @@ -460,8 +446,8 @@ mixin _WheelEventListenerMixin on _BaseAdapter { kind: kind, signalKind: ui.PointerSignalKind.scroll, device: _mouseDeviceId, - physicalX: offset.dx * ui.window.devicePixelRatio, - physicalY: offset.dy * ui.window.devicePixelRatio, + physicalX: event.clientX * ui.window.devicePixelRatio, + physicalY: event.clientY * ui.window.devicePixelRatio, buttons: event.buttons!.toInt(), pressure: 1.0, pressureMax: 1.0, @@ -748,7 +734,6 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { _callback(pointerData); }); - // Why `domWindow` you ask? See this fiddle: https://jsfiddle.net/ditman/7towxaqp _addPointerEventListener(domWindow, 'pointermove', (DomPointerEvent event) { final int device = _getPointerId(event); final _ButtonSanitizer sanitizer = _ensureSanitizer(device); @@ -776,7 +761,6 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { } }, useCapture: false, checkModifiers: false); - // TODO(dit): This must happen in the glassPane, https://github.com/flutter/flutter/issues/116561 _addPointerEventListener(domWindow, 'pointerup', (DomPointerEvent event) { final int device = _getPointerId(event); if (_hasSanitizer(device)) { @@ -790,8 +774,6 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { } }); - // TODO(dit): Synthesize a "cancel" event when 'pointerup' happens outside of the glassPane, https://github.com/flutter/flutter/issues/116561 - // A browser fires cancel event if it concludes the pointer will no longer // be able to generate events (example: device is deactivated) _addPointerEventListener(glassPaneElement, 'pointercancel', (DomPointerEvent event) { @@ -824,7 +806,6 @@ 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, glassPaneElement); _pointerDataConverter.convert( data, change: details.change, @@ -832,8 +813,8 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { kind: kind, signalKind: ui.PointerSignalKind.none, device: _getPointerId(event), - physicalX: offset.dx * ui.window.devicePixelRatio, - physicalY: offset.dy * ui.window.devicePixelRatio, + physicalX: event.clientX * ui.window.devicePixelRatio, + physicalY: event.clientY * ui.window.devicePixelRatio, buttons: details.buttons, pressure: pressure == null ? 0.0 : pressure.toDouble(), pressureMax: 1.0, @@ -853,10 +834,6 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { return coalescedEvents; } } - // Important: coalesced events lack the `eventTarget` property (because they're - // being handled in a deferred way). - // - // See the "Note" here: https://developer.mozilla.org/en-US/docs/Web/API/Event/currentTarget return [event]; } @@ -1020,7 +997,6 @@ class _TouchAdapter extends _BaseAdapter { timeStamp: timeStamp, signalKind: ui.PointerSignalKind.none, device: touch.identifier!.toInt(), - // Account for zoom/scroll in the TouchEvent physicalX: touch.clientX * ui.window.devicePixelRatio, physicalY: touch.clientY * ui.window.devicePixelRatio, buttons: pressed ? _kPrimaryMouseButton : 0, @@ -1104,7 +1080,6 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin { _callback(pointerData); }); - // Why `domWindow` you ask? See this fiddle: https://jsfiddle.net/ditman/7towxaqp _addMouseEventListener(domWindow, 'mousemove', (DomMouseEvent event) { final List pointerData = []; final _SanitizedDetails? up = _sanitizer.sanitizeMissingRightClickUp(buttons: event.buttons!.toInt()); @@ -1125,7 +1100,6 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin { } }, useCapture: false); - // TODO(dit): This must happen in the glassPane, https://github.com/flutter/flutter/issues/116561 _addMouseEventListener(domWindow, 'mouseup', (DomMouseEvent event) { final List pointerData = []; final _SanitizedDetails? sanitizedDetails = _sanitizer.sanitizeUpEvent(buttons: event.buttons?.toInt()); @@ -1150,7 +1124,6 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin { assert(data != null); assert(event != null); assert(details != null); - final ui.Offset offset = computeEventOffsetToTarget(event, glassPaneElement); _pointerDataConverter.convert( data, change: details.change, @@ -1158,8 +1131,8 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin { kind: ui.PointerDeviceKind.mouse, signalKind: ui.PointerSignalKind.none, device: _mouseDeviceId, - physicalX: offset.dx * ui.window.devicePixelRatio, - physicalY: offset.dy * ui.window.devicePixelRatio, + physicalX: event.clientX * ui.window.devicePixelRatio, + physicalY: event.clientY * ui.window.devicePixelRatio, buttons: details.buttons, pressure: 1.0, 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 deleted file mode 100644 index 6be2b9ccc2bcb..0000000000000 --- a/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart +++ /dev/null @@ -1,113 +0,0 @@ -// 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. - -import 'package:ui/ui.dart' as ui show Offset; - -import '../dom.dart'; -import '../semantics.dart' show EngineSemanticsOwner; - -/// Returns an [ui.Offset] of the position of [event], relative to the position of [actualTarget]. -/// -/// The offset is *not* multiplied by DPR or anything else, it's the closest -/// to what the DOM would return if we had currentTarget readily available. -/// -/// This needs an `actualTarget`, because the `event.currentTarget` (which is what -/// this would really need to use) gets lost when the `event` comes from a "coalesced" -/// event. -/// -/// 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) { - // On top of a platform view - if (event.target != actualTarget) { - return _computeOffsetOnPlatformView(event, actualTarget); - } - // On a TalkBack event - if (EngineSemanticsOwner.instance.semanticsEnabled && event.offsetX == 0 && event.offsetY == 0) { - return _computeOffsetForTalkbackEvent(event, actualTarget); - } - // Return the offsetX/Y in the normal case. - // (This works with 3D translations of the parent element.) - return ui.Offset(event.offsetX, event.offsetY); -} - -/// Computes the event offset when hovering over a platformView. -/// -/// This still uses offsetX/Y, but adds the offset from the top/left corner of the -/// platform view to the glass pane (`actualTarget`). -/// -/// ×--FlutterView(actualTarget)--------------+ -/// |\ | -/// | x1,y1 | -/// | | -/// | | -/// | ×-PlatformView(target)---------+ | -/// | |\ | | -/// | | x2,y2 | | -/// | | | | -/// | | × (event) | | -/// | | \ | | -/// | | offsetX, offsetY | | -/// | | (Relative to PlatformView) | | -/// | +------------------------------+ | -/// +-----------------------------------------+ -/// -/// Offset between PlatformView and FlutterView (xP, yP) = (x2 - x1, y2 - y1) -/// -/// Event offset relative to FlutterView = (offsetX + xP, offsetY + yP) -// TODO(dit): Make this understand 3D transforms, https://github.com/flutter/flutter/issues/117091 -ui.Offset _computeOffsetOnPlatformView(DomMouseEvent event, DomElement actualTarget) { - final DomElement target = event.target! as DomElement; - final DomRect targetRect = target.getBoundingClientRect(); - final DomRect actualTargetRect = actualTarget.getBoundingClientRect(); - final double offsetTop = targetRect.y - actualTargetRect.y; - final double offsetLeft = targetRect.x - actualTargetRect.x; - return ui.Offset(event.offsetX + offsetLeft, event.offsetY + offsetTop); -} - -/// Computes the event offset when TalkBack is firing the event. -/// -/// In this case, we need to use the clientX/Y position of the event (which are -/// relative to the absolute top-left corner of the page, including scroll), then -/// deduct the offsetLeft/Top from every offsetParent of the `actualTarget`. -/// -/// ×-Page----║-------------------------------+ -/// | ║ | -/// | ×-------║--------offsetParent(s)-----+ | -/// | |\ | | -/// | | offsetLeft, offsetTop | | -/// | | | | -/// | | | | -/// | | ×-----║-------------actualTarget-+ | | -/// | | | | | | -/// ═════ × ─ (scrollLeft, scrollTop)═ ═ ═ -/// | | | | | | -/// | | | × | | | -/// | | | \ | | | -/// | | | clientX, clientY | | | -/// | | | (Relative to Page + Scroll) | | | -/// | | +-----║--------------------------+ | | -/// | +-------║----------------------------+ | -/// +---------║-------------------------------+ -/// -/// Computing the offset of the event relative to the actualTarget requires to -/// compute the clientX, clientY of the actualTarget. To do that, we iterate -/// up the offsetParent elements of actualTarget adding their offset and scroll -/// positions. Finally, we deduct that from clientX, clientY of the event. -// TODO(dit): Make this understand 3D transforms, https://github.com/flutter/flutter/issues/117091 -ui.Offset _computeOffsetForTalkbackEvent(DomMouseEvent event, DomElement actualTarget) { - assert(EngineSemanticsOwner.instance.semanticsEnabled); - // Use clientX/clientY as the position of the event (this is relative to - // the top left of the page, including scroll) - double offsetX = event.clientX; - double offsetY = event.clientY; - // Compute the scroll offset of actualTarget - DomHTMLElement parent = actualTarget as DomHTMLElement; - while(parent.offsetParent != null){ - offsetX -= parent.offsetLeft - parent.scrollLeft; - offsetY -= parent.offsetTop - parent.scrollTop; - parent = parent.offsetParent!; - } - return ui.Offset(offsetX, offsetY); -} diff --git a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart b/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart deleted file mode 100644 index ce9b6b5b7a290..0000000000000 --- a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart +++ /dev/null @@ -1,88 +0,0 @@ -// 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. - -import 'dart:async'; - -import 'package:ui/src/engine/dom.dart'; -import 'package:ui/src/engine/window.dart'; -import 'package:ui/ui.dart' as ui show Size; - -import 'dimensions_provider.dart'; - -/// This class provides observable, real-time dimensions of a host element. -/// -/// All the measurements returned from this class are potentially *expensive*, -/// and should be cached as needed. Every call to every method on this class -/// WILL perform actual DOM measurements. -class CustomElementDimensionsProvider extends DimensionsProvider { - /// Creates a [CustomElementDimensionsProvider] from a [_hostElement]. - CustomElementDimensionsProvider(this._hostElement) { - // Hook up a resize observer on the hostElement (if supported!). - _hostElementResizeObserver = createDomResizeObserver(( - List entries, - DomResizeObserver _, - ) { - entries - .map((DomResizeObserverEntry entry) => - ui.Size(entry.contentRect.width, entry.contentRect.height)) - .forEach(_broadcastSize); - }); - - assert(() { - if (_hostElementResizeObserver == null) { - domWindow.console.warn('ResizeObserver API not supported. ' - 'Flutter will not resize with its hostElement.'); - } - return true; - }()); - - _hostElementResizeObserver?.observe(_hostElement); - } - - // The host element that will be used to retrieve (and observe) app size measurements. - final DomElement _hostElement; - - // Handle resize events - late DomResizeObserver? _hostElementResizeObserver; - final StreamController _onResizeStreamController = - StreamController.broadcast(); - - // Broadcasts the last seen `Size`. - void _broadcastSize(ui.Size size) { - _onResizeStreamController.add(size); - } - - @override - void close() { - _hostElementResizeObserver?.disconnect(); - // ignore:unawaited_futures - _onResizeStreamController.close(); - } - - @override - Stream get onResize => _onResizeStreamController.stream; - - @override - ui.Size computePhysicalSize() { - final double devicePixelRatio = getDevicePixelRatio(); - - return ui.Size( - _hostElement.clientWidth * devicePixelRatio, - _hostElement.clientHeight * devicePixelRatio, - ); - } - - @override - WindowPadding computeKeyboardInsets( - double physicalHeight, - bool isEditingOnMobile, - ) { - return const WindowPadding( - top: 0, - right: 0, - bottom: 0, - left: 0, - ); - } -} diff --git a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart b/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart deleted file mode 100644 index efabff6bb3e07..0000000000000 --- a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart +++ /dev/null @@ -1,65 +0,0 @@ -// 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. - -import 'dart:async'; - -import 'package:ui/src/engine/window.dart'; -import 'package:ui/ui.dart' as ui show Size; - -import '../../dom.dart'; -import 'custom_element_dimensions_provider.dart'; -import 'full_page_dimensions_provider.dart'; - -/// This class provides the dimensions of the "viewport" in which the app is rendered. -/// -/// Similarly to the `EmbeddingStrategy`, this class is specialized to handle -/// different sources of information: -/// -/// * [FullPageDimensionsProvider] - The default behavior, uses the VisualViewport -/// API to measure, and react to, the dimensions of the full browser window. -/// * [CustomElementDimensionsProvider] - Uses a custom html Element as the source -/// of dimensions, and the ResizeObserver to notify the app of changes. -/// -/// All the measurements returned from this class are potentially *expensive*, -/// and should be cached as needed. Every call to every method on this class -/// WILL perform actual DOM measurements. -abstract class DimensionsProvider { - DimensionsProvider(); - - /// Creates the appropriate DimensionsProvider depending on the incoming [hostElement]. - factory DimensionsProvider.create({DomElement? hostElement}) { - if (hostElement != null) { - return CustomElementDimensionsProvider(hostElement); - } else { - return FullPageDimensionsProvider(); - } - } - - /// Returns the DPI reported by the browser. - double getDevicePixelRatio() { - // This is overridable in tests. - return window.devicePixelRatio; - } - - /// Returns the [ui.Size] of the "viewport". - /// - /// This function is expensive. It triggers browser layout if there are - /// pending DOM writes. - ui.Size computePhysicalSize(); - - /// Returns the [WindowPadding] of the keyboard insets (if present). - WindowPadding computeKeyboardInsets( - double physicalHeight, - bool isEditingOnMobile, - ); - - /// Returns a Stream with the changes to [ui.Size] (when cheap to get). - Stream get onResize; - - /// Clears any resources grabbed by the DimensionsProvider instance. - /// - /// All internal event handlers will be disconnected, and the [onResize] Stream - /// will be closed. - void close(); -} diff --git a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart b/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart deleted file mode 100644 index 9db769fd1707d..0000000000000 --- a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart +++ /dev/null @@ -1,123 +0,0 @@ -// 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. - -import 'dart:async'; - -import 'package:js/js.dart'; -import 'package:ui/src/engine/browser_detection.dart'; -import 'package:ui/src/engine/dom.dart'; -import 'package:ui/src/engine/window.dart'; -import 'package:ui/ui.dart' as ui show Size; - -import 'dimensions_provider.dart'; - -/// This class provides the real-time dimensions of a "full page" viewport. -/// -/// All the measurements returned from this class are potentially *expensive*, -/// and should be cached as needed. Every call to every method on this class -/// WILL perform actual DOM measurements. -class FullPageDimensionsProvider extends DimensionsProvider { - /// Constructs a global [FullPageDimensionsProvider]. - /// - /// Doesn't need any parameters, because all the measurements come from the - /// globally available [DomVisualViewport]. - FullPageDimensionsProvider() { - // Determine what 'resize' event we'll be listening to. - // This is needed for older browsers (Firefox < 91, Safari < 13) - // TODO(dit): Clean this up, https://github.com/flutter/flutter/issues/117105 - final DomEventTarget resizeEventTarget = - domWindow.visualViewport ?? domWindow; - - // Subscribe to the 'resize' event, and convert it to a ui.Size stream. - _domResizeSubscription = DomSubscription( - resizeEventTarget, - 'resize', - allowInterop(_onVisualViewportResize), - ); - } - - late DomSubscription _domResizeSubscription; - final StreamController _onResizeStreamController = - StreamController.broadcast(); - - void _onVisualViewportResize(DomEvent event) { - // `event` doesn't contain any size information (as opposed to the custom - // element resize observer). If it did, we could broadcast the physical - // dimensions here and never have to re-measure the app, until the next - // resize event triggers. - // Would it be too costly to broadcast the computed physical size from here, - // and then never re-measure the app? - // Related: https://github.com/flutter/flutter/issues/117036 - _onResizeStreamController.add(null); - } - - @override - void close() { - _domResizeSubscription.cancel(); - // ignore:unawaited_futures - _onResizeStreamController.close(); - } - - @override - Stream get onResize => _onResizeStreamController.stream; - - @override - ui.Size computePhysicalSize() { - late double windowInnerWidth; - late double windowInnerHeight; - final DomVisualViewport? viewport = domWindow.visualViewport; - final double devicePixelRatio = getDevicePixelRatio(); - - if (viewport != null) { - if (operatingSystem == OperatingSystem.iOs) { - /// Chrome on iOS reports incorrect viewport.height when app - /// starts in portrait orientation and the phone is rotated to - /// landscape. - /// - /// We instead use documentElement clientWidth/Height to read - /// accurate physical size. VisualViewport api is only used during - /// text editing to make sure inset is correctly reported to - /// framework. - final double docWidth = domDocument.documentElement!.clientWidth; - final double docHeight = domDocument.documentElement!.clientHeight; - windowInnerWidth = docWidth * devicePixelRatio; - windowInnerHeight = docHeight * devicePixelRatio; - } else { - windowInnerWidth = viewport.width! * devicePixelRatio; - windowInnerHeight = viewport.height! * devicePixelRatio; - } - } else { - windowInnerWidth = domWindow.innerWidth! * devicePixelRatio; - windowInnerHeight = domWindow.innerHeight! * devicePixelRatio; - } - return ui.Size( - windowInnerWidth, - windowInnerHeight, - ); - } - - @override - WindowPadding computeKeyboardInsets( - double physicalHeight, - bool isEditingOnMobile, - ) { - final double devicePixelRatio = getDevicePixelRatio(); - final DomVisualViewport? viewport = domWindow.visualViewport; - late double windowInnerHeight; - - if (viewport != null) { - if (operatingSystem == OperatingSystem.iOs && !isEditingOnMobile) { - windowInnerHeight = - domDocument.documentElement!.clientHeight * devicePixelRatio; - } else { - windowInnerHeight = viewport.height! * devicePixelRatio; - } - } else { - windowInnerHeight = domWindow.innerHeight! * devicePixelRatio; - } - final double bottomPadding = physicalHeight - windowInnerHeight; - - return WindowPadding(bottom: bottomPadding, left: 0, right: 0, top: 0); - } -} 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 deleted file mode 100644 index ecf92bb3d8956..0000000000000 --- a/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart +++ /dev/null @@ -1,57 +0,0 @@ -// 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. - -import 'package:ui/src/engine/dom.dart'; - -import 'embedding_strategy.dart'; - -/// An [EmbeddingStrategy] that renders flutter inside a target host element. -/// -/// This strategy attempts to minimize DOM modifications outside of the host -/// element, so it plays "nice" with other web frameworks. -class CustomElementEmbeddingStrategy extends EmbeddingStrategy { - /// Creates a [CustomElementEmbeddingStrategy] to embed a Flutter view into [_hostElement]. - CustomElementEmbeddingStrategy(this._hostElement) { - _hostElement.clearChildren(); - } - - /// The target element in which this strategy will embedd Flutter. - final DomElement _hostElement; - - @override - void initialize({ - Map? hostElementAttributes, - }) { - // ignore:avoid_function_literals_in_foreach_calls - hostElementAttributes?.entries.forEach((MapEntry entry) { - _setHostAttribute(entry.key, entry.value); - }); - _setHostAttribute('flt-embedding', 'custom-element'); - } - - @override - void attachGlassPane(DomElement glassPaneElement) { - glassPaneElement - ..style.width = '100%' - ..style.height = '100%' - ..style.display = 'block' - ..style.overflow = 'hidden' - ..style.position = 'relative'; - - _hostElement.appendChild(glassPaneElement); - - registerElementForCleanup(glassPaneElement); - } - - @override - void attachResourcesHost(DomElement resourceHost, {DomElement? nextTo}) { - _hostElement.insertBefore(resourceHost, nextTo); - - registerElementForCleanup(resourceHost); - } - - void _setHostAttribute(String name, String value) { - _hostElement.setAttribute(name, value); - } -} 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 deleted file mode 100644 index bb1c9361ae2a8..0000000000000 --- a/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/embedding_strategy.dart +++ /dev/null @@ -1,58 +0,0 @@ -// 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. - -import 'package:meta/meta.dart'; - -import 'package:ui/src/engine/dom.dart'; -import 'package:ui/src/engine/view_embedder/hot_restart_cache_handler.dart'; - -import 'custom_element_embedding_strategy.dart'; -import 'full_page_embedding_strategy.dart'; - -/// Controls how a Flutter app is placed, sized and measured on the page. -/// -/// The base class handles general behavior (like hot-restart cleanup), and then -/// each specialization enables different types of DOM embeddings: -/// -/// * [FullPageEmbeddingStrategy] - The default behavior, where flutter takes -/// control of the whole page. -/// * [CustomElementEmbeddingStrategy] - Flutter is rendered inside a custom host -/// element, provided by the web app programmer through the engine -/// initialization. -abstract class EmbeddingStrategy { - EmbeddingStrategy() { - // Initialize code to handle hot-restart (debug only). - assert(() { - _hotRestartCache = HotRestartCacheHandler(); - return true; - }()); - } - - factory EmbeddingStrategy.create({DomElement? hostElement}) { - if (hostElement != null) { - return CustomElementEmbeddingStrategy(hostElement); - } else { - return FullPageEmbeddingStrategy(); - } - } - - /// Keeps a list of elements to be cleaned up at hot-restart. - HotRestartCacheHandler? _hotRestartCache; - - void initialize({ - Map? hostElementAttributes, - }); - - /// Attaches the glassPane element into the hostElement. - void attachGlassPane(DomElement glassPaneElement); - - /// Attaches the resourceHost element into the hostElement. - void attachResourcesHost(DomElement resourceHost, {DomElement? nextTo}); - - /// Registers a [DomElement] to be cleaned up after hot restart. - @mustCallSuper - void registerElementForCleanup(DomElement element) { - _hotRestartCache?.registerElement(element); - } -} 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 deleted file mode 100644 index 009b6aef4b8a0..0000000000000 --- a/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/full_page_embedding_strategy.dart +++ /dev/null @@ -1,108 +0,0 @@ -// 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. - -import 'package:ui/src/engine/dom.dart'; -import 'package:ui/src/engine/util.dart' show assertionsEnabled, setElementStyle; - -import 'embedding_strategy.dart'; - -/// An [EmbeddingStrategy] that takes over the whole web page. -/// -/// This strategy takes over the element, modifies the viewport meta-tag, -/// and ensures that the root Flutter view covers the whole screen. -class FullPageEmbeddingStrategy extends EmbeddingStrategy { - @override - void initialize({ - Map? hostElementAttributes, - }) { - // ignore:avoid_function_literals_in_foreach_calls - hostElementAttributes?.entries.forEach((MapEntry entry) { - _setHostAttribute(entry.key, entry.value); - }); - _setHostAttribute('flt-embedding', 'full-page'); - - _applyViewportMeta(); - _setHostStyles(); - } - - @override - void attachGlassPane(DomElement glassPaneElement) { - /// Tweaks style so the glassPane works well with the hostElement. - glassPaneElement.style - ..position = 'absolute' - ..top = '0' - ..right = '0' - ..bottom = '0' - ..left = '0'; - - domDocument.body!.append(glassPaneElement); - - registerElementForCleanup(glassPaneElement); - } - - @override - void attachResourcesHost(DomElement resourceHost, {DomElement? nextTo}) { - domDocument.body!.insertBefore(resourceHost, nextTo); - - registerElementForCleanup(resourceHost); - } - - void _setHostAttribute(String name, String value) { - domDocument.body!.setAttribute(name, value); - } - - // Sets the global styles for a flutter app. - void _setHostStyles() { - final DomHTMLBodyElement bodyElement = domDocument.body!; - - setElementStyle(bodyElement, 'position', 'fixed'); - setElementStyle(bodyElement, 'top', '0'); - setElementStyle(bodyElement, 'right', '0'); - setElementStyle(bodyElement, 'bottom', '0'); - setElementStyle(bodyElement, 'left', '0'); - setElementStyle(bodyElement, 'overflow', 'hidden'); - setElementStyle(bodyElement, 'padding', '0'); - setElementStyle(bodyElement, 'margin', '0'); - - setElementStyle(bodyElement, 'user-select', 'none'); - setElementStyle(bodyElement, '-webkit-user-select', 'none'); - - // This is required to prevent the browser from doing any native touch - // handling. If this is not done, the browser doesn't report 'pointermove' - // events properly. - setElementStyle(bodyElement, 'touch-action', 'none'); - } - - // Sets a meta viewport tag appropriate for Flutter Web in full screen. - void _applyViewportMeta() { - for (final DomElement viewportMeta - in domDocument.head!.querySelectorAll('meta[name="viewport"]')) { - if (assertionsEnabled) { - // Filter out the meta tag that the engine placed on the page. This is - // to avoid UI flicker during hot restart. Hot restart will clean up the - // old meta tag synchronously with the first post-restart frame. - if (!viewportMeta.hasAttribute('flt-viewport')) { - print( - 'WARNING: found an existing tag. Flutter ' - 'Web uses its own viewport configuration for better compatibility ' - 'with Flutter. This tag will be replaced.', - ); - } - } - viewportMeta.remove(); - } - - // The meta viewport is always removed by the for method above, so we don't - // need to do anything else here, other than create it again. - final DomHTMLMetaElement viewportMeta = createDomHTMLMetaElement() - ..setAttribute('flt-viewport', '') - ..name = 'viewport' - ..content = 'width=device-width, initial-scale=1.0, ' - 'maximum-scale=1.0, user-scalable=no'; - - domDocument.head!.append(viewportMeta); - - registerElementForCleanup(viewportMeta); - } -} diff --git a/lib/web_ui/lib/src/engine/view_embedder/hot_restart_cache_handler.dart b/lib/web_ui/lib/src/engine/view_embedder/hot_restart_cache_handler.dart deleted file mode 100644 index 876972141b890..0000000000000 --- a/lib/web_ui/lib/src/engine/view_embedder/hot_restart_cache_handler.dart +++ /dev/null @@ -1,57 +0,0 @@ -// 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. - -import 'package:meta/meta.dart'; - -import '../dom.dart'; -import '../safe_browser_api.dart'; - -/// Handles [DomElement]s that need to be removed after a hot-restart. -/// -/// Elements are stored in an [_elements] list, backed by a global JS variable, -/// named [defaultCacheName]. -/// -/// When the app hot-restarts (and a new instance of this class is created), -/// everything in [_elements] is removed from the DOM. -class HotRestartCacheHandler { - HotRestartCacheHandler() { - if (_elements.isNotEmpty) { - // We are in a post hot-restart world, clear the elements now. - _clearAllElements(); - } - } - - /// The name for the JS global variable backing this cache. - @visibleForTesting - static const String defaultCacheName = '__flutter_state'; - - /// The js-interop layer backing [_elements]. - /// - /// Elements are stored in a JS global array named [defaultCacheName]. - late List? _jsElements; - - /// The elements that need to be cleaned up after hot-restart. - List get _elements { - _jsElements = - getJsProperty?>(domWindow, defaultCacheName); - if (_jsElements == null) { - _jsElements = []; - setJsProperty(domWindow, defaultCacheName, _jsElements); - } - return _jsElements!; - } - - /// Removes every element from [_elements] and empties the list. - void _clearAllElements() { - for (final DomElement? element in _elements) { - element?.remove(); - } - _elements.clear(); - } - - /// Registers a [DomElement] to be removed after hot-restart. - void registerElement(DomElement element) { - _elements.add(element); - } -} diff --git a/lib/web_ui/lib/src/engine/window.dart b/lib/web_ui/lib/src/engine/window.dart index 5538055f589f7..7010ece2079c6 100644 --- a/lib/web_ui/lib/src/engine/window.dart +++ b/lib/web_ui/lib/src/engine/window.dart @@ -12,7 +12,8 @@ import 'package:js/js.dart'; import 'package:meta/meta.dart'; import 'package:ui/ui.dart' as ui; -import '../engine.dart' show DimensionsProvider, registerHotRestartListener, renderer; +import '../engine.dart' show registerHotRestartListener, renderer; +import 'browser_detection.dart'; import 'dom.dart'; import 'navigation/history.dart'; import 'navigation/js_url_strategy.dart'; @@ -54,7 +55,6 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow { registerHotRestartListener(() { _browserHistory?.dispose(); renderer.clearFragmentProgramCache(); - _dimensionsProvider.close(); }); } @@ -207,16 +207,6 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow { const ui.ViewConfiguration(); } - late DimensionsProvider _dimensionsProvider; - void configureDimensionsProvider(DimensionsProvider dimensionsProvider) { - _dimensionsProvider = dimensionsProvider; - } - - @override - double get devicePixelRatio => _dimensionsProvider.getDevicePixelRatio(); - - Stream get onResize => _dimensionsProvider.onResize; - @override ui.Size get physicalSize { if (_physicalSize == null) { @@ -242,7 +232,38 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow { }()); if (!override) { - _physicalSize = _dimensionsProvider.computePhysicalSize(); + double windowInnerWidth; + double windowInnerHeight; + final DomVisualViewport? viewport = domWindow.visualViewport; + + if (viewport != null) { + if (operatingSystem == OperatingSystem.iOs) { + /// Chrome on iOS reports incorrect viewport.height when app + /// starts in portrait orientation and the phone is rotated to + /// landscape. + /// + /// We instead use documentElement clientWidth/Height to read + /// accurate physical size. VisualViewport api is only used during + /// text editing to make sure inset is correctly reported to + /// framework. + final double docWidth = + domDocument.documentElement!.clientWidth; + final double docHeight = + domDocument.documentElement!.clientHeight; + windowInnerWidth = docWidth * devicePixelRatio; + windowInnerHeight = docHeight * devicePixelRatio; + } else { + windowInnerWidth = viewport.width! * devicePixelRatio; + windowInnerHeight = viewport.height! * devicePixelRatio; + } + } else { + windowInnerWidth = domWindow.innerWidth! * devicePixelRatio; + windowInnerHeight = domWindow.innerHeight! * devicePixelRatio; + } + _physicalSize = ui.Size( + windowInnerWidth, + windowInnerHeight, + ); } } @@ -252,10 +273,21 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow { } void computeOnScreenKeyboardInsets(bool isEditingOnMobile) { - _viewInsets = _dimensionsProvider.computeKeyboardInsets( - _physicalSize!.height, - isEditingOnMobile, - ); + double windowInnerHeight; + final DomVisualViewport? viewport = domWindow.visualViewport; + if (viewport != null) { + if (operatingSystem == OperatingSystem.iOs && !isEditingOnMobile) { + windowInnerHeight = + domDocument.documentElement!.clientHeight * devicePixelRatio; + } else { + windowInnerHeight = viewport.height! * devicePixelRatio; + } + } else { + windowInnerHeight = domWindow.innerHeight! * devicePixelRatio; + } + final double bottomPadding = _physicalSize!.height - windowInnerHeight; + _viewInsets = + WindowPadding(bottom: bottomPadding, left: 0, right: 0, top: 0); } /// Uses the previous physical size and current innerHeight/innerWidth @@ -273,16 +305,26 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow { /// height: 658 width: 393 /// height: 368 width: 393 bool isRotation() { + double height = 0; + double width = 0; + if (domWindow.visualViewport != null) { + height = + domWindow.visualViewport!.height! * devicePixelRatio; + width = domWindow.visualViewport!.width! * devicePixelRatio; + } else { + height = domWindow.innerHeight! * devicePixelRatio; + width = domWindow.innerWidth! * devicePixelRatio; + } + // This method compares the new dimensions with the previous ones. // Return false if the previous dimensions are not set. if (_physicalSize != null) { - final ui.Size current = _dimensionsProvider.computePhysicalSize(); // First confirm both height and width are effected. - if (_physicalSize!.height != current.height && _physicalSize!.width != current.width) { + if (_physicalSize!.height != height && _physicalSize!.width != width) { // If prior to rotation height is bigger than width it should be the // opposite after the rotation and vice versa. - if ((_physicalSize!.height > _physicalSize!.width && current.height < current.width) || - (_physicalSize!.width > _physicalSize!.height && current.width < current.height)) { + if ((_physicalSize!.height > _physicalSize!.width && height < width) || + (_physicalSize!.width > _physicalSize!.height && width < height)) { // Rotation detected return true; } diff --git a/lib/web_ui/test/engine/host_node_test.dart b/lib/web_ui/test/engine/host_node_test.dart index 8c01b6a840dc9..b41f7c381dd18 100644 --- a/lib/web_ui/test/engine/host_node_test.dart +++ b/lib/web_ui/test/engine/host_node_test.dart @@ -15,7 +15,7 @@ void testMain() { domDocument.body!.append(rootNode); group('ShadowDomHostNode', () { - final HostNode hostNode = ShadowDomHostNode(rootNode, '14px monospace'); + final HostNode hostNode = ShadowDomHostNode(rootNode); test('Initializes and attaches a shadow root', () { expect(domInstanceOfString(hostNode.node, 'ShadowRoot'), isTrue); @@ -33,90 +33,30 @@ void testMain() { }); test('Attaches a stylesheet to the shadow root', () { - final DomElement? style = - hostNode.querySelector('#flt-internals-stylesheet'); + final DomElement firstChild = + (hostNode.node as DomShadowRoot).childNodes.toList()[0] as DomElement; - expect(style, isNotNull); - expect(style!.tagName, equalsIgnoringCase('style')); - }); - - test('(Self-test) hasCssRule can extract rules', () { - final DomElement? style = - hostNode.querySelector('#flt-internals-stylesheet'); - - final bool hasRule = hasCssRule(style, - selector: '.flt-text-editing::placeholder', - declaration: 'opacity: 0'); - - final bool hasFakeRule = hasCssRule(style, - selector: 'input::selection', declaration: 'color: #fabada;'); - - expect(hasRule, isTrue); - expect(hasFakeRule, isFalse); - }); - - test('Attaches outrageous text styles to flt-scene-host', () { - final DomElement? style = - hostNode.querySelector('#flt-internals-stylesheet'); - - final bool hasColorRed = hasCssRule(style, - selector: 'flt-scene-host', declaration: 'color: red'); - - bool hasFont = false; - if (isSafari) { - // Safari expands the shorthand rules, so we check for all we've set (separately). - hasFont = hasCssRule(style, - selector: 'flt-scene-host', - declaration: 'font-family: monospace') && - hasCssRule(style, - selector: 'flt-scene-host', declaration: 'font-size: 14px'); - } else { - hasFont = hasCssRule(style, - selector: 'flt-scene-host', declaration: 'font: 14px monospace'); - } - - expect(hasColorRed, isTrue, - reason: 'Should make foreground color red within scene host.'); - expect(hasFont, isTrue, reason: 'Should pass default css font.'); + expect(firstChild.tagName, equalsIgnoringCase('style')); }); test('Attaches styling to remove password reveal icons on Edge', () { - final DomElement? style = - hostNode.querySelector('#flt-internals-stylesheet'); + final DomElement? edgeStyleElement = hostNode.querySelector('#ms-reveal'); - // Check that style.sheet! contains input::-ms-reveal rule - final bool hidesRevealIcons = hasCssRule(style, - selector: 'input::-ms-reveal', declaration: 'display: none'); - - final bool codeRanInFakeyBrowser = hasCssRule(style, - selector: 'input.fallback-for-fakey-browser-in-ci', - declaration: 'display: none'); - - if (codeRanInFakeyBrowser) { - print('Please, fix https://github.com/flutter/flutter/issues/116302'); - } - - expect(hidesRevealIcons || codeRanInFakeyBrowser, isTrue, - reason: 'In Edge, stylesheet must contain "input::-ms-reveal" rule.'); + expect(edgeStyleElement, isNotNull); + expect(edgeStyleElement!.innerText, 'input::-ms-reveal {display: none;}'); }, skip: !isEdge); test('Does not attach the Edge-specific style tag on non-Edge browsers', () { - final DomElement? style = - hostNode.querySelector('#flt-internals-stylesheet'); - - // Check that style.sheet! contains input::-ms-reveal rule - final bool hidesRevealIcons = hasCssRule(style, - selector: 'input::-ms-reveal', declaration: 'display: none'); - - expect(hidesRevealIcons, isFalse); + final DomElement? edgeStyleElement = hostNode.querySelector('#ms-reveal'); + expect(edgeStyleElement, isNull); }, skip: isEdge); _runDomTests(hostNode); }); group('ElementHostNode', () { - final HostNode hostNode = ElementHostNode(rootNode, ''); + final HostNode hostNode = ElementHostNode(rootNode); test('Initializes and attaches a child element', () { expect(domInstanceOfString(hostNode.node, 'Element'), isTrue); @@ -172,25 +112,3 @@ void _runDomTests(HostNode hostNode) { }); }); } - -/// Finds out whether a given CSS Rule ([selector] { [declaration]; }) exists in a [styleSheet]. -bool hasCssRule( - DomElement? styleSheet, { - required String selector, - required String declaration, -}) { - assert(styleSheet != null); - assert((styleSheet! as DomHTMLStyleElement).sheet != null); - - // regexr.com/740ff - final RegExp ruleLike = - RegExp('[^{]*(?:$selector)[^{]*{[^}]*(?:$declaration)[^}]*}'); - - final DomCSSStyleSheet sheet = - (styleSheet! as DomHTMLStyleElement).sheet! as DomCSSStyleSheet; - - // Check that the cssText of any rule matches the ruleLike RegExp. - return sheet.cssRules - .map((DomCSSRule rule) => rule.cssText) - .any((String rule) => ruleLike.hasMatch(rule)); -} diff --git a/lib/web_ui/test/engine/pointer_binding_test.dart b/lib/web_ui/test/engine/pointer_binding_test.dart index d94b34d10e816..2f5f48c0252a6 100644 --- a/lib/web_ui/test/engine/pointer_binding_test.dart +++ b/lib/web_ui/test/engine/pointer_binding_test.dart @@ -23,15 +23,13 @@ typedef _ContextTestBody = void Function(T); void _testEach( Iterable contexts, String description, - _ContextTestBody body, { - Object? skip, - } + _ContextTestBody body, ) { for (final T context in contexts) { if (context.isSupported) { test('${context.name} $description', () { body(context); - }, skip: skip); + }); } } } @@ -390,8 +388,6 @@ void testMain() { expect(event.buttons, equals(1)); expect(event.client.x, equals(100)); expect(event.client.y, equals(101)); - expect(event.offset.x, equals(100)); - expect(event.offset.y, equals(101)); event = expectCorrectType( context.mouseDown(clientX: 110, clientY: 111, button: 2, buttons: 2)); @@ -853,7 +849,7 @@ void testMain() { packets.clear(); // Release the pointer on the semantics placeholder. - glassPane.dispatchEvent(context.primaryUp( + domWindow.dispatchEvent(context.primaryUp( clientX: 100.0, clientY: 200.0, )); @@ -869,7 +865,6 @@ void testMain() { semanticsPlaceholder.remove(); }, - skip: isFirefox, // https://bugzilla.mozilla.org/show_bug.cgi?id=1804190 ); // BUTTONED ADAPTERS @@ -2477,7 +2472,7 @@ void testMain() { packets.clear(); // Move outside the glasspane. - glassPane.dispatchEvent(context.primaryMove( + domWindow.dispatchEvent(context.primaryMove( clientX: 900.0, clientY: 1900.0, )); @@ -2489,7 +2484,7 @@ void testMain() { packets.clear(); // Release outside the glasspane. - glassPane.dispatchEvent(context.primaryUp( + domWindow.dispatchEvent(context.primaryUp( clientX: 1000.0, clientY: 2000.0, )); @@ -3356,7 +3351,6 @@ class _MouseEventContext extends _BasicEventContext final List eventArgs = [ type, { - 'bubbles': true, 'buttons': buttons, 'button': button, 'clientX': clientX, @@ -3575,7 +3569,6 @@ class _PointerEventContext extends _BasicEventContext String? pointerType, }) { return createDomPointerEvent('pointerup', { - 'bubbles': true, 'pointerId': pointer, 'button': button, 'buttons': buttons, diff --git a/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart b/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart deleted file mode 100644 index a891bc0634d05..0000000000000 --- a/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart +++ /dev/null @@ -1,170 +0,0 @@ -// 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. - -@TestOn('browser') - -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/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart'; -import 'package:ui/src/engine/window.dart'; -import 'package:ui/ui.dart' as ui show Size; - -void main() { - internalBootstrapBrowserTest(() => doTests); -} - -void doTests() { - final DomElement sizeSource = createDomElement('div') - ..style.display = 'block'; - - group('computePhysicalSize', () { - late CustomElementDimensionsProvider provider; - - setUp(() { - sizeSource - ..style.width = '10px' - ..style.height = '10px'; - domDocument.body!.append(sizeSource); - provider = CustomElementDimensionsProvider(sizeSource); - }); - - tearDown(() { - provider.close(); // cleanup - sizeSource.remove(); - }); - - test('returns physical size of element (width * dpr)', () { - const double dpr = 2.5; - const double logicalWidth = 50; - const double logicalHeight = 75; - window.debugOverrideDevicePixelRatio(dpr); - - sizeSource - ..style.width = '${logicalWidth}px' - ..style.height = '${logicalHeight}px'; - - const ui.Size expected = ui.Size(logicalWidth * dpr, logicalHeight * dpr); - - final ui.Size computed = provider.computePhysicalSize(); - - expect(computed, expected); - }); - }); - - group('computeKeyboardInsets', () { - late CustomElementDimensionsProvider provider; - - setUp(() { - sizeSource - ..style.width = '10px' - ..style.height = '10px'; - domDocument.body!.append(sizeSource); - provider = CustomElementDimensionsProvider(sizeSource); - }); - - tearDown(() { - provider.close(); // cleanup - sizeSource.remove(); - }); - - test('from viewport physical size (simulated keyboard) - always zero', () { - // Simulate a 100px tall keyboard showing... - const double dpr = 2.5; - window.debugOverrideDevicePixelRatio(dpr); - const double keyboardGap = 100; - final double physicalHeight = - (domWindow.visualViewport!.height! + keyboardGap) * dpr; - - final WindowPadding computed = - provider.computeKeyboardInsets(physicalHeight, false); - - expect(computed.top, 0); - expect(computed.right, 0); - expect(computed.bottom, 0); - expect(computed.left, 0); - }); - }); - - group('onResize Stream', () { - late CustomElementDimensionsProvider provider; - - setUp(() async { - sizeSource - ..style.width = '10px' - ..style.height = '10px'; - domDocument.body!.append(sizeSource); - provider = CustomElementDimensionsProvider(sizeSource); - // Let the DOM settle before starting the test, so we don't get the first - // 10,10 Size in the test. Otherwise, the ResizeObserver may trigger - // unexpectedly after the test has started, and break our "first" result. - await Future.delayed(const Duration(milliseconds: 250)); - }); - - tearDown(() { - provider.close(); // cleanup - sizeSource.remove(); - }); - - test('funnels resize events on sizeSource', () async { - final Future event = provider.onResize.first; - final Future> events = provider.onResize.take(3).toList(); - - // The resize observer fires asynchronously, so we wait a little between - // resizes, so the observer has time to fire events separately. - await Future.delayed(const Duration(milliseconds: 100), () { - sizeSource - ..style.width = '100px' - ..style.height = '100px'; - }); - - await Future.delayed(const Duration(milliseconds: 100), () { - sizeSource - ..style.width = '200px' - ..style.height = '200px'; - }); - - await Future.delayed(const Duration(milliseconds: 100), () { - sizeSource - ..style.width = '300px' - ..style.height = '300px'; - }); - - // Let the DOM settle so the observer reports the last 300x300 mutation... - await Future.delayed(const Duration(milliseconds: 100)); - - expect(event, completion(const ui.Size(100, 100))); - expect(events, completes); - expect( - events, - completion(const [ - ui.Size(100, 100), - ui.Size(200, 200), - ui.Size(300, 300), - ])); - }); - - test('closed by onHotRestart', () async { - // Register an onDone listener for the stream - final Completer completer = Completer(); - provider.onResize.listen(null, onDone: () { - completer.complete(true); - }); - - // Should close the stream - provider.close(); - - sizeSource - ..style.width = '100px' - ..style.height = '100px'; - // Give time to the mutationObserver to fire (if needed, it won't) - await Future.delayed(const Duration(milliseconds: 100)); - - expect(provider.onResize.isEmpty, completion(isTrue)); - expect(completer.future, completion(isTrue)); - }); - }); -} diff --git a/lib/web_ui/test/engine/view_embedder/dimensions_provider/dimensions_provider_test.dart b/lib/web_ui/test/engine/view_embedder/dimensions_provider/dimensions_provider_test.dart deleted file mode 100644 index 8edfe33233d51..0000000000000 --- a/lib/web_ui/test/engine/view_embedder/dimensions_provider/dimensions_provider_test.dart +++ /dev/null @@ -1,48 +0,0 @@ -// 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. - -@TestOn('browser') - -import 'package:test/bootstrap/browser.dart'; -import 'package:test/test.dart'; -import 'package:ui/src/engine/dom.dart'; -import 'package:ui/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart'; -import 'package:ui/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart'; -import 'package:ui/src/engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart'; -import 'package:ui/src/engine/window.dart'; - -void main() { - internalBootstrapBrowserTest(() => doTests); -} - -void doTests() { - group('Factory', () { - test('Creates a FullPage instance when hostElement is null', () async { - final DimensionsProvider provider = DimensionsProvider.create(); - - expect(provider, isA()); - }); - - test('Creates a CustomElement instance when hostElement is not null', - () async { - final DomElement element = createDomElement('some-random-element'); - final DimensionsProvider provider = DimensionsProvider.create( - hostElement: element, - ); - - expect(provider, isA()); - }); - }); - - group('getDevicePixelRatio', () { - test('Returns the correct pixelRatio', () async { - // Override the DPI to something known, but weird... - window.debugOverrideDevicePixelRatio(33930); - - final DimensionsProvider provider = DimensionsProvider.create(); - - expect(provider.getDevicePixelRatio(), 33930); - }); - }); -} diff --git a/lib/web_ui/test/engine/view_embedder/dimensions_provider/full_page_dimensions_provider_test.dart b/lib/web_ui/test/engine/view_embedder/dimensions_provider/full_page_dimensions_provider_test.dart deleted file mode 100644 index aadbf6813f7c9..0000000000000 --- a/lib/web_ui/test/engine/view_embedder/dimensions_provider/full_page_dimensions_provider_test.dart +++ /dev/null @@ -1,107 +0,0 @@ -// 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. - -@TestOn('browser') - -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/view_embedder/dimensions_provider/full_page_dimensions_provider.dart'; -import 'package:ui/src/engine/window.dart'; -import 'package:ui/ui.dart' as ui show Size; - -void main() { - internalBootstrapBrowserTest(() => doTests); -} - -void doTests() { - group('computePhysicalSize', () { - late FullPageDimensionsProvider provider; - - setUp(() { - provider = FullPageDimensionsProvider(); - }); - - test('returns visualViewport physical size (width * dpr)', () { - const double dpr = 2.5; - window.debugOverrideDevicePixelRatio(dpr); - final ui.Size expected = ui.Size(domWindow.visualViewport!.width! * dpr, - domWindow.visualViewport!.height! * dpr); - - final ui.Size computed = provider.computePhysicalSize(); - - expect(computed, expected); - }); - }); - - group('computeKeyboardInsets', () { - late FullPageDimensionsProvider provider; - - setUp(() { - provider = FullPageDimensionsProvider(); - }); - - test('from viewport physical size (simulated keyboard)', () { - // Simulate a 100px tall keyboard showing... - const double dpr = 2.5; - window.debugOverrideDevicePixelRatio(dpr); - const double keyboardGap = 100; - final double physicalHeight = - (domWindow.visualViewport!.height! + keyboardGap) * dpr; - const double expectedBottom = keyboardGap * dpr; - - final WindowPadding computed = - provider.computeKeyboardInsets(physicalHeight, false); - - expect(computed.top, 0); - expect(computed.right, 0); - expect(computed.bottom, expectedBottom); - expect(computed.left, 0); - }); - }); - - group('onResize Stream', () { - // Needed to synthesize "resize" events - final DomEventTarget resizeEventTarget = - domWindow.visualViewport ?? domWindow; - - late FullPageDimensionsProvider provider; - - setUp(() { - provider = FullPageDimensionsProvider(); - }); - - test('funnels resize events on resizeEventTarget', () { - final Future event = provider.onResize.first; - - final Future> events = provider.onResize.take(3).toList(); - - resizeEventTarget.dispatchEvent(createDomEvent('Event', 'resize')); - resizeEventTarget.dispatchEvent(createDomEvent('Event', 'resize')); - resizeEventTarget.dispatchEvent(createDomEvent('Event', 'resize')); - - expect(event, completes); - expect(events, completes); - expect(events, completion(hasLength(3))); - }); - - test('closed by onHotRestart', () { - // Register an onDone listener for the stream - final Completer completer = Completer(); - provider.onResize.listen(null, onDone: () { - completer.complete(true); - }); - - // Should close the stream - provider.close(); - - resizeEventTarget.dispatchEvent(createDomEvent('Event', 'resize')); - - expect(provider.onResize.isEmpty, completion(isTrue)); - expect(completer.future, completion(isTrue)); - }); - }); -} 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 deleted file mode 100644 index 75af6a0359e70..0000000000000 --- a/lib/web_ui/test/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy_test.dart +++ /dev/null @@ -1,124 +0,0 @@ -// 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. - -@TestOn('browser') - -import 'package:test/bootstrap/browser.dart'; -import 'package:test/test.dart'; -import 'package:ui/src/engine/dom.dart'; -import 'package:ui/src/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart'; - -void main() { - internalBootstrapBrowserTest(() => doTests); -} - -void doTests() { - late CustomElementEmbeddingStrategy strategy; - late DomElement target; - - group('initialize', () { - setUp(() { - target = createDomElement('this-is-the-target'); - domDocument.body!.append(target); - strategy = CustomElementEmbeddingStrategy(target); - }); - - tearDown(() { - target.remove(); - }); - - test('Prepares target environment', () { - strategy.initialize( - hostElementAttributes: { - 'key-for-testing': 'value-for-testing', - }, - ); - - expect(target.getAttribute('key-for-testing'), 'value-for-testing', - reason: - 'Should add attributes as key=value into target element.'); - expect(target.getAttribute('flt-embedding'), 'custom-element', - reason: - 'Should identify itself as a specific key=value into the target element.'); - }); - }); - - group('attachGlassPane', () { - setUp(() { - target = createDomElement('this-is-the-target'); - domDocument.body!.append(target); - strategy = CustomElementEmbeddingStrategy(target); - strategy.initialize(); - }); - - tearDown(() { - target.remove(); - }); - - test('Should attach glasspane into embedder target (body)', () async { - final DomElement glassPane = createDomElement('some-tag-for-tests'); - final DomCSSStyleDeclaration style = glassPane.style; - - expect(glassPane.isConnected, isFalse); - expect(style.position, '', - reason: 'Should not have any specific position.'); - expect(style.width, '', reason: 'Should not have any size set.'); - - strategy.attachGlassPane(glassPane); - - // Assert injection into - expect(glassPane.isConnected, isTrue, - reason: 'Should inject glassPane into the document.'); - expect(glassPane.parent, target, - reason: 'Should inject glassPane into the target element'); - - final DomCSSStyleDeclaration styleAfter = glassPane.style; - - // Assert required styling to cover the viewport - expect(styleAfter.position, 'relative', - reason: 'Should be relatively positioned.'); - expect(styleAfter.display, 'block', reason: 'Should be display:block.'); - expect(styleAfter.width, '100%', - reason: 'Should take 100% of the available width'); - expect(styleAfter.height, '100%', - reason: 'Should take 100% of the available height'); - expect(styleAfter.overflow, 'hidden', - reason: 'Should hide the occasional oversized canvas elements.'); - }); - }); - - group('attachResourcesHost', () { - late DomElement glassPane; - - setUp(() { - target = createDomElement('this-is-the-target'); - glassPane = createDomElement('woah-a-glasspane'); - domDocument.body!.append(target); - strategy = CustomElementEmbeddingStrategy(target); - strategy.initialize(); - strategy.attachGlassPane(glassPane); - }); - - tearDown(() { - target.remove(); - }); - - test( - 'Should attach resources host into target (body), `nextTo` other element', - () async { - final DomElement resources = createDomElement('resources-host-element'); - - expect(resources.isConnected, isFalse); - - strategy.attachResourcesHost(resources, nextTo: glassPane); - - expect(resources.isConnected, isTrue, - reason: 'Should inject resources host somewhere in the document.'); - expect(resources.parent, target, - reason: 'Should inject the resources into the target element'); - expect(resources.nextSibling, glassPane, - reason: 'Should be injected `nextTo` the passed element.'); - }); - }); -} diff --git a/lib/web_ui/test/engine/view_embedder/embedding_strategy/embedding_strategy_test.dart b/lib/web_ui/test/engine/view_embedder/embedding_strategy/embedding_strategy_test.dart deleted file mode 100644 index d17c1e54c48cb..0000000000000 --- a/lib/web_ui/test/engine/view_embedder/embedding_strategy/embedding_strategy_test.dart +++ /dev/null @@ -1,58 +0,0 @@ -// 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. - -@TestOn('browser') - -import 'package:test/bootstrap/browser.dart'; -import 'package:test/test.dart'; -import 'package:ui/src/engine/dom.dart'; -import 'package:ui/src/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart'; -import 'package:ui/src/engine/view_embedder/embedding_strategy/embedding_strategy.dart'; -import 'package:ui/src/engine/view_embedder/embedding_strategy/full_page_embedding_strategy.dart'; - -import '../hot_restart_cache_handler_test.dart' show getDomCache; - -void main() { - internalBootstrapBrowserTest(() => doTests); -} - -void doTests() { - group('Factory', () { - test('Creates a FullPage instance when hostElement is null', () async { - final EmbeddingStrategy strategy = EmbeddingStrategy.create(); - - expect(strategy, isA()); - }); - - test('Creates a CustomElement instance when hostElement is not null', - () async { - final DomElement element = createDomElement('some-random-element'); - final EmbeddingStrategy strategy = EmbeddingStrategy.create( - hostElement: element, - ); - - expect(strategy, isA()); - }); - }); - - group('registerElementForCleanup', () { - test('stores elements in a global domCache', () async { - final EmbeddingStrategy strategy = EmbeddingStrategy.create(); - - final DomElement toBeCached = createDomElement('some-element-to-cache'); - final DomElement other = createDomElement('other-element-to-cache'); - final DomElement another = createDomElement('another-element-to-cache'); - - strategy.registerElementForCleanup(toBeCached); - strategy.registerElementForCleanup(other); - strategy.registerElementForCleanup(another); - - final List cache = getDomCache()!; - - expect(cache, hasLength(3)); - expect(cache.first, toBeCached); - expect(cache.last, another); - }); - }); -} 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 deleted file mode 100644 index d05effb9b3f54..0000000000000 --- a/lib/web_ui/test/engine/view_embedder/embedding_strategy/full_page_embedding_strategy_test.dart +++ /dev/null @@ -1,132 +0,0 @@ -// 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. - -@TestOn('browser') - -import 'package:test/bootstrap/browser.dart'; -import 'package:test/test.dart'; -import 'package:ui/src/engine/dom.dart'; -import 'package:ui/src/engine/view_embedder/embedding_strategy/full_page_embedding_strategy.dart'; - -void main() { - internalBootstrapBrowserTest(() => doTests); -} - -void doTests() { - late FullPageEmbeddingStrategy strategy; - late DomElement target; - - group('initialize', () { - setUp(() { - strategy = FullPageEmbeddingStrategy(); - target = domDocument.body!; - final DomHTMLMetaElement meta = createDomHTMLMetaElement(); - meta - ..id = 'my_viewport_meta_for_testing' - ..name = 'viewport' - ..content = 'width=device-width, initial-scale=1.0, ' - 'maximum-scale=1.0, user-scalable=no'; - domDocument.head!.append(meta); - }); - - test('Prepares target environment', () { - DomElement? userMeta = - domDocument.querySelector('#my_viewport_meta_for_testing'); - - expect(userMeta, isNotNull); - - strategy.initialize( - hostElementAttributes: { - 'key-for-testing': 'value-for-testing', - }, - ); - - expect(target.getAttribute('key-for-testing'), 'value-for-testing', - reason: - 'Should add attributes as key=value into target element.'); - expect(target.getAttribute('flt-embedding'), 'full-page', - reason: - 'Should identify itself as a specific key=value into the target element.'); - - // Locate the viewport metas again... - userMeta = domDocument.querySelector('#my_viewport_meta_for_testing'); - - final DomElement? flutterMeta = - domDocument.querySelector('meta[name="viewport"]'); - - expect(userMeta, isNull, - reason: 'Should delete previously existing viewport meta tags.'); - expect(flutterMeta, isNotNull); - expect(flutterMeta!.hasAttribute('flt-viewport'), isTrue, - reason: 'Should install flutter viewport meta tag.'); - }); - }); - - group('attachGlassPane', () { - setUp(() { - strategy = FullPageEmbeddingStrategy(); - strategy.initialize(); - }); - - test('Should attach glasspane into embedder target (body)', () async { - final DomElement glassPane = createDomElement('some-tag-for-tests'); - final DomCSSStyleDeclaration style = glassPane.style; - - expect(glassPane.isConnected, isFalse); - expect(style.position, '', - reason: 'Should not have any specific position.'); - expect(style.top, '', - reason: - 'Should not have any top/right/bottom/left positioning/inset.'); - - strategy.attachGlassPane(glassPane); - - // Assert injection into - expect(glassPane.isConnected, isTrue, - reason: 'Should inject glassPane into the document.'); - expect(glassPane.parent, domDocument.body, - reason: 'Should inject glassPane into the '); - - final DomCSSStyleDeclaration styleAfter = glassPane.style; - - // Assert required styling to cover the viewport - expect(styleAfter.position, 'absolute', - reason: 'Should be absolutely positioned.'); - expect(styleAfter.top, '0px', reason: 'Should cover the whole viewport.'); - expect(styleAfter.right, '0px', - reason: 'Should cover the whole viewport.'); - expect(styleAfter.bottom, '0px', - reason: 'Should cover the whole viewport.'); - expect(styleAfter.left, '0px', - reason: 'Should cover the whole viewport.'); - }); - }); - - group('attachResourcesHost', () { - late DomElement glassPane; - setUp(() { - glassPane = createDomElement('some-tag-for-tests'); - strategy = FullPageEmbeddingStrategy(); - strategy.initialize(); - strategy.attachGlassPane(glassPane); - }); - - test( - 'Should attach resources host into target (body), `nextTo` other element', - () async { - final DomElement resources = createDomElement('resources-host-element'); - - expect(resources.isConnected, isFalse); - - strategy.attachResourcesHost(resources, nextTo: glassPane); - - expect(resources.isConnected, isTrue, - reason: 'Should inject resources host somewhere in the document.'); - expect(resources.parent, domDocument.body, - reason: 'Should inject resources host into the '); - expect(resources.nextSibling, glassPane, - reason: 'Should be injected `nextTo` the passed element.'); - }); - }); -} diff --git a/lib/web_ui/test/engine/view_embedder/hot_restart_cache_handler_test.dart b/lib/web_ui/test/engine/view_embedder/hot_restart_cache_handler_test.dart deleted file mode 100644 index 6ebc7134087a7..0000000000000 --- a/lib/web_ui/test/engine/view_embedder/hot_restart_cache_handler_test.dart +++ /dev/null @@ -1,82 +0,0 @@ -// 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. - -@TestOn('browser') - -import 'package:js/js_util.dart'; - -import 'package:test/bootstrap/browser.dart'; -import 'package:test/test.dart'; -import 'package:ui/src/engine/dom.dart'; -import 'package:ui/src/engine/view_embedder/hot_restart_cache_handler.dart'; - -void main() { - internalBootstrapBrowserTest(() => doTests); -} - -void doTests() { - group('Constructor', () { - test('Creates a cache in the JS environment', () async { - final HotRestartCacheHandler cache = HotRestartCacheHandler(); - - expect(cache, isNotNull); - - final List? domCache = getDomCache(); - - expect(domCache, isNotNull); - expect(domCache, isEmpty); - }); - }); - - group('registerElement', () { - HotRestartCacheHandler? cache; - List? domCache; - - setUp(() { - cache = HotRestartCacheHandler(); - domCache = getDomCache(); - }); - - test('Registers an element in the DOM cache', () async { - final DomElement element = createDomElement('for-test'); - cache!.registerElement(element); - - expect(domCache, hasLength(1)); - expect(domCache!.last, element); - }); - - test('Registers elements in the DOM cache', () async { - final DomElement element = createDomElement('for-test'); - domDocument.body!.append(element); - - cache!.registerElement(element); - - expect(domCache, hasLength(1)); - expect(domCache!.last, element); - }); - - test('Clears registered elements from the DOM and the cache upon restart', - () async { - final DomElement element = createDomElement('for-test'); - final DomElement element2 = createDomElement('for-test-two'); - domDocument.body!.append(element); - domDocument.body!.append(element2); - - cache!.registerElement(element); - - expect(element.isConnected, isTrue); - expect(element2.isConnected, isTrue); - - // Simulate a hot restart... - cache = HotRestartCacheHandler(); - - expect(domCache, hasLength(0)); - expect(element.isConnected, isFalse); // Removed - expect(element2.isConnected, isTrue); - }); - }); -} - -List? getDomCache() => getProperty?>( - domWindow, HotRestartCacheHandler.defaultCacheName);