| // 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; |
| } |