blob: 143ff865840a15ef4bc827e17ef9e8a50774623d [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:ui/ui.dart' as ui;
import '../vector_math.dart';
import 'canvas.dart';
import 'canvaskit_api.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.
///
/// A layer is the lowest-level rendering primitive. It represents an atomic
/// painting command.
abstract class Layer implements ui.EngineLayer {
/// The layer that contains us as a child.
ContainerLayer? parent;
/// An estimated rectangle that this layer will draw into.
ui.Rect paintBounds = ui.Rect.zero;
/// Whether or not this layer actually needs to be painted in the scene.
bool get needsPainting => !paintBounds.isEmpty;
/// 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
@override
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<Layer> _layers = <Layer>[];
/// The list of child layers.
///
/// Useful in tests.
List<Layer> get debugLayers => _layers;
/// Register [child] as a child of this layer.
void add(Layer child) {
child.parent = this;
_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);
}
}
}
}
/// The top-most layer in the layer tree.
///
/// This layer does not draw anything. It's only used so we can add leaf layers
/// to [LayerSceneBuilder] without requiring a [ContainerLayer].
class RootLayer extends ContainerLayer {
@override
void paint(PaintContext paintContext) {
paintChildren(paintContext);
}
}
class BackdropFilterEngineLayer extends ContainerLayer
implements ui.BackdropFilterEngineLayer {
BackdropFilterEngineLayer(this._filter, this._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 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);
paint.dispose();
paintChildren(paintContext);
currentCanvas.restore();
}
// TODO(dnfield): dispose of the _filter
// https://github.com/flutter/flutter/issues/82832
}
/// 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);
/// The path used to clip child layers.
final CkPath _clipPath;
final ui.Clip _clipBehavior;
@override
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);
/// The rectangle used to clip child layers.
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 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);
/// The rounded rectangle used to clip child layers.
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 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);
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 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);
paint.dispose();
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);
/// The matrix with which to transform the child layers.
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 paint(PaintContext paintContext) {
assert(needsPainting);
paintContext.internalNodesCanvas.save();
paintContext.internalNodesCanvas.transform(_transform.storage);
paintChildren(paintContext);
paintContext.internalNodesCanvas.restore();
}
}
/// Translates its children along x and y coordinates.
///
/// This is a thin wrapper over [TransformEngineLayer] just so the framework
/// gets the "OffsetEngineLayer" when calling `runtimeType.toString()`. This is
/// better for debugging.
class OffsetEngineLayer extends TransformEngineLayer
implements ui.OffsetEngineLayer {
OffsetEngineLayer(double dx, double dy)
: super(Matrix4.translationValues(dx, dy, 0.0));
}
/// A layer that applies an [ui.ImageFilter] to its children.
class ImageFilterEngineLayer extends ContainerLayer
implements ui.ImageFilterEngineLayer {
ImageFilterEngineLayer(this._filter, this._offset);
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 ui.Rect childPaintBounds =
prerollChildren(prerollContext, childMatrix);
(_filter as CkManagedSkImageFilterConvertible)
.imageFilter((SkImageFilter filter) {
paintBounds =
rectFromSkIRect(filter.getOutputBounds(toSkRect(childPaintBounds)));
});
prerollContext.mutatorsStack.pop();
}
@override
void paint(PaintContext paintContext) {
assert(needsPainting);
paintContext.internalNodesCanvas.save();
paintContext.internalNodesCanvas.translate(_offset.dx, _offset.dy);
final CkPaint paint = CkPaint();
paint.imageFilter = _filter;
paintContext.internalNodesCanvas.saveLayer(paintBounds, paint);
paint.dispose();
paintChildren(paintContext);
paintContext.internalNodesCanvas.restore();
paintContext.internalNodesCanvas.restore();
}
// TODO(dnfield): dispose of the _filter
// https://github.com/flutter/flutter/issues/82832
}
class ShaderMaskEngineLayer extends ContainerLayer
implements ui.ShaderMaskEngineLayer {
ShaderMaskEngineLayer(
this.shader, this.maskRect, this.blendMode, this.filterQuality);
final ui.Shader shader;
final ui.Rect maskRect;
final ui.BlendMode blendMode;
final ui.FilterQuality filterQuality;
@override
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);
paint.dispose();
paintContext.leafNodesCanvas!.restore();
paintContext.internalNodesCanvas.restore();
}
}
/// A layer containing a [Picture].
class PictureLayer extends Layer {
PictureLayer(this.picture, this.offset, this.isComplex, this.willChange);
/// The picture to paint into the canvas.
final CkPicture picture;
/// The offset at which to paint the picture.
final ui.Offset offset;
/// A hint to the compositor about whether this picture is complex.
final bool isComplex;
/// A hint to the compositor that this picture is likely to change.
final bool willChange;
@override
void preroll(PrerollContext prerollContext, Matrix4 matrix) {
paintBounds = picture.cullRect.shift(offset);
}
@override
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].
class ColorFilterEngineLayer extends ContainerLayer
implements ui.ColorFilterEngineLayer {
ColorFilterEngineLayer(this.filter);
final ui.ColorFilter filter;
@override
void paint(PaintContext paintContext) {
assert(needsPainting);
final CkPaint paint = CkPaint();
paint.colorFilter = filter;
paintContext.internalNodesCanvas.saveLayer(paintBounds, paint);
paint.dispose();
paintChildren(paintContext);
paintContext.internalNodesCanvas.restore();
}
}
/// A layer which renders a platform view (an HTML element in this case).
class PlatformViewLayer extends Layer {
PlatformViewLayer(this.viewId, this.offset, this.width, this.height);
final int viewId;
final ui.Offset offset;
final double width;
final double height;
@override
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;
}
}
}