Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

[web] Defer injection of platform views until needed. #48960

Merged
merged 15 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ class HtmlViewEmbedder {
/// If this returns a [CkCanvas], then that canvas should be the new leaf
/// node. Otherwise, keep the same leaf node.
CkCanvas? compositeEmbeddedView(int viewId) {
// Ensure platform view with `viewId` is injected into the `rasterizer.view`.
rasterizer.view.dom.injectPlatformView(viewId);

final int overlayIndex = _context.visibleViewCount;
_compositionOrder.add(viewId);
// Keep track of the number of visible platform views.
Expand All @@ -142,10 +145,10 @@ class HtmlViewEmbedder {
return recorderToUseForRendering?.recordingCanvas;
}

void _compositeWithParams(int viewId, EmbeddedViewParams params) {
void _compositeWithParams(int platformViewId, EmbeddedViewParams params) {
// If we haven't seen this viewId yet, cache it for clips/transforms.
final ViewClipChain clipChain = _viewClipChains.putIfAbsent(viewId, () {
return ViewClipChain(view: createPlatformViewSlot(viewId));
final ViewClipChain clipChain = _viewClipChains.putIfAbsent(platformViewId, () {
return ViewClipChain(view: createPlatformViewSlot(platformViewId));
});

final DomElement slot = clipChain.slot;
Expand Down Expand Up @@ -175,7 +178,7 @@ class HtmlViewEmbedder {
}

// Apply mutators to the slot
_applyMutators(params, slot, viewId);
_applyMutators(params, slot, platformViewId);
}

int _countClips(MutatorsStack mutators) {
Expand Down
4 changes: 4 additions & 0 deletions lib/web_ui/lib/src/engine/dom.dart
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,10 @@ extension DomElementExtension on DomElement {
external DomElement? _querySelector(JSString selectors);
DomElement? querySelector(String selectors) => _querySelector(selectors.toJS);

@JS('matches')
external JSBoolean _matches(JSString selectors);
bool matches(String selectors) => _matches(selectors.toJS).toDart;

@JS('querySelectorAll')
external _DomList _querySelectorAll(JSString selectors);
Iterable<DomElement> querySelectorAll(String selectors) =>
Expand Down
21 changes: 14 additions & 7 deletions lib/web_ui/lib/src/engine/html/platform_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,29 @@
// found in the LICENSE file.

import '../dom.dart';
import '../platform_dispatcher.dart';
import '../platform_views/slots.dart';
import '../window.dart';
import 'surface.dart';

/// A surface containing a platform view, which is an HTML element.
class PersistedPlatformView extends PersistedLeafSurface {
PersistedPlatformView(this.viewId, this.dx, this.dy, this.width, this.height);
PersistedPlatformView(this.platformViewId, this.dx, this.dy, this.width, this.height) {
// Ensure platform view with `viewId` is injected into the `implicitView`
// before rendering its shadow DOM `slot`.
final EngineFlutterView implicitView = EnginePlatformDispatcher.instance.implicitView!;
implicitView.dom.injectPlatformView(platformViewId);
}

final int viewId;
final int platformViewId;
final double dx;
final double dy;
final double width;
final double height;

@override
DomElement createElement() {
return createPlatformViewSlot(viewId);
return createPlatformViewSlot(platformViewId);
}

@override
Expand All @@ -36,21 +43,21 @@ class PersistedPlatformView extends PersistedLeafSurface {
bool canUpdateAsMatch(PersistedSurface oldSurface) {
if (super.canUpdateAsMatch(oldSurface)) {
// super checks the runtimeType of the surface, so we can just cast...
return viewId == ((oldSurface as PersistedPlatformView).viewId);
return platformViewId == ((oldSurface as PersistedPlatformView).platformViewId);
}
return false;
}

@override
double matchForUpdate(PersistedPlatformView existingSurface) {
return existingSurface.viewId == viewId ? 0.0 : 1.0;
return existingSurface.platformViewId == platformViewId ? 0.0 : 1.0;
}

@override
void update(PersistedPlatformView oldSurface) {
assert(
viewId == oldSurface.viewId,
'PersistedPlatformView with different viewId should never be updated. Check the canUpdateAsMatch method.',
platformViewId == oldSurface.platformViewId,
'PersistedPlatformView with different platformViewId should never be updated. Check the canUpdateAsMatch method.',
);
super.update(oldSurface);
// Only update if the view has been resized
Expand Down
20 changes: 10 additions & 10 deletions lib/web_ui/lib/src/engine/platform_dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
_addLocaleChangedListener();
registerHotRestartListener(dispose);
_setAppLifecycleState(ui.AppLifecycleState.resumed);
viewManager.onViewDisposed.listen((_) {
// Send a metrics changed event to the framework when a view is disposed.
// View creation/resize is handled by the `_didResize` handler in the
// EngineFlutterView itself.
invokeOnMetricsChanged();
});
}

/// The [EnginePlatformDispatcher] singleton.
Expand Down Expand Up @@ -615,18 +621,12 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
_handleWebTestEnd2EndMessage(jsonCodec, data)));
return;

case 'flutter/platform_views':
case PlatformViewMessageHandler.channelName:
// `arguments` can be a Map<String, Object> for `create`,
// but an `int` for `dispose`, hence why `dynamic` everywhere.
final MethodCall(:String method, :dynamic arguments) =
standardCodec.decodeMethodCall(data);
final int? flutterViewId = tryViewId(arguments);
if (flutterViewId == null) {
implicitView!.platformViewMessageHandler
.handleLegacyPlatformViewCall(method, arguments, callback!);
return;
}
arguments as Map<dynamic, dynamic>;
viewManager[flutterViewId]!
.platformViewMessageHandler
PlatformViewMessageHandler.instance
.handlePlatformViewCall(method, arguments, callback!);
return;

Expand Down
22 changes: 17 additions & 5 deletions lib/web_ui/lib/src/engine/platform_views/content_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ class PlatformViewManager {

/// 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?
static final PlatformViewManager instance = PlatformViewManager();
static PlatformViewManager instance = PlatformViewManager();

// The factory functions, indexed by the viewType
final Map<String, Function> _factories = <String, Function>{};
Expand All @@ -65,6 +64,20 @@ class PlatformViewManager {
return _contents.containsKey(viewId);
}

/// Returns the cached contents of [viewId], to be injected into the DOM.
///
/// This is only used by the active `Renderer` object when a platform view needs
/// to be injected in the DOM, through `FlutterView.DomManager.injectPlatformView`.
///
/// This may return null, if [renderContent] was not called before this. The
/// framework seems to allow/need this for some tests, so it is allowed here
/// as well.
///
/// App programmers should not access this directly, and instead use [getViewById].
DomElement? getSlottedContent(int viewId) {
return _contents[viewId];
}

/// Returns the HTML element created by a registered factory for [viewId].
///
/// Throws an [AssertionError] if [viewId] hasn't been rendered before.
Expand Down Expand Up @@ -104,9 +117,8 @@ class PlatformViewManager {

/// 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 result of this call is cached in the `_contents` Map, so the active
/// renderer can inject it as needed.
///
/// The resulting DOM for the `contents` of a Platform View looks like this:
///
Expand Down
71 changes: 24 additions & 47 deletions lib/web_ui/lib/src/engine/platform_views/message_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,35 +21,41 @@ typedef PlatformViewContentHandler = void Function(DomElement);

/// This class handles incoming framework messages to create/dispose Platform Views.
///
/// (An instance of this class is connected to the `flutter/platform_views`
/// (The 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.
/// Platform views are injected into the DOM when needed by the correct instance
/// of the active renderer.
///
/// The rendering/compositing of Platform Views can create the other "half" of a
/// The rendering and 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]
/// [HtmlViewEmbedder.disposeViews].
class PlatformViewMessageHandler {
PlatformViewMessageHandler({
required DomElement platformViewsContainer,
PlatformViewManager? contentManager,
}) : _contentManager = contentManager ?? PlatformViewManager.instance,
_platformViewsContainer = platformViewsContainer;
required PlatformViewManager contentManager,
}) : _contentManager = contentManager;

static const String channelName = 'flutter/platform_views';

/// The shared instance of PlatformViewMessageHandler.
///
/// Unless configured differently, this connects to the shared instance of the
/// [PlatformViewManager].
static PlatformViewMessageHandler instance = PlatformViewMessageHandler(
contentManager: PlatformViewManager.instance,
);

final MethodCodec _codec = const StandardMethodCodec();
final PlatformViewManager _contentManager;
final DomElement _platformViewsContainer;

/// Handle a `create` Platform View message.
///
Expand All @@ -58,10 +64,12 @@ class PlatformViewMessageHandler {
///
/// (See [PlatformViewManager.registerFactory] for more details.)
///
/// The `contents` are inserted into the [_platformViewsContainer].
///
/// 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.
///
/// The `callback` signals when the contents of a given [platformViewId] have
/// been rendered. They're now accessible through `platformViewRegistry.getViewById`
/// from `dart:ui_web`. **(Not the DOM!)**
void _createPlatformView(
_PlatformMessageResponseCallback callback, {
required int platformViewId,
Expand All @@ -88,15 +96,12 @@ class PlatformViewMessageHandler {
return;
}

final DomElement content = _contentManager.renderContent(
_contentManager.renderContent(
platformViewType,
platformViewId,
params,
);

// For now, we don't need anything fancier. If needed, this can be converted
// to a PlatformViewStrategy class for each web-renderer backend?
_platformViewsContainer.append(content);
callback(_codec.encodeSuccessEnvelope(null));
}

Expand Down Expand Up @@ -126,7 +131,7 @@ class PlatformViewMessageHandler {
/// This is transitional code to support the old platform view channel. As
/// soon as the framework code is updated to send the Flutter View ID, this
/// method can be removed.
void handleLegacyPlatformViewCall(
void handlePlatformViewCall(
String method,
dynamic arguments,
_PlatformMessageResponseCallback callback,
Expand All @@ -141,39 +146,11 @@ class PlatformViewMessageHandler {
params: arguments['params'],
);
return;
// TODO(web): Send `arguments` as a Map for `dispose` too!
case 'dispose':
_disposePlatformView(callback, platformViewId: arguments as int);
return;
}
callback(null);
}

/// Handles a PlatformViewCall to the `flutter/platform_views` channel.
///
/// This method handles two possible messages:
/// * `create`: See [_createPlatformView]
/// * `dispose`: See [_disposePlatformView]
void handlePlatformViewCall(
String method,
Map<dynamic, dynamic> arguments,
_PlatformMessageResponseCallback callback,
) {
switch (method) {
case 'create':
_createPlatformView(
callback,
platformViewId: arguments.readInt('platformViewId'),
platformViewType: arguments.readString('platformViewType'),
params: arguments['params'],
);
return;
case 'dispose':
_disposePlatformView(
callback,
platformViewId: arguments.readInt('platformViewId'),
);
return;
}
callback(null);
}
}
6 changes: 6 additions & 0 deletions lib/web_ui/lib/src/engine/scene_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ class EngineSceneView {

case PlatformViewSlice():
for (final PlatformView view in slice.views) {
// TODO(harryterkelsen): Inject the FlutterView instance from `renderScene`,
// instead of using `EnginePlatformDispatcher...implicitView` directly,
// or make the FlutterView "register" like in canvaskit.
// Ensure the platform view contents are injected in the DOM.
EnginePlatformDispatcher.instance.implicitView?.dom.injectPlatformView(view.viewId);

// Attempt to reuse a container for the existing view
PlatformViewContainer? container;
for (int j = 0; j < reusableContainers.length; j++) {
Expand Down
28 changes: 28 additions & 0 deletions lib/web_ui/lib/src/engine/view_embedder/dom_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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 '../semantics/semantics.dart';
import 'style_manager.dart';
Expand Down Expand Up @@ -204,6 +205,33 @@ class DomManager {
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) {
Expand Down
4 changes: 0 additions & 4 deletions lib/web_ui/lib/src/engine/window.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import 'mouse/context_menu.dart';
import 'mouse/cursor.dart';
import 'navigation/history.dart';
import 'platform_dispatcher.dart';
import 'platform_views/message_handler.dart';
import 'pointer_binding.dart';
import 'semantics.dart';
import 'services.dart';
Expand Down Expand Up @@ -130,9 +129,6 @@ base class EngineFlutterView implements ui.FlutterView {

late final DomManager dom = DomManager(viewId: viewId, devicePixelRatio: devicePixelRatio);

late final PlatformViewMessageHandler platformViewMessageHandler =
PlatformViewMessageHandler(platformViewsContainer: dom.platformViewsHost);

late final PointerBinding pointerBinding;

// TODO(goderbauer): Provide API to configure constraints. See also TODO in "render".
Expand Down
Loading