Adam Barth e17aa1b63a Add a compositing update phase
We need to compute whether a RenderObject has a composited descendant so that
we can decide whether to use canvas.saveLayer or to create a new composited
layer while walking down the tree during painting.

The compositing update walks the tree from the root only to places where the
tree's structure has been mutated. In the common case during an animation loop,
we won't need to visit any render object beyond the root.
2015-08-14 10:47:39 -07:00

809 lines
27 KiB
Dart

// 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:math' as math;
import 'dart:sky' as sky;
import 'dart:sky' show Point, Offset, Size, Rect, Color, Paint, Path;
import 'package:sky/base/debug.dart';
import 'package:sky/base/hit_test.dart';
import 'package:sky/base/node.dart';
import 'package:sky/base/scheduler.dart' as scheduler;
import 'package:sky/rendering/layer.dart';
import 'package:vector_math/vector_math.dart';
export 'dart:sky' show Point, Offset, Size, Rect, Color, Paint, Path;
export 'package:sky/base/hit_test.dart' show EventDisposition, HitTestTarget, HitTestEntry, HitTestResult;
class ParentData {
void detach() {
detachSiblings();
}
void detachSiblings() { } // workaround for lack of inter-class mixins in Dart
void merge(ParentData other) {
// override this in subclasses to merge in data from other into this
assert(other.runtimeType == this.runtimeType);
}
String toString() => '<none>';
}
class PaintingCanvas extends sky.Canvas {
PaintingCanvas(sky.PictureRecorder recorder, Rect bounds) : super(recorder, bounds);
}
class PaintingContext {
PaintingCanvas canvas;
PictureLayer get layer => _layer;
PictureLayer _layer;
sky.PictureRecorder _recorder;
PaintingContext(Offset offest, Size size) {
_startRecording(offest, size);
}
PaintingContext.forTesting(this.canvas);
void _startRecording(Offset offset, Size size) {
assert(_layer == null);
assert(_recorder == null);
assert(canvas == null);
_layer = new PictureLayer(offset: offset, size: size);
_recorder = new sky.PictureRecorder();
canvas = new PaintingCanvas(_recorder, Point.origin & size);
}
void endRecording() {
assert(_layer != null);
assert(_recorder != null);
assert(canvas != null);
canvas = null;
_layer.picture = _recorder.endRecording();
_recorder = null;
_layer = null;
}
void paintChild(RenderObject child, Point point) {
final Offset offset = point.toOffset();
if (!child.requiresCompositing)
return child._paintWithContext(this, offset);
_compositChild(child, offset, layer.parent, layer.nextSibling);
}
void _compositChild(RenderObject child, Offset offset, ContainerLayer parentLayer, Layer nextSibling) {
final PictureLayer originalLayer = _layer;
endRecording();
Rect childBounds = child.paintBounds;
Offset childOffset = childBounds.topLeft.toOffset();
PaintingContext context = new PaintingContext(offset + childOffset, childBounds.size);
parentLayer.add(context.layer, before: nextSibling);
child._layer = context.layer;
child._paintWithContext(context, -childOffset);
_startRecording(originalLayer.offset, originalLayer.size);
originalLayer.parent.add(layer, before: context.layer.nextSibling);
context.endRecording();
}
}
abstract class Constraints {
const Constraints();
bool get isTight;
}
typedef void RenderObjectVisitor(RenderObject child);
typedef void LayoutCallback(Constraints constraints);
abstract class RenderObject extends AbstractNode implements HitTestTarget {
// LAYOUT
// parentData is only for use by the RenderObject that actually lays this
// node out, and any other nodes who happen to know exactly what
// kind of node that is.
dynamic parentData; // TODO(ianh): change the type of this back to ParentData once the analyzer is cleverer
void setupParentData(RenderObject child) {
// override this to setup .parentData correctly for your class
assert(debugCanPerformMutations);
if (child.parentData is! ParentData)
child.parentData = new ParentData();
}
void adoptChild(RenderObject child) { // only for use by subclasses
// call this whenever you decide a node is a child
assert(debugCanPerformMutations);
assert(child != null);
setupParentData(child);
super.adoptChild(child);
markNeedsLayout();
markNeedsCompositingUpdate();
}
void dropChild(RenderObject child) { // only for use by subclasses
assert(debugCanPerformMutations);
assert(child != null);
assert(child.parentData != null);
child._cleanRelayoutSubtreeRoot();
child.parentData.detach();
super.dropChild(child);
markNeedsLayout();
markNeedsCompositingUpdate();
}
// Override in subclasses with children and call the visitor for each child.
void visitChildren(RenderObjectVisitor visitor) { }
static bool _debugDoingLayout = false;
static bool get debugDoingLayout => _debugDoingLayout;
bool _debugDoingThisResize = false;
bool get debugDoingThisResize => _debugDoingThisResize;
bool _debugDoingThisLayout = false;
bool get debugDoingThisLayout => _debugDoingThisLayout;
static RenderObject _debugActiveLayout = null;
static RenderObject get debugActiveLayout => _debugActiveLayout;
bool _debugDoingThisLayoutWithCallback = false;
bool _debugMutationsLocked = false;
bool _debugCanParentUseSize;
bool get debugCanParentUseSize => _debugCanParentUseSize;
bool get debugCanPerformMutations {
RenderObject node = this;
while (true) {
if (node._debugDoingThisLayoutWithCallback)
return true;
if (node._debugMutationsLocked)
return false;
if (node.parent is! RenderObject)
return true;
node = node.parent;
}
}
static List<RenderObject> _nodesNeedingLayout = new List<RenderObject>();
bool _needsLayout = true;
bool get needsLayout => _needsLayout;
RenderObject _relayoutSubtreeRoot;
Constraints _constraints;
Constraints get constraints => _constraints;
bool debugDoesMeetConstraints(); // override this in a subclass to verify that your state matches the constraints object
bool debugAncestorsAlreadyMarkedNeedsLayout() {
if (_relayoutSubtreeRoot == null)
return true; // we haven't yet done layout even once, so there's nothing for us to do
RenderObject node = this;
while (node != _relayoutSubtreeRoot) {
assert(node._relayoutSubtreeRoot == _relayoutSubtreeRoot);
assert(node.parent != null);
node = node.parent as RenderObject;
if (!node._needsLayout)
return false;
}
assert(node._relayoutSubtreeRoot == node);
return true;
}
void markNeedsLayout() {
assert(debugCanPerformMutations);
if (_needsLayout) {
assert(debugAncestorsAlreadyMarkedNeedsLayout());
return;
}
_needsLayout = true;
assert(_relayoutSubtreeRoot != null);
if (_relayoutSubtreeRoot != this) {
final parent = this.parent; // TODO(ianh): Remove this once the analyzer is cleverer
assert(parent is RenderObject);
parent.markNeedsLayout();
assert(parent == this.parent); // TODO(ianh): Remove this once the analyzer is cleverer
} else {
_nodesNeedingLayout.add(this);
scheduler.ensureVisualUpdate();
}
}
void _cleanRelayoutSubtreeRoot() {
if (_relayoutSubtreeRoot != this) {
_relayoutSubtreeRoot = null;
_needsLayout = true;
visitChildren((RenderObject child) {
child._cleanRelayoutSubtreeRoot();
});
}
}
void scheduleInitialLayout() {
assert(attached);
assert(parent == null);
assert(_relayoutSubtreeRoot == null);
_relayoutSubtreeRoot = this;
assert(() {
_debugCanParentUseSize = false;
return true;
});
_nodesNeedingLayout.add(this);
scheduler.ensureVisualUpdate();
}
static void flushLayout() {
sky.tracing.begin('RenderObject.flushLayout');
_debugDoingLayout = true;
try {
List<RenderObject> dirtyNodes = _nodesNeedingLayout;
_nodesNeedingLayout = new List<RenderObject>();
dirtyNodes..sort((a, b) => a.depth - b.depth)..forEach((node) {
if (node._needsLayout && node.attached)
node.layoutWithoutResize();
});
} finally {
_debugDoingLayout = false;
sky.tracing.end('RenderObject.flushLayout');
}
}
void layoutWithoutResize() {
try {
assert(_relayoutSubtreeRoot == this);
RenderObject debugPreviousActiveLayout;
assert(!_debugMutationsLocked);
assert(!_debugDoingThisLayoutWithCallback);
assert(_debugCanParentUseSize != null);
assert(() {
_debugMutationsLocked = true;
_debugDoingThisLayout = true;
debugPreviousActiveLayout = _debugActiveLayout;
_debugActiveLayout = this;
return true;
});
performLayout();
assert(() {
_debugActiveLayout = debugPreviousActiveLayout;
_debugDoingThisLayout = false;
_debugMutationsLocked = false;
return true;
});
} catch (e) {
print('Exception raised during layout:\n${e}\nContext:\n${this}');
if (inDebugBuild)
rethrow;
return;
}
_needsLayout = false;
markNeedsPaint();
}
void layout(Constraints constraints, { bool parentUsesSize: false }) {
final parent = this.parent; // TODO(ianh): Remove this once the analyzer is cleverer
RenderObject relayoutSubtreeRoot;
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject)
relayoutSubtreeRoot = this;
else
relayoutSubtreeRoot = parent._relayoutSubtreeRoot;
assert(parent == this.parent); // TODO(ianh): Remove this once the analyzer is cleverer
if (!needsLayout && constraints == _constraints && relayoutSubtreeRoot == _relayoutSubtreeRoot)
return;
_constraints = constraints;
_relayoutSubtreeRoot = relayoutSubtreeRoot;
assert(!_debugMutationsLocked);
assert(!_debugDoingThisLayoutWithCallback);
assert(() {
_debugMutationsLocked = true;
_debugCanParentUseSize = parentUsesSize;
return true;
});
if (sizedByParent) {
assert(() { _debugDoingThisResize = true; return true; });
performResize();
assert(() { _debugDoingThisResize = false; return true; });
}
RenderObject debugPreviousActiveLayout;
assert(() {
_debugDoingThisLayout = true;
debugPreviousActiveLayout = _debugActiveLayout;
_debugActiveLayout = this;
return true;
});
performLayout();
assert(() {
_debugActiveLayout = debugPreviousActiveLayout;
_debugDoingThisLayout = false;
_debugMutationsLocked = false;
return true;
});
assert(debugDoesMeetConstraints());
_needsLayout = false;
markNeedsPaint();
assert(parent == this.parent); // TODO(ianh): Remove this once the analyzer is cleverer
}
bool get sizedByParent => false; // return true if the constraints are the only input to the sizing algorithm (in particular, child nodes have no impact)
void performResize(); // set the local dimensions, using only the constraints (only called if sizedByParent is true)
void performLayout();
// Override this to perform relayout without your parent's
// involvement.
//
// This is called during layout. If sizedByParent is true, then
// performLayout() should not change your dimensions, only do that
// in performResize(). If sizedByParent is false, then set both
// your dimensions and do your children's layout here.
//
// When calling layout() on your children, pass in
// "parentUsesSize: true" if your size or layout is dependent on
// your child's size or intrinsic dimensions.
void invokeLayoutCallback(LayoutCallback callback) {
assert(_debugMutationsLocked);
assert(_debugDoingThisLayout);
assert(!_debugDoingThisLayoutWithCallback);
assert(() {
_debugDoingThisLayoutWithCallback = true;
return true;
});
callback(constraints);
assert(() {
_debugDoingThisLayoutWithCallback = false;
return true;
});
}
// when the parent has rotated (e.g. when the screen has been turned
// 90 degrees), immediately prior to layout() being called for the
// new dimensions, rotate() is called with the old and new angles.
// The next time paint() is called, the coordinate space will have
// been rotated N quarter-turns clockwise, where:
// N = newAngle-oldAngle
// ...but the rendering is expected to remain the same, pixel for
// pixel, on the output device. Then, the layout() method or
// equivalent will be invoked.
void rotate({
int oldAngle, // 0..3
int newAngle, // 0..3
Duration time
}) { }
// PAINTING
static bool _debugDoingPaint = false;
static bool get debugDoingPaint => _debugDoingPaint;
static void set debugDoingPaint(bool value) {
_debugDoingPaint = value;
}
bool _debugDoingThisPaint = false;
bool get debugDoingThisPaint => _debugDoingThisPaint;
static RenderObject _debugActivePaint = null;
static RenderObject get debugActivePaint => _debugActivePaint;
static List<RenderObject> _nodesNeedingPaint = new List<RenderObject>();
PictureLayer _layer;
PictureLayer get layer {
assert(requiresCompositing);
return _layer;
}
bool get requiresCompositing => false;
bool _hasCompositedDescendant = false;
bool get hasCompositedDescendant {
assert(!_needsCompositingUpdate);
return _hasCompositedDescendant;
}
bool _needsCompositingUpdate = false;
void markNeedsCompositingUpdate() {
if (_needsCompositingUpdate)
return;
_needsCompositingUpdate = true;
final AbstractNode parent = this.parent;
if (parent is RenderObject)
parent.markNeedsCompositingUpdate();
}
void updateCompositing() {
if (!_needsCompositingUpdate)
return;
visitChildren((RenderObject child) {
child.updateCompositing();
if (child.hasCompositedDescendant)
_hasCompositedDescendant = true;
});
if (requiresCompositing)
_hasCompositedDescendant = true;
_needsCompositingUpdate = false;
}
bool _needsPaint = true;
bool get needsPaint => _needsPaint;
void markNeedsPaint() {
assert(!debugDoingPaint);
if (!attached) return; // Don't try painting things that aren't in the hierarchy
if (_needsPaint) return;
if (requiresCompositing) {
_needsPaint = true;
_nodesNeedingPaint.add(this);
scheduler.ensureVisualUpdate();
} else if (parent == null) {
_needsPaint = true;
scheduler.ensureVisualUpdate();
} else {
assert(parent != null); // parent always exists on this path because the root node is a RenderView, which sets createNewDisplayList.
if (parent is RenderObject) {
(parent as RenderObject).markNeedsPaint(); // TODO(ianh): remove the cast once the analyzer is cleverer
}
}
}
static void flushPaint() {
sky.tracing.begin('RenderObject.flushPaint');
_debugDoingPaint = true;
try {
List<RenderObject> dirtyNodes = _nodesNeedingPaint;
_nodesNeedingPaint = new List<RenderObject>();
for (RenderObject node in dirtyNodes..sort((a, b) => a.depth - b.depth)) {
if (node._needsPaint && node.attached)
node._repaint();
};
assert(_nodesNeedingPaint.length == 0);
} finally {
_debugDoingPaint = false;
sky.tracing.end('RenderObject.flushPaint');
}
}
void _repaint() {
assert(!_needsLayout);
assert(requiresCompositing);
assert(_layer != null);
// TODO(abarth): Using _layer.offset isn't correct if the topLeft of our
// paint bounds has changed since our last repaint.
PaintingContext context = new PaintingContext(_layer.offset, paintBounds.size);
_layer.parent.add(context.layer, before: _layer);
_layer.detach();
_layer = context._layer;
_needsPaint = false;
try {
_paintWithContext(context, Offset.zero);
} catch (e) {
print('Exception raised during _repaint:\n${e}\nContext:\n${this}');
if (inDebugBuild)
rethrow;
return;
}
assert(!_needsLayout); // check that the paint() method didn't mark us dirty again
assert(!_needsPaint); // check that the paint() method didn't mark us dirty again
}
void _paintWithContext(PaintingContext context, Offset offset) {
_needsPaint = false;
assert(!_debugDoingThisPaint);
RenderObject debugLastActivePaint;
assert(() {
_debugDoingThisPaint = true;
debugLastActivePaint = _debugActivePaint;
_debugActivePaint = this;
debugPaint(context, offset);
if (debugPaintBoundsEnabled) {
context.canvas.save();
context.canvas.clipRect(paintBounds.shift(offset));
}
return true;
});
paint(context, offset);
assert(() {
if (debugPaintBoundsEnabled)
context.canvas.restore();
_debugActivePaint = debugLastActivePaint;
_debugDoingThisPaint = false;
return true;
});
assert(!_needsPaint);
}
Rect get paintBounds;
void debugPaint(PaintingContext context, Offset offset) { }
void paint(PaintingContext context, Offset offset) { }
void applyPaintTransform(Matrix4 transform) { }
// EVENTS
EventDisposition handleEvent(sky.Event event, HitTestEntry entry) {
// override this if you have a client, to hand it to the client
// override this if you want to do anything with the event
return EventDisposition.ignored;
}
// HIT TESTING
// RenderObject subclasses are expected to have a method like the
// following (with the signature being whatever passes for coordinates
// for this particular class):
// bool hitTest(HitTestResult result, { Point position }) {
// // If (x,y) is not inside this node, then return false. (You
// // can assume that the given coordinate is inside your
// // dimensions. You only need to check this if you're an
// // irregular shape, e.g. if you have a hole.)
// // Otherwise:
// // For each child that intersects x,y, in z-order starting from the top,
// // call hitTest() for that child, passing it /result/, and the coordinates
// // converted to the child's coordinate origin, and stop at the first child
// // that returns true.
// // Then, add yourself to /result/, and return true.
// }
// You must not add yourself to /result/ if you return false.
String toString([String prefix = '']) {
RenderObject debugPreviousActiveLayout = _debugActiveLayout;
_debugActiveLayout = null;
String header = '${runtimeType}';
if (_relayoutSubtreeRoot != null && _relayoutSubtreeRoot != this) {
int count = 1;
RenderObject target = parent;
while (target != null && target != _relayoutSubtreeRoot) {
target = target.parent as RenderObject;
count += 1;
}
header += ' relayoutSubtreeRoot=up$count';
}
if (_needsLayout)
header += ' NEEDS-LAYOUT';
if (!attached)
header += ' DETACHED';
prefix += ' ';
String result = '${header}\n${debugDescribeSettings(prefix)}${debugDescribeChildren(prefix)}';
_debugActiveLayout = debugPreviousActiveLayout;
return result;
}
String debugDescribeSettings(String prefix) => '${prefix}parentData: ${parentData}\n${prefix}constraints: ${constraints}\n';
String debugDescribeChildren(String prefix) => '';
}
double clamp({ double min: 0.0, double value: 0.0, double max: double.INFINITY }) {
assert(min != null);
assert(value != null);
assert(max != null);
return math.max(min, math.min(max, value));
}
// GENERIC MIXIN FOR RENDER NODES WITH ONE CHILD
abstract class RenderObjectWithChildMixin<ChildType extends RenderObject> implements RenderObject {
ChildType _child;
ChildType get child => _child;
void set child (ChildType value) {
if (_child != null)
dropChild(_child);
_child = value;
if (_child != null)
adoptChild(_child);
}
void attachChildren() {
if (_child != null)
_child.attach();
}
void detachChildren() {
if (_child != null)
_child.detach();
}
void visitChildren(RenderObjectVisitor visitor) {
if (_child != null)
visitor(_child);
}
String debugDescribeChildren(String prefix) {
if (child != null)
return '${prefix}child: ${child.toString(prefix)}';
return '';
}
}
// GENERIC MIXIN FOR RENDER NODES WITH A LIST OF CHILDREN
abstract class ContainerParentDataMixin<ChildType extends RenderObject> {
ChildType previousSibling;
ChildType nextSibling;
void detachSiblings() {
if (previousSibling != null) {
assert(previousSibling.parentData is ContainerParentDataMixin<ChildType>);
assert(previousSibling != this);
assert(previousSibling.parentData.nextSibling == this);
previousSibling.parentData.nextSibling = nextSibling;
}
if (nextSibling != null) {
assert(nextSibling.parentData is ContainerParentDataMixin<ChildType>);
assert(nextSibling != this);
assert(nextSibling.parentData.previousSibling == this);
nextSibling.parentData.previousSibling = previousSibling;
}
previousSibling = null;
nextSibling = null;
}
}
abstract class ContainerRenderObjectMixin<ChildType extends RenderObject, ParentDataType extends ContainerParentDataMixin<ChildType>> implements RenderObject {
bool _debugUltimatePreviousSiblingOf(ChildType child, { ChildType equals }) {
assert(child.parentData is ParentDataType);
while (child.parentData.previousSibling != null) {
assert(child.parentData.previousSibling != child);
child = child.parentData.previousSibling;
assert(child.parentData is ParentDataType);
}
return child == equals;
}
bool _debugUltimateNextSiblingOf(ChildType child, { ChildType equals }) {
assert(child.parentData is ParentDataType);
while (child.parentData.nextSibling != null) {
assert(child.parentData.nextSibling != child);
child = child.parentData.nextSibling;
assert(child.parentData is ParentDataType);
}
return child == equals;
}
int _childCount = 0;
int get childCount => _childCount;
ChildType _firstChild;
ChildType _lastChild;
void _addToChildList(ChildType child, { ChildType before }) {
assert(child.parentData is ParentDataType);
assert(child.parentData.nextSibling == null);
assert(child.parentData.previousSibling == null);
_childCount += 1;
assert(_childCount > 0);
if (before == null) {
// append at the end (_lastChild)
child.parentData.previousSibling = _lastChild;
if (_lastChild != null) {
assert(_lastChild.parentData is ParentDataType);
_lastChild.parentData.nextSibling = child;
}
_lastChild = child;
if (_firstChild == null)
_firstChild = child;
} else {
assert(_firstChild != null);
assert(_lastChild != null);
assert(_debugUltimatePreviousSiblingOf(before, equals: _firstChild));
assert(_debugUltimateNextSiblingOf(before, equals: _lastChild));
assert(before.parentData is ParentDataType);
if (before.parentData.previousSibling == null) {
// insert at the start (_firstChild); we'll end up with two or more children
assert(before == _firstChild);
child.parentData.nextSibling = before;
before.parentData.previousSibling = child;
_firstChild = child;
} else {
// insert in the middle; we'll end up with three or more children
// set up links from child to siblings
child.parentData.previousSibling = before.parentData.previousSibling;
child.parentData.nextSibling = before;
// set up links from siblings to child
assert(child.parentData.previousSibling.parentData is ParentDataType);
assert(child.parentData.nextSibling.parentData is ParentDataType);
child.parentData.previousSibling.parentData.nextSibling = child;
child.parentData.nextSibling.parentData.previousSibling = child;
assert(before.parentData.previousSibling == child);
}
}
}
void add(ChildType child, { ChildType before }) {
assert(child != this);
assert(before != this);
assert(child != before);
assert(child != _firstChild);
assert(child != _lastChild);
adoptChild(child);
_addToChildList(child, before: before);
}
void addAll(List<ChildType> children) {
if (children != null)
for (ChildType child in children)
add(child);
}
void _removeFromChildList(ChildType child) {
assert(child.parentData is ParentDataType);
assert(_debugUltimatePreviousSiblingOf(child, equals: _firstChild));
assert(_debugUltimateNextSiblingOf(child, equals: _lastChild));
assert(_childCount >= 0);
if (child.parentData.previousSibling == null) {
assert(_firstChild == child);
_firstChild = child.parentData.nextSibling;
} else {
assert(child.parentData.previousSibling.parentData is ParentDataType);
child.parentData.previousSibling.parentData.nextSibling = child.parentData.nextSibling;
}
if (child.parentData.nextSibling == null) {
assert(_lastChild == child);
_lastChild = child.parentData.previousSibling;
} else {
assert(child.parentData.nextSibling.parentData is ParentDataType);
child.parentData.nextSibling.parentData.previousSibling = child.parentData.previousSibling;
}
child.parentData.previousSibling = null;
child.parentData.nextSibling = null;
_childCount -= 1;
}
void remove(ChildType child) {
_removeFromChildList(child);
dropChild(child);
}
void removeAll() {
ChildType child = _firstChild;
while (child != null) {
assert(child.parentData is ParentDataType);
ChildType next = child.parentData.nextSibling;
child.parentData.previousSibling = null;
child.parentData.nextSibling = null;
dropChild(child);
child = next;
}
_firstChild = null;
_lastChild = null;
_childCount = 0;
}
void move(ChildType child, { ChildType before }) {
assert(child != this);
assert(before != this);
assert(child != before);
assert(child.parent == this);
assert(child.parentData is ParentDataType);
if (child.parentData.nextSibling == before)
return;
_removeFromChildList(child);
_addToChildList(child, before: before);
}
void redepthChildren() {
ChildType child = _firstChild;
while (child != null) {
redepthChild(child);
assert(child.parentData is ParentDataType);
child = child.parentData.nextSibling;
}
}
void attachChildren() {
ChildType child = _firstChild;
while (child != null) {
child.attach();
assert(child.parentData is ParentDataType);
child = child.parentData.nextSibling;
}
}
void detachChildren() {
ChildType child = _firstChild;
while (child != null) {
child.detach();
assert(child.parentData is ParentDataType);
child = child.parentData.nextSibling;
}
}
void visitChildren(RenderObjectVisitor visitor) {
ChildType child = _firstChild;
while (child != null) {
visitor(child);
assert(child.parentData is ParentDataType);
child = child.parentData.nextSibling;
}
}
ChildType get firstChild => _firstChild;
ChildType get lastChild => _lastChild;
ChildType childAfter(ChildType child) {
assert(child.parentData is ParentDataType);
return child.parentData.nextSibling;
}
String debugDescribeChildren(String prefix) {
String result = '';
int count = 1;
ChildType child = _firstChild;
while (child != null) {
result += '${prefix}child ${count}: ${child.toString(prefix)}';
count += 1;
child = child.parentData.nextSibling;
}
return result;
}
}