// 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); // TODO(ianh): Just use sky.Canvas everywhere instead } class PaintingContext { // A PaintingContext wraps a canvas, so that the canvas can be // hot-swapped whenever we need to start a new layer. // Don't keep a reference to the PaintingContext.canvas, since it // can change dynamically after any call to this object's methods. PaintingContext.withOffset(Offset offset, Rect paintBounds) { _containerLayer = new ContainerLayer(offset: offset); _startRecording(paintBounds); } PaintingContext.withLayer(ContainerLayer containerLayer, Rect paintBounds) { _containerLayer = containerLayer; _startRecording(paintBounds); } factory PaintingContext.replacingLayer(ContainerLayer oldLayer, Rect paintBounds) { PaintingContext newContext = new PaintingContext.withOffset(oldLayer.offset, paintBounds); if (oldLayer.parent != null) oldLayer.replaceWith(newContext._containerLayer); return newContext; } PaintingContext.forTesting(this._canvas); ContainerLayer _containerLayer; ContainerLayer get containerLayer => _containerLayer; PictureLayer _currentLayer; sky.PictureRecorder _recorder; PaintingCanvas _canvas; PaintingCanvas get canvas => _canvas; // Paint on this. void _startRecording(Rect paintBounds) { assert(_currentLayer == null); assert(_recorder == null); assert(_canvas == null); _currentLayer = new PictureLayer(paintBounds: paintBounds); _recorder = new sky.PictureRecorder(); _canvas = new PaintingCanvas(_recorder, paintBounds); _containerLayer.add(_currentLayer); } void endRecording() { assert(_currentLayer != null); assert(_recorder != null); assert(_canvas != null); _currentLayer.picture = _recorder.endRecording(); _currentLayer = null; _recorder = null; _canvas = null; } bool debugCanPaintChild(RenderObject child) { // You need to use layers if you are applying transforms, clips, // or similar, to a child. To do so, use the paintChildWith*() // methods below. // (commented out for now because we haven't ported everything yet) assert(canvas.getSaveCount() == 1 || !child.needsCompositing); return true; } void paintChild(RenderObject child, Point childPosition) { assert(debugCanPaintChild(child)); final Offset childOffset = childPosition.toOffset(); if (!child.hasLayer) { insertChild(child, childOffset); } else { compositeChild(child, childOffset: childOffset, parentLayer: _containerLayer); } } // Below we have various variants of the paintChild() method, which // do additional work, such as clipping or transforming, at the same // time as painting the children. // If none of the descendants require compositing, then these don't // need to use a new layer, because at no point will any of the // children introduce a new layer of their own. In that case, we // just use regular canvas commands to do the work. // If at least one of the descendants requires compositing, though, // we introduce a new layer to do the work, so that when the // children are split into a new layer, the work (e.g. clip) is not // lost, as it would if we didn't introduce a new layer. static final Paint _disableAntialias = new Paint()..isAntiAlias = false; void paintChildWithClipRect(RenderObject child, Point childPosition, Rect clipRect) { // clipRect is in the parent's coordinate space assert(debugCanPaintChild(child)); final Offset childOffset = childPosition.toOffset(); if (!child.needsCompositing) { canvas.save(); canvas.clipRect(clipRect); insertChild(child, childOffset); canvas.restore(); } else { ClipRectLayer clipLayer = new ClipRectLayer(offset: childOffset, clipRect: clipRect); _containerLayer.add(clipLayer); compositeChild(child, parentLayer: clipLayer); } } void paintChildWithClipRRect(RenderObject child, Point childPosition, Rect bounds, sky.RRect clipRRect) { // clipRRect is in the parent's coordinate space assert(debugCanPaintChild(child)); final Offset childOffset = childPosition.toOffset(); if (!child.needsCompositing) { canvas.saveLayer(bounds, _disableAntialias); canvas.clipRRect(clipRRect); insertChild(child, childOffset); canvas.restore(); } else { ClipRRectLayer clipLayer = new ClipRRectLayer(offset: childOffset, bounds: bounds, clipRRect: clipRRect); _containerLayer.add(clipLayer); compositeChild(child, parentLayer: clipLayer); } } void paintChildWithClipPath(RenderObject child, Point childPosition, Rect bounds, Path clipPath) { // bounds and clipPath are in the parent's coordinate space assert(debugCanPaintChild(child)); final Offset childOffset = childPosition.toOffset(); if (!child.needsCompositing) { canvas.saveLayer(bounds, _disableAntialias); canvas.clipPath(clipPath); canvas.translate(childOffset.dx, childOffset.dy); insertChild(child, Offset.zero); canvas.restore(); } else { ClipPathLayer clipLayer = new ClipPathLayer(offset: childOffset, bounds: bounds, clipPath: clipPath); _containerLayer.add(clipLayer); compositeChild(child, parentLayer: clipLayer); } } void paintChildWithTransform(RenderObject child, Point childPosition, Matrix4 transform) { assert(debugCanPaintChild(child)); final Offset childOffset = childPosition.toOffset(); if (!child.needsCompositing) { canvas.save(); canvas.translate(childOffset.dx, childOffset.dy); canvas.concat(transform.storage); insertChild(child, Offset.zero); canvas.restore(); } else { TransformLayer transformLayer = new TransformLayer(offset: childOffset, transform: transform); _containerLayer.add(transformLayer); compositeChild(child, parentLayer: transformLayer); } } void paintChildWithOpacity(RenderObject child, Point childPosition, Rect bounds, int alpha) { assert(debugCanPaintChild(child)); final Offset childOffset = childPosition.toOffset(); if (!child.needsCompositing) { canvas.saveLayer(bounds, OpacityLayer.paintForAlpha(alpha)); canvas.translate(childOffset.dx, childOffset.dy); insertChild(child, Offset.zero); canvas.restore(); } else { OpacityLayer paintLayer = new OpacityLayer( offset: childOffset, bounds: bounds, alpha: alpha); _containerLayer.add(paintLayer); compositeChild(child, parentLayer: paintLayer); } } void paintChildWithColorFilter(RenderObject child, Point childPosition, Rect bounds, Color color, sky.TransferMode transferMode) { assert(debugCanPaintChild(child)); final Offset childOffset = childPosition.toOffset(); if (!child.needsCompositing) { canvas.saveLayer(bounds, ColorFilterLayer.paintForColorFilter(color, transferMode)); canvas.translate(childOffset.dx, childOffset.dy); insertChild(child, Offset.zero); canvas.restore(); } else { ColorFilterLayer paintLayer = new ColorFilterLayer( offset: childOffset, bounds: bounds, color: color, transferMode: transferMode); _containerLayer.add(paintLayer); compositeChild(child, parentLayer: paintLayer); } } // do not call directly void insertChild(RenderObject child, Offset offset) { child._paintWithContext(this, offset); } // do not call directly void compositeChild(RenderObject child, { Offset childOffset: Offset.zero, ContainerLayer parentLayer }) { // This ends the current layer and starts a new layer for the // remainder of our rendering. It also creates a new layer for the // child, and inserts that layer into the given parentLayer, which // must either be our current layer's parent layer, or at least // must have our current layer's parent layer as an ancestor. final PictureLayer originalLayer = _currentLayer; assert(() { assert(parentLayer != null); assert(originalLayer != null); assert(originalLayer.parent != null); ContainerLayer ancestor = parentLayer; while (ancestor != null && ancestor != originalLayer.parent) ancestor = ancestor.parent; assert(ancestor == originalLayer.parent); assert(originalLayer.parent == _containerLayer); return true; }); // End our current layer. endRecording(); // Create a layer for our child, and paint the child into it. if (child.needsPaint || !child.hasLayer) { PaintingContext newContext = new PaintingContext.withOffset(childOffset, child.paintBounds); child._layer = newContext.containerLayer; child._paintWithContext(newContext, Offset.zero); newContext.endRecording(); } else { assert(child._layer != null); child._layer.detach(); child._layer.offset = childOffset; } parentLayer.add(child._layer); // Start a new layer for anything that remains of our own paint. _startRecording(originalLayer.paintBounds); } } 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(); markNeedsCompositingBitsUpdate(); } 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(); markNeedsCompositingBitsUpdate(); } // 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 is! RenderObject); 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>(); // Override this in subclasses to indicate that instances of your // class need to have their own Layer. For example, videos. bool get hasLayer => false; ContainerLayer _layer; ContainerLayer get layer { assert(hasLayer); assert(!_needsPaint); return _layer; } // When the subtree is mutated, we need to recompute our // "needsCompositing" bit, and our ancestors need to do the // same (in case ours changed). adoptChild() and dropChild() thus // call markNeedsCompositingBitsUpdate(). bool _needsCompositingBitsUpdate = true; void markNeedsCompositingBitsUpdate() { if (_needsCompositingBitsUpdate) return; _needsCompositingBitsUpdate = true; final AbstractNode parent = this.parent; // TODO(ianh): remove the once the analyzer is cleverer if (parent is RenderObject) parent.markNeedsCompositingBitsUpdate(); } bool _needsCompositing = false; bool get needsCompositing { // needsCompositing is true if either we have a layer or one of our descendants has a layer assert(!_needsCompositingBitsUpdate); // make sure we don't use this bit when it is dirty return _needsCompositing; } void updateCompositingBits() { if (!_needsCompositingBitsUpdate) return; bool didHaveCompositedDescendant = _needsCompositing; visitChildren((RenderObject child) { child.updateCompositingBits(); if (child.needsCompositing) _needsCompositing = true; }); if (hasLayer) _needsCompositing = true; if (didHaveCompositedDescendant != _needsCompositing) markNeedsPaint(); _needsCompositingBitsUpdate = 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 (hasLayer) { // If we always have our own layer, then we can just repaint // ourselves without involving any other nodes. assert(_layer != null); _needsPaint = true; _nodesNeedingPaint.add(this); scheduler.ensureVisualUpdate(); } else if (parent is RenderObject) { // We don't have our own layer; one of our ancestors will take // care of updating the layer we're in and when they do that // we'll get our paint() method called. assert(_layer == null); (parent as RenderObject).markNeedsPaint(); // TODO(ianh): remove the cast once the analyzer is cleverer } else { // If we're the root of the render tree (probably a RenderView), // then we have to paint ourselves, since nobody else can paint // us. We don't add ourselves to _nodesNeedingPaint in this // case, because the root is always told to paint regardless. _needsPaint = true; scheduler.ensureVisualUpdate(); } } static void flushPaint() { sky.tracing.begin('RenderObject.flushPaint'); _debugDoingPaint = true; try { List<RenderObject> dirtyNodes = _nodesNeedingPaint; _nodesNeedingPaint = new List<RenderObject>(); // Sort the dirty nodes in reverse order (deepest first). for (RenderObject node in dirtyNodes..sort((a, b) => b.depth - a.depth)) { assert(node._needsPaint); if (node.attached) node._repaint(); }; assert(_nodesNeedingPaint.length == 0); } finally { _debugDoingPaint = false; sky.tracing.end('RenderObject.flushPaint'); } } void initialPaint(ContainerLayer rootLayer, Size size) { assert(attached); assert(parent is! RenderObject); assert(!_debugDoingPaint); assert(hasLayer); PaintingContext newContext = new PaintingContext.withLayer(rootLayer, Point.origin & size); _paintLayer(newContext); } void _repaint() { assert(hasLayer); assert(_layer != null); PaintingContext newContext = new PaintingContext.replacingLayer(_layer, paintBounds); _paintLayer(newContext); } void _paintLayer(PaintingContext context) { _layer = context._containerLayer; try { _paintWithContext(context, Offset.zero); context.endRecording(); } catch (e) { print('Exception raised during _paintLayer:\n${e}\nContext:\n${this}'); if (inDebugBuild) rethrow; return; } } void _paintWithContext(PaintingContext context, Offset offset) { assert(!_debugDoingThisPaint); assert(!_needsLayout); assert(!_needsCompositingBitsUpdate); RenderObject debugLastActivePaint; assert(() { _debugDoingThisPaint = true; debugLastActivePaint = _debugActivePaint; _debugActivePaint = this; debugPaint(context, offset); if (debugPaintBoundsEnabled) { context.canvas.save(); context.canvas.clipRect(paintBounds.shift(offset)); } assert(!hasLayer || _layer != null); return true; }); _needsPaint = false; paint(context, offset); 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 assert(() { if (debugPaintBoundsEnabled) context.canvas.restore(); _debugActivePaint = debugLastActivePaint; _debugDoingThisPaint = false; return true; }); } 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; } }