Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
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
64 changes: 45 additions & 19 deletions lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ class HtmlViewEmbedder {
/// The list of view ids that should be composited, in order.
List<int> _compositionOrder = <int>[];

/// The number of platform views in this frame which are visible.
///
/// These platform views will require overlays.
int _visibleViewCount = 0;

/// The most recent composition order.
List<int> _activeCompositionOrder = <int>[];

Expand Down Expand Up @@ -127,7 +132,7 @@ class HtmlViewEmbedder {
}

void prerollCompositeEmbeddedView(int viewId, EmbeddedViewParams params) {
if (!disableOverlays) {
if (!disableOverlays && platformViewManager.isVisible(viewId)) {
// We must decide in the preroll phase if a platform view will use the
// backup overlay, so that draw commands after the platform view will
// correctly paint to the backup surface.
Expand Down Expand Up @@ -170,12 +175,17 @@ 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) {
final int compositedViewCount = _compositionOrder.length;
final int overlayIndex = _visibleViewCount;
_compositionOrder.add(viewId);
if (!disableOverlays) {
if (compositedViewCount < _pictureRecordersCreatedDuringPreroll.length) {
if (platformViewManager.isVisible(viewId)) {
_visibleViewCount++;
}
final bool needOverlay =
!disableOverlays && platformViewManager.isVisible(viewId);
if (needOverlay) {
if (overlayIndex < _pictureRecordersCreatedDuringPreroll.length) {
_pictureRecorders[viewId] =
_pictureRecordersCreatedDuringPreroll[compositedViewCount];
_pictureRecordersCreatedDuringPreroll[overlayIndex];
} else {
_viewsUsingBackupSurface.add(viewId);
_pictureRecorders[viewId] = _backupPictureRecorder!;
Expand All @@ -184,15 +194,15 @@ class HtmlViewEmbedder {

// Do nothing if this view doesn't need to be composited.
if (!_viewsToRecomposite.contains(viewId)) {
if (!disableOverlays) {
if (needOverlay) {
return _pictureRecorders[viewId]!.recordingCanvas;
} else {
return null;
}
}
_compositeWithParams(viewId, _currentCompositionParams[viewId]!);
_viewsToRecomposite.remove(viewId);
if (!disableOverlays) {
if (needOverlay) {
return _pictureRecorders[viewId]!.recordingCanvas;
} else {
return null;
Expand Down Expand Up @@ -336,9 +346,8 @@ class HtmlViewEmbedder {
final svg.ClipPathElement newClipPath = svg.ClipPathElement();
newClipPath.id = clipId;
newClipPath.append(
svg.PathElement()
..setAttribute('d', path.toSvgString()!)
);
svg.PathElement()
..setAttribute('d', path.toSvgString()!));

pathDefs.append(newClipPath);
// Store the id of the node instead of [newClipPath] directly. For
Expand All @@ -356,9 +365,8 @@ class HtmlViewEmbedder {
final svg.ClipPathElement newClipPath = svg.ClipPathElement();
newClipPath.id = clipId;
newClipPath.append(
svg.PathElement()
..setAttribute('d', path.toSvgString()!)
);
svg.PathElement()
..setAttribute('d', path.toSvgString()!));
pathDefs.append(newClipPath);
// Store the id of the node instead of [newClipPath] directly. For
// some reason, calling `newClipPath.remove()` doesn't remove it
Expand Down Expand Up @@ -421,13 +429,22 @@ class HtmlViewEmbedder {
_compositionOrder.isEmpty ||
disableOverlays)
? null
: diffViewList(_activeCompositionOrder, _compositionOrder);
: diffViewList(
_activeCompositionOrder
.where((int viewId) => platformViewManager.isVisible(viewId))
.toList(),
_compositionOrder
.where((int viewId) => platformViewManager.isVisible(viewId))
.toList());
final Map<int, int>? insertBeforeMap = _updateOverlays(diffResult);

bool _didPaintBackupSurface = false;
if (!disableOverlays) {
for (int i = 0; i < _compositionOrder.length; i++) {
final int viewId = _compositionOrder[i];
if (platformViewManager.isInvisible(viewId)) {
continue;
}
if (_viewsUsingBackupSurface.contains(viewId)) {
// Only draw the picture to the backup surface once.
if (!_didPaintBackupSurface) {
Expand Down Expand Up @@ -455,6 +472,7 @@ class HtmlViewEmbedder {
_viewsUsingBackupSurface.clear();
if (listEquals(_compositionOrder, _activeCompositionOrder)) {
_compositionOrder.clear();
_visibleViewCount = 0;
return;
}

Expand Down Expand Up @@ -542,6 +560,7 @@ class HtmlViewEmbedder {
}

_compositionOrder.clear();
_visibleViewCount = 0;

disposeViews(unusedViews);

Expand Down Expand Up @@ -601,12 +620,15 @@ class HtmlViewEmbedder {
// to the backup surface.
SurfaceFactory.instance.releaseSurfaces();
_overlays.clear();
final List<int> viewsNeedingOverlays = _compositionOrder
.where((int viewId) => platformViewManager.isVisible(viewId))
.toList();
final int numOverlays = math.min(
SurfaceFactory.instance.maximumOverlays,
_compositionOrder.length,
viewsNeedingOverlays.length,
);
for (int i = 0; i < numOverlays; i++) {
final int viewId = _compositionOrder[i];
final int viewId = viewsNeedingOverlays[i];
assert(!_viewsUsingBackupSurface.contains(viewId));
_initializeOverlay(viewId);
}
Expand Down Expand Up @@ -662,7 +684,8 @@ class HtmlViewEmbedder {
while (overlaysToAssign > 0 && index < _compositionOrder.length) {
final bool activeView = index < lastOriginalIndex;
final int viewId = _compositionOrder[index];
if (!_overlays.containsKey(viewId)) {
if (!_overlays.containsKey(viewId) &&
platformViewManager.isVisible(viewId)) {
_initializeOverlay(viewId);
overlaysToAssign--;
if (activeView) {
Expand All @@ -686,6 +709,7 @@ class HtmlViewEmbedder {
for (int i = 0; i < _compositionOrder.length; i++) {
final int viewId = _compositionOrder[i];
assert(_viewsUsingBackupSurface.contains(viewId) ||
platformViewManager.isInvisible(viewId) ||
_overlays[viewId] != null);
}
}
Expand Down Expand Up @@ -728,6 +752,7 @@ class HtmlViewEmbedder {
_viewsToRecomposite.clear();
_activeCompositionOrder.clear();
_compositionOrder.clear();
_visibleViewCount = 0;
}
}

Expand Down Expand Up @@ -945,8 +970,9 @@ class ViewListDiffResult {
// similar to `Surface._insertChildDomNodes` to efficiently handle more cases,
// https://github.com/flutter/flutter/issues/89611.
ViewListDiffResult? diffViewList(List<int> active, List<int> next) {
assert(active.isNotEmpty && next.isNotEmpty,
'diffViewList called with empty view list');
if (active.isEmpty || next.isEmpty) {
return null;
}
// If the [active] and [next] lists are in the expected form described above,
// then either the first or last element of [next] will be in [active].
int index = active.indexOf(next.first);
Expand Down
22 changes: 21 additions & 1 deletion lib/web_ui/lib/src/engine/platform_views/content_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ class PlatformViewManager {
// The references to content tags, indexed by their framework-given ID.
final Map<int, html.Element> _contents = <int, html.Element>{};

final Set<String> _invisibleViews = <String>{};
final Map<int, String> _viewIdToType = <int, String>{};

/// Returns `true` if the passed in `viewType` has been registered before.
///
/// See [registerViewFactory] to understand how factories are registered.
Expand All @@ -64,14 +67,18 @@ class PlatformViewManager {
/// it's been set.
///
/// `factoryFunction` needs to be a [PlatformViewFactory].
bool registerFactory(String viewType, Function factoryFunction) {
bool registerFactory(String viewType, Function factoryFunction,
{bool isVisible = true}) {
assert(factoryFunction is PlatformViewFactory ||
factoryFunction is ParameterizedPlatformViewFactory);

if (_factories.containsKey(viewType)) {
return false;
}
_factories[viewType] = factoryFunction;
if (!isVisible) {
_invisibleViews.add(viewType);
}
return true;
}

Expand Down Expand Up @@ -105,6 +112,7 @@ class PlatformViewManager {
'Attempted to render contents of unregistered viewType: $viewType');

final String slotName = getPlatformViewSlotName(viewId);
_viewIdToType[viewId] = viewType;

return _contents.putIfAbsent(viewId, () {
final html.Element wrapper = html.document
Expand Down Expand Up @@ -186,6 +194,16 @@ class PlatformViewManager {
}
}

/// Returns `true` if the given [viewId] is for an invisible platform view.
bool isInvisible(int viewId) {
final String? viewType = _viewIdToType[viewId];
return viewType != null && _invisibleViews.contains(viewType);
}

/// Returns `true` if the given [viewId] is a platform view with a visible
/// component.
bool isVisible(int viewId) => !isInvisible(viewId);

/// Clears the state. Used in tests.
///
/// Returns the set of know view ids, so they can be cleaned up.
Expand All @@ -194,6 +212,8 @@ class PlatformViewManager {
result.forEach(clearPlatformView);
_factories.clear();
_contents.clear();
_invisibleViews.clear();
_viewIdToType.clear();
return result;
}
}
6 changes: 4 additions & 2 deletions lib/web_ui/lib/ui.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,11 @@ typedef PlatformViewFactory = html.Element Function(int viewId);
/// A registry for factories that create platform views.
class PlatformViewRegistry {
/// Register [viewTypeId] as being creating by the given [factory].
bool registerViewFactory(String viewTypeId, PlatformViewFactory viewFactory) {
bool registerViewFactory(String viewTypeId, PlatformViewFactory viewFactory,
{bool isVisible = true}) {
// TODO(web): Deprecate this once there's another way of calling `registerFactory` (js interop?)
return engine.platformViewManager.registerFactory(viewTypeId, viewFactory);
return engine.platformViewManager
.registerFactory(viewTypeId, viewFactory, isVisible: isVisible);
}
}

Expand Down
111 changes: 111 additions & 0 deletions lib/web_ui/test/canvaskit/embedded_views_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,117 @@ void testMain() {

HtmlViewEmbedder.debugDisableOverlays = false;
});

test('does not create overlays for invisible platform views', () async {
ui.platformViewRegistry.registerViewFactory(
'test-visible-view',
(int viewId) =>
html.DivElement()..className = 'visible-platform-view');
ui.platformViewRegistry.registerViewFactory(
'test-invisible-view',
(int viewId) =>
html.DivElement()..className = 'invisible-platform-view',
isVisible: false,
);
await createPlatformView(0, 'test-visible-view');
await createPlatformView(1, 'test-invisible-view');
await createPlatformView(2, 'test-visible-view');
await createPlatformView(3, 'test-invisible-view');
await createPlatformView(4, 'test-invisible-view');
await createPlatformView(5, 'test-invisible-view');
await createPlatformView(6, 'test-invisible-view');

final EnginePlatformDispatcher dispatcher =
ui.window.platformDispatcher as EnginePlatformDispatcher;

int countCanvases() {
return domRenderer.sceneElement!.querySelectorAll('canvas').length;
}

expect(platformViewManager.isInvisible(0), isFalse);
expect(platformViewManager.isInvisible(1), isTrue);

LayerSceneBuilder sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
sb.addPlatformView(1, width: 10, height: 10);
sb.pop();
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(countCanvases(), 1);

sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
sb.addPlatformView(0, width: 10, height: 10);
sb.addPlatformView(1, width: 10, height: 10);
sb.pop();
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(countCanvases(), 2);

sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
sb.addPlatformView(0, width: 10, height: 10);
sb.addPlatformView(1, width: 10, height: 10);
sb.addPlatformView(2, width: 10, height: 10);
sb.pop();
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(countCanvases(), 3);

sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
sb.addPlatformView(0, width: 10, height: 10);
sb.addPlatformView(1, width: 10, height: 10);
sb.addPlatformView(2, width: 10, height: 10);
sb.addPlatformView(3, width: 10, height: 10);
sb.pop();
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(countCanvases(), 3);

sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
sb.addPlatformView(0, width: 10, height: 10);
sb.addPlatformView(1, width: 10, height: 10);
sb.addPlatformView(2, width: 10, height: 10);
sb.addPlatformView(3, width: 10, height: 10);
sb.addPlatformView(4, width: 10, height: 10);
sb.pop();
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(countCanvases(), 3);

sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
sb.addPlatformView(0, width: 10, height: 10);
sb.addPlatformView(1, width: 10, height: 10);
sb.addPlatformView(2, width: 10, height: 10);
sb.addPlatformView(3, width: 10, height: 10);
sb.addPlatformView(4, width: 10, height: 10);
sb.addPlatformView(5, width: 10, height: 10);
sb.pop();
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(countCanvases(), 3);

sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
sb.addPlatformView(0, width: 10, height: 10);
sb.addPlatformView(1, width: 10, height: 10);
sb.addPlatformView(2, width: 10, height: 10);
sb.addPlatformView(3, width: 10, height: 10);
sb.addPlatformView(4, width: 10, height: 10);
sb.addPlatformView(5, width: 10, height: 10);
sb.addPlatformView(6, width: 10, height: 10);
sb.pop();
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(countCanvases(), 3);

sb = LayerSceneBuilder();
sb.pushOffset(0, 0);
sb.addPlatformView(1, width: 10, height: 10);
sb.addPlatformView(3, width: 10, height: 10);
sb.addPlatformView(4, width: 10, height: 10);
sb.addPlatformView(5, width: 10, height: 10);
sb.addPlatformView(6, width: 10, height: 10);
sb.pop();
dispatcher.rasterizer!.draw(sb.build().layerTree);
expect(countCanvases(), 1);
});
// TODO(dit): https://github.com/flutter/flutter/issues/60040
}, skip: isIosSafari);
}