blob: d30dc573a16b2cbb6589bd1084d37396df03d0fd [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.
// @dart = 2.6
part of engine;
/// 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);
}
/// A context shared by all layers during the preroll pass.
class PrerollContext {
/// 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();
PrerollContext(this.rasterCache, this.viewEmbedder);
}
/// A context shared by all layers during the paint pass.
class PaintContext {
/// A multi-canvas that applies clips, transforms, and opacity
/// operations to all canvases (root canvas and overlay canvases for the
/// platform views).
SkNWayCanvas internalNodesCanvas;
/// The canvas for leaf nodes to paint to.
SkCanvas leafNodesCanvas;
/// A raster cache potentially containing pre-rendered pictures.
final RasterCache rasterCache;
/// A compositor for embedded HTML views.
final HtmlViewEmbedder viewEmbedder;
PaintContext(
this.internalNodesCanvas,
this.leafNodesCanvas,
this.rasterCache,
this.viewEmbedder,
);
}
/// A layer that contains child layers.
abstract class ContainerLayer extends Layer {
final List<Layer> _layers = <Layer>[];
/// 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 (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 (Layer layer in _layers) {
if (layer.needsPainting) {
layer.paint(context);
}
}
}
}
class BackdropFilterLayer extends ContainerLayer {
final ui.ImageFilter _filter;
BackdropFilterLayer(this._filter);
@override
void paint(PaintContext context) {
context.internalNodesCanvas.saveLayerWithFilter(paintBounds, _filter);
paintChildren(context);
context.internalNodesCanvas.restore();
}
}
/// A layer that clips its child layers by a given [Path].
class ClipPathLayer extends ContainerLayer {
/// The path used to clip child layers.
final ui.Path _clipPath;
final ui.Clip _clipBehavior;
ClipPathLayer(this._clipPath, this._clipBehavior)
: assert(_clipBehavior != ui.Clip.none);
@override
void preroll(PrerollContext context, Matrix4 matrix) {
context.mutatorsStack.pushClipPath(_clipPath);
final ui.Rect childPaintBounds = prerollChildren(context, matrix);
final ui.Rect clipBounds = _clipPath.getBounds();
if (childPaintBounds.overlaps(clipBounds)) {
paintBounds = childPaintBounds.intersect(clipBounds);
}
context.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 ClipRectLayer extends ContainerLayer {
/// The rectangle used to clip child layers.
final ui.Rect _clipRect;
final ui.Clip _clipBehavior;
ClipRectLayer(this._clipRect, this._clipBehavior)
: assert(_clipBehavior != ui.Clip.none);
@override
void preroll(PrerollContext context, Matrix4 matrix) {
context.mutatorsStack.pushClipRect(_clipRect);
final ui.Rect childPaintBounds = prerollChildren(context, matrix);
if (childPaintBounds.overlaps(_clipRect)) {
paintBounds = childPaintBounds.intersect(_clipRect);
}
context.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 ClipRRectLayer extends ContainerLayer {
/// The rounded rectangle used to clip child layers.
final ui.RRect _clipRRect;
final ui.Clip _clipBehavior;
ClipRRectLayer(this._clipRRect, this._clipBehavior)
: assert(_clipBehavior != ui.Clip.none);
@override
void preroll(PrerollContext context, Matrix4 matrix) {
context.mutatorsStack.pushClipRRect(_clipRRect);
final ui.Rect childPaintBounds = prerollChildren(context, matrix);
if (childPaintBounds.overlaps(_clipRRect.outerRect)) {
paintBounds = childPaintBounds.intersect(_clipRRect.outerRect);
}
context.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 OpacityLayer extends ContainerLayer implements ui.OpacityEngineLayer {
final int _alpha;
final ui.Offset _offset;
OpacityLayer(this._alpha, this._offset);
@override
void preroll(PrerollContext context, Matrix4 matrix) {
final Matrix4 childMatrix = Matrix4.copy(matrix);
childMatrix.translate(_offset.dx, _offset.dy);
context.mutatorsStack
.pushTransform(Matrix4.translationValues(_offset.dx, _offset.dy, 0.0));
context.mutatorsStack.pushOpacity(_alpha);
super.preroll(context, childMatrix);
context.mutatorsStack.pop();
context.mutatorsStack.pop();
paintBounds = paintBounds.translate(_offset.dx, _offset.dy);
}
@override
void paint(PaintContext paintContext) {
assert(needsPainting);
final ui.Paint paint = ui.Paint();
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 TransformLayer extends ContainerLayer
implements ui.OffsetEngineLayer, ui.TransformEngineLayer {
/// The matrix with which to transform the child layers.
final Matrix4 _transform;
TransformLayer(this._transform);
@override
void preroll(PrerollContext context, Matrix4 matrix) {
final Matrix4 childMatrix = matrix * _transform;
context.mutatorsStack.pushTransform(_transform);
final ui.Rect childPaintBounds = prerollChildren(context, childMatrix);
paintBounds = _transformRect(_transform, childPaintBounds);
context.mutatorsStack.pop();
}
/// Applies the given matrix as a perspective transform to the given point.
///
/// This function assumes the given point has a z-coordinate of 0.0. The
/// z-coordinate of the result is ignored.
static ui.Offset _transformPoint(Matrix4 transform, ui.Offset point) {
final Vector3 position3 = Vector3(point.dx, point.dy, 0.0);
final Vector3 transformed3 = transform.perspectiveTransform(position3);
return ui.Offset(transformed3.x, transformed3.y);
}
/// Returns a rect that bounds the result of applying the given matrix as a
/// perspective transform to the given rect.
///
/// This function assumes the given rect is in the plane with z equals 0.0.
/// The transformed rect is then projected back into the plane with z equals
/// 0.0 before computing its bounding rect.
static ui.Rect _transformRect(Matrix4 transform, ui.Rect rect) {
final ui.Offset point1 = _transformPoint(transform, rect.topLeft);
final ui.Offset point2 = _transformPoint(transform, rect.topRight);
final ui.Offset point3 = _transformPoint(transform, rect.bottomLeft);
final ui.Offset point4 = _transformPoint(transform, rect.bottomRight);
return ui.Rect.fromLTRB(
_min4(point1.dx, point2.dx, point3.dx, point4.dx),
_min4(point1.dy, point2.dy, point3.dy, point4.dy),
_max4(point1.dx, point2.dx, point3.dx, point4.dx),
_max4(point1.dy, point2.dy, point3.dy, point4.dy));
}
static double _min4(double a, double b, double c, double d) {
return math.min(a, math.min(b, math.min(c, d)));
}
static double _max4(double a, double b, double c, double d) {
return math.max(a, math.max(b, math.max(c, d)));
}
@override
void paint(PaintContext paintContext) {
assert(needsPainting);
paintContext.internalNodesCanvas.save();
paintContext.internalNodesCanvas.transform(_transform.storage);
paintChildren(paintContext);
paintContext.internalNodesCanvas.restore();
}
}
/// A layer that applies an [ui.ImageFilter] to its children.
class ImageFilterLayer extends ContainerLayer implements ui.OpacityEngineLayer {
ImageFilterLayer(this._filter);
final ui.ImageFilter _filter;
@override
void paint(PaintContext paintContext) {
assert(needsPainting);
final ui.Paint paint = ui.Paint();
paint.imageFilter = _filter;
paintContext.internalNodesCanvas.saveLayer(paintBounds, paint);
paintChildren(paintContext);
paintContext.internalNodesCanvas.restore();
}
}
/// A layer containing a [Picture].
class PictureLayer extends Layer {
/// The picture to paint into the canvas.
final SkPicture 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;
PictureLayer(this.picture, this.offset, this.isComplex, this.willChange);
@override
void preroll(PrerollContext prerollContext, Matrix4 matrix) {
paintBounds = picture.cullRect.shift(offset);
}
@override
void paint(PaintContext paintContext) {
assert(picture != null);
assert(needsPainting);
paintContext.leafNodesCanvas.save();
paintContext.leafNodesCanvas.translate(offset.dx, offset.dy);
paintContext.leafNodesCanvas.drawPicture(picture);
paintContext.leafNodesCanvas.restore();
}
}
/// A layer representing a physical shape.
///
/// The shape clips its children to a given [Path], and casts a shadow based
/// on the given elevation.
class PhysicalShapeLayer extends ContainerLayer
implements ui.PhysicalShapeEngineLayer {
final double _elevation;
final ui.Color _color;
final ui.Color _shadowColor;
final ui.Path _path;
final ui.Clip _clipBehavior;
PhysicalShapeLayer(
this._elevation,
this._color,
this._shadowColor,
this._path,
this._clipBehavior,
);
@override
void preroll(PrerollContext prerollContext, Matrix4 matrix) {
prerollChildren(prerollContext, matrix);
paintBounds = _path.getBounds();
if (_elevation == 0.0) {
// No need to extend the paint bounds if there is no shadow.
return;
} else {
// Add some margin to the paint bounds to leave space for the shadow.
// We fill this whole region and clip children to it so we don't need to
// join the child paint bounds.
// The offset is calculated as follows:
// .--- (kLightRadius)
// -------/ (light)
// | /
// | /
// |/
// |O
// /| (kLightHeight)
// / |
// / |
// / |
// / |
// ------------- (layer)
// /| |
// / | | (elevation)
// A / | |B
// ------------------------------------------------ (canvas)
// --- (extent of shadow)
//
// E = lt } t = (r + w/2)/h
// } =>
// r + w/2 = ht } E = (l/h)(r + w/2)
//
// Where: E = extent of shadow
// l = elevation of layer
// r = radius of the light source
// w = width of the layer
// h = light height
// t = tangent of AOB, i.e., multiplier for elevation to extent
final double devicePixelRatio = ui.window.devicePixelRatio;
final double radius = kLightRadius * devicePixelRatio;
// tangent for x
double tx = (radius + paintBounds.width * 0.5) / kLightHeight;
// tangent for y
double ty = (radius + paintBounds.height * 0.5) / kLightHeight;
paintBounds = ui.Rect.fromLTRB(
paintBounds.left - tx,
paintBounds.top - ty,
paintBounds.right + tx,
paintBounds.bottom + ty,
);
}
}
@override
void paint(PaintContext paintContext) {
assert(needsPainting);
if (_elevation != 0) {
drawShadow(paintContext.leafNodesCanvas, _path, _shadowColor, _elevation,
_color.alpha != 0xff);
}
final ui.Paint paint = ui.Paint()..color = _color;
if (_clipBehavior != ui.Clip.antiAliasWithSaveLayer) {
paintContext.leafNodesCanvas.drawPath(_path, paint);
}
final int saveCount = paintContext.internalNodesCanvas.save();
switch (_clipBehavior) {
case ui.Clip.hardEdge:
paintContext.internalNodesCanvas.clipPath(_path, false);
break;
case ui.Clip.antiAlias:
paintContext.internalNodesCanvas.clipPath(_path, true);
break;
case ui.Clip.antiAliasWithSaveLayer:
paintContext.internalNodesCanvas.clipPath(_path, true);
paintContext.internalNodesCanvas.saveLayer(paintBounds, null);
break;
case ui.Clip.none:
break;
}
if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) {
// If we want to avoid the bleeding edge artifact
// (https://github.com/flutter/flutter/issues/18057#issue-328003931)
// using saveLayer, we have to call drawPaint instead of drawPath as
// anti-aliased drawPath will always have such artifacts.
paintContext.leafNodesCanvas.drawPaint(paint);
}
paintChildren(paintContext);
paintContext.internalNodesCanvas.restoreToCount(saveCount);
}
/// Draws a shadow on the given [canvas] for the given [path].
///
/// The blur of the shadow is decided by the [elevation], and the
/// shadow is painted with the given [color].
static void drawShadow(SkCanvas canvas, ui.Path path, ui.Color color,
double elevation, bool transparentOccluder) {
canvas.drawShadow(path, color, elevation, transparentOccluder);
}
}
/// 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 context, Matrix4 matrix) {
paintBounds = ui.Rect.fromLTWH(offset.dx, offset.dy, width, height);
context.viewEmbedder.prerollCompositeEmbeddedView(
viewId,
EmbeddedViewParams(
offset,
ui.Size(width, height),
context.mutatorsStack,
),
);
}
@override
void paint(PaintContext context) {
SkCanvas canvas = context.viewEmbedder.compositeEmbeddedView(viewId);
context.leafNodesCanvas = canvas;
}
}