| // 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/ui.dart' as ui; |
| |
| import '../engine.dart' show buildMode, renderer, window; |
| import 'browser_detection.dart'; |
| import 'configuration.dart'; |
| import 'dom.dart'; |
| import 'host_node.dart'; |
| import 'keyboard_binding.dart'; |
| import 'platform_dispatcher.dart'; |
| import 'pointer_binding.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'; |
| |
| /// Controls the placement and lifecycle of a Flutter view on the web page. |
| /// |
| /// Manages several top-level elements that host Flutter-generated content, |
| /// including: |
| /// |
| /// - [glassPaneElement], the root element of a Flutter view. |
| /// - [glassPaneShadow], the shadow root used to isolate Flutter-rendered |
| /// content from the surrounding page content, including from the platform |
| /// views. |
| /// - [sceneElement], the element that hosts Flutter layers and pictures, and |
| /// projects platform views. |
| /// - [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, |
| )); |
| |
| reset(); |
| } |
| |
| /// 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'; |
| |
| /// The element that contains the [sceneElement]. |
| /// |
| /// This element is created and inserted in the HTML DOM once. It is never |
| /// removed or moved. However the [sceneElement] may be replaced inside it. |
| /// |
| /// This element is inserted after the [semanticsHostElement] so that |
| /// platform views take precedence in DOM event handling. |
| DomElement? get sceneHostElement => _sceneHostElement; |
| DomElement? _sceneHostElement; |
| |
| /// A child element of body outside the shadowroot that hosts |
| /// global resources such svg filters and clip paths when using webkit. |
| DomElement? _resourcesHost; |
| |
| /// The element that contains the semantics tree. |
| /// |
| /// This element is created and inserted in the HTML DOM once. It is never |
| /// removed or moved. |
| /// |
| /// Render semantics inside the glasspane for proper focus and event |
| /// handling. If semantics is behind the glasspane, the phone will disable |
| /// focusing by touch, only by tabbing around the UI. If semantics is in |
| /// front of glasspane, then DOM event won't bubble up to the glasspane so |
| /// it can forward events to the framework. |
| /// |
| /// This element is inserted before the [semanticsHostElement] so that |
| /// platform views take precedence in DOM event handling. |
| DomElement? get semanticsHostElement => _semanticsHostElement; |
| DomElement? _semanticsHostElement; |
| |
| /// The last scene element rendered by the [render] method. |
| DomElement? get sceneElement => _sceneElement; |
| DomElement? _sceneElement; |
| |
| /// 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 |
| /// text selection. |
| void addSceneToSceneHost(DomElement? sceneElement) { |
| if (sceneElement != _sceneElement) { |
| _sceneElement?.remove(); |
| _sceneElement = sceneElement; |
| _sceneHostElement!.append(sceneElement!); |
| } |
| } |
| |
| /// The element that captures input events, such as pointer events. |
| /// |
| /// If semantics is enabled this element also contains the semantics DOM tree, |
| /// which captures semantics input events. The semantics DOM tree must be a |
| /// child of the glass pane element so that events bubble up to the glass pane |
| /// if they are not handled by semantics. |
| DomElement get glassPaneElement => _glassPaneElement; |
| late DomElement _glassPaneElement; |
| |
| /// The [HostNode] of the [glassPaneElement], which contains the whole Flutter app. |
| HostNode get glassPaneShadow => _glassPaneShadow; |
| late HostNode _glassPaneShadow; |
| |
| static const String defaultFontStyle = 'normal'; |
| static const String defaultFontWeight = 'normal'; |
| static const double defaultFontSize = 14; |
| static const String defaultFontFamily = 'sans-serif'; |
| static const String defaultCssFont = |
| '$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: <String, String>{ |
| 'flt-renderer': '${renderer.rendererTag} ($rendererSelection)', |
| 'flt-build-mode': buildMode, |
| // TODO(mdebbar): Disable spellcheck until changes in the framework and |
| // engine are complete. |
| 'spellcheck': 'false', |
| }, |
| ); |
| |
| // Create and inject the [_glassPaneElement]. |
| _glassPaneElement = domDocument.createElement(glassPaneTagName); |
| |
| // 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); |
| |
| // 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, |
| ); |
| _glassPaneShadow = glassPaneElementHostNode; |
| |
| // Don't allow the scene to receive pointer events. |
| _sceneHostElement = domDocument.createElement('flt-scene-host') |
| ..style.pointerEvents = 'none'; |
| |
| renderer.reset(this); |
| |
| final DomElement semanticsHostElement = |
| domDocument.createElement('flt-semantics-host'); |
| semanticsHostElement.style |
| ..position = 'absolute' |
| ..transformOrigin = '0 0 0'; |
| _semanticsHostElement = semanticsHostElement; |
| updateSemanticsScreenProperties(); |
| |
| final DomElement accessibilityPlaceholder = EngineSemanticsOwner |
| .instance.semanticsHelper |
| .prepareAccessibilityPlaceholder(); |
| |
| glassPaneElementHostNode.appendAll(<DomNode>[ |
| accessibilityPlaceholder, |
| _sceneHostElement!, |
| |
| // The semantic host goes last because hit-test order-wise it must be |
| // first. If semantics goes under the scene host, platform views will |
| // obscure semantic elements. |
| // |
| // You may be wondering: wouldn't semantics obscure platform views and |
| // make then not accessible? At least with some careful planning, that |
| // should not be the case. The semantics tree makes all of its non-leaf |
| // elements transparent. This way, if a platform view appears among other |
| // interactive Flutter widgets, as long as those widgets do not intersect |
| // with the platform view, the platform view will be reachable. |
| semanticsHostElement, |
| ]); |
| |
| // When debugging semantics, make the scene semi-transparent so that the |
| // semantics tree is more prominent. |
| if (configuration.debugShowSemanticsNodes) { |
| _sceneHostElement!.style.opacity = '0.3'; |
| } |
| |
| KeyboardBinding.initInstance(); |
| PointerBinding.initInstance( |
| glassPaneElement, |
| KeyboardBinding.instance!.converter, |
| ); |
| |
| window.onResize.listen(_metricsDidChange); |
| } |
| |
| /// 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})'); |
| } |
| |
| /// Called immediately after browser window metrics change. |
| /// |
| /// When there is a text editing going on in mobile devices, do not change |
| /// the physicalSize, change the [window.viewInsets]. See: |
| /// https://api.flutter.dev/flutter/dart-ui/FlutterView/viewInsets.html |
| /// https://api.flutter.dev/flutter/dart-ui/FlutterView/physicalSize.html |
| /// |
| /// 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) { |
| 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(); |
| } else { |
| window.computePhysicalSize(); |
| // When physical size changes this value has to be recalculated. |
| window.computeOnScreenKeyboardInsets(false); |
| EnginePlatformDispatcher.instance.invokeOnMetricsChanged(); |
| } |
| } |
| |
| static const String orientationLockTypeAny = 'any'; |
| static const String orientationLockTypeNatural = 'natural'; |
| static const String orientationLockTypeLandscape = 'landscape'; |
| static const String orientationLockTypePortrait = 'portrait'; |
| static const String orientationLockTypePortraitPrimary = 'portrait-primary'; |
| static const String orientationLockTypePortraitSecondary = |
| 'portrait-secondary'; |
| static const String orientationLockTypeLandscapePrimary = 'landscape-primary'; |
| static const String orientationLockTypeLandscapeSecondary = |
| 'landscape-secondary'; |
| |
| /// Sets preferred screen orientation. |
| /// |
| /// Specifies the set of orientations the application interface can be |
| /// displayed in. |
| /// |
| /// The [orientations] argument is a list of DeviceOrientation values. |
| /// The empty list uses Screen unlock api and causes the application to |
| /// defer to the operating system default. |
| /// |
| /// See w3c screen api: https://www.w3.org/TR/screen-orientation/ |
| Future<bool> setPreferredOrientation(List<dynamic> orientations) { |
| final DomScreen? screen = domWindow.screen; |
| if (screen != null) { |
| final DomScreenOrientation? screenOrientation = screen.orientation; |
| if (screenOrientation != null) { |
| if (orientations.isEmpty) { |
| screenOrientation.unlock(); |
| return Future<bool>.value(true); |
| } else { |
| final String? lockType = |
| _deviceOrientationToLockType(orientations.first as String?); |
| if (lockType != null) { |
| final Completer<bool> completer = Completer<bool>(); |
| try { |
| screenOrientation.lock(lockType).then((dynamic _) { |
| completer.complete(true); |
| }).catchError((dynamic error) { |
| // On Chrome desktop an error with 'not supported on this device |
| // error' is fired. |
| completer.complete(false); |
| }); |
| } catch (_) { |
| return Future<bool>.value(false); |
| } |
| return completer.future; |
| } |
| } |
| } |
| } |
| // API is not supported on this browser return false. |
| return Future<bool>.value(false); |
| } |
| |
| // Converts device orientation to w3c OrientationLockType enum. |
| // |
| // See also: https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation/lock |
| static String? _deviceOrientationToLockType(String? deviceOrientation) { |
| switch (deviceOrientation) { |
| case 'DeviceOrientation.portraitUp': |
| return orientationLockTypePortraitPrimary; |
| case 'DeviceOrientation.portraitDown': |
| return orientationLockTypePortraitSecondary; |
| case 'DeviceOrientation.landscapeLeft': |
| return orientationLockTypeLandscapePrimary; |
| case 'DeviceOrientation.landscapeRight': |
| return orientationLockTypeLandscapeSecondary; |
| default: |
| 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 |
| /// place it as first element of body(webkit), or as a child of |
| /// glass pane element for other browsers to make sure url resolution |
| /// works correctly when content is inside a shadow root. |
| void addResource(DomElement element) { |
| final bool isWebKit = browserEngine == BrowserEngine.webkit; |
| if (_resourcesHost == null) { |
| final DomElement resourcesHost = domDocument |
| .createElement('flt-svg-filters') |
| ..style.visibility = 'hidden'; |
| if (isWebKit) { |
| // The resourcesHost *must* be a sibling of the glassPaneElement. |
| _embeddingStrategy.attachResourcesHost(resourcesHost, |
| nextTo: glassPaneElement); |
| } else { |
| glassPaneShadow.node |
| .insertBefore(resourcesHost, glassPaneShadow.node.firstChild); |
| } |
| _resourcesHost = resourcesHost; |
| } |
| _resourcesHost!.append(element); |
| } |
| |
| /// Removes a global resource element. |
| void removeResource(DomElement? element) { |
| if (element == null) { |
| return; |
| } |
| assert(element.parentNode == _resourcesHost); |
| element.remove(); |
| } |
| |
| /// Disables the browser's context menu for this part of the DOM. |
| /// |
| /// By default, when a Flutter web app starts, the context menu is enabled. |
| /// |
| /// Can be re-enabled by calling [enableContextMenu]. |
| void disableContextMenu() => _embeddingStrategy.disableContextMenu(); |
| |
| /// Enables the browser's context menu for this part of the DOM. |
| /// |
| /// By default, when a Flutter web app starts, the context menu is already |
| /// enabled. Typically, this method would be used after calling |
| /// [disableContextMenu] to first disable it. |
| void enableContextMenu() => _embeddingStrategy.enableContextMenu(); |
| } |
| |
| /// The embedder singleton. |
| /// |
| /// [ensureFlutterViewEmbedderInitialized] must be called prior to calling this |
| /// getter. |
| FlutterViewEmbedder get flutterViewEmbedder { |
| final FlutterViewEmbedder? embedder = _flutterViewEmbedder; |
| assert(() { |
| if (embedder == null) { |
| throw StateError( |
| '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); |