Sitelet https://github.com/flutter/flutter/pull/190071/files
Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -517,23 +517,38 @@ class PlatformViewEmbedder {
canvas.displayCanvas!.setIsOverlay(needsOverlayPositioning);
}

// Platform views which were disposed while they were still being
// composited. They are left out of the DOM.
final missingViewIds = <int>{};

// At this point, the DOM contains the static elements and the elements from
// the previous composition which need to move. We iterate over the static
// elements and insert the elements which come before them into the DOM.
var staticElementIndex = 0;
var nextCompositionIndex = 0;
while (staticElementIndex < staticElements.length) {
final int staticElementIndexInActiveComposition = staticElements[staticElementIndex];
final DomElement staticDomElement = _getElement(
final DomElement? staticDomElement = _getElement(
_activeComposition.entities[staticElementIndexInActiveComposition],
);
if (staticDomElement == null) {
// This view is not in the DOM, so it can't position other elements.
// Use the next static element instead.
staticElementIndex++;
continue;
}
// Go through next composition elements until we reach the static element.
while (indexMap[nextCompositionIndex] != staticElementIndexInActiveComposition) {
final CompositionEntity nextEntity = composition.entities[nextCompositionIndex];
if (nextEntity is CompositionCanvas) {
updateCompositionCanvasWithDisplay(nextEntity, nextCompositionIndex);
}
sceneHost.insertBefore(_getElement(nextEntity), staticDomElement);
final DomElement? nextDomElement = _getElement(nextEntity);
if (nextDomElement != null) {
sceneHost.insertBefore(nextDomElement, staticDomElement);
} else if (nextEntity is CompositionPlatformView) {
missingViewIds.add(nextEntity.viewId);
}
nextCompositionIndex++;
}
if (composition.entities[nextCompositionIndex] is CompositionCanvas) {
Expand All @@ -554,15 +569,35 @@ class PlatformViewEmbedder {
if (nextEntity is CompositionCanvas) {
updateCompositionCanvasWithDisplay(nextEntity, nextCompositionIndex);
}
sceneHost.append(_getElement(nextEntity));
final DomElement? nextDomElement = _getElement(nextEntity);
if (nextDomElement != null) {
sceneHost.append(nextDomElement);
} else if (nextEntity is CompositionPlatformView) {
missingViewIds.add(nextEntity.viewId);
}
nextCompositionIndex++;
}

if (missingViewIds.isNotEmpty) {
// Drop the skipped views, so the next frame doesn't expect them to be
// in the DOM.
composition.entities.removeWhere(
(CompositionEntity entity) =>
entity is CompositionPlatformView && missingViewIds.contains(entity.viewId),
);
printWarning(
'Cannot render platform views: ${missingViewIds.join(', ')}. '
'These views were disposed while they were being composited.',
);
}
}

DomElement _getElement(CompositionEntity entity) {
/// Returns the DOM element for [entity], or null if the platform view no
/// longer has a clip chain because it was disposed.
DomElement? _getElement(CompositionEntity entity) {
return switch (entity) {
CompositionCanvas() => entity.displayCanvas!.hostElement,
CompositionPlatformView() => _viewClipChains[entity.viewId]!.root,
CompositionPlatformView() => _viewClipChains[entity.viewId]?.root,
};
}

Expand Down
53 changes: 53 additions & 0 deletions engine/src/flutter/lib/web_ui/test/ui/platform_view_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,59 @@ Future<void> testMain() async {
expect(platformViewsHost.querySelector('flt-platform-view'), isNull);
});

test('does not crash when a platform view is disposed mid-frame', () async {
await createPlatformView(0, platformViewType);
await createPlatformView(1, platformViewType);

final sb = ui.SceneBuilder()
..pushOffset(0, 0)
..addPlatformView(0, width: 10, height: 10)
..addPlatformView(1, width: 10, height: 10)
..pop();
await renderScene(sb.build());
_expectSceneMatches(<_EmbeddedViewMarker>[_platformView, _platformView]);

// Build the next frame by hand so that a platform view can be disposed
// after the composition was created, but before the frame is submitted.
final ViewRasterizer rasterizer = renderer.rasterizers[implicitView.viewId]!;
final PlatformViewEmbedder embedder = rasterizer.viewEmbedder;
final rootLayer = RootLayer();
// The views are composited in the opposite order, so the DOM needs to be
// updated for this composition.
rootLayer.children.add(PlatformViewLayer(1, ui.Offset.zero, 10, 10));
rootLayer.children.add(PlatformViewLayer(0, ui.Offset.zero, 10, 10));

embedder.frameSize = rasterizer.currentFrameSize;
final Frame frame = rasterizer.context.acquireFrame(embedder);
frame.raster(LayerTree(rootLayer), rasterizer.currentFrameSize, null);

// View 0 is disposed while it is still part of the composition which is
// about to be submitted.
embedder.disposeView(0);

final warnings = <String>[];
final void Function(String) originalPrintWarning = printWarning;
printWarning = (String warning) => warnings.add(warning);
try {
await expectLater(embedder.submitFrame(null), completes);
} finally {
printWarning = originalPrintWarning;
}

// The disposed view is left out of the composition.
_expectSceneMatches(<_EmbeddedViewMarker>[_platformView]);
expect(embedder.debugActiveComposition.entities, hasLength(1));
expect(
warnings,
contains(
contains(
'Cannot render platform views: 0. '
'These views were disposed while they were being composited.',
),
),
);
});

test('preserves the DOM node of an unrendered platform view', () async {
await createPlatformView(1, platformViewType);

Expand Down