blob: 52de171182e713a2cf08612e76a28dd6e04776a1 [file] [log] [blame]
// 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;
import '../configuration.dart';
import '../dom.dart';
import '../platform_views/content_manager.dart';
import '../safe_browser_api.dart';
import 'style_manager.dart';
/// Manages DOM elements and the DOM structure for a [ui.FlutterView].
///
/// Here's the general DOM structure of a Flutter View:
///
/// [rootElement] <flutter-view>
/// |
/// +- [platformViewsHost] <flt-glass-pane>
/// | |
/// | +- [renderingHost] #shadow-root
/// | | |
/// | | +- <flt-semantics-placeholder>
/// | | |
/// | | +- [sceneHost] <flt-scene-host>
/// | | | |
/// | | | +- <flt-scene>
/// | | |
/// | | +- <style>
/// | |
/// | +- ...platform views
/// |
/// +- [textEditingHost] <flt-text-editing-host>
/// | |
/// | +- ...text fields
/// |
/// +- [semanticsHost] <flt-semantics-host>
/// | |
/// | +- ...semantics nodes
/// |
/// +- <style>
///
class DomManager {
factory DomManager({required double devicePixelRatio}) {
final DomElement rootElement = domDocument.createElement(DomManager.flutterViewTagName);
final DomElement platformViewsHost = domDocument.createElement(DomManager.glassPaneTagName);
final DomShadowRoot renderingHost = _attachShadowRoot(platformViewsHost);
final DomElement sceneHost = domDocument.createElement(DomManager.sceneHostTagName);
final DomElement textEditingHost = domDocument.createElement(DomManager.textEditingHostTagName);
final DomElement semanticsHost = domDocument.createElement(DomManager.semanticsHostTagName);
// Root element children.
rootElement.appendChild(platformViewsHost);
rootElement.appendChild(textEditingHost);
// 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.
rootElement.appendChild(semanticsHost);
// Rendering host (shadow root) children.
renderingHost.append(sceneHost);
// Styling.
StyleManager.attachGlobalStyles(
node: rootElement,
styleId: 'flt-text-editing-stylesheet',
styleNonce: configuration.nonce,
cssSelectorPrefix: DomManager.flutterViewTagName,
);
StyleManager.attachGlobalStyles(
node: renderingHost,
styleId: 'flt-internals-stylesheet',
styleNonce: configuration.nonce,
cssSelectorPrefix: '',
);
StyleManager.styleSceneHost(
sceneHost,
debugShowSemanticsNodes: configuration.debugShowSemanticsNodes,
);
StyleManager.styleSemanticsHost(
semanticsHost,
devicePixelRatio,
);
return DomManager._(
rootElement: rootElement,
platformViewsHost: platformViewsHost,
renderingHost: renderingHost,
sceneHost: sceneHost,
textEditingHost: textEditingHost,
semanticsHost: semanticsHost,
);
}
DomManager._({
required this.rootElement,
required this.platformViewsHost,
required this.renderingHost,
required this.sceneHost,
required this.textEditingHost,
required this.semanticsHost,
});
/// The tag name for the Flutter View root element.
static const String flutterViewTagName = 'flutter-view';
/// The tag name for the glass-pane.
static const String glassPaneTagName = 'flt-glass-pane';
/// The tag name for the scene host.
static const String sceneHostTagName = 'flt-scene-host';
/// The tag name for the text editing host.
static const String textEditingHostTagName = 'flt-text-editing-host';
/// The tag name for the semantics host.
static const String semanticsHostTagName = 'flt-semantics-host';
/// The root DOM element for the entire Flutter View.
///
/// This is where input events are captured, such as pointer events.
///
/// If semantics is enabled, this element also contains the semantics DOM tree,
/// which captures semantics input events.
final DomElement rootElement;
/// Hosts all platform view elements.
final DomElement platformViewsHost;
/// Hosts all rendering elements and canvases.
final DomShadowRoot renderingHost;
/// Hosts the <flt-scene> element.
///
/// This element is created and inserted in the HTML DOM once. It is never
/// removed or moved. However the <flt-scene> inside of it may be replaced.
final DomElement sceneHost;
/// Hosts all text editing elements.
final DomElement textEditingHost;
/// Hosts the semantics tree.
///
/// This element is in front of the [renderingHost] and [platformViewsHost].
/// Otherwise, the phone will disable focusing by touch, only by tabbing
/// around the UI.
final DomElement semanticsHost;
DomElement? _lastSceneElement;
/// Inserts the [sceneElement] into the DOM and removes the existing scene (if
/// any).
///
/// The [sceneElement] is inserted as a child of the <flt-scene-host> element
/// inside the [renderingHost].
///
/// If the [sceneElement] has already been inserted, this method does nothing
/// to avoid unnecessary DOM mutations. This is both faster and more correct,
/// because moving DOM nodes loses internal state, such as text selection.
void setScene(DomElement sceneElement) {
if (sceneElement != _lastSceneElement) {
_lastSceneElement?.remove();
_lastSceneElement = sceneElement;
sceneHost.append(sceneElement);
}
}
/// Injects a platform view with [platformViewId] into [platformViewsHost].
///
/// If the platform view is already injected, this method does *nothing*.
///
/// The `platformViewsHost` can only be different if `platformViewId` is moving
/// from one [FlutterView] to another. In that case, the browser will move the
/// slot contents from the old `platformViewsHost` to the new one, but that
/// will cause the platformView to reset its state (an iframe will re-render,
/// text selections will be lost, video playback interrupted, etc...)
///
/// Try not to move platform views across views!
void injectPlatformView(int platformViewId) {
// For now, we don't need anything fancier. If needed, this can be converted
// to a PlatformViewStrategy class for each web-renderer backend?
final DomElement? pv = PlatformViewManager.instance.getSlottedContent(platformViewId);
if (pv == null) {
domWindow.console.debug('Failed to inject Platform View Id: $platformViewId. '
'Render seems to be happening before a `flutter/platform_views:create` platform message!');
return;
}
// If pv is already a descendant of platformViewsHost -> noop
if (pv.parent == platformViewsHost) {
return;
}
platformViewsHost.append(pv);
}
}
DomShadowRoot _attachShadowRoot(DomElement element) {
assert(
getJsProperty<Object?>(element, 'attachShadow') != null,
'ShadowDOM is not supported in this browser.',
);
return element.attachShadow(<String, dynamic>{
'mode': 'open',
// This needs to stay false to prevent issues like this:
// - https://github.com/flutter/flutter/issues/85759
'delegatesFocus': false,
});
}