Unverified Commit 025397ae authored by Dan Field's avatar Dan Field Committed by GitHub

Release retained resources from layers/dispose pictures (#84740)

parent 1977ee75
......@@ -524,12 +524,18 @@ class _RenderCupertinoSwitch extends RenderConstrainedBox {
thumbCenterY + CupertinoThumbPainter.radius,
);
_clipRRectLayer = context.pushClipRRect(needsCompositing, Offset.zero, thumbBounds, trackRRect, (PaintingContext innerContext, Offset offset) {
_clipRRectLayer.layer = context.pushClipRRect(needsCompositing, Offset.zero, thumbBounds, trackRRect, (PaintingContext innerContext, Offset offset) {
const CupertinoThumbPainter.switchThumb().paint(innerContext.canvas, thumbBounds);
}, oldLayer: _clipRRectLayer);
}, oldLayer: _clipRRectLayer.layer);
}
ClipRRectLayer? _clipRRectLayer;
final LayerHandle<ClipRRectLayer> _clipRRectLayer = LayerHandle<ClipRRectLayer>();
@override
void dispose() {
_clipRRectLayer.layer = null;
super.dispose();
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
......
......@@ -289,19 +289,25 @@ class _RenderCupertinoTextSelectionToolbarShape extends RenderShiftedBox {
}
final BoxParentData childParentData = child!.parentData! as BoxParentData;
_clipPathLayer = context.pushClipPath(
_clipPathLayer.layer = context.pushClipPath(
needsCompositing,
offset + childParentData.offset,
Offset.zero & child!.size,
_clipPath(),
(PaintingContext innerContext, Offset innerOffset) => innerContext.paintChild(child!, innerOffset),
oldLayer: _clipPathLayer,
oldLayer: _clipPathLayer.layer,
);
}
ClipPathLayer? _clipPathLayer;
final LayerHandle<ClipPathLayer> _clipPathLayer = LayerHandle<ClipPathLayer>();
Paint? _debugPaint;
@override
void dispose() {
_clipPathLayer.layer = null;
super.dispose();
}
@override
void debugPaintSize(PaintingContext context, Offset offset) {
assert(() {
......
......@@ -1502,15 +1502,15 @@ class _RenderDecoration extends RenderBox {
_labelTransform = Matrix4.identity()
..translate(dx, labelOffset.dy + dy)
..scale(scale);
_transformLayer = context.pushTransform(
layer = context.pushTransform(
needsCompositing,
offset,
_labelTransform!,
_paintLabel,
oldLayer: _transformLayer,
oldLayer: layer as TransformLayer?,
);
} else {
_transformLayer = null;
layer = null;
}
doPaint(icon);
......@@ -1524,8 +1524,6 @@ class _RenderDecoration extends RenderBox {
doPaint(counter);
}
TransformLayer? _transformLayer;
@override
bool hitTestSelf(Offset position) => true;
......
......@@ -327,19 +327,25 @@ class RenderAnimatedSize extends RenderAligningShiftedBox {
void paint(PaintingContext context, Offset offset) {
if (child != null && _hasVisualOverflow && clipBehavior != Clip.none) {
final Rect rect = Offset.zero & size;
_clipRectLayer = context.pushClipRect(
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
rect,
super.paint,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer,
oldLayer: _clipRectLayer.layer,
);
} else {
_clipRectLayer = null;
_clipRectLayer.layer = null;
super.paint(context, offset);
}
}
ClipRectLayer? _clipRectLayer;
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
}
......@@ -301,6 +301,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
_foregroundRenderObject = null;
_backgroundRenderObject?.dispose();
_backgroundRenderObject = null;
_clipRectLayer.layer = null;
_cachedBuiltInForegroundPainters?.dispose();
_cachedBuiltInPainters?.dispose();
super.dispose();
......@@ -4007,22 +4008,22 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
void paint(PaintingContext context, Offset offset) {
_computeTextMetricsIfNeeded();
if (_hasVisualOverflow && clipBehavior != Clip.none) {
_clipRectLayer = context.pushClipRect(
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
_paintContents,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer,
oldLayer: _clipRectLayer.layer,
);
} else {
_clipRectLayer = null;
_clipRectLayer.layer = null;
_paintContents(context, offset);
}
_paintHandleLayers(context, getEndpointsForSelection(selection!));
}
ClipRectLayer? _clipRectLayer;
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
Rect? describeApproximatePaintClip(RenderObject child) => _hasVisualOverflow ? Offset.zero & size : null;
......
......@@ -1084,17 +1084,17 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, Fl
return;
if (clipBehavior == Clip.none) {
_clipRectLayer = null;
_clipRectLayer.layer = null;
defaultPaint(context, offset);
} else {
// We have overflow and the clipBehavior isn't none. Clip it.
_clipRectLayer = context.pushClipRect(
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
defaultPaint,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer,
oldLayer: _clipRectLayer.layer,
);
}
......@@ -1141,7 +1141,13 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, Fl
}());
}
ClipRectLayer? _clipRectLayer;
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
@override
Rect? describeApproximatePaintClip(RenderObject child) => _hasOverflow ? Offset.zero & size : null;
......
......@@ -389,21 +389,27 @@ class RenderFlow extends RenderBox
@override
void paint(PaintingContext context, Offset offset) {
if (clipBehavior == Clip.none) {
_clipRectLayer = null;
_clipRectLayer.layer = null;
_paintWithDelegate(context, offset);
} else {
_clipRectLayer = context.pushClipRect(
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
_paintWithDelegate,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer,
oldLayer: _clipRectLayer.layer,
);
}
}
ClipRectLayer? _clipRectLayer;
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
......
......@@ -88,11 +88,146 @@ class AnnotationResult<T> {
/// [SceneBuilder.build] to obtain a [Scene]. A [Scene] can then be painted
/// using [dart:ui.FlutterView.render].
///
/// ## Memory
///
/// Layers retain resources between frames to speed up rendering. A layer will
/// retain these resources until all [LayerHandle]s referring to the layer have
/// nulled out their references.
///
/// Layers must not be used after disposal. If a RenderObject needs to maintain
/// a layer for later usage, it must create a handle to that layer. This is
/// handled automatically for the [RenderObject.layer] property, but additional
/// layers must use their own [LayerHandle].
///
/// {@tool snippet}
///
/// This [RenderObject] is a repaint boundary that pushes an additional
/// [ClipRectLayer].
///
/// ```dart
/// class ClippingRenderObject extends RenderBox {
/// final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
///
/// @override
/// bool get isRepaintBoundary => true; // The [layer] property will be used.
///
/// @override
/// void paint(PaintingContext context, Offset offset) {
/// _clipRectLayer.layer = context.pushClipRect(
/// needsCompositing,
/// offset,
/// Offset.zero & size,
/// super.paint,
/// clipBehavior: Clip.hardEdge,
/// oldLayer: _clipRectLayer.layer,
/// );
/// }
///
/// @override
/// void dispose() {
/// _clipRectLayer.layer = null;
/// super.dispose();
/// }
/// }
/// ```
/// {@end-tool}
/// See also:
///
/// * [RenderView.compositeFrame], which implements this recomposition protocol
/// for painting [RenderObject] trees on the display.
abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
/// If asserts are enabled, returns whether [dispose] has
/// been called since the last time any retained resources were created.
///
/// Throws an exception if asserts are disabled.
bool get debugDisposed {
late bool disposed;
assert(() {
disposed = _debugDisposed;
return true;
}());
return disposed;
}
// TODO(dnfield): https://github.com/flutter/flutter/issues/85066
final bool _debugDisposed = false;
/// Set when this layer is appended to a [ContainerLayer], and
/// unset when it is removed.
///
/// This cannot be set from [attach] or [detach] which is called when an
/// entire subtree is attached to or detached from an owner. Layers may be
/// appended to or removed from a [ContainerLayer] regardless of whether they
/// are attached or detached, and detaching a layer from an owner does not
/// imply that it has been removed from its parent.
final LayerHandle<Layer> _parentHandle = LayerHandle<Layer>();
/// Incremeneted by [LayerHandle].
int _refCount = 0;
/// Called by [LayerHandle].
void _unref() {
assert(_refCount > 0);
_refCount -= 1;
if (_refCount == 0) {
dispose();
}
}
/// Returns the number of objects holding a [LayerHandle] to this layer.
///
/// This method throws if asserts are disabled.
int get debugHandleCount {
late int count;
assert(() {
count = _refCount;
return true;
}());
return count;
}
/// Clears any retained resources that this layer holds.
///
/// This method must dispose resources such as [EngineLayer] and [Picture]
/// objects. The layer is still usable after this call, but any graphics
/// related resources it holds will need to be recreated.
///
/// This method _only_ disposes resources for this layer. For example, if it
/// is a [ContainerLayer], it does not dispose resources of any children.
/// However, [ContainerLayer]s do remove any children they have when
/// this method is called, and if this layer was the last holder of a removed
/// child handle, the child may recursively clean up its resources.
///
/// This method automatically gets called when all outstanding [LayerHandle]s
/// are disposed. [LayerHandle] objects are typically held by the [parent]
/// layer of this layer and any [RenderObject]s that participated in creating
/// it.
///
/// After calling this method, the object is unusable.
@mustCallSuper
@protected
@visibleForTesting
void dispose() {
assert(
!_debugDisposed,
'Layers must only be disposed once. This is typically handled by '
'LayerHandle and createHandle. Subclasses should not directly call '
'dispose, except to call super.dispose() in an overridden dispose '
'method. Tests must only call dispose once.',
);
assert(() {
assert(
_refCount == 0,
'Do not directly call dispose on a $runtimeType. Instead, '
'use createHandle and LayerHandle.dispose.',
);
// TODO(dnfield): enable this. https://github.com/flutter/flutter/issues/85066
// _debugDisposed = true;
return true;
}());
_engineLayer?.dispose();
_engineLayer = null;
}
/// This layer's parent in the layer tree.
///
/// The [parent] of the root node in the layer tree is null.
......@@ -134,6 +269,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
'$runtimeType with alwaysNeedsAddToScene set called markNeedsAddToScene.\n'
"The layer's alwaysNeedsAddToScene is set to true, and therefore it should not call markNeedsAddToScene.",
);
assert(!_debugDisposed);
// Already marked. Short-circuit.
if (_needsAddToScene) {
......@@ -187,6 +323,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
/// layer. The web engine could, for example, update the properties of
/// previously rendered HTML DOM nodes rather than creating new nodes.
@protected
@visibleForTesting
ui.EngineLayer? get engineLayer => _engineLayer;
/// Sets the engine layer used to render this layer.
......@@ -195,7 +332,11 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
/// in turn returns the engine layer produced by one of [ui.SceneBuilder]'s
/// "push" methods, such as [ui.SceneBuilder.pushOpacity].
@protected
@visibleForTesting
set engineLayer(ui.EngineLayer? value) {
assert(!_debugDisposed);
_engineLayer?.dispose();
_engineLayer = value;
if (!alwaysNeedsAddToScene) {
// The parent must construct a new engine layer to add this layer to, and
......@@ -415,7 +556,7 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
///
/// Defaults to the value of [RenderObject.debugCreator] for the render object
/// that created this layer. Used in debug messages.
dynamic debugCreator;
Object? debugCreator;
@override
String toStringShort() => '${super.toStringShort()}${ owner == null ? " DETACHED" : ""}';
......@@ -424,14 +565,83 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Object>('owner', owner, level: parent != null ? DiagnosticLevel.hidden : DiagnosticLevel.info, defaultValue: null));
properties.add(DiagnosticsProperty<dynamic>('creator', debugCreator, defaultValue: null, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<String>('engine layer', describeIdentity(_engineLayer)));
properties.add(DiagnosticsProperty<Object?>('creator', debugCreator, defaultValue: null, level: DiagnosticLevel.debug));
if (_engineLayer != null) {
properties.add(DiagnosticsProperty<String>('engine layer', describeIdentity(_engineLayer)));
}
properties.add(DiagnosticsProperty<int>('handles', debugHandleCount));
}
}
/// A handle to prevent a [Layer]'s platform graphics resources from being
/// disposed.
///
/// [Layer] objects retain native resourses such as [EngineLayer]s and [Picture]
/// objects. These objects may in turn retain large chunks of texture memory,
/// either directly or indirectly.
///
/// The layer's native resources must be retained as long as there is some
/// object that can add it to a scene. Typically, this is either its
/// [Layer.parent] or an undisposed [RenderObject] that will append it to a
/// [ContainerLayer]. Layers automatically hold a handle to their children, and
/// RenderObjects automatically hold a handle to their [RenderObject.layer] as
/// well as any [PictureLayer]s that they paint into using the
/// [PaintingContext.canvas]. A layer automatically releases its resources once
/// at least one handle has been acquired and all handles have been disposed.
/// [RenderObject]s that create additional layer objects must manually manage
/// the handles for that layer similarly to the implementation of
/// [RenderObject.layer].
///
/// A handle is automatically managed for [RenderObject.layer].
///
/// If a [RenderObject] creates layers in addition to its [RenderObject.layer]
/// and it intends to reuse those layers separately from [RenderObject.layer],
/// it must create a handle to that layer and dispose of it when the layer is
/// no longer needed. For example, if it re-creates or nulls out an existing
/// layer in [RenderObject.paint], it should dispose of the handle to the
/// old layer. It should also dispose of any layer handles it holds in
/// [RenderObject.dispose].
class LayerHandle<T extends Layer> {
/// Create a new layer handle, optionally referencing a [Layer].
LayerHandle([this._layer]) {
if (_layer != null) {
_layer!._refCount += 1;
}
}
T? _layer;
/// The [Layer] whose resources this object keeps alive.
///
/// Setting a new value will or null dispose the previously held layer if
/// there are no other open handles to that layer.
T? get layer => _layer;
set layer(T? layer) {
assert(
layer?.debugDisposed != true,
'Attempted to create a handle to an already disposed layer: $layer.',
);
if (identical(layer, _layer)) {
return;
}
_layer?._unref();
_layer = layer;
if (_layer != null) {
_layer!._refCount += 1;
}
}
@override
String toString() => 'LayerHandle(${_layer != null ? _layer.toString() : 'DISPOSED'})';
}
/// A composited layer containing a [Picture].
///
/// Picture layers are always leaves in the layer tree.
/// Picture layers are always leaves in the layer tree. They are also
/// responsible for disposing of the [Picture] object they hold. This is
/// typically done when their parent and all [RenderObject]s that participated
/// in painting the picture have been disposed.
class PictureLayer extends Layer {
/// Creates a leaf layer for the layer tree.
PictureLayer(this.canvasBounds);
......@@ -453,7 +663,9 @@ class PictureLayer extends Layer {
ui.Picture? get picture => _picture;
ui.Picture? _picture;
set picture(ui.Picture? picture) {
assert(!_debugDisposed);
markNeedsAddToScene();
_picture?.dispose();
_picture = picture;
}
......@@ -492,6 +704,12 @@ class PictureLayer extends Layer {
}
}
@override
void dispose() {
picture = null; // Will dispose _picture.
super.dispose();
}
@override
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
assert(picture != null);
......@@ -865,6 +1083,12 @@ class ContainerLayer extends Layer {
return addedLayers;
}
@override
void dispose() {
removeAllChildren();
super.dispose();
}
@override
void updateSubtreeNeedsAddToScene() {
super.updateSubtreeNeedsAddToScene();
......@@ -917,6 +1141,7 @@ class ContainerLayer extends Layer {
assert(!child.attached);
assert(child.nextSibling == null);
assert(child.previousSibling == null);
assert(child._parentHandle.layer == null);
assert(() {
Layer node = this;
while (node.parent != null)
......@@ -930,6 +1155,7 @@ class ContainerLayer extends Layer {
lastChild!._nextSibling = child;
_lastChild = child;
_firstChild ??= child;
child._parentHandle.layer = child;
assert(child.attached == attached);
}
......@@ -939,6 +1165,7 @@ class ContainerLayer extends Layer {
assert(child.attached == attached);
assert(_debugUltimatePreviousSiblingOf(child, equals: firstChild));
assert(_debugUltimateNextSiblingOf(child, equals: lastChild));
assert(child._parentHandle.layer != null);
if (child._previousSibling == null) {
assert(_firstChild == child);
_firstChild = child._nextSibling;
......@@ -959,6 +1186,7 @@ class ContainerLayer extends Layer {
child._previousSibling = null;
child._nextSibling = null;
dropChild(child);
child._parentHandle.layer = null;
assert(!child.attached);
}
......@@ -971,6 +1199,8 @@ class ContainerLayer extends Layer {
child._nextSibling = null;
assert(child.attached == attached);
dropChild(child);
assert(child._parentHandle != null);
child._parentHandle.layer = null;
child = next;
}
_firstChild = null;
......
......@@ -785,22 +785,28 @@ class RenderListWheelViewport
void paint(PaintingContext context, Offset offset) {
if (childCount > 0) {
if (_shouldClipAtCurrentOffset() && clipBehavior != Clip.none) {
_clipRectLayer = context.pushClipRect(
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
_paintVisibleChildren,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer,
oldLayer: _clipRectLayer.layer,
);
} else {
_clipRectLayer = null;
_clipRectLayer.layer = null;
_paintVisibleChildren(context, offset);
}
}
}
ClipRectLayer? _clipRectLayer;
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
/// Paints all children visible in the current viewport.
void _paintVisibleChildren(PaintingContext context, Offset offset) {
......
......@@ -117,30 +117,32 @@ class PaintingContext extends ClipContext {
);
return true;
}());
OffsetLayer? childLayer = child._layer as OffsetLayer?;
OffsetLayer? childLayer = child._layerHandle.layer as OffsetLayer?;
if (childLayer == null) {
assert(debugAlsoPaintedParent);
assert(child._layerHandle.layer == null);
// Not using the `layer` setter because the setter asserts that we not
// replace the layer for repaint boundaries. That assertion does not
// apply here because this is exactly the place designed to create a
// layer for repaint boundaries.
child._layer = childLayer = OffsetLayer();
final OffsetLayer layer = OffsetLayer();
child._layerHandle.layer = childLayer = layer;
} else {
assert(debugAlsoPaintedParent || childLayer.attached);
childLayer.removeAllChildren();
}
assert(identical(childLayer, child._layer));
assert(child._layer is OffsetLayer);
assert(identical(childLayer, child._layerHandle.layer));
assert(child._layerHandle.layer is OffsetLayer);
assert(() {
child._layer!.debugCreator = child.debugCreator ?? child.runtimeType;
childLayer!.debugCreator = child.debugCreator ?? child.runtimeType;
return true;
}());
childContext ??= PaintingContext(child._layer!, child.paintBounds);
childContext ??= PaintingContext(childLayer, child.paintBounds);
child._paintWithContext(childContext, Offset.zero);
// Double-check that the paint method did not replace the layer (the first
// check is done in the [layer] setter itself).
assert(identical(childLayer, child._layer));
assert(identical(childLayer, child._layerHandle.layer));
childContext.stopRecordingIfNeeded();
}
......@@ -209,14 +211,14 @@ class PaintingContext extends ClipContext {
includedParent: true,
includedChild: false,
);
child._layer!.debugCreator = child.debugCreator ?? child;
child._layerHandle.layer!.debugCreator = child.debugCreator ?? child;
return true;
}());
}
assert(child._layer is OffsetLayer);
final OffsetLayer childOffsetLayer = child._layer! as OffsetLayer;
assert(child._layerHandle.layer is OffsetLayer);
final OffsetLayer childOffsetLayer = child._layerHandle.layer! as OffsetLayer;
childOffsetLayer.offset = offset;
appendLayer(child._layer!);
appendLayer(childOffsetLayer);
}
/// Adds a layer to the recording requiring that the recording is already
......@@ -266,6 +268,7 @@ class PaintingContext extends ClipContext {
Canvas get canvas {
if (_canvas == null)
_startRecording();
assert(_currentLayer != null);
return _canvas!;
}
......@@ -391,6 +394,7 @@ class PaintingContext extends ClipContext {
stopRecordingIfNeeded();
appendLayer(childLayer);
final PaintingContext childContext = createChildContext(childLayer, childPaintBounds ?? estimatedBounds);
painter(childContext, offset);
childContext.stopRecordingIfNeeded();
}
......@@ -969,9 +973,9 @@ class PipelineOwner {
_nodesNeedingPaint = <RenderObject>[];
// Sort the dirty nodes in reverse order (deepest first).
for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
assert(node._layer != null);
assert(node._layerHandle.layer != null);
if (node._needsPaint && node.owner == this) {
if (node._layer!.attached) {
if (node._layerHandle.layer!.attached) {
PaintingContext.repaintCompositedChild(node);
} else {
node._skippedPaintingOnLayer();
......@@ -1274,7 +1278,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
@mustCallSuper
void dispose() {
assert(!_debugDisposed);
_layer = null;
_layerHandle.layer = null;
assert(() {
// TODO(dnfield): Enable this assert once clients have had a chance to
// migrate.
......@@ -1478,7 +1482,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
_needsCompositingBitsUpdate = false;
markNeedsCompositingBitsUpdate();
}
if (_needsPaint && _layer != null) {
if (_needsPaint && _layerHandle.layer != null) {
// Don't enter this block if we've never painted at all;
// scheduleInitialPaint() will handle it
_needsPaint = false;
......@@ -2050,14 +2054,22 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// never populating this field, or by setting it to null when the value of
/// [needsCompositing] changes from true to false.
///
/// If a new layer is created and stored in some other field on the render
/// object, the render object must use a [LayerHandle] to store it. A layer
/// handle will prevent the layer from being disposed before the render
/// object is finished with it, and it will also make sure that the layer
/// gets appropriately disposed when the render object creates a replacement
/// or nulls it out. The render object must null out the [LayerHandle.layer]
/// in its [dispose] method.
///
/// If this render object is a repaint boundary, the framework automatically
/// creates an [OffsetLayer] and populates this field prior to calling the
/// [paint] method. The [paint] method must not replace the value of this
/// field.
@protected
ContainerLayer? get layer {
assert(!isRepaintBoundary || (_layer == null || _layer is OffsetLayer));
return _layer;
assert(!isRepaintBoundary || _layerHandle.layer == null || _layerHandle.layer is OffsetLayer);
return _layerHandle.layer;
}
@protected
......@@ -2068,9 +2080,10 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
'The framework creates and assigns an OffsetLayer to a repaint '
'boundary automatically.',
);
_layer = newLayer;
_layerHandle.layer = newLayer;
}
ContainerLayer? _layer;
final LayerHandle<ContainerLayer> _layerHandle = LayerHandle<ContainerLayer>();
/// In debug mode, the compositing layer that this render object uses to repaint.
///
......@@ -2082,7 +2095,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
ContainerLayer? get debugLayer {
ContainerLayer? result;
assert(() {
result = _layer;
result = _layerHandle.layer;
return true;
}());
return result;
......@@ -2218,7 +2231,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
}());
// If we always have our own layer, then we can just repaint
// ourselves without involving any other nodes.
assert(_layer is OffsetLayer);
assert(_layerHandle.layer is OffsetLayer);
if (owner != null) {
owner!._nodesNeedingPaint.add(this);
owner!.requestVisualUpdate();
......@@ -2251,14 +2264,14 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
assert(attached);
assert(isRepaintBoundary);
assert(_needsPaint);
assert(_layer != null);
assert(!_layer!.attached);
assert(_layerHandle.layer != null);
assert(!_layerHandle.layer!.attached);
AbstractNode? node = parent;
while (node is RenderObject) {
if (node.isRepaintBoundary) {
if (node._layer == null)
if (node._layerHandle.layer == null)
break; // looks like the subtree here has never been painted. let it handle itself.
if (node._layer!.attached)
if (node._layerHandle.layer!.attached)
break; // it's the one that detached us, so it's the one that will decide to repaint us.
node._needsPaint = true;
}
......@@ -2278,8 +2291,8 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
assert(parent is! RenderObject);
assert(!owner!._debugDoingPaint);
assert(isRepaintBoundary);
assert(_layer == null);
_layer = rootLayer;
assert(_layerHandle.layer == null);
_layerHandle.layer = rootLayer;
assert(_needsPaint);
owner!._nodesNeedingPaint.add(this);
}
......@@ -2296,9 +2309,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
assert(parent is! RenderObject);
assert(!owner!._debugDoingPaint);
assert(isRepaintBoundary);
assert(_layer != null); // use scheduleInitialPaint the first time
_layer!.detach();
_layer = rootLayer;
assert(_layerHandle.layer != null); // use scheduleInitialPaint the first time
_layerHandle.layer!.detach();
_layerHandle.layer = rootLayer;
markNeedsPaint();
}
......@@ -2388,7 +2401,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
_debugDoingThisPaint = true;
debugLastActivePaint = _debugActivePaint;
_debugActivePaint = this;
assert(!isRepaintBoundary || _layer != null);
assert(!isRepaintBoundary || _layerHandle.layer != null);
return true;
}());
_needsPaint = false;
......@@ -2983,7 +2996,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
properties.add(DiagnosticsProperty<ParentData>('parentData', parentData, tooltip: _debugCanParentUseSize == true ? 'can use size' : null, missingIfNull: true));
properties.add(DiagnosticsProperty<Constraints>('constraints', _constraints, missingIfNull: true));
// don't access it via the "layer" getter since that's only valid when we don't need paint
properties.add(DiagnosticsProperty<ContainerLayer>('layer', _layer, defaultValue: null));
properties.add(DiagnosticsProperty<ContainerLayer>('layer', _layerHandle.layer, defaultValue: null));
properties.add(DiagnosticsProperty<SemanticsNode>('semantics node', _semantics, defaultValue: null));
properties.add(FlagProperty(
'isBlockingSemanticsOfPreviouslyPaintedNodes',
......
......@@ -209,21 +209,27 @@ class RenderAndroidView extends RenderBox with _PlatformViewGestureMixin {
// Clip the texture if it's going to paint out of the bounds of the renter box
// (see comment in _paintTexture for an explanation of when this happens).
if ((size.width < _currentAndroidViewSize.width || size.height < _currentAndroidViewSize.height) && clipBehavior != Clip.none) {
_clipRectLayer = context.pushClipRect(
_clipRectLayer.layer = context.pushClipRect(
true,
offset,
offset & size,
_paintTexture,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer,
oldLayer: _clipRectLayer.layer,
);
return;
}
_clipRectLayer = null;
_clipRectLayer.layer = null;
_paintTexture(context, offset);
}
ClipRectLayer? _clipRectLayer;
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
void _paintTexture(PaintingContext context, Offset offset) {
// As resizing the Android view happens asynchronously we don't know exactly when is a
......
......@@ -117,19 +117,25 @@ class RenderRotatedBox extends RenderBox with RenderObjectWithChildMixin<RenderB
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
_transformLayer = context.pushTransform(
_transformLayer.layer = context.pushTransform(
needsCompositing,
offset,
_paintTransform!,
_paintChild,
oldLayer: _transformLayer,
oldLayer: _transformLayer.layer,
);
} else {
_transformLayer = null;
_transformLayer.layer = null;
}
}
TransformLayer? _transformLayer;
final LayerHandle<TransformLayer> _transformLayer = LayerHandle<TransformLayer>();
@override
void dispose() {
_transformLayer.layer = null;
super.dispose();
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
......
......@@ -801,17 +801,17 @@ class RenderConstraintsTransformBox extends RenderAligningShiftedBox with DebugO
}
if (clipBehavior == Clip.none) {
_clipRectLayer = null;
_clipRectLayer.layer = null;
super.paint(context, offset);
} else {
// We have overflow and the clipBehavior isn't none. Clip it.
_clipRectLayer = context.pushClipRect(
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
super.paint,
clipBehavior: clipBehavior,
oldLayer:_clipRectLayer,
oldLayer: _clipRectLayer.layer,
);
}
......@@ -822,7 +822,13 @@ class RenderConstraintsTransformBox extends RenderAligningShiftedBox with DebugO
}());
}
ClipRectLayer? _clipRectLayer;
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
@override
Rect? describeApproximatePaintClip(RenderObject child) {
......
......@@ -632,21 +632,27 @@ class RenderStack extends RenderBox
@override
void paint(PaintingContext context, Offset offset) {
if (clipBehavior != Clip.none && _hasVisualOverflow) {
_clipRectLayer = context.pushClipRect(
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
paintStack,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer,
oldLayer: _clipRectLayer.layer,
);
} else {
_clipRectLayer = null;
_clipRectLayer.layer = null;
paintStack(context, offset);
}
}
ClipRectLayer? _clipRectLayer;
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
@override
Rect? describeApproximatePaintClip(RenderObject child) => _hasVisualOverflow ? Offset.zero & size : null;
......
......@@ -632,21 +632,27 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
if (firstChild == null)
return;
if (hasVisualOverflow && clipBehavior != Clip.none) {
_clipRectLayer = context.pushClipRect(
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
_paintContents,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer,
oldLayer: _clipRectLayer.layer,
);
} else {
_clipRectLayer = null;
_clipRectLayer.layer = null;
_paintContents(context, offset);
}
}
ClipRectLayer? _clipRectLayer;
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
void _paintContents(PaintingContext context, Offset offset) {
for (final RenderSliver child in childrenInPaintOrder) {
......
......@@ -761,21 +761,27 @@ class RenderWrap extends RenderBox
// TODO(ianh): move the debug flex overflow paint logic somewhere common so
// it can be reused here
if (_hasVisualOverflow && clipBehavior != Clip.none) {
_clipRectLayer = context.pushClipRect(
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
defaultPaint,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer,
oldLayer: _clipRectLayer.layer,
);
} else {
_clipRectLayer = null;
_clipRectLayer.layer = null;
defaultPaint(context, offset);
}
}
ClipRectLayer? _clipRectLayer;
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
......
......@@ -787,21 +787,27 @@ class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox
@override
void paint(PaintingContext context, Offset offset) {
if (_hasVisualOverflow && clipBehavior != Clip.none) {
_clipRectLayer = context.pushClipRect(
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
paintStack,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer,
oldLayer: _clipRectLayer.layer,
);
} else {
_clipRectLayer = null;
_clipRectLayer.layer = null;
paintStack(context, offset);
}
}
ClipRectLayer? _clipRectLayer;
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
......
......@@ -620,22 +620,28 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
}
if (_shouldClipAtPaintOffset(paintOffset) && clipBehavior != Clip.none) {
_clipRectLayer = context.pushClipRect(
_clipRectLayer.layer = context.pushClipRect(
needsCompositing,
offset,
Offset.zero & size,
paintContents,
clipBehavior: clipBehavior,
oldLayer: _clipRectLayer,
oldLayer: _clipRectLayer.layer,
);
} else {
_clipRectLayer = null;
_clipRectLayer.layer = null;
paintContents(context, offset);
}
}
}
ClipRectLayer? _clipRectLayer;
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
void dispose() {
_clipRectLayer.layer = null;
super.dispose();
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
......
......@@ -262,6 +262,7 @@ void main() {
r' owner: RenderView#[0-9a-f]{5}\n'
r' creator: RenderView\n'
r' engine layer: (TransformEngineLayer|PersistedTransform)#[0-9a-f]{5}\n'
r' handles: 1\n'
r' offset: Offset\(0\.0, 0\.0\)\n'
r' transform:\n'
r' \[0] 3\.0,0\.0,0\.0,0\.0\n'
......
......@@ -648,7 +648,7 @@ void main() {
child: box200x200,
);
layout(defaultBox, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
defaultBox.paint(context, Offset.zero);
context.paintChild(defaultBox, Offset.zero);
expect(context.clipBehavior, equals(Clip.none));
for (final Clip clip in Clip.values) {
......@@ -659,7 +659,7 @@ void main() {
clipBehavior: clip,
);
layout(box, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
box.paint(context, Offset.zero);
context.paintChild(box, Offset.zero);
expect(context.clipBehavior, equals(clip));
}
});
......
......@@ -163,9 +163,10 @@ void main() {
);
layout(root);
dynamic error;
final PaintingContext context = PaintingContext(ContainerLayer(), const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0));
try {
s.debugPaint(
PaintingContext(ContainerLayer(), const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)),
context,
const Offset(0.0, 500),
);
} catch (e) {
......@@ -195,9 +196,10 @@ void main() {
);
layout(root);
dynamic error;
final PaintingContext context = PaintingContext(ContainerLayer(), const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0));
try {
s.debugPaint(
PaintingContext(ContainerLayer(), const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)),
context,
const Offset(0.0, 500),
);
} catch (e) {
......
......@@ -47,7 +47,7 @@ void main() {
selection: const TextSelection(baseOffset: 0, extentOffset: 0),
);
layout(defaultEditable, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
defaultEditable.paint(context, Offset.zero);
context.paintChild(defaultEditable, Offset.zero);
expect(context.clipBehavior, equals(Clip.hardEdge));
context.clipBehavior = Clip.none; // Reset as Clip.none won't write into clipBehavior.
......@@ -63,7 +63,7 @@ void main() {
clipBehavior: clip,
);
layout(editable, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
editable.paint(context, Offset.zero);
context.paintChild(editable, Offset.zero);
expect(context.clipBehavior, equals(clip));
}
});
......
......@@ -62,13 +62,13 @@ void main() {
// By default, clipBehavior should be Clip.none
final RenderFlex defaultFlex = RenderFlex(direction: Axis.vertical, children: <RenderBox>[box200x200]);
layout(defaultFlex, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
defaultFlex.paint(context, Offset.zero);
context.paintChild(defaultFlex, Offset.zero);
expect(context.clipBehavior, equals(Clip.none));
for (final Clip clip in Clip.values) {
final RenderFlex flex = RenderFlex(direction: Axis.vertical, children: <RenderBox>[box200x200], clipBehavior: clip);
layout(flex, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
flex.paint(context, Offset.zero);
context.paintChild(flex, Offset.zero);
expect(context.clipBehavior, equals(clip));
}
});
......
......@@ -256,7 +256,7 @@ void main() {
);
});
test('PictureLayer prints picture, engine layer, and raster cache hints in debug info', () {
test('PictureLayer prints picture, raster cache hints in debug info', () {
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.drawPaint(Paint());
......@@ -267,10 +267,20 @@ void main() {
layer.willChangeHint = false;
final List<String> info = _getDebugInfo(layer);
expect(info, contains('picture: ${describeIdentity(picture)}'));
expect(info, contains('engine layer: ${describeIdentity(null)}'));
expect(info, isNot(contains('engine layer: ${describeIdentity(null)}')));
expect(info, contains('raster cache hints: isComplex = true, willChange = false'));
});
test('Layer prints engineLayer if it is not null in debug info', () {
final ConcreteLayer layer = ConcreteLayer();
List<String> info = _getDebugInfo(layer);
expect(info, isNot(contains('engine layer: ${describeIdentity(null)}')));
layer.engineLayer = FakeEngineLayer();
info = _getDebugInfo(layer);
expect(info, contains('engine layer: ${describeIdentity(layer.engineLayer)}'));
});
test('mutating PictureLayer fields triggers needsAddToScene', () {
final PictureLayer pictureLayer = PictureLayer(Rect.zero);
checkNeedsAddToScene(pictureLayer, () {
......@@ -591,6 +601,135 @@ void main() {
// layer.
parent.buildScene(SceneBuilder());
}, skip: isBrowser); // TODO(yjbanov): `toImage` doesn't work on the Web: https://github.com/flutter/flutter/issues/42767
// TODO(dnfield): remove this when https://github.com/flutter/flutter/issues/85066 is resolved.
const bool bug85066 = true;
test('PictureLayer does not let you call dispose unless refcount is 0', () {
PictureLayer layer = PictureLayer(Rect.zero);
expect(layer.debugHandleCount, 0);
layer.dispose();
expect(layer.debugDisposed, true, skip: bug85066);
layer = PictureLayer(Rect.zero);
final LayerHandle<PictureLayer> handle = LayerHandle<PictureLayer>(layer);
expect(layer.debugHandleCount, 1);
expect(() => layer.dispose(), throwsAssertionError);
handle.layer = null;
expect(layer.debugHandleCount, 0);
expect(layer.debugDisposed, true, skip: bug85066);
expect(() => layer.dispose(), throwsAssertionError, skip: bug85066); // already disposed.
});
test('Layer append/remove increases/decreases handle count', () {
final PictureLayer layer = PictureLayer(Rect.zero);
final ContainerLayer parent = ContainerLayer();
expect(layer.debugHandleCount, 0);
expect(layer.debugDisposed, false);
parent.append(layer);
expect(layer.debugHandleCount, 1);
expect(layer.debugDisposed, false);
layer.remove();
expect(layer.debugHandleCount, 0);
expect(layer.debugDisposed, true, skip: bug85066);
});
test('Layer.dispose disposes the engineLayer', () {
final Layer layer = ConcreteLayer();
final FakeEngineLayer engineLayer = FakeEngineLayer();
layer.engineLayer = engineLayer;
expect(engineLayer.disposed, false);
layer.dispose();
expect(engineLayer.disposed, true);
expect(layer.engineLayer, null);
});
test('Layer.engineLayer (set) disposes the engineLayer', () {
final Layer layer = ConcreteLayer();
final FakeEngineLayer engineLayer = FakeEngineLayer();
layer.engineLayer = engineLayer;
expect(engineLayer.disposed, false);
layer.engineLayer = null;
expect(engineLayer.disposed, true);
});
test('PictureLayer.picture (set) disposes the picture', () {
final PictureLayer layer = PictureLayer(Rect.zero);
final FakePicture picture = FakePicture();
layer.picture = picture;
expect(picture.disposed, false);
layer.picture = null;
expect(picture.disposed, true);
});
test('PictureLayer disposes the picture', () {
final PictureLayer layer = PictureLayer(Rect.zero);
final FakePicture picture = FakePicture();
layer.picture = picture;
expect(picture.disposed, false);
layer.dispose();
expect(picture.disposed, true);
});
test('LayerHandle disposes the layer', () {
final ConcreteLayer layer = ConcreteLayer();
final ConcreteLayer layer2 = ConcreteLayer();
expect(layer.debugHandleCount, 0);
expect(layer2.debugHandleCount, 0);
final LayerHandle<ConcreteLayer> holder = LayerHandle<ConcreteLayer>(layer);
expect(layer.debugHandleCount, 1);
expect(layer.debugDisposed, false);
expect(layer2.debugHandleCount, 0);
expect(layer2.debugDisposed, false);
holder.layer = layer;
expect(layer.debugHandleCount, 1);
expect(layer.debugDisposed, false);
expect(layer2.debugHandleCount, 0);
expect(layer2.debugDisposed, false);
holder.layer = layer2;
expect(layer.debugHandleCount, 0);
expect(layer.debugDisposed, true, skip: bug85066);
expect(layer2.debugHandleCount, 1);
expect(layer2.debugDisposed, false);
holder.layer = null;
expect(layer.debugHandleCount, 0);
expect(layer.debugDisposed, true, skip: bug85066);
expect(layer2.debugHandleCount, 0);
expect(layer2.debugDisposed, true, skip: bug85066);
expect(() => holder.layer = layer, throwsAssertionError, skip: bug85066);
});
}
class FakeEngineLayer extends Fake implements EngineLayer {
bool disposed = false;
@override
void dispose() {
assert(!disposed);
disposed = true;
}
}
class FakePicture extends Fake implements Picture {
bool disposed = false;
@override
void dispose() {
assert(!disposed);
disposed = true;
}
}
class ConcreteLayer extends Layer {
@override
void addToScene(SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {}
}
class _TestAlwaysNeedsAddToSceneLayer extends ContainerLayer {
......
// Copyright 2014 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:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
// TODO(dnfield): remove when https://github.com/flutter/flutter/issues/85066 is resolved.
const bool bug85066 = true;
testWidgets('Tracks picture layers accurately when painting is interleaved with a pushLayer', (WidgetTester tester) async {
// Creates a RenderObject that will paint into multiple picture layers.
// Asserts that both layers get a handle, and that all layers get correctly
// released.
final GlobalKey key = GlobalKey();
await tester.pumpWidget(RepaintBoundary(
child: CustomPaint(
key: key,
painter: SimplePainter(),
child: const RepaintBoundary(child: Placeholder()),
foregroundPainter: SimplePainter(),
),
));
final List<Layer> layers = tester.binding.renderView.debugLayer!.depthFirstIterateChildren();
final RenderObject renderObject = key.currentContext!.findRenderObject()!;
for (final Layer layer in layers) {
expect(layer.debugDisposed, false);
}
await tester.pumpWidget(const SizedBox());
for (final Layer layer in layers) {
expect(layer.debugDisposed, true, skip: bug85066);
}
expect(renderObject.debugDisposed, true);
});
}
class SimplePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
canvas.drawPaint(Paint());
}
@override
bool shouldRepaint(SimplePainter oldDelegate) => true;
}
......@@ -305,7 +305,6 @@ class TestClipPaintingContext extends PaintingContext {
ClipRectLayer? oldLayer,
}) {
this.clipBehavior = clipBehavior;
return null;
}
Clip clipBehavior = Clip.none;
......
......@@ -83,7 +83,7 @@ void main() {
parentData.left = parentData.right = 0;
}
layout(stack, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
stack.paint(context, Offset.zero);
context.paintChild(stack, Offset.zero);
expect(context.clipBehavior, equals(clip));
}
});
......
......@@ -209,13 +209,13 @@ void main() {
// By default, clipBehavior should be Clip.none
final RenderWrap defaultWrap = RenderWrap(textDirection: TextDirection.ltr, children: <RenderBox>[box200x200]);
layout(defaultWrap, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
defaultWrap.paint(context, Offset.zero);
context.paintChild(defaultWrap, Offset.zero);
expect(context.clipBehavior, equals(Clip.none));
for (final Clip clip in Clip.values) {
final RenderWrap wrap = RenderWrap(textDirection: TextDirection.ltr, children: <RenderBox>[box200x200], clipBehavior: clip);
layout(wrap, constraints: viewport, phase: EnginePhase.composite, onErrors: expectOverflowedErrors);
wrap.paint(context, Offset.zero);
context.paintChild(wrap, Offset.zero);
expect(context.clipBehavior, equals(clip));
}
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment