未验证 提交 b5f8141e 编写于 作者: D David Iglesias 提交者: GitHub

[web] Render PlatformViews with SLOT tags. (#25747)

上级 acea7c42
......@@ -543,6 +543,9 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/onscreen_logging.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/picture.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_views.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_views/content_manager.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_views/message_handler.dart
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_converter.dart
......
......@@ -248,6 +248,9 @@ part 'engine/onscreen_logging.dart';
part 'engine/picture.dart';
part 'engine/platform_dispatcher.dart';
part 'engine/platform_views.dart';
part 'engine/platform_views/content_manager.dart';
part 'engine/platform_views/message_handler.dart';
part 'engine/platform_views/slots.dart';
part 'engine/profiler.dart';
part 'engine/rrect_renderer.dart';
part 'engine/semantics/accessibility.dart';
......@@ -413,6 +416,11 @@ class NullTreeSanitizer implements html.NodeTreeSanitizer {
void sanitizeTree(html.Node node) {}
}
/// The shared instance of PlatformViewManager shared across the engine to handle
/// rendering of PlatformViews into the web app.
/// TODO(dit): How to make this overridable from tests?
final PlatformViewManager platformViewManager = PlatformViewManager();
/// Converts a matrix represented using [Float64List] to one represented using
/// [Float32List].
///
......
......@@ -3,13 +3,11 @@
// found in the LICENSE file.
import 'dart:html' as html;
import 'dart:typed_data';
import 'package:ui/src/engine.dart' show window, NullTreeSanitizer;
import 'package:ui/src/engine.dart' show window, NullTreeSanitizer, platformViewManager, createPlatformViewSlot;
import 'package:ui/ui.dart' as ui;
import '../html/path_to_svg_clip.dart';
import '../services.dart';
import '../util.dart';
import '../vector_math.dart';
import 'canvas.dart';
......@@ -37,11 +35,13 @@ class HtmlViewEmbedder {
final Map<int, EmbeddedViewParams> _currentCompositionParams =
<int, EmbeddedViewParams>{};
/// The HTML element associated with the given view id.
final Map<int?, html.Element> _views = <int?, html.Element>{};
/// The root view in the stack of mutator elements for the view id.
final Map<int?, html.Element?> _rootViews = <int?, html.Element?>{};
/// The clip chain for a view Id.
///
/// This contains:
/// * The root view in the stack of mutator elements for the view id.
/// * The slot view in the stack (what shows the actual platform view contents).
/// * The number of clipping elements used last time the view was composited.
final Map<int, ViewClipChain> _viewClipChains = <int, ViewClipChain>{};
/// Surfaces used to draw on top of platform views, keyed by platform view ID.
///
......@@ -51,18 +51,12 @@ class HtmlViewEmbedder {
/// The views that need to be recomposited into the scene on the next frame.
final Set<int> _viewsToRecomposite = <int>{};
/// The views that need to be disposed of on the next frame.
final Set<int> _viewsToDispose = <int>{};
/// The list of view ids that should be composited, in order.
List<int> _compositionOrder = <int>[];
/// The most recent composition order.
List<int> _activeCompositionOrder = <int>[];
/// The number of clipping elements used last time the view was composited.
Map<int, int> _clipCount = <int, int>{};
/// The size of the frame, in physical pixels.
ui.Size _frameSize = ui.window.physicalSize;
......@@ -74,76 +68,6 @@ class HtmlViewEmbedder {
_frameSize = size;
}
void handlePlatformViewCall(
ByteData? data,
ui.PlatformMessageResponseCallback? callback,
) {
const MethodCodec codec = StandardMethodCodec();
final MethodCall decoded = codec.decodeMethodCall(data);
switch (decoded.method) {
case 'create':
_create(decoded, callback);
return;
case 'dispose':
_dispose(decoded, callback!);
return;
}
callback!(null);
}
void _create(
MethodCall methodCall, ui.PlatformMessageResponseCallback? callback) {
final Map<dynamic, dynamic> args = methodCall.arguments;
final int? viewId = args['id'];
final String? viewType = args['viewType'];
const MethodCodec codec = StandardMethodCodec();
if (_views[viewId] != null) {
callback!(codec.encodeErrorEnvelope(
code: 'recreating_view',
message: 'trying to create an already created view',
details: 'view id: $viewId',
));
return;
}
final ui.PlatformViewFactory? factory =
ui.platformViewRegistry.registeredFactories[viewType];
if (factory == null) {
callback!(codec.encodeErrorEnvelope(
code: 'unregistered_view_type',
message: 'trying to create a view with an unregistered type',
details: 'unregistered view type: $viewType',
));
return;
}
// TODO(het): Support creation parameters.
html.Element embeddedView = factory(viewId!);
_views[viewId] = embeddedView;
_rootViews[viewId] = embeddedView;
callback!(codec.encodeSuccessEnvelope(null));
}
void _dispose(
MethodCall methodCall, ui.PlatformMessageResponseCallback callback) {
final int? viewId = methodCall.arguments;
const MethodCodec codec = StandardMethodCodec();
if (viewId == null || !_views.containsKey(viewId)) {
callback(codec.encodeErrorEnvelope(
code: 'unknown_view',
message: 'trying to dispose an unknown view',
details: 'view id: $viewId',
));
return;
}
_viewsToDispose.add(viewId);
callback(codec.encodeSuccessEnvelope(null));
}
List<CkCanvas> getCurrentCanvases() {
final List<CkCanvas> canvases = <CkCanvas>[];
for (int i = 0; i < _compositionOrder.length; i++) {
......@@ -179,28 +103,39 @@ class HtmlViewEmbedder {
}
void _compositeWithParams(int viewId, EmbeddedViewParams params) {
final html.Element platformView = _views[viewId]!;
platformView.style.width = '${params.size.width}px';
platformView.style.height = '${params.size.height}px';
platformView.style.position = 'absolute';
// If we haven't seen this viewId yet, cache it for clips/transforms.
ViewClipChain clipChain = _viewClipChains.putIfAbsent(viewId, () {
return ViewClipChain(view: createPlatformViewSlot(viewId));
});
// <flt-scene-host> disables pointer events. Reenable them here because the
// underlying platform view would want to handle the pointer events.
platformView.style.pointerEvents = 'auto';
html.Element slot = clipChain.slot;
// See `apply()` in the PersistedPlatformView class for the HTML version
// of this code.
slot.style
..width = '${params.size.width}px'
..height = '${params.size.height}px'
..position = 'absolute';
// Recompute the position in the DOM of the `slot` element...
final int currentClippingCount = _countClips(params.mutators);
final int? previousClippingCount = _clipCount[viewId];
final int previousClippingCount = clipChain.clipCount;
if (currentClippingCount != previousClippingCount) {
_clipCount[viewId] = currentClippingCount;
html.Element oldPlatformViewRoot = _rootViews[viewId]!;
html.Element? newPlatformViewRoot = _reconstructClipViewsChain(
html.Element oldPlatformViewRoot = clipChain.root;
html.Element newPlatformViewRoot = _reconstructClipViewsChain(
currentClippingCount,
platformView,
slot,
oldPlatformViewRoot,
);
_rootViews[viewId] = newPlatformViewRoot;
// Store the updated root element, and clip count
clipChain.updateClipChain(
root: newPlatformViewRoot,
clipCount: currentClippingCount,
);
}
_applyMutators(params.mutators, platformView, viewId);
// Apply mutators to the slot
_applyMutators(params.mutators, slot, viewId);
}
int _countClips(MutatorsStack mutators) {
......@@ -213,7 +148,7 @@ class HtmlViewEmbedder {
return clipCount;
}
html.Element? _reconstructClipViewsChain(
html.Element _reconstructClipViewsChain(
int numClips,
html.Element platformView,
html.Element headClipView,
......@@ -386,8 +321,6 @@ class HtmlViewEmbedder {
}
void submitFrame() {
disposeViews();
for (int i = 0; i < _compositionOrder.length; i++) {
int viewId = _compositionOrder[i];
_ensureOverlayInitialized(viewId);
......@@ -412,7 +345,7 @@ class HtmlViewEmbedder {
int viewId = _compositionOrder[i];
if (assertionsEnabled) {
if (!_views.containsKey(viewId)) {
if (!platformViewManager.knowsViewId(viewId)) {
debugInvalidViewIds ??= <int>[];
debugInvalidViewIds.add(viewId);
continue;
......@@ -420,7 +353,7 @@ class HtmlViewEmbedder {
}
unusedViews.remove(viewId);
html.Element platformViewRoot = _rootViews[viewId]!;
html.Element platformViewRoot = _viewClipChains[viewId]!.root;
html.Element overlay = _overlays[viewId]!.htmlElement;
platformViewRoot.remove();
skiaSceneHost!.append(platformViewRoot);
......@@ -428,12 +361,10 @@ class HtmlViewEmbedder {
skiaSceneHost!.append(overlay);
_activeCompositionOrder.add(viewId);
}
_compositionOrder.clear();
for (final int unusedViewId in unusedViews) {
_releaseOverlay(unusedViewId);
_rootViews[unusedViewId]?.remove();
}
disposeViews(unusedViews);
if (assertionsEnabled) {
if (debugInvalidViewIds != null && debugInvalidViewIds.isNotEmpty) {
......@@ -445,24 +376,18 @@ class HtmlViewEmbedder {
}
}
void disposeViews() {
if (_viewsToDispose.isEmpty) {
return;
}
for (final int viewId in _viewsToDispose) {
final html.Element rootView = _rootViews[viewId]!;
rootView.remove();
_views.remove(viewId);
_rootViews.remove(viewId);
void disposeViews(Set<int> viewsToDispose) {
for (final int viewId in viewsToDispose) {
// Remove viewId from the _viewClipChains Map, and then from the DOM.
ViewClipChain clipChain = _viewClipChains.remove(viewId)!;
clipChain.root.remove();
// More cleanup
_releaseOverlay(viewId);
_currentCompositionParams.remove(viewId);
_clipCount.remove(viewId);
_viewsToRecomposite.remove(viewId);
_cleanUpClipDefs(viewId);
_svgClipDefs.remove(viewId);
}
_viewsToDispose.clear();
}
void _releaseOverlay(int viewId) {
......@@ -499,6 +424,29 @@ class HtmlViewEmbedder {
}
}
/// Represents a Clip Chain (for a view).
///
/// Objects of this class contain:
/// * The root view in the stack of mutator elements for the view id.
/// * The slot view in the stack (the actual contents of the platform view).
/// * The number of clipping elements used last time the view was composited.
class ViewClipChain {
html.Element _root;
html.Element _slot;
int _clipCount = -1;
ViewClipChain({required html.Element view}) : this._root = view, this._slot = view;
html.Element get root => _root;
html.Element get slot => _slot;
int get clipCount => _clipCount;
void updateClipChain({required html.Element root, required int clipCount}) {
_root = root;
_clipCount = clipCount;
}
}
/// Caches surfaces used to overlay platform views.
class OverlayCache {
static const int kDefaultCacheSize = 5;
......
......@@ -12,7 +12,10 @@ class DomRenderer {
reset();
TextMeasurementService.initialize(rulerCacheCapacity: 10);
TextMeasurementService.initialize(
rulerCacheCapacity: 10,
root: _glassPaneShadow!,
);
assert(() {
_setupHotRestart();
......@@ -26,6 +29,9 @@ class DomRenderer {
static const int vibrateHeavyImpact = 30;
static const int vibrateSelectionClick = 10;
// The tag name for the root view of the flutter app (glass-pane)
static const String _glassPaneTagName = 'flt-glass-pane';
/// Fires when browser language preferences change.
static const html.EventStreamProvider<html.Event> languageChangeEvent =
const html.EventStreamProvider<html.Event>('languagechange');
......@@ -154,6 +160,10 @@ class DomRenderer {
html.Element? get glassPaneElement => _glassPaneElement;
html.Element? _glassPaneElement;
/// The ShadowRoot of the [glassPaneElement].
html.ShadowRoot? get glassPaneShadow => _glassPaneShadow;
html.ShadowRoot? _glassPaneShadow;
final html.Element rootElement = html.document.body!;
void addElementClass(html.Element element, String className) {
......@@ -252,11 +262,8 @@ class DomRenderer {
static const String defaultCssFont =
'$defaultFontStyle $defaultFontWeight ${defaultFontSize}px $defaultFontFamily';
void reset() {
_styleElement?.remove();
_styleElement = html.StyleElement();
html.document.head!.append(_styleElement!);
final html.CssStyleSheet sheet = _styleElement!.sheet as html.CssStyleSheet;
// Applies the required global CSS to an incoming [html.CssStyleSheet] `sheet`.
void _applyCssRulesToSheet(html.CssStyleSheet sheet) {
final bool isWebKit = browserEngine == BrowserEngine.webkit;
final bool isFirefox = browserEngine == BrowserEngine.firefox;
// TODO(butterfly): use more efficient CSS selectors; descendant selectors
......@@ -341,7 +348,7 @@ flt-semantics [contentEditable="true"] {
// on using gray background. This CSS rule disables that.
if (isWebKit) {
sheet.insertRule('''
flt-glass-pane * {
$_glassPaneTagName * {
-webkit-tap-highlight-color: transparent;
}
''', sheet.cssRules.length);
......@@ -360,6 +367,16 @@ flt-glass-pane * {
}
''', sheet.cssRules.length);
}
}
void reset() {
final bool isWebKit = browserEngine == BrowserEngine.webkit;
_styleElement?.remove();
_styleElement = html.StyleElement();
html.document.head!.append(_styleElement!);
final html.CssStyleSheet sheet = _styleElement!.sheet as html.CssStyleSheet;
_applyCssRulesToSheet(sheet);
final html.BodyElement bodyElement = html.document.body!;
......@@ -432,7 +449,7 @@ flt-glass-pane * {
// 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 html.Element glassPaneElement = createElement('flt-glass-pane');
final html.Element glassPaneElement = createElement(_glassPaneTagName);
_glassPaneElement = glassPaneElement;
glassPaneElement.style
..position = 'absolute'
......@@ -440,9 +457,28 @@ flt-glass-pane * {
..right = '0'
..bottom = '0'
..left = '0';
// Create a Shadow Root under the glass panel, and attach everything there,
// instead of directly underneath the glass panel.
final html.ShadowRoot glassPaneElementShadowRoot = glassPaneElement.attachShadow(<String, String>{
'mode': 'open',
'delegatesFocus': 'true',
});
_glassPaneShadow = glassPaneElementShadowRoot;
bodyElement.append(glassPaneElement);
_sceneHostElement = createElement('flt-scene-host');
final html.StyleElement shadowRootStyleElement = html.StyleElement();
// The shadowRootStyleElement must be appended to the DOM, or its `sheet` will be null later...
glassPaneElementShadowRoot.append(shadowRootStyleElement);
final html.CssStyleSheet shadowRootStyleSheet = shadowRootStyleElement.sheet as html.CssStyleSheet;
_applyCssRulesToSheet(shadowRootStyleSheet); // TODO: Apply only rules for the shadow root
// Don't allow the scene to receive pointer events.
_sceneHostElement = createElement('flt-scene-host')
..style
.pointerEvents = 'none';
final html.Element semanticsHostElement =
createElement('flt-semantics-host');
......@@ -451,23 +487,16 @@ flt-glass-pane * {
..transformOrigin = '0 0 0';
_semanticsHostElement = semanticsHostElement;
updateSemanticsScreenProperties();
glassPaneElement.append(semanticsHostElement);
// Don't allow the scene to receive pointer events.
_sceneHostElement!.style.pointerEvents = 'none';
glassPaneElement.append(_sceneHostElement!);
final html.Element _accesibilityPlaceholder = EngineSemanticsOwner
final html.Element _accessibilityPlaceholder = EngineSemanticsOwner
.instance.semanticsHelper
.prepareAccessibilityPlaceholder();
// Insert the semantics placeholder after the scene host. For all widgets
// in the scene, except for platform widgets, the scene host will pass the
// pointer events through to the semantics tree. However, for platform
// views, the pointer events will not pass through, and will be handled
// by the platform view.
glassPaneElement.insertBefore(_accesibilityPlaceholder, _sceneHostElement);
glassPaneElementShadowRoot.nodes.addAll([
semanticsHostElement,
_accessibilityPlaceholder,
_sceneHostElement!,
]);
// When debugging semantics, make the scene semi-transparent so that the
// semantics tree is visible.
......
......@@ -12,48 +12,11 @@ class PersistedPlatformView extends PersistedLeafSurface {
final double width;
final double height;
late html.ShadowRoot _shadowRoot;
PersistedPlatformView(this.viewId, this.dx, this.dy, this.width, this.height);
@override
html.Element createElement() {
html.Element element = defaultCreateElement('flt-platform-view');
// Allow the platform view host element to receive pointer events.
//
// This is to allow platform view HTML elements to be interactive.
//
// ACCESSIBILITY NOTE: The way we enable accessibility on Flutter for web
// is to have a full-page button which waits for a double tap. Placing this
// full-page button in front of the scene would cause platform views not
// to receive pointer events. The tradeoff is that by placing the scene in
// front of the semantics placeholder will cause platform views to block
// pointer events from reaching the placeholder. This means that in order
// to enable accessibility, you must double tap the app *outside of a
// platform view*. As a consequence, a full-screen platform view will make
// it impossible to enable accessibility.
element.style.pointerEvents = 'auto';
// Enforce the effective size of the PlatformView.
element.style.overflow = 'hidden';
_shadowRoot = element.attachShadow(<String, String>{'mode': 'open'});
final html.StyleElement _styleReset = html.StyleElement();
_styleReset.innerHtml = '''
:host {
all: initial;
cursor: inherit;
}''';
_shadowRoot.append(_styleReset);
final html.Element? platformView =
ui.platformViewRegistry.getCreatedView(viewId);
if (platformView != null) {
_shadowRoot.append(platformView);
} else {
printWarning('No platform view created for id $viewId');
}
return element;
return createPlatformViewSlot(viewId);
}
@override
......@@ -61,18 +24,12 @@ class PersistedPlatformView extends PersistedLeafSurface {
@override
void apply() {
// See `_compositeWithParams` in the HtmlViewEmbedder for the canvaskit equivalent.
rootElement!.style
..transform = 'translate(${dx}px, ${dy}px)'
..width = '${width}px'
..height = '${height}px';
// Set size of the root element created by the PlatformView.
final html.Element? platformView =
ui.platformViewRegistry.getCreatedView(viewId);
if (platformView != null) {
platformView.style
..width = '${width}px'
..height = '${height}px';
}
..height = '${height}px'
..position = 'absolute';
}
// Platform Views can only be updated if their viewId matches.
......
......@@ -314,6 +314,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
};
}
PlatformViewMessageHandler? _platformViewMessageHandler;
void _sendPlatformMessage(
String name,
ByteData? data,
......@@ -449,12 +451,13 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
return;
case 'flutter/platform_views':
if (useCanvasKit) {
rasterizer!.surface.viewEmbedder
.handlePlatformViewCall(data, callback);
} else {
ui.handlePlatformViewCall(data!, callback!);
}
_platformViewMessageHandler ??= PlatformViewMessageHandler(
contentManager: platformViewManager,
contentHandler: (html.Element content) {
domRenderer.glassPaneElement!.append(content);
}
);
_platformViewMessageHandler!.handlePlatformViewCall(data, callback!);
return;
case 'flutter/accessibility':
......
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
part of engine;
/// A function which takes a unique `id` and some `params` and creates an HTML element.
///
/// This is made available to end-users through dart:ui in web.
typedef ParameterizedPlatformViewFactory = html.Element Function(
int viewId, {
Object? params,
});
/// A function which takes a unique `id` and creates an HTML element.
///
/// This is made available to end-users through dart:ui in web.
typedef PlatformViewFactory = html.Element Function(int viewId);
/// This class handles the lifecycle of Platform Views in the DOM of a Flutter Web App.
///
/// There are three important parts of Platform Views. This class manages two of
/// them:
///
/// * `factories`: The functions used to render the contents of any given Platform
/// View by its `viewType`.
/// * `contents`: The result [html.Element] of calling a `factory` function.
///
/// The third part is `slots`, which are created on demand by the
/// [createPlatformViewSlot] function.
///
/// This class keeps a registry of `factories`, `contents` so the framework can
/// CRUD Platform Views as needed, regardless of the rendering backend.
class PlatformViewManager {
// The factory functions, indexed by the viewType
final Map<String, Function> _factories = {};
// The references to content tags, indexed by their framework-given ID.
final Map<int, html.Element> _contents = {};
/// Returns `true` if the passed in `viewType` has been registered before.
///
/// See [registerViewFactory] to understand how factories are registered.
bool knowsViewType(String viewType) {
return _factories.containsKey(viewType);
}
/// Returns `true` if the passed in `viewId` has been rendered (and not disposed) before.
///
/// See [renderContent] and [createPlatformViewSlot] to understand how platform views are
/// rendered.
bool knowsViewId(int viewId) {
return _contents.containsKey(viewId);
}
/// Registers a `factoryFunction` that knows how to render a Platform View of `viewType`.
///
/// `viewType` is selected by the programmer, but it can't be overridden once
/// it's been set.
///
/// `factoryFunction` needs to be a [PlatformViewFactory].
bool registerFactory(String viewType, Function factoryFunction) {
assert(factoryFunction is PlatformViewFactory ||
factoryFunction is ParameterizedPlatformViewFactory);
if (_factories.containsKey(viewType)) {
return false;
}
_factories[viewType] = factoryFunction;
return true;
}
/// Creates the HTML markup for the `contents` of a Platform View.
///
/// The result of this call is cached in the `_contents` Map. This is only
/// cached so it can be disposed of later by [clearPlatformView]. _Note that
/// there's no `getContents` function in this class._
///
/// The resulting DOM for the `contents` of a Platform View looks like this:
///
/// ```html
/// <flt-platform-view slot="...">
/// <arbitrary-html-elements />
/// </flt-platform-view-slot>
/// ```
///
/// The `arbitrary-html-elements` are the result of the call to the user-supplied
/// `factory` function for this Platform View (see [registerFactory]).
///
/// The outer `flt-platform-view` tag is a simple wrapper that we add to have
/// a place where to attach the `slot` property, that will tell the browser
/// what `slot` tag will reveal this `contents`, **without modifying the returned
/// html from the `factory` function**.
html.Element renderContent(
String viewType,
int viewId,
Object? params,
) {
assert(knowsViewType(viewType),
'Attempted to render contents of unregistered viewType: $viewType');
final String slotName = getPlatformViewSlotName(viewId);
return _contents.putIfAbsent(viewId, () {
final html.Element wrapper = html.document
.createElement('flt-platform-view')
..setAttribute('slot', slotName);
final Function factoryFunction = _factories[viewType]!;
late html.Element content;
if (factoryFunction is ParameterizedPlatformViewFactory) {
content = factoryFunction(viewId, params: params);
} else {
content = factoryFunction(viewId);
}
_ensureContentCorrectlySized(content, viewType);
return wrapper..append(content);
});
}
/// Removes a PlatformView by its `viewId` from the manager, and from the DOM.
///
/// Once a view has been cleared, calls [knowsViewId] will fail, as if it had
/// never been rendered before.
void clearPlatformView(int viewId) {
// Remove from our cache, and then from the DOM...
_contents.remove(viewId)?.remove();
}
/// Attempt to ensure that the contents of the user-supplied DOM element will
/// fill the space allocated for this platform view by the framework.
void _ensureContentCorrectlySized(html.Element content, String viewType) {
// Scrutinize closely any other modifications to `content`.
// We shouldn't modify users' returned `content` if at all possible.
// Note there's also no getContent(viewId) function anymore, to prevent
// from later modifications too.
if (content.style.height.isEmpty) {
printWarning('Height of Platform View type: [$viewType] may not be set.'
' Defaulting to `height: 100%`.\n'
'Set `style.height` to any appropriate value to stop this message.');
content.style.height = '100%';
}
if (content.style.width.isEmpty) {
printWarning('Width of Platform View type: [$viewType] may not be set.'
' Defaulting to `width: 100%`.\n'
'Set `style.width` to any appropriate value to stop this message.');
content.style.width = '100%';
}
}
}
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
part of engine;
/// The signature for a callback for a Platform Message. From the `ui` package.
/// Copied here so there's no circular dependencies.
typedef _PlatformMessageResponseCallback = void Function(ByteData? data);
/// A function that handle a newly created [html.Element] with the contents of a
/// platform view with a unique [int] id.
typedef PlatformViewContentHandler = void Function(html.Element);
/// This class handles incoming framework messages to create/dispose Platform Views.
///
/// (An instance of this class is connected to the `flutter/platform_views`
/// Platform Channel in the [EnginePlatformDispatcher] class.)
///
/// It uses a [PlatformViewManager] to handle the CRUD of the DOM of Platform Views.
/// This `contentManager` is shared across the engine, to perform
/// all operations related to platform views (registration, rendering, etc...),
/// regardless of the rendering backend.
///
/// When the `contents` of a Platform View are created, a [PlatformViewContentHandler]
/// function (passed from the outside) will decide where in the DOM to inject
/// said content.
///
/// The rendering/compositing of Platform Views can create the other "half" of a
/// Platform View: the `slot`, through the [createPlatformViewSlot] method.
///
/// When a Platform View is disposed of, it is removed from the cache (and DOM)
/// directly by the `contentManager`. The canvaskit rendering backend needs to do
/// some extra cleanup of its internal state, but it can do it automatically. See
/// [HtmlViewEmbedder.disposeViews]
class PlatformViewMessageHandler {
final MethodCodec _codec = StandardMethodCodec();
final PlatformViewManager _contentManager;
final PlatformViewContentHandler? _contentHandler;
PlatformViewMessageHandler({
required PlatformViewManager contentManager,
PlatformViewContentHandler? contentHandler,
}) : this._contentManager = contentManager,
this._contentHandler = contentHandler;
/// Handle a `create` Platform View message.
///
/// This will attempt to render the `contents` and of a Platform View, if its
/// `viewType` has been registered previously.
///
/// (See [PlatformViewContentManager.registerFactory] for more details.)
///
/// The `contents` are delegated to a [_contentHandler] function, so the
/// active rendering backend can inject them in the right place of the DOM.
///
/// If all goes well, this function will `callback` with an empty success envelope.
/// In case of error, this will `callback` with an error envelope describing the error.
void _createPlatformView(
MethodCall methodCall,
_PlatformMessageResponseCallback callback,
) {
final Map<dynamic, dynamic> args = methodCall.arguments;
final int viewId = args['id'];
final String viewType = args['viewType'];
if (!_contentManager.knowsViewType(viewType)) {
callback(_codec.encodeErrorEnvelope(
code: 'unregistered_view_type',
message: 'trying to create a view with an unregistered type',
details: 'unregistered view type: $viewType',
));
return;
}
if (_contentManager.knowsViewId(viewId)) {
callback(_codec.encodeErrorEnvelope(
code: 'recreating_view',
message: 'trying to create an already created view',
details: 'view id: $viewId',
));
return;
}
// TODO: How can users add extra `args` from the HtmlElementView widget?
final html.Element content = _contentManager.renderContent(
viewType,
viewId,
args,
);
// For now, we don't need anything fancier. If needed, this can be converted
// to a PlatformViewStrategy class for each web-renderer backend?
if (_contentHandler != null) {
_contentHandler!(content);
}
callback(_codec.encodeSuccessEnvelope(null));
}
/// Handle a `dispose` Platform View message.
///
/// This will clear the cached information that the framework has about a given
/// `viewId`, through the [_contentManager].
///
/// Once that's done, the dispose call is delegated to the [_disposeHandler]
/// function, so the active rendering backend can dispose of whatever resources
/// it needed to get ahold of.
///
/// This function should always `callback` with an empty success envelope.
void _disposePlatformView(
MethodCall methodCall,
_PlatformMessageResponseCallback callback,
) {
final int viewId = methodCall.arguments;
// The contentManager removes the slot and the contents from its internal
// cache, and the DOM.
_contentManager.clearPlatformView(viewId);
callback(_codec.encodeSuccessEnvelope(null));
}
/// Handles a PlatformViewCall to the `flutter/platform_views` channel.
///
/// This method handles two possible messages:
/// * `create`: See [_createPlatformView]
/// * `dispose`: See [_disposePlatformView]
void handlePlatformViewCall(
ByteData? data,
_PlatformMessageResponseCallback callback,
) {
final MethodCall decoded = _codec.decodeMethodCall(data);
switch (decoded.method) {
case 'create':
_createPlatformView(decoded, callback);
return;
case 'dispose':
_disposePlatformView(decoded, callback);
return;
}
callback(null);
}
}
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
part of engine;
/// Returns the name of a slot from its `viewId`.
///
/// This is used by the [renderContent] function of the [PlatformViewManager]
/// class, and the [createPlatformViewSlot] method below, to keep the slot name
/// attribute consistent across the framework.
String getPlatformViewSlotName(int viewId) {
return 'flt-pv-slot-$viewId';
}
/// Creates the HTML markup for the `slot` of a Platform View.
///
/// The resulting DOM for a `slot` looks like this:
///
/// ```html
/// <flt-platform-view-slot style="...">
/// <slot name="..." />
/// </flt-platform-view-slot>
/// ```
///
/// The inner `SLOT` tag is standard HTML to reveal an element that is rendered
/// elsewhere in the DOM. Its `name` attribute must match the value of the `slot`
/// attribute of the contents being revealed (see [getPlatformViewSlotName].)
///
/// The outer `flt-platform-view-slot` tag is a simple wrapper that the framework
/// can position/style as needed.
///
/// (When the framework accesses a `slot`, it's really accessing its wrapper
/// `flt-platform-view-slot` tag)
html.Element createPlatformViewSlot(int viewId) {
final String slotName = getPlatformViewSlotName(viewId);
final html.Element wrapper = html.document
.createElement('flt-platform-view-slot')
..style.pointerEvents = 'auto';
final html.Element slot = html.document.createElement('slot')
..setAttribute('name', slotName);
return wrapper..append(slot);
}
......@@ -22,9 +22,11 @@ bool _newlinePredicate(int char) {
prop == LineCharProperty.CR;
}
/// Hosts ruler DOM elements in a hidden container.
/// Hosts ruler DOM elements in a hidden container under a `root` [html.Node].
///
/// The `root` [html.Node] is optional. Defaults to [domRenderer.glassPaneShadow].
class RulerHost {
RulerHost() {
RulerHost({html.Node? root}) {
_rulerHost.style
..position = 'fixed'
..visibility = 'hidden'
......@@ -33,7 +35,8 @@ class RulerHost {
..left = '0'
..width = '0'
..height = '0';
html.document.body!.append(_rulerHost);
(root ?? domRenderer.glassPaneShadow!).append(_rulerHost);
registerHotRestartListener(dispose);
}
......@@ -62,8 +65,14 @@ class RulerHost {
/// [ParagraphGeometricStyle].
///
/// All instances of [ParagraphRuler] should be created through this class.
///
/// An optional `root` [html.Node] can be passed, under which the DOM required
/// to perform measurements will be hosted.
class RulerManager extends RulerHost {
RulerManager({required this.rulerCacheCapacity}): super();
RulerManager({
required this.rulerCacheCapacity,
html.Node? root,
}) : super(root: root);
final int rulerCacheCapacity;
......@@ -174,10 +183,16 @@ abstract class TextMeasurementService {
/// Initializes the text measurement service with a specific
/// [rulerCacheCapacity] that gets passed to the [RulerManager].
static void initialize({required int rulerCacheCapacity}) {
///
/// An optional `root` [html.Node] can be passed, under which the DOM required
/// to perform measurements will be hosted. Defaults to [domRenderer.glassPaneShadow].
static void initialize({required int rulerCacheCapacity, html.Node? root}) {
rulerManager?.dispose();
rulerManager = null;
rulerManager = RulerManager(rulerCacheCapacity: rulerCacheCapacity);
rulerManager = RulerManager(
rulerCacheCapacity: rulerCacheCapacity,
root: root,
);
}
@visibleForTesting
......
......@@ -26,6 +26,12 @@ const String transparentTextEditingClass = 'transparentTextEditing';
void _emptyCallback(dynamic _) {}
/// The default root that hosts all DOM required for text editing when a11y is not enabled.
///
/// This is something similar to [html.Document]. Currently, it's a [html.ShadowRoot].
@visibleForTesting
html.ShadowRoot get defaultTextEditingRoot => domRenderer.glassPaneShadow!;
/// These style attributes are constant throughout the life time of an input
/// element.
///
......@@ -232,7 +238,7 @@ class EngineAutofillForm {
void placeForm(html.HtmlElement mainTextEditingElement) {
formElement.append(mainTextEditingElement);
domRenderer.glassPaneElement!.append(formElement);
defaultTextEditingRoot.append(formElement);
}
void storeForm() {
......@@ -832,7 +838,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
// DOM later, when the first location information arrived.
// Otherwise, on Blink based Desktop browsers, the autofill menu appears
// on top left of the screen.
domRenderer.glassPaneElement!.append(activeDomElement);
defaultTextEditingRoot.append(activeDomElement);
_appendedToForm = false;
}
......@@ -1207,7 +1213,7 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
if (hasAutofillGroup) {
placeForm();
} else {
domRenderer.glassPaneElement!.append(activeDomElement);
defaultTextEditingRoot.append(activeDomElement);
}
inputConfig.textCapitalization.setAutocapitalizeAttribute(activeDomElement);
}
......
......@@ -58,93 +58,20 @@ void webOnlySetPluginHandler(Future<void> Function(String, ByteData?, PlatformMe
// does not allow exported non-migrated libraries from migrated libraries. When `dart:_engine`
// is migrated, we can move it back.
/// A function which takes a unique `id` and creates an HTML element.
typedef PlatformViewFactory = html.Element Function(int viewId);
/// A registry for factories that create platform views.
class PlatformViewRegistry {
final Map<String, PlatformViewFactory> registeredFactories =
<String, PlatformViewFactory>{};
final Map<int, html.Element> _createdViews = <int, html.Element>{};
/// Private constructor so this class can be a singleton.
PlatformViewRegistry._();
/// Register [viewTypeId] as being creating by the given [factory].
bool registerViewFactory(String viewTypeId, PlatformViewFactory factory) {
if (registeredFactories.containsKey(viewTypeId)) {
return false;
}
registeredFactories[viewTypeId] = factory;
return true;
}
/// Returns the view that has been created with the given [id], or `null` if
/// no such view exists.
html.Element? getCreatedView(int id) {
return _createdViews[id];
bool registerViewFactory(String viewTypeId, PlatformViewFactory viewFactory) {
// TODO(web): Deprecate this once there's another way of calling `registerFactory` (js interop?)
return engine.platformViewManager.registerFactory(viewTypeId, viewFactory);
}
}
/// A function which takes a unique [id] and creates an HTML element.
typedef PlatformViewFactory = html.Element Function(int viewId);
/// The platform view registry for this app.
final PlatformViewRegistry platformViewRegistry = PlatformViewRegistry._();
/// Handles a platform call to `flutter/platform_views`.
///
/// Used to create platform views.
void handlePlatformViewCall(
ByteData data,
PlatformMessageResponseCallback callback,
) {
const engine.MethodCodec codec = engine.StandardMethodCodec();
final engine.MethodCall decoded = codec.decodeMethodCall(data);
switch (decoded.method) {
case 'create':
_createPlatformView(decoded, callback);
return;
case 'dispose':
_disposePlatformView(decoded, callback);
return;
}
callback(null);
}
void _createPlatformView(
engine.MethodCall methodCall, PlatformMessageResponseCallback callback) {
final Map<dynamic, dynamic> args = methodCall.arguments;
final int id = args['id'];
final String viewType = args['viewType'];
const engine.MethodCodec codec = engine.StandardMethodCodec();
// TODO(het): Use 'direction', 'width', and 'height'.
final PlatformViewFactory? platformViewFactory = platformViewRegistry.registeredFactories[viewType];
if (platformViewFactory == null) {
callback(codec.encodeErrorEnvelope(
code: 'Unregistered factory',
message: "No factory registered for viewtype '$viewType'",
));
return;
}
// TODO(het): Use creation parameters.
final html.Element element = platformViewFactory(id);
platformViewRegistry._createdViews[id] = element;
callback(codec.encodeSuccessEnvelope(null));
}
void _disposePlatformView(
engine.MethodCall methodCall, PlatformMessageResponseCallback callback) {
final int id = methodCall.arguments;
const engine.MethodCodec codec = engine.StandardMethodCodec();
// Remove the root element of the view from the DOM.
platformViewRegistry._createdViews[id]?.remove();
platformViewRegistry._createdViews.remove(id);
callback(codec.encodeSuccessEnvelope(null));
}
final PlatformViewRegistry platformViewRegistry = PlatformViewRegistry();
// TODO(yjbanov): remove _Callback, _Callbacker, and _futurize. They are here only
// because the analyzer wasn't able to infer the correct types during
......
......@@ -45,14 +45,25 @@ void testMain() {
sb.pushOffset(0, 0);
sb.addPlatformView(0, width: 10, height: 10);
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(
domRenderer.sceneElement!
.querySelectorAll('#view-0')
.single
.style
.pointerEvents,
'auto',
);
// The platform view is now split in two parts. The contents live
// as a child of the glassPane, and the slot lives in the glassPane
// shadow root. The slot is the one that has pointer events auto.
final contents = domRenderer.glassPaneElement!.querySelector('#view-0')!;
final slot = domRenderer.sceneElement!.querySelector('slot')!;
final contentsHost = contents.parent!;
final slotHost = slot.parent!;
expect(contents, isNotNull,
reason: 'The view from the factory is injected in the DOM.');
expect(contentsHost.tagName, equalsIgnoringCase('flt-platform-view'));
expect(slotHost.tagName, equalsIgnoringCase('flt-platform-view-slot'));
expect(slotHost.style.pointerEvents, 'auto',
reason: 'The slot reenables pointer events.');
expect(contentsHost.getAttribute('slot'), slot.getAttribute('name'),
reason: 'The contents and slot are correctly related.');
});
test('clips platform views with RRects', () async {
......@@ -69,6 +80,7 @@ void testMain() {
sb.pushClipRRect(ui.RRect.fromLTRBR(0, 0, 10, 10, ui.Radius.circular(3)));
sb.addPlatformView(0, width: 10, height: 10);
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(
domRenderer.sceneElement!.querySelectorAll('#sk_path_defs').single,
isNotNull,
......@@ -109,12 +121,12 @@ void testMain() {
sb.pushOffset(3, 3);
sb.addPlatformView(0, width: 10, height: 10);
dispatcher.rasterizer!.draw(sb.build().layerTree);
// Transformations happen on the slot element.
final html.Element slotHost = domRenderer.sceneElement!.querySelector('flt-platform-view-slot')!;
expect(
domRenderer.sceneElement!
.querySelectorAll('#view-0')
.single
.style
.transform,
slotHost.style.transform,
// We should apply the scale matrix first, then the offset matrix.
// So the translate should be 515 (5 * 100 + 5 * 3), and not
// 503 (5 * 100 + 3).
......@@ -150,11 +162,12 @@ void testMain() {
sb.pushOffset(3, 3);
sb.addPlatformView(0, width: 10, height: 10);
dispatcher.rasterizer!.draw(sb.build().layerTree);
final html.Element viewHost =
domRenderer.sceneElement!.querySelectorAll('#view-0').single;
// Transformations happen on the slot element.
final html.Element slotHost = domRenderer.sceneElement!.querySelector('flt-platform-view-slot')!;
expect(
getTransformChain(viewHost),
getTransformChain(slotHost),
<String>['matrix(0.25, 0, 0, 0.25, 1.5, 1.5)'],
);
});
......@@ -177,11 +190,12 @@ void testMain() {
sb.pushOffset(9, 9);
sb.addPlatformView(0, width: 10, height: 10);
dispatcher.rasterizer!.draw(sb.build().layerTree);
final html.Element viewHost =
domRenderer.sceneElement!.querySelectorAll('#view-0').single;
// Transformations happen on the slot element.
final html.Element slotHost = domRenderer.sceneElement!.querySelector('flt-platform-view-slot')!;
expect(
getTransformChain(viewHost),
getTransformChain(slotHost),
<String>[
'matrix(1, 0, 0, 1, 9, 9)',
'matrix(1, 0, 0, 1, 6, 6)',
......@@ -314,8 +328,12 @@ void testMain() {
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(
domRenderer.sceneElement!.querySelectorAll('#view-0'),
hasLength(1),
domRenderer.sceneElement!.querySelector('flt-platform-view-slot'),
isNotNull,
);
expect(
domRenderer.glassPaneElement!.querySelector('flt-platform-view'),
isNotNull,
);
await _disposePlatformView(0);
......@@ -325,8 +343,12 @@ void testMain() {
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(
domRenderer.sceneElement!.querySelectorAll('#view-0'),
hasLength(0),
domRenderer.sceneElement!.querySelector('flt-platform-view-slot'),
isNull,
);
expect(
domRenderer.glassPaneElement!.querySelector('flt-platform-view'),
isNull,
);
});
......@@ -346,8 +368,12 @@ void testMain() {
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(
domRenderer.sceneElement!.querySelectorAll('#view-0'),
hasLength(1),
domRenderer.sceneElement!.querySelector('flt-platform-view-slot'),
isNotNull,
);
expect(
domRenderer.glassPaneElement!.querySelector('flt-platform-view'),
isNotNull,
);
// Render a frame without a platform view, but also without disposing of
......@@ -357,8 +383,14 @@ void testMain() {
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(
domRenderer.sceneElement!.querySelectorAll('#view-0'),
hasLength(0),
domRenderer.sceneElement!.querySelector('flt-platform-view-slot'),
isNull,
);
// The actual contents of the platform view are kept in the dom, until
// it's actually disposed of!
expect(
domRenderer.glassPaneElement!.querySelector('flt-platform-view'),
isNotNull,
);
});
......
......@@ -111,9 +111,11 @@ void testMain() {
browserEngine == BrowserEngine.edge));
test('accesibility placeholder is attached after creation', () {
DomRenderer();
final DomRenderer renderer = DomRenderer();
expect(html.document.getElementsByTagName('flt-semantics-placeholder'),
isNotEmpty);
expect(
renderer.glassPaneShadow?.querySelectorAll('flt-semantics-placeholder'),
isNotEmpty,
);
});
}
// 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:html' as html;
import 'package:ui/src/engine.dart';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import '../../matchers.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
group('PlatformViewManager', () {
final String viewType = 'forTest';
final int viewId = 6;
late PlatformViewManager contentManager;
setUp(() {
contentManager = PlatformViewManager();
});
group('knowsViewType', () {
test('recognizes viewTypes after registering them', () async {
expect(contentManager.knowsViewType(viewType), isFalse);
contentManager.registerFactory(viewType, (int id) => html.DivElement());
expect(contentManager.knowsViewType(viewType), isTrue);
});
});
group('knowsViewId', () {
test('recognizes viewIds after *rendering* them', () async {
expect(contentManager.knowsViewId(viewId), isFalse);
contentManager.registerFactory(viewType, (int id) => html.DivElement());
expect(contentManager.knowsViewId(viewId), isFalse);
contentManager.renderContent(viewType, viewId, null);
expect(contentManager.knowsViewId(viewId), isTrue);
});
test('forgets viewIds after clearing them', () {
contentManager.registerFactory(viewType, (int id) => html.DivElement());
contentManager.renderContent(viewType, viewId, null);
expect(contentManager.knowsViewId(viewId), isTrue);
contentManager.clearPlatformView(viewId);
expect(contentManager.knowsViewId(viewId), isFalse);
});
});
group('registerFactory', () {
test('does NOT re-register factories', () async {
contentManager.registerFactory(
viewType, (int id) => html.DivElement()..id = 'pass');
// this should be rejected
contentManager.registerFactory(
viewType, (int id) => html.SpanElement()..id = 'fail');
final html.Element contents =
contentManager.renderContent(viewType, viewId, null);
expect(contents.querySelector('#pass'), isNotNull);
expect(contents.querySelector('#fail'), isNull,
reason: 'Factories cannot be overridden once registered');
});
});
group('renderContent', () {
final String unregisteredViewType = 'unregisteredForTest';
final String anotherViewType = 'anotherViewType';
setUp(() {
contentManager.registerFactory(viewType, (int id) {
return html.DivElement()..setAttribute('data-viewId', '$id');
});
contentManager.registerFactory(anotherViewType, (int id) {
return html.DivElement()
..setAttribute('data-viewId', '$id')
..style.height = 'auto'
..style.width = '55%';
});
});
test('refuse to render views for unregistered factories', () async {
try {
contentManager.renderContent(unregisteredViewType, viewId, null);
fail('renderContent should have thrown an Assertion error!');
} catch (e) {
expect(e, isAssertionError);
expect((e as AssertionError).message, contains(unregisteredViewType));
}
});
test('rendered markup contains required attributes', () async {
final html.Element content =
contentManager.renderContent(viewType, viewId, null);
expect(content.getAttribute('slot'), contains('$viewId'));
final html.Element userContent = content.querySelector('div')!;
expect(userContent.style.height, '100%');
expect(userContent.style.width, '100%');
});
test('slot property has the same value as createPlatformViewSlot', () async {
final html.Element content =
contentManager.renderContent(viewType, viewId, null);
final html.Element slot = createPlatformViewSlot(viewId);
final html.Element innerSlot = slot.querySelector('slot')!;
expect(content.getAttribute('slot'), innerSlot.getAttribute('name'),
reason:
'The slot attribute of the rendered content must match the name attribute of the SLOT of a given viewId');
});
test('do not modify style.height / style.width if passed by the user (anotherViewType)',
() async {
final html.Element content =
contentManager.renderContent(anotherViewType, viewId, null);
final html.Element userContent = content.querySelector('div')!;
expect(userContent.style.height, 'auto');
expect(userContent.style.width, '55%');
});
test('returns cached instances of already-rendered content', () async {
final html.Element firstRender =
contentManager.renderContent(viewType, viewId, null);
final html.Element anotherRender =
contentManager.renderContent(viewType, viewId, null);
expect(firstRender, same(anotherRender));
});
});
});
}
// 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 'dart:html' as html;
import 'dart:typed_data';
import 'package:ui/src/engine.dart';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
final MethodCodec codec = StandardMethodCodec();
void testMain() {
group('PlatformViewMessageHandler', () {
group('handlePlatformViewCall', () {
final String viewType = 'forTest';
final int viewId = 6;
late PlatformViewManager contentManager;
late Completer<ByteData?> completer;
late Completer<html.Element> contentCompleter;
setUp(() {
contentManager = PlatformViewManager();
completer = Completer<ByteData?>();
contentCompleter = Completer<html.Element>();
});
group('"create" message', () {
test('unregistered viewType, fails with descriptive exception',
() async {
final messageHandler = PlatformViewMessageHandler(
contentManager: contentManager,
);
final ByteData? message = _getCreateMessage(viewType, viewId);
messageHandler.handlePlatformViewCall(message, completer.complete);
final ByteData? response = await completer.future;
try {
codec.decodeEnvelope(response!);
} on PlatformException catch (e) {
expect(e.code, 'unregistered_view_type');
expect(e.details, contains(viewType));
}
});
test('duplicate viewId, fails with descriptive exception', () async {
contentManager.registerFactory(
viewType, (int id) => html.DivElement());
contentManager.renderContent(viewType, viewId, null);
final messageHandler = PlatformViewMessageHandler(
contentManager: contentManager,
);
final ByteData? message = _getCreateMessage(viewType, viewId);
messageHandler.handlePlatformViewCall(message, completer.complete);
final ByteData? response = await completer.future;
try {
codec.decodeEnvelope(response!);
} on PlatformException catch (e) {
expect(e.code, 'recreating_view');
expect(e.details, contains('$viewId'));
}
});
test('returns a successEnvelope when the view is created normally',
() async {
contentManager.registerFactory(
viewType, (int id) => html.DivElement()..id = 'success');
final messageHandler = PlatformViewMessageHandler(
contentManager: contentManager,
);
final ByteData? message = _getCreateMessage(viewType, viewId);
messageHandler.handlePlatformViewCall(message, completer.complete);
final ByteData? response = await completer.future;
expect(codec.decodeEnvelope(response!), isNull,
reason:
'The response should be a success envelope, with null in it.');
});
test('calls a contentHandler with the result of creating a view',
() async {
contentManager.registerFactory(
viewType, (int id) => html.DivElement()..id = 'success');
final messageHandler = PlatformViewMessageHandler(
contentManager: contentManager,
contentHandler: contentCompleter.complete,
);
final ByteData? message = _getCreateMessage(viewType, viewId);
messageHandler.handlePlatformViewCall(message, completer.complete);
final html.Element contents = await contentCompleter.future;
final ByteData? response = await completer.future;
expect(contents.querySelector('div#success'), isNotNull,
reason:
'The element created by the factory should be present in the created view.');
expect(codec.decodeEnvelope(response!), isNull,
reason:
'The response should be a success envelope, with null in it.');
});
});
group('"dispose" message', () {
late Completer<int> viewIdCompleter;
setUp(() {
viewIdCompleter = Completer<int>();
});
test('never fails, even for unknown viewIds', () async {
final messageHandler = PlatformViewMessageHandler(
contentManager: contentManager,
);
final ByteData? message = _getDisposeMessage(viewId);
messageHandler.handlePlatformViewCall(message, completer.complete);
final ByteData? response = await completer.future;
expect(codec.decodeEnvelope(response!), isNull,
reason:
'The response should be a success envelope, with null in it.');
});
test('never fails, even for unknown viewIds', () async {
final messageHandler = PlatformViewMessageHandler(
contentManager: _FakePlatformViewManager(viewIdCompleter.complete),
);
final ByteData? message = _getDisposeMessage(viewId);
messageHandler.handlePlatformViewCall(message, completer.complete);
final int disposedViewId = await viewIdCompleter.future;
expect(disposedViewId, viewId,
reason:
'The viewId to dispose should be passed to the contentManager');
});
});
});
});
}
class _FakePlatformViewManager extends PlatformViewManager {
_FakePlatformViewManager(void Function(int) clearFunction)
: this._clearPlatformView = clearFunction;
void Function(int) _clearPlatformView;
@override
void clearPlatformView(int viewId) {
return _clearPlatformView(viewId);
}
}
ByteData? _getCreateMessage(String viewType, int viewId) {
return codec.encodeMethodCall(MethodCall(
'create',
{
'id': viewId,
'viewType': viewType,
},
));
}
ByteData? _getDisposeMessage(int viewId) {
return codec.encodeMethodCall(MethodCall(
'dispose',
viewId,
));
}
// 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:html' as html;
import 'package:ui/src/engine.dart';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
group('PlatformViewManager', () {
final int viewId = 6;
group('createPlatformViewSlot', () {
test(
'can render slot, even for views that might have never been rendered before',
() async {
final html.Element slot = createPlatformViewSlot(viewId);
expect(slot, isNotNull);
expect(slot.querySelector('slot'), isNotNull);
});
test('rendered markup contains required attributes', () async {
final html.Element slot = createPlatformViewSlot(viewId);
expect(slot.style.pointerEvents, 'auto',
reason:
'Should re-enable pointer events for the contents of the view.');
final html.Element innerSlot = slot.querySelector('slot')!;
expect(innerSlot.getAttribute('name'), contains('$viewId'),
reason:
'The name attribute of the inner SLOT tag must refer to the viewId.');
});
});
});
}
......@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.6
// @dart = 2.9
@TestOn('chrome || safari || firefox')
import 'dart:async';
......@@ -89,8 +89,10 @@ void _testEngineSemanticsOwner() {
// Synthesize a click on the placeholder.
final html.Element placeholder =
html.document.querySelectorAll('flt-semantics-placeholder').single;
appShadowRoot.querySelector('flt-semantics-placeholder');
expect(placeholder.isConnected, true);
final html.Rectangle<num> rect = placeholder.getBoundingClientRect();
placeholder.dispatchEvent(html.MouseEvent(
'click',
......@@ -113,7 +115,8 @@ void _testEngineSemanticsOwner() {
expect(semantics().semanticsEnabled, false);
final html.Element placeholder =
html.document.querySelectorAll('flt-semantics-placeholder').single;
appShadowRoot.querySelector('flt-semantics-placeholder');
expect(placeholder.isConnected, true);
// Sending a semantics update should auto-enable engine semantics.
......@@ -354,9 +357,9 @@ void _testContainer() {
</sem>''');
final html.Element parentElement =
html.document.querySelector('flt-semantics');
appShadowRoot.querySelector('flt-semantics');
final html.Element container =
html.document.querySelector('flt-semantics-container');
appShadowRoot.querySelector('flt-semantics-container');
if (isMacOrIOS) {
expect(parentElement.style.top, '0px');
......@@ -402,9 +405,9 @@ void _testContainer() {
</sem>''');
final html.Element parentElement =
html.document.querySelector('flt-semantics');
appShadowRoot.querySelector('flt-semantics');
final html.Element container =
html.document.querySelector('flt-semantics-container');
appShadowRoot.querySelector('flt-semantics-container');
expect(parentElement.style.transform, 'matrix(1, 0, 0, 1, 10, 10)');
expect(parentElement.style.transformOrigin, '0px 0px 0px');
......@@ -446,10 +449,12 @@ void _testContainer() {
</sem-c>
</sem>''');
}
final html.Element parentElement =
html.document.querySelector('flt-semantics');
appShadowRoot.querySelector('flt-semantics');
final html.Element container =
html.document.querySelector('flt-semantics-container');
appShadowRoot.querySelector('flt-semantics-container');
if (isMacOrIOS) {
expect(parentElement.style.top, '0px');
expect(parentElement.style.left, '0px');
......@@ -804,8 +809,7 @@ void _testIncrementables() {
<input aria-valuenow="1" aria-valuetext="d" aria-valuemax="2" aria-valuemin="1">
</sem>''');
final html.InputElement input =
html.document.querySelectorAll('input').single;
final html.InputElement input = appShadowRoot.querySelector('input');
input.value = '2';
input.dispatchEvent(html.Event('change'));
......@@ -839,8 +843,7 @@ void _testIncrementables() {
<input aria-valuenow="1" aria-valuetext="d" aria-valuemax="1" aria-valuemin="0">
</sem>''');
final html.InputElement input =
html.document.querySelectorAll('input').single;
final html.InputElement input = appShadowRoot.querySelector('input');
input.value = '0';
input.dispatchEvent(html.Event('change'));
......@@ -933,20 +936,19 @@ void _testTextField() {
semantics().updateSemantics(builder.build());
final html.Element textField = html.document
.querySelectorAll('input[data-semantics-role="text-field"]')
.single;
final html.Element textField =
appShadowRoot.querySelector('input[data-semantics-role="text-field"]');
expect(html.document.activeElement, isNot(textField));
expect(appShadowRoot.activeElement, isNot(textField));
textField.focus();
expect(html.document.activeElement, textField);
expect(appShadowRoot.activeElement, textField);
expect(await logger.idLog.first, 0);
expect(await logger.actionLog.first, ui.SemanticsAction.tap);
semantics().semanticsEnabled = false;
}, // TODO(nurhan): https://github.com/flutter/flutter/issues/46638
}, // TODO(nurhan): https://github.com/flutter/flutter/issues/46638
// TODO(nurhan): https://github.com/flutter/flutter/issues/50590
// TODO(nurhan): https://github.com/flutter/flutter/issues/50754
skip: (browserEngine != BrowserEngine.blink));
......
......@@ -12,6 +12,21 @@ import 'package:ui/ui.dart' as ui;
import '../../matchers.dart';
/// Gets the DOM host where the Flutter app is being rendered.
///
/// This function returns the correct host for the flutter app under testing,
/// so we don't have to hardcode html.document across the test. (The host of a
/// normal flutter app used to be html.document, but now that the app is wrapped
/// in a Shadow DOM, that's not the case anymore.)
///
/// A [html.ShadowRoot] quacks very similarly to a [html.Document], but
/// unfortunately they don't share any class/implement any interface that let us
/// use them interchangeably.
///
/// This flutterRoot can be changed to return ShadowRoot or Document without
/// the need to modify (most of) your code.
html.ShadowRoot get appShadowRoot => domRenderer.glassPaneShadow!;
/// CSS style applied to the root of the semantics tree.
// TODO(yjbanov): this should be handled internally by [expectSemanticsTree].
// No need for every test to inject it.
......@@ -336,14 +351,14 @@ class SemanticsTester {
/// Verifies the HTML structure of the current semantics tree.
void expectSemanticsTree(String semanticsHtml) {
expect(
canonicalizeHtml(html.document.querySelector('flt-semantics')!.outerHtml!),
canonicalizeHtml(appShadowRoot.querySelector('flt-semantics')!.outerHtml!),
canonicalizeHtml(semanticsHtml),
);
}
/// Finds the first HTML element in the semantics tree used for scrolling.
html.Element? findScrollable() {
return html.document.querySelectorAll('flt-semantics').cast<html.Element?>().firstWhere(
return appShadowRoot.querySelectorAll('flt-semantics').cast<html.Element?>().firstWhere(
(html.Element? element) =>
element!.style.overflow == 'hidden' ||
element.style.overflowY == 'scroll' ||
......
......@@ -75,15 +75,14 @@ void testMain() {
createTextFieldSemantics(value: 'hello');
final html.Element textField = html.document
.querySelectorAll('input[data-semantics-role="text-field"]')
.single;
final html.Element textField = appShadowRoot
.querySelector('input[data-semantics-role="text-field"]')!;
expect(html.document.activeElement, isNot(textField));
expect(appShadowRoot.activeElement, isNot(textField));
textField.focus();
expect(html.document.activeElement, textField);
expect(appShadowRoot.activeElement, textField);
expect(await logger.idLog.first, 0);
expect(await logger.actionLog.first, ui.SemanticsAction.tap);
......@@ -99,6 +98,7 @@ void testMain() {
..semanticsEnabled = true;
expect(html.document.activeElement, html.document.body);
expect(appShadowRoot.activeElement, null);
int changeCount = 0;
int actionCount = 0;
......@@ -121,8 +121,9 @@ void testMain() {
);
TextField textField = textFieldSemantics.debugRoleManagerFor(Role.textField) as TextField;
expect(html.document.activeElement, domRenderer.glassPaneElement);
expect(appShadowRoot.activeElement, strategy.domElement);
expect(textField.editableElement, strategy.domElement);
expect(html.document.activeElement, strategy.domElement);
expect((textField.editableElement as dynamic).value, 'hello');
expect(textField.editableElement.getAttribute('aria-label'), 'greeting');
expect(textField.editableElement.style.width, '10px');
......@@ -137,6 +138,7 @@ void testMain() {
);
expect(html.document.activeElement, html.document.body);
expect(appShadowRoot.activeElement, null);
expect(strategy.domElement, null);
expect((textField.editableElement as dynamic).value, 'bye');
expect(textField.editableElement.getAttribute('aria-label'), 'farewell');
......@@ -158,6 +160,8 @@ void testMain() {
..semanticsEnabled = true;
expect(html.document.activeElement, html.document.body);
expect(appShadowRoot.activeElement, null);
strategy.enable(
singlelineConfig,
onChange: (_) {},
......@@ -170,11 +174,13 @@ void testMain() {
final TextField textField = textFieldSemantics.debugRoleManagerFor(Role.textField) as TextField;
expect(textField.editableElement, strategy.domElement);
expect(html.document.activeElement, strategy.domElement);
expect(html.document.activeElement, domRenderer.glassPaneElement);
expect(appShadowRoot.activeElement, strategy.domElement);
// The input should not refocus after blur.
textField.editableElement.blur();
expect(html.document.activeElement, html.document.body);
expect(appShadowRoot.activeElement, null);
strategy.disable();
semantics().semanticsEnabled = false;
});
......@@ -199,17 +205,19 @@ void testMain() {
isFocused: true,
);
expect(strategy.domElement, isNotNull);
expect(html.document.activeElement, strategy.domElement);
expect(html.document.activeElement, domRenderer.glassPaneElement);
expect(appShadowRoot.activeElement, strategy.domElement);
strategy.disable();
expect(strategy.domElement, isNull);
// It doesn't remove the DOM element.
final TextField textField = textFieldSemantics.debugRoleManagerFor(Role.textField) as TextField;
expect(html.document.body!.contains(textField.editableElement), isTrue);
expect(appShadowRoot.contains(textField.editableElement), isTrue);
// Editing element is not enabled.
expect(strategy.isEnabled, isFalse);
expect(html.document.activeElement, html.document.body);
expect(appShadowRoot.activeElement, null);
semantics().semanticsEnabled = false;
});
......@@ -229,11 +237,13 @@ void testMain() {
isFocused: true,
);
expect(strategy.domElement, isNotNull);
expect(html.document.activeElement, strategy.domElement);
expect(html.document.activeElement, domRenderer.glassPaneElement);
expect(appShadowRoot.activeElement, strategy.domElement);
// Blur the element without telling the framework.
strategy.activeDomElement.blur();
expect(html.document.activeElement, html.document.body);
expect(appShadowRoot.activeElement, null);
// The input will have focus after editing state is set and semantics updated.
strategy.setEditingState(EditingState(text: 'foo'));
......@@ -251,7 +261,8 @@ void testMain() {
value: 'hello',
isFocused: true,
);
expect(html.document.activeElement, strategy.domElement);
expect(html.document.activeElement, domRenderer.glassPaneElement);
expect(appShadowRoot.activeElement, strategy.domElement);
strategy.disable();
semantics().semanticsEnabled = false;
......@@ -274,7 +285,10 @@ void testMain() {
);
final html.TextAreaElement textArea = strategy.domElement as html.TextAreaElement;
expect(html.document.activeElement, textArea);
expect(html.document.activeElement, domRenderer.glassPaneElement);
expect(appShadowRoot.activeElement, strategy.domElement);
strategy.enable(
singlelineConfig,
onChange: (_) {},
......@@ -283,10 +297,11 @@ void testMain() {
textArea.blur();
expect(html.document.activeElement, html.document.body);
expect(appShadowRoot.activeElement, null);
strategy.disable();
// It doesn't remove the textarea from the DOM.
expect(html.document.body!.contains(textArea), isTrue);
expect(appShadowRoot.contains(textArea), isTrue);
// Editing element is not enabled.
expect(strategy.isEnabled, isFalse);
semantics().semanticsEnabled = false;
......@@ -376,12 +391,14 @@ void testMain() {
final SemanticsTester tester = SemanticsTester(semantics());
createTwoFieldSemantics(tester, focusFieldId: 1);
expect(tester.apply().length, 3);
expect(html.document.activeElement, tester.getTextField(1).editableElement);
expect(html.document.activeElement, domRenderer.glassPaneElement);
expect(appShadowRoot.activeElement, tester.getTextField(1).editableElement);
expect(strategy.domElement, tester.getTextField(1).editableElement);
createTwoFieldSemantics(tester, focusFieldId: 2);
expect(tester.apply().length, 3);
expect(html.document.activeElement, tester.getTextField(2).editableElement);
expect(appShadowRoot.activeElement, tester.getTextField(2).editableElement);
expect(strategy.domElement, tester.getTextField(2).editableElement);
}
......
......@@ -69,14 +69,11 @@ void testMain() {
});
group('createElement', () {
test('adds reset to stylesheet', () {
test('creates slot element that can receive pointer events', () {
final element = view.createElement();
_assertShadowRootStylesheetContains(element, 'all: initial;');
});
test('creates element transparent to "cursor" property', () {
final element = view.createElement();
_assertShadowRootStylesheetContains(element, 'cursor: inherit;');
expect(element.tagName, equalsIgnoringCase('flt-platform-view-slot'));
expect(element.style.pointerEvents, 'auto');
});
});
});
......@@ -98,14 +95,3 @@ Future<void> _createPlatformView(int id, String viewType) {
);
return completer.future;
}
void _assertShadowRootStylesheetContains(html.Element element, String rule) {
final shadow = element.shadowRoot;
expect(shadow, isNotNull);
final html.StyleElement style = shadow.children.first;
expect(style, isNotNull);
expect(style.innerHtml, contains(rule));
}
......@@ -27,13 +27,13 @@ void testMain() async {
// Create a <flt-scene> element to make sure our CSS reset applies correctly.
final html.Element testScene = html.Element.tag('flt-scene');
testScene.append(canvas.rootElement);
html.document.querySelector('flt-scene-host')!.append(testScene);
domRenderer.glassPaneShadow!.querySelector('flt-scene-host')!.append(testScene);
}
setUpStableTestFonts();
tearDown(() {
html.document.querySelector('flt-scene')!.remove();
domRenderer.glassPaneShadow?.querySelector('flt-scene')?.remove();
});
/// Draws several lines, some aligned precisely with the pixel grid, and some
......@@ -249,7 +249,7 @@ void testMain() async {
final html.Element sceneElement = scene.webOnlyRootElement!;
sceneElement.querySelector('flt-clip')!.append(canvas.rootElement);
html.document.querySelector('flt-scene-host')!.append(sceneElement);
domRenderer.glassPaneShadow!.querySelector('flt-scene-host')!.append(sceneElement);
await matchGoldenFile(
'bitmap_canvas_draws_text_on_top_of_canvas.png',
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册