diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 6dae3b2eb7593..f6cdc665d4706 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -43589,7 +43589,6 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.da ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart + ../../../flutter/LICENSE -ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/multi_surface_rasterizer.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart + ../../../flutter/LICENSE @@ -46470,7 +46469,6 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/image_web_codecs.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/multi_surface_rasterizer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index f50b7cf78c73e..cdf615a388617 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -33,7 +33,6 @@ export 'engine/canvaskit/image_web_codecs.dart'; export 'engine/canvaskit/layer.dart'; export 'engine/canvaskit/layer_scene_builder.dart'; export 'engine/canvaskit/layer_tree.dart'; -export 'engine/canvaskit/layer_visitor.dart'; export 'engine/canvaskit/mask_filter.dart'; export 'engine/canvaskit/multi_surface_rasterizer.dart'; export 'engine/canvaskit/n_way_canvas.dart'; diff --git a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart index f51456f6a546b..a99216b763748 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart @@ -14,7 +14,6 @@ import '../svg.dart'; import '../util.dart'; import '../vector_math.dart'; import 'canvas.dart'; -import 'layer.dart'; import 'overlay_scene_optimizer.dart'; import 'painting.dart'; import 'path.dart'; @@ -67,9 +66,6 @@ class HtmlViewEmbedder { /// Returns the most recent rendering. Only used in tests. Rendering get debugActiveRendering => _activeRendering; - /// If [debugOverlayOptimizationBounds] is true, this canvas will draw - /// semitransparent rectangles showing the computed bounds of the platform - /// views and pictures in the scene. DisplayCanvas? debugBoundsCanvas; /// The size of the frame, in physical pixels. @@ -79,23 +75,27 @@ class HtmlViewEmbedder { _frameSize = size; } - /// Returns a list of recording canvases which the pictures in the upcoming - /// paint step will be drawn into. These recording canvases are combined into - /// an N-way canvas for the rasterizer to record clip and transform operations - /// during the measure step. - Iterable getPictureCanvases() { - return _context.measuringPictureRecorders.values - .map((CkPictureRecorder r) => r.recordingCanvas!); - } - - /// Returns a list of canvases for the optimized rendering. These are used in - /// the paint step. - Iterable getOptimizedCanvases() { - return _context.optimizedCanvasRecorders! + /// 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. + Iterable getOverlayCanvases() { + return _context.pictureRecordersCreatedDuringPreroll .map((CkPictureRecorder r) => r.recordingCanvas!); } void prerollCompositeEmbeddedView(int viewId, EmbeddedViewParams params) { + final CkPictureRecorder pictureRecorder = CkPictureRecorder(); + pictureRecorder.beginRecording(ui.Offset.zero & _frameSize.toSize()); + _context.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 @@ -109,38 +109,30 @@ class HtmlViewEmbedder { _viewsToRecomposite.add(viewId); } - /// Record that a picture recorder is needed for [picture] to be measured. - void prerollPicture(PictureLayer picture) { - final CkPictureRecorder pictureRecorder = CkPictureRecorder(); - pictureRecorder.beginRecording(ui.Offset.zero & _frameSize.toSize()); - _context.measuringPictureRecorders[picture] = pictureRecorder; - } - - /// Returns the canvas that was created to measure [picture]. - CkCanvas getMeasuringCanvasFor(PictureLayer picture) { - return _context.measuringPictureRecorders[picture]!.recordingCanvas!; - } - - /// Adds the picture recorder associated with [picture] to the unoptimized - /// scene. - void addPictureToUnoptimizedScene(PictureLayer picture) { - final CkPictureRecorder recorder = - _context.measuringPictureRecorders[picture]!; - _context.sceneElements.add(PictureSceneElement(picture, recorder)); - } - /// Prepares to composite [viewId]. - void compositeEmbeddedView(int 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) { // Ensure platform view with `viewId` is injected into the `rasterizer.view`. rasterizer.view.dom.injectPlatformView(viewId); + final int overlayIndex = _context.viewCount; _compositionOrder.add(viewId); - _context.sceneElements.add(PlatformViewSceneElement(viewId)); + _context.viewCount++; + + CkPictureRecorder? recorderToUseForRendering; + if (overlayIndex < _context.pictureRecordersCreatedDuringPreroll.length) { + recorderToUseForRendering = + _context.pictureRecordersCreatedDuringPreroll[overlayIndex]; + _context.pictureRecorders.add(recorderToUseForRendering); + } if (_viewsToRecomposite.contains(viewId)) { _compositeWithParams(viewId, _currentCompositionParams[viewId]!); _viewsToRecomposite.remove(viewId); } + return recorderToUseForRendering?.recordingCanvas; } void _compositeWithParams(int platformViewId, EmbeddedViewParams params) { @@ -363,57 +355,14 @@ class HtmlViewEmbedder { sceneHost.append(_svgPathDefs!); } - /// Optimizes the scene to use the fewest possible canvases. This sets up - /// the final paint pass to paint the pictures into the optimized canvases. - void optimizeRendering() { - final Map scenePictureToRawPicture = - {}; - final Iterable unoptimizedRendering = - _context.sceneElements.map((SceneElement element) { - if (element is PictureSceneElement) { - final CkPicture scenePicture = element.pictureRecorder.endRecording(); - if (scenePicture.cullRect.isEmpty) { - element.picture.isCulled = true; - } - element.scenePicture = scenePicture; - scenePictureToRawPicture[scenePicture] = element.picture; - return element; - } else { - return element; - } - }); + Future submitFrame(CkPicture basePicture) async { + final List pictures = [basePicture]; + for (final CkPictureRecorder recorder in _context.pictureRecorders) { + pictures.add(recorder.endRecording()); + } Rendering rendering = createOptimizedRendering( - unoptimizedRendering, _currentCompositionParams); + pictures, _compositionOrder, _currentCompositionParams); rendering = _modifyRenderingForMaxCanvases(rendering); - _context.optimizedRendering = rendering; - // Create new picture recorders for the optimized render canvases and record - // which pictures go in which canvas. - final List optimizedCanvasRecorders = - []; - final Map pictureToOptimizedCanvasMap = - {}; - for (final RenderingRenderCanvas renderCanvas in rendering.canvases) { - final CkPictureRecorder pictureRecorder = CkPictureRecorder(); - pictureRecorder.beginRecording(ui.Offset.zero & _frameSize.toSize()); - optimizedCanvasRecorders.add(pictureRecorder); - for (final CkPicture picture in renderCanvas.pictures) { - pictureToOptimizedCanvasMap[scenePictureToRawPicture[picture]!] = - pictureRecorder; - } - } - _context.optimizedCanvasRecorders = optimizedCanvasRecorders; - _context.pictureToOptimizedCanvasMap = pictureToOptimizedCanvasMap; - } - - /// Returns the canvas that this picture layer should draw into in the - /// optimized scene. - CkCanvas getOptimizedCanvasFor(PictureLayer picture) { - assert(_context.optimizedRendering != null); - return _context.pictureToOptimizedCanvasMap![picture]!.recordingCanvas!; - } - - Future submitFrame() async { - final Rendering rendering = _context.optimizedRendering!; _updateDomForNewRendering(rendering); if (rendering.equalsForRendering(_activeRendering)) { // Copy the display canvases to the new rendering. @@ -426,17 +375,13 @@ class HtmlViewEmbedder { _activeRendering = rendering; final List renderCanvases = rendering.canvases; - int renderCanvasIndex = 0; for (final RenderingRenderCanvas renderCanvas in renderCanvases) { - final CkPicture renderPicture = _context - .optimizedCanvasRecorders![renderCanvasIndex++] - .endRecording(); await rasterizer.rasterizeToCanvas( - renderCanvas.displayCanvas!, [renderPicture]); + renderCanvas.displayCanvas!, renderCanvas.pictures); } for (final CkPictureRecorder recorder - in _context.measuringPictureRecorders.values) { + in _context.pictureRecordersCreatedDuringPreroll) { if (recorder.isRecording) { recorder.endRecording(); } @@ -448,11 +393,11 @@ class HtmlViewEmbedder { debugBoundsCanvas ??= rasterizer.displayFactory.getCanvas(); final CkPictureRecorder boundsRecorder = CkPictureRecorder(); final CkCanvas boundsCanvas = boundsRecorder.beginRecording( - ui.Rect.fromLTWH( - 0, - 0, - _frameSize.width.toDouble(), - _frameSize.height.toDouble(), + ui.Rect.fromLTWH( + 0, + 0, + _frameSize.width.toDouble(), + _frameSize.height.toDouble(), ), ); final CkPaint platformViewBoundsPaint = CkPaint() @@ -958,45 +903,20 @@ class MutatorsStack extends Iterable { Iterable get reversed => _mutators; } -sealed class SceneElement {} - -class PictureSceneElement extends SceneElement { - PictureSceneElement(this.picture, this.pictureRecorder); - - final PictureLayer picture; - final CkPictureRecorder pictureRecorder; - - /// The picture as it would be painted in the final scene, with clips and - /// transforms applied. This is set by [optimizeRendering]. - CkPicture? scenePicture; -} - -class PlatformViewSceneElement extends SceneElement { - PlatformViewSceneElement(this.viewId); - - final int viewId; -} - /// The state for the current frame. class EmbedderFrameContext { - /// Picture recorders which were created d the final bounds of the picture in the scene. - final Map measuringPictureRecorders = - {}; - - /// List of picture recorders and platform view ids in the order they were - /// painted. - final List sceneElements = []; - - /// The optimized rendering for this frame. This is set by calling - /// [optimizeRendering]. - Rendering? optimizedRendering; - - /// The picture recorders for the optimized rendering. This is set by calling - /// [optimizeRendering]. - List? optimizedCanvasRecorders; - - /// A map from the original PictureLayer to the picture recorder it should go - /// into in the optimized rendering. This is set by calling - /// [optimizedRendering]. - Map? pictureToOptimizedCanvasMap; + /// 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 pictureRecordersCreatedDuringPreroll = + []; + + /// Picture recorders which were actually used in the paint phase. + /// + /// This is a subset of [_pictureRecordersCreatedDuringPreroll]. + final List pictureRecorders = []; + + /// The number of platform views in this frame. + int viewCount = 0; } diff --git a/lib/web_ui/lib/src/engine/canvaskit/layer.dart b/lib/web_ui/lib/src/engine/canvaskit/layer.dart index 79792c4cdb2f7..010fc012786d3 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/layer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/layer.dart @@ -4,10 +4,18 @@ import 'package:ui/ui.dart' as ui; +import '../color_filter.dart'; import '../vector_math.dart'; -import 'layer_visitor.dart'; +import 'canvas.dart'; +import 'canvaskit_api.dart'; +import 'color_filter.dart'; +import 'embedded_views.dart'; +import 'image_filter.dart'; +import 'n_way_canvas.dart'; +import 'painting.dart'; import 'path.dart'; import 'picture.dart'; +import 'raster_cache.dart'; /// A layer to be composed into a scene. /// @@ -23,8 +31,15 @@ abstract class Layer implements ui.EngineLayer { /// Whether or not this layer actually needs to be painted in the scene. bool get needsPainting => !paintBounds.isEmpty; - /// Implement layer visitor. - void accept(LayerVisitor visitor); + /// Pre-process this layer before painting. + /// + /// In this step, we compute the estimated [paintBounds] as well as + /// apply heuristics to prepare the render cache for pictures that + /// should be cached. + void preroll(PrerollContext prerollContext, Matrix4 matrix); + + /// Paint this layer into the scene. + void paint(PaintContext paintContext); // TODO(dnfield): Implement ui.EngineLayer.dispose for CanvasKit. // https://github.com/flutter/flutter/issues/82878 @@ -32,19 +47,109 @@ abstract class Layer implements ui.EngineLayer { void dispose() {} } +/// A context shared by all layers during the preroll pass. +class PrerollContext { + PrerollContext(this.rasterCache, this.viewEmbedder); + + /// A raster cache. Used to register candidates for caching. + final RasterCache? rasterCache; + + /// A compositor for embedded HTML views. + final HtmlViewEmbedder? viewEmbedder; + + final MutatorsStack mutatorsStack = MutatorsStack(); + + ui.Rect get cullRect { + ui.Rect cullRect = ui.Rect.largest; + for (final Mutator m in mutatorsStack) { + ui.Rect clipRect; + switch (m.type) { + case MutatorType.clipRect: + clipRect = m.rect!; + case MutatorType.clipRRect: + clipRect = m.rrect!.outerRect; + case MutatorType.clipPath: + clipRect = m.path!.getBounds(); + default: + continue; + } + cullRect = cullRect.intersect(clipRect); + } + return cullRect; + } +} + +/// A context shared by all layers during the paint pass. +class PaintContext { + PaintContext( + this.internalNodesCanvas, + this.leafNodesCanvas, + this.rasterCache, + this.viewEmbedder, + ); + + /// A multi-canvas that applies clips, transforms, and opacity + /// operations to all canvases (root canvas and overlay canvases for the + /// platform views). + CkNWayCanvas internalNodesCanvas; + + /// The canvas for leaf nodes to paint to. + CkCanvas? leafNodesCanvas; + + /// A raster cache potentially containing pre-rendered pictures. + final RasterCache? rasterCache; + + /// A compositor for embedded HTML views. + final HtmlViewEmbedder? viewEmbedder; +} + /// A layer that contains child layers. abstract class ContainerLayer extends Layer { - final List children = []; + final List _layers = []; /// The list of child layers. /// /// Useful in tests. - List get debugLayers => children; + List get debugLayers => _layers; /// Register [child] as a child of this layer. void add(Layer child) { child.parent = this; - children.add(child); + _layers.add(child); + } + + @override + void preroll(PrerollContext prerollContext, Matrix4 matrix) { + paintBounds = prerollChildren(prerollContext, matrix); + } + + /// Run [preroll] on all of the child layers. + /// + /// Returns a [Rect] that covers the paint bounds of all of the child layers. + /// If all of the child layers have empty paint bounds, then the returned + /// [Rect] is empty. + ui.Rect prerollChildren(PrerollContext context, Matrix4 childMatrix) { + ui.Rect childPaintBounds = ui.Rect.zero; + for (final Layer layer in _layers) { + layer.preroll(context, childMatrix); + if (childPaintBounds.isEmpty) { + childPaintBounds = layer.paintBounds; + } else if (!layer.paintBounds.isEmpty) { + childPaintBounds = childPaintBounds.expandToInclude(layer.paintBounds); + } + } + return childPaintBounds; + } + + /// Calls [paint] on all child layers that need painting. + void paintChildren(PaintContext context) { + assert(needsPainting); + + for (final Layer layer in _layers) { + if (layer.needsPainting) { + layer.paint(context); + } + } } } @@ -54,21 +159,36 @@ abstract class ContainerLayer extends Layer { /// to [LayerSceneBuilder] without requiring a [ContainerLayer]. class RootLayer extends ContainerLayer { @override - void accept(LayerVisitor visitor) { - visitor.visitRoot(this); + void paint(PaintContext paintContext) { + paintChildren(paintContext); } } class BackdropFilterEngineLayer extends ContainerLayer implements ui.BackdropFilterEngineLayer { - BackdropFilterEngineLayer(this.filter, this.blendMode); + BackdropFilterEngineLayer(this._filter, this._blendMode); - final ui.ImageFilter filter; - final ui.BlendMode blendMode; + final ui.ImageFilter _filter; + final ui.BlendMode _blendMode; + + @override + void preroll(PrerollContext prerollContext, Matrix4 matrix) { + final ui.Rect childBounds = prerollChildren(prerollContext, matrix); + paintBounds = childBounds.expandToInclude(prerollContext.cullRect); + } @override - void accept(LayerVisitor visitor) { - visitor.visitBackdropFilter(this); + void paint(PaintContext paintContext) { + final CkPaint paint = CkPaint()..blendMode = _blendMode; + + // Only apply the backdrop filter to the current canvas. If we apply the + // backdrop filter to every canvas (i.e. by applying it to the + // [internalNodesCanvas]), then later when we compose the canvases into a + // single canvas, the backdrop filter will be applied multiple times. + final CkCanvas currentCanvas = paintContext.leafNodesCanvas!; + currentCanvas.saveLayerWithFilter(paintBounds, _filter, paint); + paintChildren(paintContext); + currentCanvas.restore(); } // TODO(dnfield): dispose of the _filter @@ -78,76 +198,189 @@ class BackdropFilterEngineLayer extends ContainerLayer /// A layer that clips its child layers by a given [Path]. class ClipPathEngineLayer extends ContainerLayer implements ui.ClipPathEngineLayer { - ClipPathEngineLayer(this.clipPath, this.clipBehavior) - : assert(clipBehavior != ui.Clip.none); + ClipPathEngineLayer(this._clipPath, this._clipBehavior) + : assert(_clipBehavior != ui.Clip.none); /// The path used to clip child layers. - final CkPath clipPath; - final ui.Clip clipBehavior; + final CkPath _clipPath; + final ui.Clip _clipBehavior; @override - void accept(LayerVisitor visitor) { - visitor.visitClipPath(this); + void preroll(PrerollContext prerollContext, Matrix4 matrix) { + prerollContext.mutatorsStack.pushClipPath(_clipPath); + final ui.Rect childPaintBounds = prerollChildren(prerollContext, matrix); + final ui.Rect clipBounds = _clipPath.getBounds(); + if (childPaintBounds.overlaps(clipBounds)) { + paintBounds = childPaintBounds.intersect(clipBounds); + } + prerollContext.mutatorsStack.pop(); + } + + @override + void paint(PaintContext paintContext) { + assert(needsPainting); + + paintContext.internalNodesCanvas.save(); + paintContext.internalNodesCanvas + .clipPath(_clipPath, _clipBehavior != ui.Clip.hardEdge); + + if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + paintContext.internalNodesCanvas.saveLayer(paintBounds, null); + } + paintChildren(paintContext); + if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + paintContext.internalNodesCanvas.restore(); + } + paintContext.internalNodesCanvas.restore(); } } /// A layer that clips its child layers by a given [Rect]. class ClipRectEngineLayer extends ContainerLayer implements ui.ClipRectEngineLayer { - ClipRectEngineLayer(this.clipRect, this.clipBehavior) - : assert(clipBehavior != ui.Clip.none); + ClipRectEngineLayer(this._clipRect, this._clipBehavior) + : assert(_clipBehavior != ui.Clip.none); /// The rectangle used to clip child layers. - final ui.Rect clipRect; - final ui.Clip clipBehavior; + final ui.Rect _clipRect; + final ui.Clip _clipBehavior; + + @override + void preroll(PrerollContext prerollContext, Matrix4 matrix) { + prerollContext.mutatorsStack.pushClipRect(_clipRect); + final ui.Rect childPaintBounds = prerollChildren(prerollContext, matrix); + if (childPaintBounds.overlaps(_clipRect)) { + paintBounds = childPaintBounds.intersect(_clipRect); + } + prerollContext.mutatorsStack.pop(); + } @override - void accept(LayerVisitor visitor) { - visitor.visitClipRect(this); + void paint(PaintContext paintContext) { + assert(needsPainting); + + paintContext.internalNodesCanvas.save(); + paintContext.internalNodesCanvas.clipRect( + _clipRect, + ui.ClipOp.intersect, + _clipBehavior != ui.Clip.hardEdge, + ); + if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + paintContext.internalNodesCanvas.saveLayer(_clipRect, null); + } + paintChildren(paintContext); + if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + paintContext.internalNodesCanvas.restore(); + } + paintContext.internalNodesCanvas.restore(); } } /// A layer that clips its child layers by a given [RRect]. class ClipRRectEngineLayer extends ContainerLayer implements ui.ClipRRectEngineLayer { - ClipRRectEngineLayer(this.clipRRect, this.clipBehavior) - : assert(clipBehavior != ui.Clip.none); + ClipRRectEngineLayer(this._clipRRect, this._clipBehavior) + : assert(_clipBehavior != ui.Clip.none); /// The rounded rectangle used to clip child layers. - final ui.RRect clipRRect; - final ui.Clip? clipBehavior; + final ui.RRect _clipRRect; + final ui.Clip? _clipBehavior; + + @override + void preroll(PrerollContext prerollContext, Matrix4 matrix) { + prerollContext.mutatorsStack.pushClipRRect(_clipRRect); + final ui.Rect childPaintBounds = prerollChildren(prerollContext, matrix); + if (childPaintBounds.overlaps(_clipRRect.outerRect)) { + paintBounds = childPaintBounds.intersect(_clipRRect.outerRect); + } + prerollContext.mutatorsStack.pop(); + } @override - void accept(LayerVisitor visitor) { - visitor.visitClipRRect(this); + void paint(PaintContext paintContext) { + assert(needsPainting); + + paintContext.internalNodesCanvas.save(); + paintContext.internalNodesCanvas + .clipRRect(_clipRRect, _clipBehavior != ui.Clip.hardEdge); + if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + paintContext.internalNodesCanvas.saveLayer(paintBounds, null); + } + paintChildren(paintContext); + if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) { + paintContext.internalNodesCanvas.restore(); + } + paintContext.internalNodesCanvas.restore(); } } /// A layer that paints its children with the given opacity. class OpacityEngineLayer extends ContainerLayer implements ui.OpacityEngineLayer { - OpacityEngineLayer(this.alpha, this.offset); + OpacityEngineLayer(this._alpha, this._offset); - final int alpha; - final ui.Offset offset; + final int _alpha; + final ui.Offset _offset; + + @override + void preroll(PrerollContext prerollContext, Matrix4 matrix) { + final Matrix4 childMatrix = Matrix4.copy(matrix); + childMatrix.translate(_offset.dx, _offset.dy); + prerollContext.mutatorsStack + .pushTransform(Matrix4.translationValues(_offset.dx, _offset.dy, 0.0)); + prerollContext.mutatorsStack.pushOpacity(_alpha); + super.preroll(prerollContext, childMatrix); + prerollContext.mutatorsStack.pop(); + prerollContext.mutatorsStack.pop(); + paintBounds = paintBounds.translate(_offset.dx, _offset.dy); + } @override - void accept(LayerVisitor visitor) { - visitor.visitOpacity(this); + void paint(PaintContext paintContext) { + assert(needsPainting); + + final CkPaint paint = CkPaint(); + paint.color = ui.Color.fromARGB(_alpha, 0, 0, 0); + + paintContext.internalNodesCanvas.save(); + paintContext.internalNodesCanvas.translate(_offset.dx, _offset.dy); + + final ui.Rect saveLayerBounds = paintBounds.shift(-_offset); + + paintContext.internalNodesCanvas.saveLayer(saveLayerBounds, paint); + paintChildren(paintContext); + // Restore twice: once for the translate and once for the saveLayer. + paintContext.internalNodesCanvas.restore(); + paintContext.internalNodesCanvas.restore(); } } /// A layer that transforms its child layers by the given transform matrix. class TransformEngineLayer extends ContainerLayer implements ui.TransformEngineLayer { - TransformEngineLayer(this.transform); + TransformEngineLayer(this._transform); /// The matrix with which to transform the child layers. - final Matrix4 transform; + final Matrix4 _transform; + + @override + void preroll(PrerollContext prerollContext, Matrix4 matrix) { + final Matrix4 childMatrix = matrix.multiplied(_transform); + prerollContext.mutatorsStack.pushTransform(_transform); + final ui.Rect childPaintBounds = + prerollChildren(prerollContext, childMatrix); + paintBounds = _transform.transformRect(childPaintBounds); + prerollContext.mutatorsStack.pop(); + } @override - void accept(LayerVisitor visitor) { - visitor.visitTransform(this); + void paint(PaintContext paintContext) { + assert(needsPainting); + + paintContext.internalNodesCanvas.save(); + paintContext.internalNodesCanvas.transform(_transform.storage); + paintChildren(paintContext); + paintContext.internalNodesCanvas.restore(); } } @@ -160,24 +393,59 @@ class OffsetEngineLayer extends TransformEngineLayer implements ui.OffsetEngineLayer { OffsetEngineLayer(double dx, double dy) : super(Matrix4.translationValues(dx, dy, 0.0)); - - @override - void accept(LayerVisitor visitor) { - visitor.visitOffset(this); - } } /// A layer that applies an [ui.ImageFilter] to its children. class ImageFilterEngineLayer extends ContainerLayer implements ui.ImageFilterEngineLayer { - ImageFilterEngineLayer(this.filter, this.offset); + ImageFilterEngineLayer(this._filter, this._offset); - final ui.Offset offset; - final ui.ImageFilter filter; + final ui.Offset _offset; + final ui.ImageFilter _filter; + + @override + void preroll(PrerollContext prerollContext, Matrix4 matrix) { + final Matrix4 childMatrix = Matrix4.copy(matrix); + childMatrix.translate(_offset.dx, _offset.dy); + prerollContext.mutatorsStack + .pushTransform(Matrix4.translationValues(_offset.dx, _offset.dy, 0.0)); + final CkManagedSkImageFilterConvertible convertible; + if (_filter is ui.ColorFilter) { + convertible = createCkColorFilter(_filter as EngineColorFilter)!; + } else { + convertible = _filter as CkManagedSkImageFilterConvertible; + } + ui.Rect childPaintBounds = + prerollChildren(prerollContext, childMatrix); + childPaintBounds = childPaintBounds.translate(_offset.dx, _offset.dy); + if (_filter is ui.ColorFilter) { + // If the filter is a ColorFilter, the extended paint bounds will be the + // entire screen, which is not what we want. + paintBounds = childPaintBounds; + } else { + convertible.withSkImageFilter((skFilter) { + paintBounds = rectFromSkIRect( + skFilter.getOutputBounds(toSkRect(childPaintBounds)), + ); + }); + } + prerollContext.mutatorsStack.pop(); + } @override - void accept(LayerVisitor visitor) { - visitor.visitImageFilter(this); + void paint(PaintContext paintContext) { + assert(needsPainting); + final ui.Rect offsetPaintBounds = paintBounds.shift(-_offset); + paintContext.internalNodesCanvas.save(); + paintContext.internalNodesCanvas.translate(_offset.dx, _offset.dy); + paintContext.internalNodesCanvas + .clipRect(offsetPaintBounds, ui.ClipOp.intersect, false); + final CkPaint paint = CkPaint(); + paint.imageFilter = _filter; + paintContext.internalNodesCanvas.saveLayer(offsetPaintBounds, paint); + paintChildren(paintContext); + paintContext.internalNodesCanvas.restore(); + paintContext.internalNodesCanvas.restore(); } // TODO(dnfield): dispose of the _filter @@ -195,8 +463,25 @@ class ShaderMaskEngineLayer extends ContainerLayer final ui.FilterQuality filterQuality; @override - void accept(LayerVisitor visitor) { - visitor.visitShaderMask(this); + void paint(PaintContext paintContext) { + assert(needsPainting); + + paintContext.internalNodesCanvas.saveLayer(paintBounds, null); + paintChildren(paintContext); + + final CkPaint paint = CkPaint(); + paint.shader = shader; + paint.blendMode = blendMode; + paint.filterQuality = filterQuality; + + paintContext.leafNodesCanvas!.save(); + paintContext.leafNodesCanvas!.translate(maskRect.left, maskRect.top); + + paintContext.leafNodesCanvas!.drawRect( + ui.Rect.fromLTWH(0, 0, maskRect.width, maskRect.height), paint); + paintContext.leafNodesCanvas!.restore(); + + paintContext.internalNodesCanvas.restore(); } } @@ -216,17 +501,21 @@ class PictureLayer extends Layer { /// A hint to the compositor that this picture is likely to change. final bool willChange; - /// Whether or not this picture is culled in the final scene. We compute this - /// when we optimize the scene. - bool isCulled = false; - @override - void accept(LayerVisitor visitor) { - visitor.visitPicture(this); + void preroll(PrerollContext prerollContext, Matrix4 matrix) { + paintBounds = picture.cullRect.shift(offset); } @override - bool get needsPainting => super.needsPainting && !isCulled; + void paint(PaintContext paintContext) { + assert(needsPainting); + + paintContext.leafNodesCanvas!.save(); + paintContext.leafNodesCanvas!.translate(offset.dx, offset.dy); + + paintContext.leafNodesCanvas!.drawPicture(picture); + paintContext.leafNodesCanvas!.restore(); + } } /// A layer which contains a [ui.ColorFilter]. @@ -237,8 +526,26 @@ class ColorFilterEngineLayer extends ContainerLayer final ui.ColorFilter filter; @override - void accept(LayerVisitor visitor) { - visitor.visitColorFilter(this); + void paint(PaintContext paintContext) { + assert(needsPainting); + + final CkPaint paint = CkPaint(); + paint.colorFilter = filter; + + // We need to clip because if the ColorFilter affects transparent black, + // then it will fill the entire `cullRect` of the picture, ignoring the + // `paintBounds` passed to `saveLayer`. See: + // https://github.com/flutter/flutter/issues/88866 + paintContext.internalNodesCanvas.save(); + + // TODO(hterkelsen): Only clip if the ColorFilter affects transparent black. + paintContext.internalNodesCanvas + .clipRect(paintBounds, ui.ClipOp.intersect, false); + + paintContext.internalNodesCanvas.saveLayer(paintBounds, paint); + paintChildren(paintContext); + paintContext.internalNodesCanvas.restore(); + paintContext.internalNodesCanvas.restore(); } } @@ -252,7 +559,27 @@ class PlatformViewLayer extends Layer { final double height; @override - void accept(LayerVisitor visitor) { - visitor.visitPlatformView(this); + void preroll(PrerollContext prerollContext, Matrix4 matrix) { + paintBounds = ui.Rect.fromLTWH(offset.dx, offset.dy, width, height); + + /// ViewEmbedder is set to null when screenshotting. Therefore, skip + /// rendering + prerollContext.viewEmbedder?.prerollCompositeEmbeddedView( + viewId, + EmbeddedViewParams( + offset, + ui.Size(width, height), + prerollContext.mutatorsStack, + ), + ); + } + + @override + void paint(PaintContext paintContext) { + final CkCanvas? canvas = + paintContext.viewEmbedder?.compositeEmbeddedView(viewId); + if (canvas != null) { + paintContext.leafNodesCanvas = canvas; + } } } diff --git a/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart b/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart index 0a6d57ae467c8..2aca00f35dd3c 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart @@ -6,10 +6,10 @@ import 'package:ui/ui.dart' as ui; import '../../engine.dart' show kProfileApplyFrame, kProfilePrerollFrame; import '../profiler.dart'; +import '../vector_math.dart'; import 'canvas.dart'; import 'embedded_views.dart'; import 'layer.dart'; -import 'layer_visitor.dart'; import 'n_way_canvas.dart'; import 'picture_recorder.dart'; import 'raster_cache.dart'; @@ -31,25 +31,11 @@ class LayerTree { /// to raster. If [ignoreRasterCache] is `true`, then there will be no /// attempt to register pictures to cache. void preroll(Frame frame, {bool ignoreRasterCache = false}) { - final PrerollVisitor prerollVisitor = PrerollVisitor(frame.viewEmbedder); - rootLayer.accept(prerollVisitor); - } - - /// Performs a paint pass with a recording canvas for each picture in the - /// tree. This paint pass is just used to measure the bounds for each picture - /// so we can optimize the total number of canvases required. - void measure(Frame frame, {bool ignoreRasterCache = false}) { - final CkNWayCanvas nWayCanvas = CkNWayCanvas(); - final Iterable recordingCanvases = - frame.viewEmbedder!.getPictureCanvases(); - recordingCanvases.forEach(nWayCanvas.addCanvas); - final MeasureVisitor measureVisitor = MeasureVisitor( - nWayCanvas, - frame.viewEmbedder!, + final PrerollContext context = PrerollContext( + ignoreRasterCache ? null : frame.rasterCache, + frame.viewEmbedder, ); - if (rootLayer.needsPainting) { - rootLayer.accept(measureVisitor); - } + rootLayer.preroll(context, Matrix4.identity()); } /// Paints the layer tree into the given [frame]. @@ -58,15 +44,18 @@ class LayerTree { /// not be used. void paint(Frame frame, {bool ignoreRasterCache = false}) { final CkNWayCanvas internalNodesCanvas = CkNWayCanvas(); + internalNodesCanvas.addCanvas(frame.canvas); final Iterable overlayCanvases = - frame.viewEmbedder!.getOptimizedCanvases(); + frame.viewEmbedder!.getOverlayCanvases(); overlayCanvases.forEach(internalNodesCanvas.addCanvas); - final PaintVisitor paintVisitor = PaintVisitor( + final PaintContext context = PaintContext( internalNodesCanvas, - frame.viewEmbedder!, + frame.canvas, + ignoreRasterCache ? null : frame.rasterCache, + frame.viewEmbedder, ); if (rootLayer.needsPainting) { - rootLayer.accept(paintVisitor); + rootLayer.paint(context); } } @@ -76,15 +65,15 @@ class LayerTree { ui.Picture flatten(ui.Size size) { final CkPictureRecorder recorder = CkPictureRecorder(); final CkCanvas canvas = recorder.beginRecording(ui.Offset.zero & size); - final PrerollVisitor prerollVisitor = PrerollVisitor(null); - rootLayer.accept(prerollVisitor); + final PrerollContext prerollContext = PrerollContext(null, null); + rootLayer.preroll(prerollContext, Matrix4.identity()); final CkNWayCanvas internalNodesCanvas = CkNWayCanvas(); internalNodesCanvas.addCanvas(canvas); - final PaintVisitor paintVisitor = - PaintVisitor.forToImage(internalNodesCanvas, canvas); + final PaintContext paintContext = + PaintContext(internalNodesCanvas, canvas, null, null); if (rootLayer.needsPainting) { - rootLayer.accept(paintVisitor); + rootLayer.paint(paintContext); } return recorder.endRecording(); } @@ -92,7 +81,10 @@ class LayerTree { /// A single frame to be rendered. class Frame { - Frame(this.rasterCache, this.viewEmbedder); + Frame(this.canvas, this.rasterCache, this.viewEmbedder); + + /// The canvas to render this frame to. + final CkCanvas canvas; /// A cache of pre-rastered pictures. final RasterCache? rasterCache; @@ -104,8 +96,6 @@ class Frame { bool raster(LayerTree layerTree, {bool ignoreRasterCache = false}) { timeAction(kProfilePrerollFrame, () { layerTree.preroll(this, ignoreRasterCache: ignoreRasterCache); - layerTree.measure(this, ignoreRasterCache: ignoreRasterCache); - viewEmbedder?.optimizeRendering(); }); timeAction(kProfileApplyFrame, () { layerTree.paint(this, ignoreRasterCache: ignoreRasterCache); @@ -120,7 +110,7 @@ class CompositorContext { RasterCache? rasterCache; /// Acquire a frame using this compositor's settings. - Frame acquireFrame(HtmlViewEmbedder? viewEmbedder) { - return Frame(rasterCache, viewEmbedder); + Frame acquireFrame(CkCanvas canvas, HtmlViewEmbedder? viewEmbedder) { + return Frame(canvas, rasterCache, viewEmbedder); } } diff --git a/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart b/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart deleted file mode 100644 index 8c7631a7f1051..0000000000000 --- a/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart +++ /dev/null @@ -1,674 +0,0 @@ -// 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 'package:ui/ui.dart' as ui; - -import '../color_filter.dart'; -import '../vector_math.dart'; -import 'canvas.dart'; -import 'canvaskit_api.dart'; -import 'color_filter.dart'; -import 'embedded_views.dart'; -import 'image_filter.dart'; -import 'layer.dart'; -import 'n_way_canvas.dart'; -import 'painting.dart'; - -abstract class LayerVisitor { - void visitRoot(RootLayer root); - void visitBackdropFilter(BackdropFilterEngineLayer backdropFilter); - void visitClipPath(ClipPathEngineLayer clipPath); - void visitClipRect(ClipRectEngineLayer clipRect); - void visitClipRRect(ClipRRectEngineLayer clipRRect); - void visitOpacity(OpacityEngineLayer opacity); - void visitTransform(TransformEngineLayer transform); - void visitOffset(OffsetEngineLayer offset); - void visitImageFilter(ImageFilterEngineLayer imageFilter); - void visitShaderMask(ShaderMaskEngineLayer shaderMask); - void visitPicture(PictureLayer picture); - void visitColorFilter(ColorFilterEngineLayer colorFilter); - void visitPlatformView(PlatformViewLayer platformView); -} - -/// Pre-process the layer tree before painting. -/// -/// In this step, we compute the estimated [paintBounds] as well as -/// apply heuristics to prepare the render cache for pictures that -/// should be cached. -class PrerollVisitor extends LayerVisitor { - PrerollVisitor(this.viewEmbedder); - - final MutatorsStack mutatorsStack = MutatorsStack(); - - /// A compositor for embedded HTML views. - final HtmlViewEmbedder? viewEmbedder; - - ui.Rect get cullRect { - ui.Rect cullRect = ui.Rect.largest; - for (final Mutator m in mutatorsStack) { - ui.Rect clipRect; - switch (m.type) { - case MutatorType.clipRect: - clipRect = m.rect!; - case MutatorType.clipRRect: - clipRect = m.rrect!.outerRect; - case MutatorType.clipPath: - clipRect = m.path!.getBounds(); - default: - continue; - } - cullRect = cullRect.intersect(clipRect); - } - return cullRect; - } - - /// Run [preroll] on all of the child layers. - /// - /// Returns a [Rect] that covers the paint bounds of all of the child layers. - /// If all of the child layers have empty paint bounds, then the returned - /// [Rect] is empty. - ui.Rect prerollChildren(ContainerLayer layer) { - ui.Rect childPaintBounds = ui.Rect.zero; - for (final Layer layer in layer.children) { - layer.accept(this); - if (childPaintBounds.isEmpty) { - childPaintBounds = layer.paintBounds; - } else if (!layer.paintBounds.isEmpty) { - childPaintBounds = childPaintBounds.expandToInclude(layer.paintBounds); - } - } - return childPaintBounds; - } - - void prerollContainerLayer(ContainerLayer container) { - container.paintBounds = prerollChildren(container); - } - - @override - void visitRoot(RootLayer root) { - prerollContainerLayer(root); - } - - @override - void visitBackdropFilter(BackdropFilterEngineLayer backdropFilter) { - final ui.Rect childBounds = prerollChildren(backdropFilter); - backdropFilter.paintBounds = childBounds.expandToInclude(cullRect); - } - - @override - void visitClipPath(ClipPathEngineLayer clipPath) { - mutatorsStack.pushClipPath(clipPath.clipPath); - final ui.Rect childPaintBounds = prerollChildren(clipPath); - final ui.Rect clipBounds = clipPath.clipPath.getBounds(); - if (childPaintBounds.overlaps(clipBounds)) { - clipPath.paintBounds = childPaintBounds.intersect(clipBounds); - } - mutatorsStack.pop(); - } - - @override - void visitClipRRect(ClipRRectEngineLayer clipRRect) { - mutatorsStack.pushClipRRect(clipRRect.clipRRect); - final ui.Rect childPaintBounds = prerollChildren(clipRRect); - if (childPaintBounds.overlaps(clipRRect.clipRRect.outerRect)) { - clipRRect.paintBounds = - childPaintBounds.intersect(clipRRect.clipRRect.outerRect); - } - mutatorsStack.pop(); - } - - @override - void visitClipRect(ClipRectEngineLayer clipRect) { - mutatorsStack.pushClipRect(clipRect.clipRect); - final ui.Rect childPaintBounds = prerollChildren(clipRect); - if (childPaintBounds.overlaps(clipRect.clipRect)) { - clipRect.paintBounds = childPaintBounds.intersect(clipRect.clipRect); - } - mutatorsStack.pop(); - } - - @override - void visitColorFilter(ColorFilterEngineLayer colorFilter) { - prerollContainerLayer(colorFilter); - } - - @override - void visitImageFilter(ImageFilterEngineLayer imageFilter) { - mutatorsStack.pushTransform(Matrix4.translationValues( - imageFilter.offset.dx, imageFilter.offset.dy, 0.0)); - final CkManagedSkImageFilterConvertible convertible; - if (imageFilter.filter is ui.ColorFilter) { - convertible = - createCkColorFilter(imageFilter.filter as EngineColorFilter)!; - } else { - convertible = imageFilter.filter as CkManagedSkImageFilterConvertible; - } - ui.Rect childPaintBounds = prerollChildren(imageFilter); - childPaintBounds = childPaintBounds.translate( - imageFilter.offset.dx, imageFilter.offset.dy); - if (imageFilter.filter is ui.ColorFilter) { - // If the filter is a ColorFilter, the extended paint bounds will be the - // entire screen, which is not what we want. - imageFilter.paintBounds = childPaintBounds; - } else { - convertible.withSkImageFilter((SkImageFilter skFilter) { - imageFilter.paintBounds = rectFromSkIRect( - skFilter.getOutputBounds(toSkRect(childPaintBounds)), - ); - }); - } - mutatorsStack.pop(); - } - - @override - void visitOffset(OffsetEngineLayer offset) { - visitTransform(offset); - } - - @override - void visitOpacity(OpacityEngineLayer opacity) { - mutatorsStack.pushTransform( - Matrix4.translationValues(opacity.offset.dx, opacity.offset.dy, 0.0)); - mutatorsStack.pushOpacity(opacity.alpha); - prerollContainerLayer(opacity); - mutatorsStack.pop(); - mutatorsStack.pop(); - opacity.paintBounds = - opacity.paintBounds.translate(opacity.offset.dx, opacity.offset.dy); - } - - @override - void visitPicture(PictureLayer picture) { - picture.paintBounds = picture.picture.cullRect.shift(picture.offset); - // The picture may have been culled on a previous frame, but has since - // scrolled back into the clip region. Reset the `isCulled` flag. - picture.isCulled = false; - viewEmbedder?.prerollPicture(picture); - } - - @override - void visitPlatformView(PlatformViewLayer platformView) { - platformView.paintBounds = ui.Rect.fromLTWH( - platformView.offset.dx, - platformView.offset.dy, - platformView.width, - platformView.height, - ); - - /// ViewEmbedder is set to null when screenshotting. Therefore, skip - /// rendering - viewEmbedder?.prerollCompositeEmbeddedView( - platformView.viewId, - EmbeddedViewParams( - platformView.offset, - ui.Size(platformView.width, platformView.height), - mutatorsStack, - ), - ); - } - - @override - void visitShaderMask(ShaderMaskEngineLayer shaderMask) { - shaderMask.paintBounds = prerollChildren(shaderMask); - } - - @override - void visitTransform(TransformEngineLayer transform) { - mutatorsStack.pushTransform(transform.transform); - final ui.Rect childPaintBounds = prerollChildren(transform); - transform.paintBounds = transform.transform.transformRect(childPaintBounds); - mutatorsStack.pop(); - } -} - -/// A layer visitor which measures the pictures that make up the scene and -/// prepares for them to be optimized into few canvases. -class MeasureVisitor extends LayerVisitor { - MeasureVisitor( - this.nWayCanvas, - this.viewEmbedder, - ); - - /// A multi-canvas that applies clips, transforms, and opacity - /// operations to all canvases (root canvas and overlay canvases for the - /// platform views). - CkNWayCanvas nWayCanvas; - - /// A compositor for embedded HTML views. - final HtmlViewEmbedder viewEmbedder; - - /// Measures all child layers that need painting. - void measureChildren(ContainerLayer container) { - assert(container.needsPainting); - - for (final Layer layer in container.children) { - if (layer.needsPainting) { - layer.accept(this); - } - } - } - - @override - void visitRoot(RootLayer root) { - measureChildren(root); - } - - @override - void visitBackdropFilter(BackdropFilterEngineLayer backdropFilter) { - measureChildren(backdropFilter); - } - - @override - void visitClipPath(ClipPathEngineLayer clipPath) { - assert(clipPath.needsPainting); - - nWayCanvas.save(); - nWayCanvas.clipPath( - clipPath.clipPath, clipPath.clipBehavior != ui.Clip.hardEdge); - - if (clipPath.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - nWayCanvas.saveLayer(clipPath.paintBounds, null); - } - measureChildren(clipPath); - if (clipPath.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - nWayCanvas.restore(); - } - nWayCanvas.restore(); - } - - @override - void visitClipRect(ClipRectEngineLayer clipRect) { - assert(clipRect.needsPainting); - - nWayCanvas.save(); - nWayCanvas.clipRect( - clipRect.clipRect, - ui.ClipOp.intersect, - clipRect.clipBehavior != ui.Clip.hardEdge, - ); - if (clipRect.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - nWayCanvas.saveLayer(clipRect.clipRect, null); - } - measureChildren(clipRect); - if (clipRect.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - nWayCanvas.restore(); - } - nWayCanvas.restore(); - } - - @override - void visitClipRRect(ClipRRectEngineLayer clipRRect) { - assert(clipRRect.needsPainting); - - nWayCanvas.save(); - nWayCanvas.clipRRect( - clipRRect.clipRRect, clipRRect.clipBehavior != ui.Clip.hardEdge); - if (clipRRect.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - nWayCanvas.saveLayer(clipRRect.paintBounds, null); - } - measureChildren(clipRRect); - if (clipRRect.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - nWayCanvas.restore(); - } - nWayCanvas.restore(); - } - - @override - void visitOpacity(OpacityEngineLayer opacity) { - assert(opacity.needsPainting); - - final CkPaint paint = CkPaint(); - paint.color = ui.Color.fromARGB(opacity.alpha, 0, 0, 0); - - nWayCanvas.save(); - nWayCanvas.translate(opacity.offset.dx, opacity.offset.dy); - - final ui.Rect saveLayerBounds = opacity.paintBounds.shift(-opacity.offset); - - nWayCanvas.saveLayer(saveLayerBounds, paint); - measureChildren(opacity); - // Restore twice: once for the translate and once for the saveLayer. - nWayCanvas.restore(); - nWayCanvas.restore(); - } - - @override - void visitTransform(TransformEngineLayer transform) { - assert(transform.needsPainting); - - nWayCanvas.save(); - nWayCanvas.transform(transform.transform.storage); - measureChildren(transform); - nWayCanvas.restore(); - } - - @override - void visitOffset(OffsetEngineLayer offset) { - visitTransform(offset); - } - - @override - void visitImageFilter(ImageFilterEngineLayer imageFilter) { - assert(imageFilter.needsPainting); - final ui.Rect offsetPaintBounds = - imageFilter.paintBounds.shift(-imageFilter.offset); - nWayCanvas.save(); - nWayCanvas.translate(imageFilter.offset.dx, imageFilter.offset.dy); - nWayCanvas.clipRect(offsetPaintBounds, ui.ClipOp.intersect, false); - final CkPaint paint = CkPaint(); - paint.imageFilter = imageFilter.filter; - nWayCanvas.saveLayer(offsetPaintBounds, paint); - measureChildren(imageFilter); - nWayCanvas.restore(); - nWayCanvas.restore(); - } - - @override - void visitShaderMask(ShaderMaskEngineLayer shaderMask) { - assert(shaderMask.needsPainting); - - nWayCanvas.saveLayer(shaderMask.paintBounds, null); - measureChildren(shaderMask); - - nWayCanvas.restore(); - } - - @override - void visitPicture(PictureLayer picture) { - assert(picture.needsPainting); - - final CkCanvas pictureRecorderCanvas = - viewEmbedder.getMeasuringCanvasFor(picture); - - pictureRecorderCanvas.save(); - pictureRecorderCanvas.translate(picture.offset.dx, picture.offset.dy); - - pictureRecorderCanvas.drawPicture(picture.picture); - pictureRecorderCanvas.restore(); - - viewEmbedder.addPictureToUnoptimizedScene(picture); - } - - @override - void visitColorFilter(ColorFilterEngineLayer colorFilter) { - assert(colorFilter.needsPainting); - - final CkPaint paint = CkPaint(); - paint.colorFilter = colorFilter.filter; - - // We need to clip because if the ColorFilter affects transparent black, - // then it will fill the entire `cullRect` of the picture, ignoring the - // `paintBounds` passed to `saveLayer`. See: - // https://github.com/flutter/flutter/issues/88866 - nWayCanvas.save(); - - // TODO(hterkelsen): Only clip if the ColorFilter affects transparent black. - nWayCanvas.clipRect(colorFilter.paintBounds, ui.ClipOp.intersect, false); - - nWayCanvas.saveLayer(colorFilter.paintBounds, paint); - measureChildren(colorFilter); - nWayCanvas.restore(); - nWayCanvas.restore(); - } - - @override - void visitPlatformView(PlatformViewLayer platformView) { - // TODO(harryterkelsen): Warn if we are a child of a backdrop filter or - // shader mask. - viewEmbedder.compositeEmbeddedView(platformView.viewId); - } -} - -/// A layer visitor which paints the layer tree into one or more canvases. -/// -/// The canvases are the optimized canvases that were created when the view -/// embedder optimized the canvases after the measure step. -class PaintVisitor extends LayerVisitor { - PaintVisitor( - this.nWayCanvas, - HtmlViewEmbedder this.viewEmbedder, - ) : toImageCanvas = null; - - PaintVisitor.forToImage( - this.nWayCanvas, - this.toImageCanvas, - ) : viewEmbedder = null; - - /// A multi-canvas that applies clips, transforms, and opacity - /// operations to all canvases (root canvas and overlay canvases for the - /// platform views). - CkNWayCanvas nWayCanvas; - - /// A compositor for embedded HTML views. - final HtmlViewEmbedder? viewEmbedder; - - final List shaderMaskStack = []; - - final Map> picturesUnderShaderMask = - >{}; - - final CkCanvas? toImageCanvas; - - /// Calls [paint] on all child layers that need painting. - void paintChildren(ContainerLayer container) { - assert(container.needsPainting); - - for (final Layer layer in container.children) { - if (layer.needsPainting) { - layer.accept(this); - } - } - } - - @override - void visitRoot(RootLayer root) { - paintChildren(root); - } - - @override - void visitBackdropFilter(BackdropFilterEngineLayer backdropFilter) { - final CkPaint paint = CkPaint()..blendMode = backdropFilter.blendMode; - - nWayCanvas.saveLayerWithFilter( - backdropFilter.paintBounds, backdropFilter.filter, paint); - paintChildren(backdropFilter); - nWayCanvas.restore(); - } - - @override - void visitClipPath(ClipPathEngineLayer clipPath) { - assert(clipPath.needsPainting); - - nWayCanvas.save(); - nWayCanvas.clipPath( - clipPath.clipPath, clipPath.clipBehavior != ui.Clip.hardEdge); - - if (clipPath.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - nWayCanvas.saveLayer(clipPath.paintBounds, null); - } - paintChildren(clipPath); - if (clipPath.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - nWayCanvas.restore(); - } - nWayCanvas.restore(); - } - - @override - void visitClipRect(ClipRectEngineLayer clipRect) { - assert(clipRect.needsPainting); - - nWayCanvas.save(); - nWayCanvas.clipRect( - clipRect.clipRect, - ui.ClipOp.intersect, - clipRect.clipBehavior != ui.Clip.hardEdge, - ); - if (clipRect.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - nWayCanvas.saveLayer(clipRect.clipRect, null); - } - paintChildren(clipRect); - if (clipRect.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - nWayCanvas.restore(); - } - nWayCanvas.restore(); - } - - @override - void visitClipRRect(ClipRRectEngineLayer clipRRect) { - assert(clipRRect.needsPainting); - - nWayCanvas.save(); - nWayCanvas.clipRRect( - clipRRect.clipRRect, clipRRect.clipBehavior != ui.Clip.hardEdge); - if (clipRRect.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - nWayCanvas.saveLayer(clipRRect.paintBounds, null); - } - paintChildren(clipRRect); - if (clipRRect.clipBehavior == ui.Clip.antiAliasWithSaveLayer) { - nWayCanvas.restore(); - } - nWayCanvas.restore(); - } - - @override - void visitOpacity(OpacityEngineLayer opacity) { - assert(opacity.needsPainting); - - final CkPaint paint = CkPaint(); - paint.color = ui.Color.fromARGB(opacity.alpha, 0, 0, 0); - - nWayCanvas.save(); - nWayCanvas.translate(opacity.offset.dx, opacity.offset.dy); - - final ui.Rect saveLayerBounds = opacity.paintBounds.shift(-opacity.offset); - - nWayCanvas.saveLayer(saveLayerBounds, paint); - paintChildren(opacity); - // Restore twice: once for the translate and once for the saveLayer. - nWayCanvas.restore(); - nWayCanvas.restore(); - } - - @override - void visitTransform(TransformEngineLayer transform) { - assert(transform.needsPainting); - - nWayCanvas.save(); - nWayCanvas.transform(transform.transform.storage); - paintChildren(transform); - nWayCanvas.restore(); - } - - @override - void visitOffset(OffsetEngineLayer offset) { - visitTransform(offset); - } - - @override - void visitImageFilter(ImageFilterEngineLayer imageFilter) { - assert(imageFilter.needsPainting); - final ui.Rect offsetPaintBounds = - imageFilter.paintBounds.shift(-imageFilter.offset); - nWayCanvas.save(); - nWayCanvas.translate(imageFilter.offset.dx, imageFilter.offset.dy); - nWayCanvas.clipRect(offsetPaintBounds, ui.ClipOp.intersect, false); - final CkPaint paint = CkPaint(); - paint.imageFilter = imageFilter.filter; - nWayCanvas.saveLayer(offsetPaintBounds, paint); - paintChildren(imageFilter); - nWayCanvas.restore(); - nWayCanvas.restore(); - } - - @override - void visitShaderMask(ShaderMaskEngineLayer shaderMask) { - assert(shaderMask.needsPainting); - - shaderMaskStack.add(shaderMask); - nWayCanvas.saveLayer(shaderMask.paintBounds, null); - paintChildren(shaderMask); - - final CkPaint paint = CkPaint(); - paint.shader = shaderMask.shader; - paint.blendMode = shaderMask.blendMode; - paint.filterQuality = shaderMask.filterQuality; - - late List canvasesToApplyShaderMask; - if (viewEmbedder != null) { - final Set canvases = {}; - for (final PictureLayer picture in picturesUnderShaderMask[shaderMask]!) { - canvases.add(viewEmbedder!.getOptimizedCanvasFor(picture)); - } - canvasesToApplyShaderMask = canvases.toList(); - } else { - canvasesToApplyShaderMask = [toImageCanvas!]; - } - - for (final CkCanvas canvas in canvasesToApplyShaderMask) { - canvas.save(); - canvas.translate(shaderMask.maskRect.left, shaderMask.maskRect.top); - - canvas.drawRect( - ui.Rect.fromLTWH( - 0, 0, shaderMask.maskRect.width, shaderMask.maskRect.height), - paint); - canvas.restore(); - } - nWayCanvas.restore(); - shaderMaskStack.removeLast(); - } - - @override - void visitPicture(PictureLayer picture) { - assert(picture.needsPainting); - - // For each shader mask this picture is a child of, record that it needs - // to have the shader mask applied to it. - for (final ShaderMaskEngineLayer shaderMask in shaderMaskStack) { - picturesUnderShaderMask.putIfAbsent(shaderMask, () => []); - picturesUnderShaderMask[shaderMask]!.add(picture); - } - - late CkCanvas pictureRecorderCanvas; - if (viewEmbedder != null) { - pictureRecorderCanvas = viewEmbedder!.getOptimizedCanvasFor(picture); - } else { - pictureRecorderCanvas = toImageCanvas!; - } - - pictureRecorderCanvas.save(); - pictureRecorderCanvas.translate(picture.offset.dx, picture.offset.dy); - - pictureRecorderCanvas.drawPicture(picture.picture); - pictureRecorderCanvas.restore(); - } - - @override - void visitColorFilter(ColorFilterEngineLayer colorFilter) { - assert(colorFilter.needsPainting); - - final CkPaint paint = CkPaint(); - paint.colorFilter = colorFilter.filter; - - // We need to clip because if the ColorFilter affects transparent black, - // then it will fill the entire `cullRect` of the picture, ignoring the - // `paintBounds` passed to `saveLayer`. See: - // https://github.com/flutter/flutter/issues/88866 - nWayCanvas.save(); - - // TODO(hterkelsen): Only clip if the ColorFilter affects transparent black. - nWayCanvas.clipRect(colorFilter.paintBounds, ui.ClipOp.intersect, false); - - nWayCanvas.saveLayer(colorFilter.paintBounds, paint); - paintChildren(colorFilter); - nWayCanvas.restore(); - nWayCanvas.restore(); - } - - @override - void visitPlatformView(PlatformViewLayer platformView) { - // Do nothing. The platform view was already measured and placed in the - // optimized rendering in the measure step. - } -} diff --git a/lib/web_ui/lib/src/engine/canvaskit/overlay_scene_optimizer.dart b/lib/web_ui/lib/src/engine/canvaskit/overlay_scene_optimizer.dart index 2d70f2329e1e2..b98f969b1fbe3 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/overlay_scene_optimizer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/overlay_scene_optimizer.dart @@ -156,104 +156,108 @@ ui.Rect computePlatformViewBounds(EmbeddedViewParams params) { /// [platformViews]. /// /// [paramsForViews] is required to compute the bounds of the platform views. +// TODO(harryterkelsen): Extend this to work for any sequence of platform views +// and pictures, https://github.com/flutter/flutter/issues/149863. Rendering createOptimizedRendering( - Iterable renderObjects, + List pictures, + List platformViews, Map paramsForViews, ) { final Map cachedComputedRects = {}; + assert(pictures.length == platformViews.length + 1); final Rendering result = Rendering(); // The first picture is added to the rendering in a new render canvas. RenderingRenderCanvas tentativeCanvas = RenderingRenderCanvas(); + if (!pictures[0].cullRect.isEmpty) { + tentativeCanvas.add(pictures[0]); + } - for (final SceneElement renderObject in renderObjects) { - if (renderObject is PlatformViewSceneElement) { - final int viewId = renderObject.viewId; - final RenderingPlatformView platformView = RenderingPlatformView(viewId); - if (PlatformViewManager.instance.isVisible(viewId)) { - final ui.Rect platformViewBounds = cachedComputedRects[viewId] = - computePlatformViewBounds(paramsForViews[viewId]!); - - if (debugOverlayOptimizationBounds) { - platformView.debugComputedBounds = platformViewBounds; - } + for (int i = 0; i < platformViews.length; i++) { + final RenderingPlatformView platformView = + RenderingPlatformView(platformViews[i]); + if (PlatformViewManager.instance.isVisible(platformViews[i])) { + final ui.Rect platformViewBounds = cachedComputedRects[platformViews[i]] = + computePlatformViewBounds(paramsForViews[platformViews[i]]!); - // If the platform view intersects with any pictures in the tentative canvas - // then add the tentative canvas to the rendering. - for (final CkPicture picture in tentativeCanvas.pictures) { - if (!picture.cullRect.intersect(platformViewBounds).isEmpty) { - result.add(tentativeCanvas); - tentativeCanvas = RenderingRenderCanvas(); - break; - } - } + if (debugOverlayOptimizationBounds) { + platformView.debugComputedBounds = platformViewBounds; } - result.add(platformView); - } else if (renderObject is PictureSceneElement) { - final CkPicture scenePicture = renderObject.scenePicture!; - if (scenePicture.cullRect.isEmpty) { - continue; - } - - // Find the first render canvas which comes after the last entity (picture - // or platform view) that the next picture intersects with, and add the - // picture to that render canvas, or create a new render canvas. - // First check if the picture intersects with any pictures in the - // tentative canvas, as this will be the last canvas in the rendering - // when it is eventually added. - bool addedToTentativeCanvas = false; + // If the platform view intersects with any pictures in the tentative canvas + // then add the tentative canvas to the rendering. for (final CkPicture picture in tentativeCanvas.pictures) { - if (!picture.cullRect.intersect(scenePicture.cullRect).isEmpty) { - tentativeCanvas.add(scenePicture); - addedToTentativeCanvas = true; + if (!picture.cullRect.intersect(platformViewBounds).isEmpty) { + result.add(tentativeCanvas); + tentativeCanvas = RenderingRenderCanvas(); break; } } - if (addedToTentativeCanvas) { - continue; + } + result.add(platformView); + + if (pictures[i + 1].cullRect.isEmpty) { + continue; + } + + // Find the first render canvas which comes after the last entity (picture + // or platform view) that the next picture intersects with, and add the + // picture to that render canvas, or create a new render canvas. + + // First check if the picture intersects with any pictures in the tentative + // canvas, as this will be the last canvas in the rendering when it is + // eventually added. + bool addedToTentativeCanvas = false; + for (final CkPicture picture in tentativeCanvas.pictures) { + if (!picture.cullRect.intersect(pictures[i + 1].cullRect).isEmpty) { + tentativeCanvas.add(pictures[i + 1]); + addedToTentativeCanvas = true; + break; } + } + if (addedToTentativeCanvas) { + continue; + } - RenderingRenderCanvas? lastCanvasSeen; - bool addedPictureToRendering = false; - for (final RenderingEntity entity in result.entities.reversed) { - if (entity is RenderingPlatformView) { - if (PlatformViewManager.instance.isVisible(entity.viewId)) { - final ui.Rect platformViewBounds = - cachedComputedRects[entity.viewId]!; - if (!platformViewBounds.intersect(scenePicture.cullRect).isEmpty) { - // The next picture intersects with a platform view already in the - // result. Add this picture to the first render canvas which comes - // after this platform view or create one if none exists. - if (lastCanvasSeen != null) { - lastCanvasSeen.add(scenePicture); - } else { - tentativeCanvas.add(scenePicture); - } - addedPictureToRendering = true; - break; + RenderingRenderCanvas? lastCanvasSeen; + bool addedPictureToRendering = false; + for (final RenderingEntity entity in result.entities.reversed) { + if (entity is RenderingPlatformView) { + if (PlatformViewManager.instance.isVisible(entity.viewId)) { + final ui.Rect platformViewBounds = + cachedComputedRects[entity.viewId]!; + if (!platformViewBounds.intersect(pictures[i + 1].cullRect).isEmpty) { + // The next picture intersects with a platform view already in the + // result. Add this picture to the first render canvas which comes + // after this platform view or create one if none exists. + if (lastCanvasSeen != null) { + lastCanvasSeen.add(pictures[i + 1]); + } else { + tentativeCanvas.add(pictures[i + 1]); } + addedPictureToRendering = true; + break; } - } else if (entity is RenderingRenderCanvas) { - lastCanvasSeen = entity; - // Check if we intersect with any pictures in this render canvas. - for (final CkPicture picture in entity.pictures) { - if (!picture.cullRect.intersect(scenePicture.cullRect).isEmpty) { - lastCanvasSeen.add(scenePicture); - addedPictureToRendering = true; - break; - } + } + } else if (entity is RenderingRenderCanvas) { + lastCanvasSeen = entity; + // Check if we intersect with any pictures in this render canvas. + for (final CkPicture picture in entity.pictures) { + if (!picture.cullRect.intersect(pictures[i + 1].cullRect).isEmpty) { + lastCanvasSeen.add(pictures[i + 1]); + addedPictureToRendering = true; + break; } } } - if (!addedPictureToRendering) { - if (lastCanvasSeen != null) { - // Add it to the last canvas seen in the rendering, if any. - lastCanvasSeen.add(scenePicture); - } else { - tentativeCanvas.add(scenePicture); - } + } + if (!addedPictureToRendering) { + if (lastCanvasSeen != null) { + // Add it to the last canvas seen in the rendering, if any. + lastCanvasSeen.add(pictures[i + 1]); + } else { + tentativeCanvas.add(pictures[i + 1]); } } } diff --git a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart index 2e38674cd95d7..640e09562ad7a 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart @@ -53,7 +53,7 @@ abstract class ViewRasterizer { // The [frameSize] may be slightly imprecise if the `devicePixelRatio` isn't // an integer. For example, is you zoom to 110% in Chrome on a Macbook, the // `devicePixelRatio` is `2.200000047683716`, so when the physical size is - // computed by multiplying the logical size by the device pixel ratio, the + // computed by multiplying the logical size by the devie pixel ratio, the // result is slightly imprecise as well. Nevertheless, the number should // be close to an integer, so round the frame size to be more precice. final BitmapSize bitmapSize = BitmapSize.fromSize(frameSize); @@ -61,11 +61,14 @@ abstract class ViewRasterizer { currentFrameSize = bitmapSize; prepareToDraw(); viewEmbedder.frameSize = currentFrameSize; - final Frame compositorFrame = context.acquireFrame(viewEmbedder); + final CkPictureRecorder pictureRecorder = CkPictureRecorder(); + pictureRecorder.beginRecording(ui.Offset.zero & currentFrameSize.toSize()); + final Frame compositorFrame = + context.acquireFrame(pictureRecorder.recordingCanvas!, viewEmbedder); compositorFrame.raster(layerTree, ignoreRasterCache: true); - await viewEmbedder.submitFrame(); + await viewEmbedder.submitFrame(pictureRecorder.endRecording()); } /// Do some initialization to prepare to draw a frame. diff --git a/lib/web_ui/test/canvaskit/embedded_views_test.dart b/lib/web_ui/test/canvaskit/embedded_views_test.dart index cf71addabe271..99b540884a892 100644 --- a/lib/web_ui/test/canvaskit/embedded_views_test.dart +++ b/lib/web_ui/test/canvaskit/embedded_views_test.dart @@ -1152,7 +1152,8 @@ void testMain() { // Scene 5: A combination of scene 1 and scene 4, where a subtitle is // painted over each platform view and a placeholder is painted under each - // one. + // one. Unfortunately, we need an overlay for each platform view in this + // case. final LayerSceneBuilder sb5 = LayerSceneBuilder(); sb5.pushOffset(0, 0); sb5.addPicture( @@ -1180,7 +1181,9 @@ void testMain() { _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, + _overlay, _platformView, + _overlay, _platformView, _overlay, ]); @@ -1311,6 +1314,7 @@ void testMain() { _expectSceneMatches(<_EmbeddedViewMarker>[ _overlay, _platformView, + _overlay, _platformView, _overlay, ]); diff --git a/lib/web_ui/test/ui/scene_builder_test.dart b/lib/web_ui/test/ui/scene_builder_test.dart index 8ebde3a3156e1..aeaef07ad0c5a 100644 --- a/lib/web_ui/test/ui/scene_builder_test.dart +++ b/lib/web_ui/test/ui/scene_builder_test.dart @@ -32,12 +32,14 @@ Future testMain() async { sceneBuilder.pushOffset(150, 150); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { canvas.drawCircle( - ui.Offset.zero, 50, ui.Paint()..color = const ui.Color(0xFF00FF00)); + ui.Offset.zero, + 50, + ui.Paint()..color = const ui.Color(0xFF00FF00) + ); })); await renderScene(sceneBuilder.build()); - await matchGoldenFile('scene_builder_centered_circle.png', - region: region); + await matchGoldenFile('scene_builder_centered_circle.png', region: region); }); test('Test transform layer', () async { @@ -52,83 +54,93 @@ Future testMain() async { sceneBuilder.pushTransform(transform.toFloat64()); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { canvas.drawRRect( - ui.RRect.fromRectAndRadius( - ui.Rect.fromCircle(center: ui.Offset.zero, radius: 50), - const ui.Radius.circular(10)), - ui.Paint()..color = const ui.Color(0xFF0000FF)); + ui.RRect.fromRectAndRadius( + ui.Rect.fromCircle(center: ui.Offset.zero, radius: 50), + const ui.Radius.circular(10) + ), + ui.Paint()..color = const ui.Color(0xFF0000FF) + ); })); await renderScene(sceneBuilder.build()); - await matchGoldenFile('scene_builder_rotated_rounded_square.png', - region: region); + await matchGoldenFile('scene_builder_rotated_rounded_square.png', region: region); }); test('Test clipRect layer', () async { final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); sceneBuilder.pushClipRect(const ui.Rect.fromLTRB(0, 0, 150, 150)); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { - canvas.drawCircle(const ui.Offset(150, 150), 50, - ui.Paint()..color = const ui.Color(0xFFFF0000)); + canvas.drawCircle( + const ui.Offset(150, 150), + 50, + ui.Paint()..color = const ui.Color(0xFFFF0000) + ); })); await renderScene(sceneBuilder.build()); - await matchGoldenFile('scene_builder_circle_clip_rect.png', - region: region); + await matchGoldenFile('scene_builder_circle_clip_rect.png', region: region); }); test('Test clipRRect layer', () async { final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); - sceneBuilder.pushClipRRect( - ui.RRect.fromRectAndRadius( - const ui.Rect.fromLTRB(0, 0, 150, 150), - const ui.Radius.circular(25), - ), - clipBehavior: ui.Clip.antiAlias); + sceneBuilder.pushClipRRect(ui.RRect.fromRectAndRadius( + const ui.Rect.fromLTRB(0, 0, 150, 150), + const ui.Radius.circular(25), + ), clipBehavior: ui.Clip.antiAlias); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { - canvas.drawCircle(const ui.Offset(150, 150), 50, - ui.Paint()..color = const ui.Color(0xFFFF00FF)); + canvas.drawCircle( + const ui.Offset(150, 150), + 50, + ui.Paint()..color = const ui.Color(0xFFFF00FF) + ); })); await renderScene(sceneBuilder.build()); - await matchGoldenFile('scene_builder_circle_clip_rrect.png', - region: region); + await matchGoldenFile('scene_builder_circle_clip_rrect.png', region: region); }); test('Test clipPath layer', () async { final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); final ui.Path path = ui.Path(); - path.addOval( - ui.Rect.fromCircle(center: const ui.Offset(150, 150), radius: 60)); + path.addOval(ui.Rect.fromCircle(center: const ui.Offset(150, 150), radius: 60)); sceneBuilder.pushClipPath(path); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { canvas.drawRect( - ui.Rect.fromCircle(center: const ui.Offset(150, 150), radius: 50), - ui.Paint()..color = const ui.Color(0xFF00FFFF)); + ui.Rect.fromCircle(center: const ui.Offset(150, 150), radius: 50), + ui.Paint()..color = const ui.Color(0xFF00FFFF) + ); })); await renderScene(sceneBuilder.build()); - await matchGoldenFile('scene_builder_rectangle_clip_circular_path.png', - region: region); + await matchGoldenFile('scene_builder_rectangle_clip_circular_path.png', region: region); }); test('Test opacity layer', () async { final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { canvas.drawRect( - ui.Rect.fromCircle(center: const ui.Offset(150, 150), radius: 50), - ui.Paint()..color = const ui.Color(0xFF00FF00)); + ui.Rect.fromCircle(center: const ui.Offset(150, 150), radius: 50), + ui.Paint()..color = const ui.Color(0xFF00FF00) + ); })); sceneBuilder.pushOpacity(0x7F, offset: const ui.Offset(150, 150)); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { final ui.Paint paint = ui.Paint()..color = const ui.Color(0xFFFF0000); - canvas.drawCircle(const ui.Offset(-25, 0), 50, paint); - canvas.drawCircle(const ui.Offset(25, 0), 50, paint); + canvas.drawCircle( + const ui.Offset(-25, 0), + 50, + paint + ); + canvas.drawCircle( + const ui.Offset(25, 0), + 50, + paint + ); })); await renderScene(sceneBuilder.build()); - await matchGoldenFile('scene_builder_opacity_circles_on_square.png', - region: region); + await matchGoldenFile('scene_builder_opacity_circles_on_square.png', region: region); }); test('shader mask layer', () async { @@ -136,39 +148,48 @@ Future testMain() async { sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { final ui.Paint paint = ui.Paint()..color = const ui.Color(0xFFFF0000); - canvas.drawCircle(const ui.Offset(125, 150), 50, paint); - canvas.drawCircle(const ui.Offset(175, 150), 50, paint); + canvas.drawCircle( + const ui.Offset(125, 150), + 50, + paint + ); + canvas.drawCircle( + const ui.Offset(175, 150), + 50, + paint + ); })); final ui.Shader shader = ui.Gradient.linear( - ui.Offset.zero, const ui.Offset(50, 50), [ - const ui.Color(0xFFFFFFFF), - const ui.Color(0x00000000), - ]); - sceneBuilder.pushShaderMask(shader, - const ui.Rect.fromLTRB(125, 125, 175, 175), ui.BlendMode.srcATop); + ui.Offset.zero, + const ui.Offset(50, 50), [ + const ui.Color(0xFFFFFFFF), + const ui.Color(0x00000000), + ]); + sceneBuilder.pushShaderMask( + shader, + const ui.Rect.fromLTRB(125, 125, 175, 175), + ui.BlendMode.srcATop + ); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { canvas.drawRect( - ui.Rect.fromCircle(center: const ui.Offset(150, 150), radius: 50), - ui.Paint()..color = const ui.Color(0xFF00FF00)); + ui.Rect.fromCircle(center: const ui.Offset(150, 150), radius: 50), + ui.Paint()..color = const ui.Color(0xFF00FF00) + ); })); await renderScene(sceneBuilder.build()); await matchGoldenFile('scene_builder_shader_mask.png', region: region); - }, - skip: isFirefox && - isHtml); // https://github.com/flutter/flutter/issues/86623 + }, skip: isFirefox && isHtml); // https://github.com/flutter/flutter/issues/86623 test('backdrop filter layer', () async { final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { // Create a red and blue checkerboard pattern - final ui.Paint redPaint = ui.Paint() - ..color = const ui.Color(0xFFFF0000); - final ui.Paint bluePaint = ui.Paint() - ..color = const ui.Color(0xFF0000FF); + final ui.Paint redPaint = ui.Paint()..color = const ui.Color(0xFFFF0000); + final ui.Paint bluePaint = ui.Paint()..color = const ui.Color(0xFF0000FF); for (double y = 0; y < 300; y += 10) { for (double x = 0; x < 300; x += 10) { final ui.Paint paint = ((x + y) % 20 == 0) ? redPaint : bluePaint; @@ -183,13 +204,15 @@ Future testMain() async { )); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { - canvas.drawCircle(const ui.Offset(150, 150), 50, - ui.Paint()..color = const ui.Color(0xFF00FF00)); + canvas.drawCircle( + const ui.Offset(150, 150), + 50, + ui.Paint()..color = const ui.Color(0xFF00FF00) + ); })); await renderScene(sceneBuilder.build()); - await matchGoldenFile('scene_builder_backdrop_filter.png', - region: region); + await matchGoldenFile('scene_builder_backdrop_filter.png', region: region); }); test('empty backdrop filter layer with clip', () async { @@ -200,10 +223,8 @@ Future testMain() async { sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { // Create a red and blue checkerboard pattern - final ui.Paint redPaint = ui.Paint() - ..color = const ui.Color(0xFFFF0000); - final ui.Paint bluePaint = ui.Paint() - ..color = const ui.Color(0xFF0000FF); + final ui.Paint redPaint = ui.Paint()..color = const ui.Color(0xFFFF0000); + final ui.Paint bluePaint = ui.Paint()..color = const ui.Color(0xFF0000FF); for (double y = 0; y < 300; y += 10) { for (double x = 0; x < 300; x += 10) { final ui.Paint paint = ((x + y) % 20 == 0) ? redPaint : bluePaint; @@ -219,8 +240,7 @@ Future testMain() async { sigmaY: 3.0, )); await renderScene(sceneBuilder.build()); - await matchGoldenFile('scene_builder_empty_backdrop_filter_with_clip.png', - region: region); + await matchGoldenFile('scene_builder_empty_backdrop_filter_with_clip.png', region: region); }); test('image filter layer', () async { @@ -231,8 +251,11 @@ Future testMain() async { )); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { - canvas.drawCircle(const ui.Offset(150, 150), 50, - ui.Paint()..color = const ui.Color(0xFF00FF00)); + canvas.drawCircle( + const ui.Offset(150, 150), + 50, + ui.Paint()..color = const ui.Color(0xFF00FF00) + ); })); await renderScene(sceneBuilder.build()); @@ -267,111 +290,24 @@ Future testMain() async { test('color filter layer', () async { final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); const ui.ColorFilter sepia = ui.ColorFilter.matrix([ - 0.393, - 0.769, - 0.189, - 0, - 0, - 0.349, - 0.686, - 0.168, - 0, - 0, - 0.272, - 0.534, - 0.131, - 0, - 0, - 0, - 0, - 0, - 1, - 0, + 0.393, 0.769, 0.189, 0, 0, + 0.349, 0.686, 0.168, 0, 0, + 0.272, 0.534, 0.131, 0, 0, + 0, 0, 0, 1, 0, ]); sceneBuilder.pushColorFilter(sepia); sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { - canvas.drawCircle(const ui.Offset(150, 150), 50, - ui.Paint()..color = const ui.Color(0xFF00FF00)); + canvas.drawCircle( + const ui.Offset(150, 150), + 50, + ui.Paint()..color = const ui.Color(0xFF00FF00) + ); })); await renderScene(sceneBuilder.build()); await matchGoldenFile('scene_builder_color_filter.png', region: region); }); - - test('overlapping pictures in opacity layer', () async { - final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); - sceneBuilder.pushOpacity(128); - sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { - canvas.drawCircle(const ui.Offset(100, 150), 100, - ui.Paint()..color = const ui.Color(0xFFFF0000)); - })); - sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { - canvas.drawCircle(const ui.Offset(200, 150), 100, - ui.Paint()..color = const ui.Color(0xFFFF0000)); - })); - sceneBuilder.pop(); - - await renderScene(sceneBuilder.build()); - await matchGoldenFile('scene_builder_overlapping_pictures_in_opacity.png', - region: region); - }); - - test('picture clipped out in final scene', () async { - final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); - sceneBuilder.pushClipRect(const ui.Rect.fromLTRB(0, 0, 125, 300)); - sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { - canvas.drawCircle(const ui.Offset(50, 150), 50, - ui.Paint()..color = const ui.Color(0xFFFF0000)); - })); - sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { - canvas.drawCircle(const ui.Offset(200, 150), 50, - ui.Paint()..color = const ui.Color(0xFFFF0000)); - })); - sceneBuilder.pop(); - - await renderScene(sceneBuilder.build()); - await matchGoldenFile('scene_builder_picture_clipped_out.png', - region: region); - }); - - test('picture clipped but scrolls back in', () async { - // Frame 1: Clip out the right circle - final ui.SceneBuilder sceneBuilder = ui.SceneBuilder(); - sceneBuilder.pushClipRect(const ui.Rect.fromLTRB(0, 0, 125, 300)); - // Save this offsetLayer to add back in so we are using the same - // picture layers on the next scene. - final ui.OffsetEngineLayer offsetLayer = sceneBuilder.pushOffset(0, 0); - sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { - canvas.drawCircle(const ui.Offset(50, 150), 50, - ui.Paint()..color = const ui.Color(0xFFFF0000)); - })); - sceneBuilder.addPicture(ui.Offset.zero, drawPicture((ui.Canvas canvas) { - canvas.drawCircle(const ui.Offset(200, 150), 50, - ui.Paint()..color = const ui.Color(0xFFFF0000)); - })); - sceneBuilder.pop(); - sceneBuilder.pop(); - await renderScene(sceneBuilder.build()); - - // Frame 2: Clip out the left circle - final ui.SceneBuilder sceneBuilder2 = ui.SceneBuilder(); - sceneBuilder2.pushClipRect(const ui.Rect.fromLTRB(150, 0, 300, 300)); - sceneBuilder2.addRetained(offsetLayer); - sceneBuilder2.pop(); - await renderScene(sceneBuilder2.build()); - - // Frame 3: Clip out the right circle again - final ui.SceneBuilder sceneBuilder3 = ui.SceneBuilder(); - sceneBuilder3.pushClipRect(const ui.Rect.fromLTRB(0, 0, 125, 300)); - sceneBuilder3.addRetained(offsetLayer); - sceneBuilder3.pop(); - await renderScene(sceneBuilder3.build()); - - await matchGoldenFile( - 'scene_builder_picture_clipped_out_then_clipped_in.png', - region: region); - }); }); }