blob: aa307d4db8fb4b27d12a5b93ea2b4b326c8d4189 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:html' as html;
import 'dart:math' as math;
import 'dart:svg' as svg;
import 'package:ui/ui.dart' as ui;
import '../../engine.dart' show platformViewManager;
import '../configuration.dart';
import '../html/path_to_svg_clip.dart';
import '../platform_views/slots.dart';
import '../util.dart';
import '../vector_math.dart';
import '../window.dart';
import 'canvas.dart';
import 'initialization.dart';
import 'path.dart';
import 'picture_recorder.dart';
import 'surface.dart';
import 'surface_factory.dart';
/// This composites HTML views into the [ui.Scene].
class HtmlViewEmbedder {
/// The [HtmlViewEmbedder] singleton.
static HtmlViewEmbedder instance = HtmlViewEmbedder._();
HtmlViewEmbedder._();
/// If `true`, overlay canvases are disabled.
///
/// This causes all drawing to go to a single canvas, with all of the platform
/// views rendered over top. This may result in incorrect rendering with
/// platform views.
static bool get disableOverlays =>
debugDisableOverlays || configuration.canvasKitMaximumSurfaces <= 1;
/// Force the view embedder to disable overlays.
///
/// This should never be used outside of tests.
static bool debugDisableOverlays = false;
/// The set of platform views using the backup surface.
final Set<int> _viewsUsingBackupSurface = <int>{};
/// Picture recorders which were created during the preroll phase.
///
/// These picture recorders will be "claimed" in the paint phase by platform
/// views being composited into the scene.
final List<CkPictureRecorder> _pictureRecordersCreatedDuringPreroll =
<CkPictureRecorder>[];
/// The picture recorder shared by all platform views which paint to the
/// backup surface.
CkPictureRecorder? _backupPictureRecorder;
/// A picture recorder associated with a view id.
///
/// When we composite in the platform view, we need to create a new canvas
/// for further paint commands to paint to, since the composited view will
/// be on top of the current canvas, and we want further paint commands to
/// be on top of the platform view.
final Map<int, CkPictureRecorder> _pictureRecorders =
<int, CkPictureRecorder>{};
/// The most recent composition parameters for a given view id.
///
/// If we receive a request to composite a view, but the composition
/// parameters haven't changed, we can avoid having to recompute the
/// element stack that correctly composites the view into the scene.
final Map<int, EmbeddedViewParams> _currentCompositionParams =
<int, EmbeddedViewParams>{};
/// 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.
///
/// These surfaces are cached in the [OverlayCache] and reused.
final Map<int, Surface> _overlays = <int, Surface>{};
/// The views that need to be recomposited into the scene on the next frame.
final Set<int> _viewsToRecomposite = <int>{};
/// 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>[];
/// The size of the frame, in physical pixels.
ui.Size _frameSize = ui.window.physicalSize;
set frameSize(ui.Size size) {
_frameSize = size;
}
/// Returns a list of canvases which will be overlaid on top of the "base"
/// canvas after a platform view is composited into the scene.
///
/// The engine asks for the overlay canvases immediately before the paint
/// phase, after the preroll phase. In the preroll phase we must be
/// conservative and assume that every platform view which is prerolled is
/// also composited, and therefore requires an overlay canvas. However, not
/// every platform view which is prerolled ends up being composited (it may be
/// clipped out and not actually drawn). This means that we may end up
/// overallocating canvases. This isn't a problem in practice, however, as
/// unused recording canvases are simply deleted at the end of the frame.
List<CkCanvas> getOverlayCanvases() {
if (disableOverlays) {
return const <CkCanvas>[];
}
final List<CkCanvas> overlayCanvases = _pictureRecordersCreatedDuringPreroll
.map((CkPictureRecorder r) => r.recordingCanvas!)
.toList();
if (_backupPictureRecorder != null) {
overlayCanvases.add(_backupPictureRecorder!.recordingCanvas!);
}
return overlayCanvases;
}
void prerollCompositeEmbeddedView(int viewId, EmbeddedViewParams params) {
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.
bool needBackupSurface = false;
if (_pictureRecordersCreatedDuringPreroll.length >=
SurfaceFactory.instance.maximumOverlays) {
needBackupSurface = true;
}
if (needBackupSurface) {
if (_backupPictureRecorder == null) {
// Only initialize the picture recorder for the backup surface once.
final CkPictureRecorder pictureRecorder = CkPictureRecorder();
pictureRecorder.beginRecording(ui.Offset.zero & _frameSize);
pictureRecorder.recordingCanvas!.clear(const ui.Color(0x00000000));
_backupPictureRecorder = pictureRecorder;
}
} else {
final CkPictureRecorder pictureRecorder = CkPictureRecorder();
pictureRecorder.beginRecording(ui.Offset.zero & _frameSize);
pictureRecorder.recordingCanvas!.clear(const ui.Color(0x00000000));
_pictureRecordersCreatedDuringPreroll.add(pictureRecorder);
}
}
// Do nothing if the params didn't change.
if (_currentCompositionParams[viewId] == params) {
// If the view was prerolled but not composited, then it needs to be
// recomposited.
if (!_activeCompositionOrder.contains(viewId)) {
_viewsToRecomposite.add(viewId);
}
return;
}
_currentCompositionParams[viewId] = params;
_viewsToRecomposite.add(viewId);
}
/// Prepares to composite [viewId].
///
/// 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 overlayIndex = _visibleViewCount;
_compositionOrder.add(viewId);
if (platformViewManager.isVisible(viewId)) {
_visibleViewCount++;
}
final bool needOverlay =
!disableOverlays && platformViewManager.isVisible(viewId);
if (needOverlay) {
if (overlayIndex < _pictureRecordersCreatedDuringPreroll.length) {
_pictureRecorders[viewId] =
_pictureRecordersCreatedDuringPreroll[overlayIndex];
} else {
_viewsUsingBackupSurface.add(viewId);
_pictureRecorders[viewId] = _backupPictureRecorder!;
}
}
// Do nothing if this view doesn't need to be composited.
if (!_viewsToRecomposite.contains(viewId)) {
if (needOverlay) {
return _pictureRecorders[viewId]!.recordingCanvas;
} else {
return null;
}
}
_compositeWithParams(viewId, _currentCompositionParams[viewId]!);
_viewsToRecomposite.remove(viewId);
if (needOverlay) {
return _pictureRecorders[viewId]!.recordingCanvas;
} else {
return null;
}
}
void _compositeWithParams(int viewId, 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 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 = clipChain.clipCount;
if (currentClippingCount != previousClippingCount) {
final html.Element oldPlatformViewRoot = clipChain.root;
final html.Element newPlatformViewRoot = _reconstructClipViewsChain(
currentClippingCount,
slot,
oldPlatformViewRoot,
);
// Store the updated root element, and clip count
clipChain.updateClipChain(
root: newPlatformViewRoot,
clipCount: currentClippingCount,
);
}
// Apply mutators to the slot
_applyMutators(params, slot, viewId);
}
int _countClips(MutatorsStack mutators) {
int clipCount = 0;
for (final Mutator mutator in mutators) {
if (mutator.isClipType) {
clipCount++;
}
}
return clipCount;
}
html.Element _reconstructClipViewsChain(
int numClips,
html.Element platformView,
html.Element headClipView,
) {
int indexInFlutterView = -1;
if (headClipView.parent != null) {
indexInFlutterView = skiaSceneHost!.children.indexOf(headClipView);
headClipView.remove();
}
html.Element head = platformView;
int clipIndex = 0;
// Re-use as much existing clip views as needed.
while (head != headClipView && clipIndex < numClips) {
head = head.parent!;
clipIndex++;
}
// If there weren't enough existing clip views, add more.
while (clipIndex < numClips) {
final html.Element clippingView = html.Element.tag('flt-clip');
clippingView.append(head);
head = clippingView;
clipIndex++;
}
head.remove();
// If the chain was previously attached, attach it to the same position.
if (indexInFlutterView > -1) {
skiaSceneHost!.children.insert(indexInFlutterView, head);
}
return head;
}
/// Clean up the old SVG clip definitions, as this platform view is about to
/// be recomposited.
void _cleanUpClipDefs(int viewId) {
if (_svgClipDefs.containsKey(viewId)) {
final html.Element clipDefs =
_svgPathDefs!.querySelector('#sk_path_defs')!;
final List<html.Element> nodesToRemove = <html.Element>[];
final Set<String> oldDefs = _svgClipDefs[viewId]!;
for (final html.Element child in clipDefs.children) {
if (oldDefs.contains(child.id)) {
nodesToRemove.add(child);
}
}
for (final html.Element node in nodesToRemove) {
node.remove();
}
_svgClipDefs[viewId]!.clear();
}
}
void _applyMutators(
EmbeddedViewParams params, html.Element embeddedView, int viewId) {
final MutatorsStack mutators = params.mutators;
html.Element head = embeddedView;
Matrix4 headTransform = params.offset == ui.Offset.zero
? Matrix4.identity()
: Matrix4.translationValues(params.offset.dx, params.offset.dy, 0);
double embeddedOpacity = 1.0;
_resetAnchor(head);
_cleanUpClipDefs(viewId);
for (final Mutator mutator in mutators) {
switch (mutator.type) {
case MutatorType.transform:
headTransform = mutator.matrix!.multiplied(headTransform);
head.style.transform =
float64ListToCssTransform(headTransform.storage);
break;
case MutatorType.clipRect:
case MutatorType.clipRRect:
case MutatorType.clipPath:
final html.Element clipView = head.parent!;
clipView.style.clip = '';
clipView.style.clipPath = '';
headTransform = Matrix4.identity();
clipView.style.transform = '';
if (mutator.rect != null) {
final ui.Rect rect = mutator.rect!;
clipView.style.clip = 'rect(${rect.top}px, ${rect.right}px, '
'${rect.bottom}px, ${rect.left}px)';
} else if (mutator.rrect != null) {
final CkPath path = CkPath();
path.addRRect(mutator.rrect!);
_ensureSvgPathDefs();
final html.Element pathDefs =
_svgPathDefs!.querySelector('#sk_path_defs')!;
_clipPathCount += 1;
final String clipId = 'svgClip$_clipPathCount';
final svg.ClipPathElement newClipPath = svg.ClipPathElement();
newClipPath.id = clipId;
newClipPath.append(
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
// from the DOM.
_svgClipDefs.putIfAbsent(viewId, () => <String>{}).add(clipId);
clipView.style.clipPath = 'url(#$clipId)';
} else if (mutator.path != null) {
final CkPath path = mutator.path! as CkPath;
_ensureSvgPathDefs();
final html.Element pathDefs =
_svgPathDefs!.querySelector('#sk_path_defs')!;
_clipPathCount += 1;
final String clipId = 'svgClip$_clipPathCount';
final svg.ClipPathElement newClipPath = svg.ClipPathElement();
newClipPath.id = clipId;
newClipPath.append(
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
// from the DOM.
_svgClipDefs.putIfAbsent(viewId, () => <String>{}).add(clipId);
clipView.style.clipPath = 'url(#$clipId)';
}
_resetAnchor(clipView);
head = clipView;
break;
case MutatorType.opacity:
embeddedOpacity *= mutator.alphaFloat;
break;
}
}
embeddedView.style.opacity = embeddedOpacity.toString();
// Reverse scale based on screen scale.
//
// HTML elements use logical (CSS) pixels, but we have been using physical
// pixels, so scale down the head element to match the logical resolution.
final double scale = window.devicePixelRatio;
final double inverseScale = 1 / scale;
final Matrix4 scaleMatrix =
Matrix4.diagonal3Values(inverseScale, inverseScale, 1);
headTransform = scaleMatrix.multiplied(headTransform);
head.style.transform = float64ListToCssTransform(headTransform.storage);
}
/// Sets the transform origin to the top-left corner of the element.
///
/// By default, the transform origin is the center of the element, but
/// Flutter assumes the transform origin is the top-left point.
void _resetAnchor(html.Element element) {
element.style.transformOrigin = '0 0 0';
element.style.position = 'absolute';
}
int _clipPathCount = 0;
html.Element? _svgPathDefs;
/// The nodes containing the SVG clip definitions needed to clip this view.
Map<int, Set<String>> _svgClipDefs = <int, Set<String>>{};
/// Ensures we add a container of SVG path defs to the DOM so they can
/// be referred to in clip-path: url(#blah).
void _ensureSvgPathDefs() {
if (_svgPathDefs != null) {
return;
}
_svgPathDefs = kSvgResourceHeader.clone(false) as svg.SvgSvgElement;
_svgPathDefs!.append(svg.DefsElement()..id = 'sk_path_defs');
skiaSceneHost!.append(_svgPathDefs!);
}
void submitFrame() {
final ViewListDiffResult? diffResult = (_activeCompositionOrder.isEmpty ||
_compositionOrder.isEmpty ||
disableOverlays)
? null
: 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) {
final SurfaceFrame backupFrame =
SurfaceFactory.instance.backupSurface.acquireFrame(_frameSize);
backupFrame.skiaCanvas
.drawPicture(_backupPictureRecorder!.endRecording());
_backupPictureRecorder = null;
backupFrame.submit();
_didPaintBackupSurface = true;
}
} else {
final SurfaceFrame frame =
_overlays[viewId]!.acquireFrame(_frameSize);
final CkCanvas canvas = frame.skiaCanvas;
canvas.drawPicture(
_pictureRecorders[viewId]!.endRecording(),
);
frame.submit();
}
}
}
_pictureRecordersCreatedDuringPreroll.clear();
_pictureRecorders.clear();
_viewsUsingBackupSurface.clear();
if (listEquals(_compositionOrder, _activeCompositionOrder)) {
_compositionOrder.clear();
_visibleViewCount = 0;
return;
}
final Set<int> unusedViews = Set<int>.from(_activeCompositionOrder);
_activeCompositionOrder.clear();
List<int>? debugInvalidViewIds;
if (diffResult != null) {
disposeViews(diffResult.viewsToRemove.toSet());
_activeCompositionOrder.addAll(_compositionOrder);
unusedViews.removeAll(_compositionOrder);
html.Element? elementToInsertBefore;
if (diffResult.addToBeginning) {
elementToInsertBefore =
_viewClipChains[diffResult.viewToInsertBefore!]!.root;
}
for (final int viewId in diffResult.viewsToAdd) {
if (assertionsEnabled) {
if (!platformViewManager.knowsViewId(viewId)) {
debugInvalidViewIds ??= <int>[];
debugInvalidViewIds.add(viewId);
continue;
}
}
if (diffResult.addToBeginning) {
final html.Element platformViewRoot = _viewClipChains[viewId]!.root;
skiaSceneHost!.insertBefore(platformViewRoot, elementToInsertBefore);
final Surface? overlay = _overlays[viewId];
if (overlay != null) {
skiaSceneHost!
.insertBefore(overlay.htmlElement, elementToInsertBefore);
}
} else {
final html.Element platformViewRoot = _viewClipChains[viewId]!.root;
skiaSceneHost!.append(platformViewRoot);
final Surface? overlay = _overlays[viewId];
if (overlay != null) {
skiaSceneHost!.append(overlay.htmlElement);
}
}
}
insertBeforeMap?.forEach((int viewId, int viewIdToInsertBefore) {
final html.Element overlay = _overlays[viewId]!.htmlElement;
if (viewIdToInsertBefore != -1) {
final html.Element nextSibling =
_viewClipChains[viewIdToInsertBefore]!.root;
skiaSceneHost!.insertBefore(overlay, nextSibling);
} else {
skiaSceneHost!.append(overlay);
}
});
if (_didPaintBackupSurface) {
skiaSceneHost!
.append(SurfaceFactory.instance.backupSurface.htmlElement);
}
} else {
SurfaceFactory.instance.removeSurfacesFromDom();
for (int i = 0; i < _compositionOrder.length; i++) {
final int viewId = _compositionOrder[i];
if (assertionsEnabled) {
if (!platformViewManager.knowsViewId(viewId)) {
debugInvalidViewIds ??= <int>[];
debugInvalidViewIds.add(viewId);
continue;
}
}
final html.Element platformViewRoot = _viewClipChains[viewId]!.root;
final Surface? overlay = _overlays[viewId];
skiaSceneHost!.append(platformViewRoot);
if (overlay != null) {
skiaSceneHost!.append(overlay.htmlElement);
}
_activeCompositionOrder.add(viewId);
unusedViews.remove(viewId);
}
if (_didPaintBackupSurface) {
skiaSceneHost!
.append(SurfaceFactory.instance.backupSurface.htmlElement);
}
}
_compositionOrder.clear();
_visibleViewCount = 0;
disposeViews(unusedViews);
_pictureRecorders.clear();
if (assertionsEnabled) {
if (debugInvalidViewIds != null && debugInvalidViewIds.isNotEmpty) {
throw AssertionError(
'Cannot render platform views: ${debugInvalidViewIds.join(', ')}. '
'These views have not been created, or they have been deleted.',
);
}
}
}
void disposeViews(Set<int> viewsToDispose) {
for (final int viewId in viewsToDispose) {
// Remove viewId from the _viewClipChains Map, and then from the DOM.
final ViewClipChain? clipChain = _viewClipChains.remove(viewId);
clipChain?.root.remove();
// More cleanup
_currentCompositionParams.remove(viewId);
_viewsToRecomposite.remove(viewId);
_cleanUpClipDefs(viewId);
_svgClipDefs.remove(viewId);
}
}
void _releaseOverlay(int viewId) {
if (_overlays[viewId] != null) {
final Surface overlay = _overlays[viewId]!;
SurfaceFactory.instance.releaseSurface(overlay);
_overlays.remove(viewId);
}
}
// Called right before compositing the scene.
//
// [_compositionOrder] and [_activeComposition] order should contain the
// composition order of the current and previous frame, respectively.
//
// TODO(hterkelsen): Test this more thoroughly.
Map<int, int>? _updateOverlays(ViewListDiffResult? diffResult) {
if (_viewsUsingBackupSurface.isEmpty) {
SurfaceFactory.instance
.releaseSurface(SurfaceFactory.instance.backupSurface);
}
if (diffResult != null &&
diffResult.viewsToAdd.isEmpty &&
diffResult.viewsToRemove.isEmpty) {
return null;
}
if (diffResult == null) {
// Everything is going to be explicitly recomposited anyway. Release all
// the surfaces and assign an overlay to the first N surfaces where
// N = [SurfaceFactory.instance.maximumOverlays] and assign the rest
// 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,
viewsNeedingOverlays.length,
);
for (int i = 0; i < numOverlays; i++) {
final int viewId = viewsNeedingOverlays[i];
assert(!_viewsUsingBackupSurface.contains(viewId));
_initializeOverlay(viewId);
}
_assertOverlaysInitialized();
return null;
} else {
// We want to preserve the overlays in the "unchanged" section of the
// diff result as much as possible. If `addToBeginning` is `false`, then
// release the overlays for the views which were deleted from the
// beginning of the composition order and try to reuse those overlays in
// either the unchanged segment or the newly added views. If
// `addToBeginning` is `true` then release the overlays for the deleted
// views and from the unchanged segment and assign the newly added views
// to them.
diffResult.viewsToRemove.forEach(_releaseOverlay);
final int availableOverlays =
SurfaceFactory.instance.numAvailableOverlays;
if (diffResult.addToBeginning) {
// If we have enough overlays for the newly added views, then just use
// them. Otherwise, we will need to release overlays from the unchanged
// segment of view ids.
if (diffResult.viewsToAdd.length > availableOverlays) {
int viewsToDispose = math.min(SurfaceFactory.instance.maximumOverlays,
diffResult.viewsToAdd.length - availableOverlays);
// The first `maximumSurfaces` views in the previous composition order
// had an overlay.
int index = SurfaceFactory.instance.maximumOverlays -
diffResult.viewsToAdd.length;
while (viewsToDispose > 0) {
// The first [maxOverlays - viewsAdded] active views should have
// overlays. The rest should be removed.
_releaseOverlay(_activeCompositionOrder[index++]);
viewsToDispose--;
}
}
// Now assign an overlay to the newly added views.
final int overlaysToAssign = math.min(diffResult.viewsToAdd.length,
SurfaceFactory.instance.numAvailableOverlays);
for (int i = 0; i < overlaysToAssign; i++) {
_initializeOverlay(diffResult.viewsToAdd[i]);
}
_assertOverlaysInitialized();
return null;
} else {
// Use the overlays we just released for any platform views at the
// beginning of the list which previously used the backup surface.
int overlaysToAssign =
math.min(_compositionOrder.length, availableOverlays);
int index = 0;
final int lastOriginalIndex =
_activeCompositionOrder.length - diffResult.viewsToRemove.length;
final Map<int, int> insertBeforeMap = <int, int>{};
while (overlaysToAssign > 0 && index < _compositionOrder.length) {
final bool activeView = index < lastOriginalIndex;
final int viewId = _compositionOrder[index];
if (!_overlays.containsKey(viewId) &&
platformViewManager.isVisible(viewId)) {
_initializeOverlay(viewId);
overlaysToAssign--;
if (activeView) {
if (index + 1 < _compositionOrder.length) {
insertBeforeMap[viewId] = _compositionOrder[index + 1];
} else {
insertBeforeMap[viewId] = -1;
}
}
}
index++;
}
_assertOverlaysInitialized();
return insertBeforeMap;
}
}
}
void _assertOverlaysInitialized() {
if (assertionsEnabled) {
for (int i = 0; i < _compositionOrder.length; i++) {
final int viewId = _compositionOrder[i];
assert(_viewsUsingBackupSurface.contains(viewId) ||
platformViewManager.isInvisible(viewId) ||
_overlays[viewId] != null);
}
}
}
void _initializeOverlay(int viewId) {
assert(!_overlays.containsKey(viewId) &&
!_viewsUsingBackupSurface.contains(viewId));
// Try reusing a cached overlay created for another platform view.
final Surface overlay = SurfaceFactory.instance.getOverlay()!;
overlay.createOrUpdateSurface(_frameSize);
_overlays[viewId] = overlay;
}
/// Deletes SVG clip paths, useful for tests.
void debugCleanupSvgClipPaths() {
_svgPathDefs?.children.single.children.forEach(removeElement);
_svgClipDefs.clear();
}
static void removeElement(html.Element element) {
element.remove();
}
/// Clears the state of this view embedder. Used in tests.
void debugClear() {
final Set<int> allViews = platformViewManager.debugClear();
disposeViews(allViews);
_backupPictureRecorder?.endRecording();
_backupPictureRecorder = null;
_viewsUsingBackupSurface.clear();
_pictureRecordersCreatedDuringPreroll.clear();
_pictureRecorders.clear();
_currentCompositionParams.clear();
debugCleanupSvgClipPaths();
_currentCompositionParams.clear();
_viewClipChains.clear();
_overlays.clear();
_viewsToRecomposite.clear();
_activeCompositionOrder.clear();
_compositionOrder.clear();
_visibleViewCount = 0;
}
}
/// 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})
: _root = view,
_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;
}
}
/// The parameters passed to the view embedder.
class EmbeddedViewParams {
EmbeddedViewParams(this.offset, this.size, MutatorsStack mutators)
: mutators = MutatorsStack._copy(mutators);
final ui.Offset offset;
final ui.Size size;
final MutatorsStack mutators;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
return other is EmbeddedViewParams &&
other.offset == offset &&
other.size == size &&
other.mutators == mutators;
}
@override
int get hashCode => ui.hashValues(offset, size, mutators);
}
enum MutatorType {
clipRect,
clipRRect,
clipPath,
transform,
opacity,
}
/// Stores mutation information like clipping or transform.
class Mutator {
const Mutator._(
this.type,
this.rect,
this.rrect,
this.path,
this.matrix,
this.alpha,
);
final MutatorType type;
final ui.Rect? rect;
final ui.RRect? rrect;
final ui.Path? path;
final Matrix4? matrix;
final int? alpha;
const Mutator.clipRect(ui.Rect rect)
: this._(MutatorType.clipRect, rect, null, null, null, null);
const Mutator.clipRRect(ui.RRect rrect)
: this._(MutatorType.clipRRect, null, rrect, null, null, null);
const Mutator.clipPath(ui.Path path)
: this._(MutatorType.clipPath, null, null, path, null, null);
const Mutator.transform(Matrix4 matrix)
: this._(MutatorType.transform, null, null, null, matrix, null);
const Mutator.opacity(int alpha)
: this._(MutatorType.opacity, null, null, null, null, alpha);
bool get isClipType =>
type == MutatorType.clipRect ||
type == MutatorType.clipRRect ||
type == MutatorType.clipPath;
double get alphaFloat => alpha! / 255.0;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other is! Mutator) {
return false;
}
final Mutator typedOther = other;
if (type != typedOther.type) {
return false;
}
switch (type) {
case MutatorType.clipRect:
return rect == typedOther.rect;
case MutatorType.clipRRect:
return rrect == typedOther.rrect;
case MutatorType.clipPath:
return path == typedOther.path;
case MutatorType.transform:
return matrix == typedOther.matrix;
case MutatorType.opacity:
return alpha == typedOther.alpha;
default:
return false;
}
}
@override
int get hashCode => ui.hashValues(type, rect, rrect, path, matrix, alpha);
}
/// A stack of mutators that can be applied to an embedded view.
class MutatorsStack extends Iterable<Mutator> {
MutatorsStack() : _mutators = <Mutator>[];
MutatorsStack._copy(MutatorsStack original)
: _mutators = List<Mutator>.from(original._mutators);
final List<Mutator> _mutators;
void pushClipRect(ui.Rect rect) {
_mutators.add(Mutator.clipRect(rect));
}
void pushClipRRect(ui.RRect rrect) {
_mutators.add(Mutator.clipRRect(rrect));
}
void pushClipPath(ui.Path path) {
_mutators.add(Mutator.clipPath(path));
}
void pushTransform(Matrix4 matrix) {
_mutators.add(Mutator.transform(matrix));
}
void pushOpacity(int alpha) {
_mutators.add(Mutator.opacity(alpha));
}
void pop() {
_mutators.removeLast();
}
@override
bool operator ==(Object other) {
if (identical(other, this)) {
return true;
}
return other is MutatorsStack &&
listEquals<Mutator>(other._mutators, _mutators);
}
@override
int get hashCode => ui.hashList(_mutators);
@override
Iterator<Mutator> get iterator => _mutators.reversed.iterator;
}
/// The results of diffing the current composition order with the active
/// composition order.
class ViewListDiffResult {
/// Views which should be removed from the scene.
final List<int> viewsToRemove;
/// Views to add to the scene.
final List<int> viewsToAdd;
/// If `true`, [viewsToAdd] should be added at the beginning of the scene.
/// Otherwise, they should be added at the end of the scene.
final bool addToBeginning;
/// If [addToBeginning] is `true`, then this is the id of the platform view
/// to insert [viewsToAdd] before.
///
/// `null` if [addToBeginning] is `false`.
final int? viewToInsertBefore;
const ViewListDiffResult(
this.viewsToRemove, this.viewsToAdd, this.addToBeginning,
{this.viewToInsertBefore});
}
/// Diff the composition order with the active composition order. It is
/// common for the composition order and active composition order to differ
/// only slightly.
///
/// Consider a scrolling list of platform views; from frame
/// to frame the composition order will change in one of two ways, depending
/// on which direction the list is scrolling. One or more views will be added
/// to the beginning of the list, and one or more views will be removed from
/// the end of the list, with the order of the unchanged middle views
/// remaining the same.
// TODO(hterkelsen): Refactor to use [longestIncreasingSubsequence] and logic
// similar to `Surface._insertChildDomNodes` to efficiently handle more cases,
// https://github.com/flutter/flutter/issues/89611.
ViewListDiffResult? diffViewList(List<int> active, List<int> next) {
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);
if (index != -1) {
// Verify that the active composition order is contained, in order, in the
// next composition order.
for (int i = 0; i + index < active.length; i++) {
if (active[i + index] != next[i]) {
return null;
}
if (i == next.length - 1) {
if (index == 0) {
return ViewListDiffResult(active.sublist(i + 1), const <int>[], true,
viewToInsertBefore: next.first);
} else {
return ViewListDiffResult(
active.sublist(0, index), const <int>[], false);
}
}
}
return ViewListDiffResult(
active.sublist(0, index),
next.sublist(active.length - index),
false,
);
}
index = active.lastIndexOf(next.last);
if (index != -1) {
for (int i = 0; index - i >= 0; i++) {
if (next.length <= i || active[index - i] != next[next.length - 1 - i]) {
return null;
}
}
return ViewListDiffResult(
active.sublist(index + 1),
next.sublist(0, next.length - index - 1),
true,
viewToInsertBefore: active.first,
);
}
return null;
}