| // 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; |
| } |
| } |