blob: e0f448201586426dc21f9bf224ec34e78e90d87f [file]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui show ImageFilter, Picture, SceneBuilder;
import 'dart:ui' show Offset;
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:vector_math/vector_math_64.dart';
import 'debug.dart';
import 'node.dart';
/// A composited layer.
///
/// During painting, the render tree generates a tree of composited layers that
/// are uploaded into the engine and displayed by the compositor. This class is
/// the base class for all composited layers.
///
/// Most layers can have their properties mutated, and layers can be moved to
/// different parents. The scene must be explicitly recomposited after such
/// changes are made; the layer tree does not maintain its own dirty state.
///
/// To composite the tree, create a [ui.SceneBuilder] object, pass it to the
/// root [Layer] object's [addToScene] method, and then call
/// [ui.SceneBuilder.build] to obtain a [Scene]. A [Scene] can then be painted
/// using [ui.window.render].
///
/// See also:
///
/// * [RenderView.compositeFrame], which implements this recomposition protocol
/// for painting [RenderObject] trees on the the display.
abstract class Layer extends AbstractNode with TreeDiagnosticsMixin {
/// This layer's parent in the layer tree.
///
/// The [parent] of the root node in the layer tree is null.
///
/// Only subclasses of [ContainerLayer] can have children in the layer tree.
/// All other layer classes are used for leaves in the layer tree.
@override
ContainerLayer get parent => super.parent;
/// This layer's next sibling in the parent layer's child list.
Layer get nextSibling => _nextSibling;
Layer _nextSibling;
/// This layer's previous sibling in the parent layer's child list.
Layer get previousSibling => _previousSibling;
Layer _previousSibling;
/// Removes this layer from its parent layer's child list.
@mustCallSuper
void remove() {
parent?._removeChild(this);
}
/// Replaces this layer with the given layer in the parent layer's child list.
void replaceWith(Layer newLayer) {
assert(parent != null);
assert(attached == parent.attached);
assert(newLayer.parent == null);
assert(newLayer._nextSibling == null);
assert(newLayer._previousSibling == null);
assert(!newLayer.attached);
newLayer._nextSibling = nextSibling;
if (_nextSibling != null)
_nextSibling._previousSibling = newLayer;
newLayer._previousSibling = previousSibling;
if (_previousSibling != null)
_previousSibling._nextSibling = newLayer;
assert(() {
Layer node = this;
while (node.parent != null)
node = node.parent;
assert(node != newLayer); // indicates we are about to create a cycle
return true;
});
parent.adoptChild(newLayer);
assert(newLayer.attached == parent.attached);
if (parent.firstChild == this)
parent._firstChild = newLayer;
if (parent.lastChild == this)
parent._lastChild = newLayer;
_nextSibling = null;
_previousSibling = null;
parent.dropChild(this);
assert(!attached);
}
/// Override this method to upload this layer to the engine.
///
/// The layerOffset is the accumulated offset of this layer's parent from the
/// origin of the builder's coordinate system.
void addToScene(ui.SceneBuilder builder, Offset layerOffset);
/// The object responsible for creating this layer.
///
/// Defaults to the value of [RenderObject.debugCreator] for the render object
/// that created this layer. Used in debug messages.
dynamic debugCreator;
@override
String toString() => '${super.toString()}${ owner == null ? " DETACHED" : ""}';
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (parent == null && owner != null)
description.add('owner: $owner');
if (debugCreator != null)
description.add('creator: $debugCreator');
}
}
/// A composited layer containing a [Picture].
///
/// Picture layers are always leaves in the layer tree.
class PictureLayer extends Layer {
/// The picture recorded for this layer.
///
/// The picture's coodinate system matches this layer's coodinate system.
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
ui.Picture picture;
/// Hints that the painting in this layer is complex and would benefit from
/// caching.
///
/// If this hint is not set, the compositor will apply its own heuristics to
/// decide whether the this layer is complex enough to benefit from caching.
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
bool isComplexHint = false;
/// Hints that the painting in this layer is likely to change next frame.
///
/// This hint tells the compositor not to cache this layer because the cache
/// will not be used in the future. If this hint is not set, the compositor
/// will apply its own heuristics to decide whether this layer is likely to be
/// reused in the future.
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
bool willChangeHint = false;
@override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
builder.addPicture(layerOffset, picture, isComplexHint: isComplexHint, willChangeHint: willChangeHint);
}
}
/// A layer that indicates to the compositor that it should display
/// certain performance statistics within it.
///
/// Performance overlay layers are always leaves in the layer tree.
class PerformanceOverlayLayer extends Layer {
/// Creates a layer that displays a performance overlay.
PerformanceOverlayLayer({
@required this.overlayRect,
@required this.optionsMask,
@required this.rasterizerThreshold,
@required this.checkerboardRasterCacheImages,
});
/// The rectangle in this layer's coordinate system that the overlay should occupy.
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
Rect overlayRect;
/// The mask is created by shifting 1 by the index of the specific
/// [PerformanceOverlayOption] to enable.
final int optionsMask;
/// The rasterizer threshold is an integer specifying the number of frame
/// intervals that the rasterizer must miss before it decides that the frame
/// is suitable for capturing an SkPicture trace for further analysis.
final int rasterizerThreshold;
/// Whether the raster cache should checkerboard cached entries.
///
/// The compositor can sometimes decide to cache certain portions of the
/// widget hierarchy. Such portions typically don't change often from frame to
/// frame and are expensive to render. This can speed up overall rendering. However,
/// there is certain upfront cost to constructing these cache entries. And, if
/// the cache entries are not used very often, this cost may not be worth the
/// speedup in rendering of subsequent frames. If the developer wants to be certain
/// that populating the raster cache is not causing stutters, this option can be
/// set. Depending on the observations made, hints can be provided to the compositor
/// that aid it in making better decisions about caching.
final bool checkerboardRasterCacheImages;
@override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
assert(optionsMask != null);
builder.addPerformanceOverlay(optionsMask, overlayRect.shift(layerOffset));
builder.setRasterizerTracingThreshold(rasterizerThreshold);
builder.setCheckerboardRasterCacheImages(checkerboardRasterCacheImages);
}
}
/// A composited layer that has a list of children.
///
/// A [ContainerLayer] instance merely takes a list of children and inserts them
/// into the composited rendering in order. There are subclasses of
/// [ContainerLayer] which apply more elaborate effects in the process.
class ContainerLayer extends Layer {
/// The first composited layer in this layer's child list.
Layer get firstChild => _firstChild;
Layer _firstChild;
/// The last composited layer in this layer's child list.
Layer get lastChild => _lastChild;
Layer _lastChild;
bool _debugUltimatePreviousSiblingOf(Layer child, { Layer equals }) {
assert(child.attached == attached);
while (child.previousSibling != null) {
assert(child.previousSibling != child);
child = child.previousSibling;
assert(child.attached == attached);
}
return child == equals;
}
bool _debugUltimateNextSiblingOf(Layer child, { Layer equals }) {
assert(child.attached == attached);
while (child._nextSibling != null) {
assert(child._nextSibling != child);
child = child._nextSibling;
assert(child.attached == attached);
}
return child == equals;
}
@override
void attach(Object owner) {
super.attach(owner);
Layer child = firstChild;
while (child != null) {
child.attach(owner);
child = child.nextSibling;
}
}
@override
void detach() {
super.detach();
Layer child = firstChild;
while (child != null) {
child.detach();
child = child.nextSibling;
}
}
/// Adds the given layer to the end of this layer's child list.
void append(Layer child) {
assert(child != this);
assert(child != firstChild);
assert(child != lastChild);
assert(child.parent == null);
assert(!child.attached);
assert(child.nextSibling == null);
assert(child.previousSibling == null);
assert(() {
Layer node = this;
while (node.parent != null)
node = node.parent;
assert(node != child); // indicates we are about to create a cycle
return true;
});
adoptChild(child);
child._previousSibling = lastChild;
if (lastChild != null)
lastChild._nextSibling = child;
_lastChild = child;
_firstChild ??= child;
assert(child.attached == attached);
}
// Implementation of [Layer.remove].
void _removeChild(Layer child) {
assert(child.parent == this);
assert(child.attached == attached);
assert(_debugUltimatePreviousSiblingOf(child, equals: firstChild));
assert(_debugUltimateNextSiblingOf(child, equals: lastChild));
if (child._previousSibling == null) {
assert(_firstChild == child);
_firstChild = child._nextSibling;
} else {
child._previousSibling._nextSibling = child.nextSibling;
}
if (child._nextSibling == null) {
assert(lastChild == child);
_lastChild = child.previousSibling;
} else {
child.nextSibling._previousSibling = child.previousSibling;
}
assert((firstChild == null) == (lastChild == null));
assert(firstChild == null || firstChild.attached == attached);
assert(lastChild == null || lastChild.attached == attached);
assert(firstChild == null || _debugUltimateNextSiblingOf(firstChild, equals: lastChild));
assert(lastChild == null || _debugUltimatePreviousSiblingOf(lastChild, equals: firstChild));
child._previousSibling = null;
child._nextSibling = null;
dropChild(child);
assert(!child.attached);
}
/// Removes all of this layer's children from its child list.
void removeAllChildren() {
Layer child = firstChild;
while (child != null) {
final Layer next = child.nextSibling;
child._previousSibling = null;
child._nextSibling = null;
assert(child.attached == attached);
dropChild(child);
child = next;
}
_firstChild = null;
_lastChild = null;
}
@override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
addChildrenToScene(builder, layerOffset);
}
/// Uploads all of this layer's children to the engine.
///
/// This method is typically used by [addToScene] to insert the children into
/// the scene. Subclasses of [ContainerLayer] typically override [addToScene]
/// to apply effects to the scene using the [ui.SceneBuilder] API, then insert
/// their children using [addChildrenToScene], then reverse the aforementioned
/// effects before returning from [addToScene].
void addChildrenToScene(ui.SceneBuilder builder, Offset childOffset) {
Layer child = firstChild;
while (child != null) {
child.addToScene(builder, childOffset);
child = child.nextSibling;
}
}
@override
String debugDescribeChildren(String prefix) {
if (firstChild == null)
return '';
String result = '$prefix \u2502\n';
Layer child = firstChild;
int count = 1;
while (child != lastChild) {
result += '${child.toStringDeep("$prefix \u251C\u2500child $count: ", "$prefix \u2502")}';
count += 1;
child = child.nextSibling;
}
if (child != null) {
assert(child == lastChild);
result += '${child.toStringDeep("$prefix \u2514\u2500child $count: ", "$prefix ")}';
}
return result;
}
}
/// A layer that is displayed at an offset from its parent layer.
///
/// Offset layers are key to efficient repainting because they are created by
/// repaint boundaries in the [RenderObject] tree (see
/// [RenderObject.isRepaintBoundary]). When a render object that is a repaint
/// boundary is asked to paint at given offset in a [PaintingContext], the
/// render object first checks whether it needs to repaint itself. If not, it
/// reuses its existing [OffsetLayer] (and its entire subtree) by mutating its
/// [offset] property, cutting off the paint walk.
class OffsetLayer extends ContainerLayer {
/// Creates an offset layer.
///
/// By default, [offset] is zero.
OffsetLayer({ this.offset: Offset.zero });
/// Offset from parent in the parent's coordinate system.
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
Offset offset;
@override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
addChildrenToScene(builder, offset + layerOffset);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('offset: $offset');
}
}
/// A composite layer that clips its children using a rectangle.
class ClipRectLayer extends ContainerLayer {
/// Creates a layer with a rectangular clip.
///
/// The [clipRect] property must be non-null before the compositing phase of
/// the pipeline.
ClipRectLayer({ this.clipRect });
/// The rectangle to clip in the parent's coordinate system.
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
Rect clipRect;
@override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
builder.pushClipRect(clipRect.shift(layerOffset));
addChildrenToScene(builder, layerOffset);
builder.pop();
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('clipRect: $clipRect');
}
}
/// A composite layer that clips its children using a rounded rectangle.
class ClipRRectLayer extends ContainerLayer {
/// Creates a layer with a rounded-rectangular clip.
///
/// The [clipRRect] property must be non-null before the compositing phase of
/// the pipeline.
ClipRRectLayer({ this.clipRRect });
/// The rounded-rect to clip in the parent's coordinate system.
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
RRect clipRRect;
@override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
builder.pushClipRRect(clipRRect.shift(layerOffset));
addChildrenToScene(builder, layerOffset);
builder.pop();
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('clipRRect: $clipRRect');
}
}
/// A composite layer that clips its children using a path.
class ClipPathLayer extends ContainerLayer {
/// Creates a layer with a path-based clip.
///
/// The [clipPath] property must be non-null before the compositing phase of
/// the pipeline.
ClipPathLayer({ this.clipPath });
/// The path to clip in the parent's coordinate system.
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
Path clipPath;
@override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
builder.pushClipPath(clipPath.shift(layerOffset));
addChildrenToScene(builder, layerOffset);
builder.pop();
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('clipPath: $clipPath');
}
}
/// A composited layer that applies a transformation matrix to its children.
class TransformLayer extends OffsetLayer {
/// Creates a transform layer.
///
/// The [transform] property must be non-null before the compositing phase of
/// the pipeline.
TransformLayer({
this.transform
});
/// The matrix to apply.
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
Matrix4 transform;
@override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
assert(offset == Offset.zero);
Matrix4 effectiveTransform = transform;
if (layerOffset != Offset.zero) {
effectiveTransform = new Matrix4.translationValues(layerOffset.dx, layerOffset.dy, 0.0)
..multiply(transform);
}
builder.pushTransform(effectiveTransform.storage);
addChildrenToScene(builder, Offset.zero);
builder.pop();
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('transform:');
description.addAll(debugDescribeTransform(transform));
}
}
/// A composited layer that makes its children partially transparent.
class OpacityLayer extends ContainerLayer {
/// Creates an opacity layer.
///
/// The [alpha] property must be non-null before the compositing phase of
/// the pipeline.
OpacityLayer({ this.alpha });
/// The amount to multiply into the alpha channel.
///
/// The opacity is expressed as an integer from 0 to 255, where 0 is fully
/// transparent and 255 is fully opaque.
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
int alpha;
@override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
builder.pushOpacity(alpha);
addChildrenToScene(builder, layerOffset);
builder.pop();
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('alpha: $alpha');
}
}
/// A composited layer that applies a shader to hits children.
class ShaderMaskLayer extends ContainerLayer {
/// Creates a shader mask layer.
///
/// The [shader], [maskRect], and [blendMode] properties must be non-null
/// before the compositing phase of the pipeline.
ShaderMaskLayer({ this.shader, this.maskRect, this.blendMode });
/// The shader to apply to the children.
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
Shader shader;
/// The size of the shader.
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
Rect maskRect;
/// The blend mode to apply when blending the shader with the children.
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
BlendMode blendMode;
@override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
builder.pushShaderMask(shader, maskRect.shift(layerOffset), blendMode);
addChildrenToScene(builder, layerOffset);
builder.pop();
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('shader: $shader');
description.add('maskRect: $maskRect');
description.add('blendMode: $blendMode');
}
}
/// A composited layer that applies a filter to the existing contents of the scene.
class BackdropFilterLayer extends ContainerLayer {
/// Creates a backdrop filter layer.
///
/// The [filter] property must be non-null before the compositing phase of the
/// pipeline.
BackdropFilterLayer({ this.filter });
/// The filter to apply to the existing contents of the scene.
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
ui.ImageFilter filter;
@override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
builder.pushBackdropFilter(filter);
addChildrenToScene(builder, layerOffset);
builder.pop();
}
}
/// A composited layer that uses a physical model to producing lighting effects.
///
/// For example, the layer casts a shadow according to its geometry and the
/// relative position of lights and other physically modelled objects in the
/// scene.
class PhysicalModelLayer extends ContainerLayer {
/// Creates a composited layer that uses a physical model to producing
/// lighting effects.
///
/// The [clipRRect], [elevation], and [color] arguments must not be null.
PhysicalModelLayer({
@required this.clipRRect,
@required this.elevation,
@required this.color,
}) {
assert(clipRRect != null);
assert(elevation != null);
assert(color != null);
}
/// The rounded-rect to clip in the parent's coordinate system.
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
RRect clipRRect;
/// The z-coordinate at which to place this physical object.
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
int elevation;
/// The background color.
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
Color color;
@override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
builder.pushPhysicalModel(
rrect: clipRRect.shift(layerOffset),
elevation: elevation.toDouble(),
color: color,
);
addChildrenToScene(builder, layerOffset);
builder.pop();
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('clipRRect: $clipRRect');
}
}