Chromium Code Reviews| Index: sky/framework/layout.dart |
| diff --git a/sky/framework/layout.dart b/sky/framework/layout.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..70f4001caba6930166dd765bbac8b37cebf755bf |
| --- /dev/null |
| +++ b/sky/framework/layout.dart |
| @@ -0,0 +1,799 @@ |
| +library layout; |
| + |
| +import 'node.dart'; |
| + |
| +// VALUE TYPES |
| + |
| +class EdgeDims { |
| + // used for e.g. padding |
| + const EdgeDims(this.top, this.right, this.bottom, this.left); |
| + final double top; |
| + final double right; |
| + final double bottom; |
| + final double left; |
| + operator ==(EdgeDims other) => (top == other.top) || |
| + (right == other.right) || |
| + (bottom == other.bottom) || |
| + (left == other.left); |
| +} |
| + |
| +class Matrix { |
| + external static Matrix get IDENTITY; |
| + external void rotate(double cx, double cy, double theta); // rotate theta radians clockwise around cx,cy |
| +} |
| + |
| + |
| +// PAINTING |
| + |
| +class DisplayList { |
| + List<RenderNode> _children; |
| + void paintChild(RenderNode child, double x, double y) { |
| + _addChildToDisplayList(child, x, y); |
| + if (_children == null) |
| + _children = new List<RenderNode>(); |
| + assert(!_children.contains(child)); |
| + _children.add(child); |
| + } |
| + external void _addChildToDisplayList(RenderNode child, double x, double y); |
| +} |
| + |
| + |
| +// ABSTRACT LAYOUT |
| + |
| +class ParentData { |
| + void detach() { |
| + detachSiblings(); |
| + } |
| + void detachSiblings() { } // workaround for lack of inter-class mixins in Dart |
| +} |
| + |
| +class PaintOptions { |
| + Matrix transform = Matrix.IDENTITY; |
| +} |
| + |
| +const kLayoutDirections = 4; |
| + |
| +double clamp({double min: 0.0, double value: 0.0, double max: double.INFINITY}) { |
| + if (value > max) |
| + value = max; |
| + if (value < min) |
| + value = min; |
| + return value; |
| +} |
| + |
| +abstract class RenderNode extends Node { |
| + |
| + // LAYOUT |
| + |
| + // pos is only for use by the RenderNode that actually lays this |
| + // node out, and any other nodes who happen to know exactly what |
| + // kind of node that is. |
| + ParentData pos; |
| + void setupPos(RenderNode child) { |
| + // override this to setup .pos correctly for your class |
| + if (child.pos is! ParentData) |
| + child.pos = new ParentData(); |
| + } |
| + |
| + void setAsChild(RenderNode child) { // only for use by subclasses |
| + // call this whenever you decide a node is a child |
| + assert(child != null); |
| + setupPos(child); |
| + super.setAsChild(child); |
| + } |
| + void dropChild(RenderNode child) { // only for use by subclasses |
| + assert(child != null); |
| + assert(child.pos != null); |
| + child.pos.detach(); |
| + super.dropChild(child); |
| + } |
| + |
| + static List<RenderNode> _nodesNeedingLayout = new List<RenderNode>(); |
| + static bool _debugDoingLayout = false; |
| + bool _needsLayout = true; |
| + bool get needsLayout => _needsLayout; |
| + RenderNode _relayoutSubtreeRoot; |
| + void saveRelayoutSubtreeRoot(RenderNode relayoutSubtreeRoot) { |
| + assert(_relayoutSubtreeRoot._relayoutSubtreeRoot == null); |
| + _relayoutSubtreeRoot = relayoutSubtreeRoot; |
| + } |
| + void markNeedsLayout() { |
| + assert(!_debugDoingLayout); |
| + assert(!_debugDoingPaint); |
| + if (_needsLayout) return; |
| + _needsLayout = true; |
| + _nodesNeedingLayout.add(this); |
| + } |
| + static void flushLayout() { |
| + _debugDoingLayout = true; |
| + List<RenderNode> dirtyNodes = _nodesNeedingLayout; |
| + _nodesNeedingLayout = new List<RenderNode>(); |
| + dirtyNodes..sort((a, b) => a.order - b.order)..forEach((node) { |
| + if (node._needsLayout && node.attached) |
| + node._doLayout(); |
| + }); |
| + _debugDoingLayout = false; |
| + } |
| + void _doLayout() { |
| + try { |
| + if (_relayoutSubtreeRoot != null) { |
| + assert(_relayoutSubtreeRoot._relayoutSubtreeRoot == null); |
| + _relayoutSubtreeRoot._doLayout(); |
| + } else { |
| + relayout(); |
|
ojan
2015/05/09 01:23:34
I was talking Elliott through this design and we r
Hixie
2015/05/13 20:29:21
Hm, yeah.
I don't see a way to work around this.
|
| + } |
| + } catch (e, stack) { |
| + print('Exception raised during layout of ${this}: ${e}'); |
| + print(stack); |
| + return; |
| + } |
| + assert(!_needsLayout); // check that the relayout() method marked us "not dirty" |
| + } |
| + /* // this method's signature is subclass-specific, but will exist in |
| + // some form in all subclasses: |
| + void layout({arguments..., RenderNode relayoutSubtreeRoot}) { |
| + if (this node has an opinion about its size, e.g. because it autosizes based on kids, or has an intrinsic dimension) { |
| + if (relayoutSubtreeRoot != null) { |
| + saveRelayoutSubtreeRoot(relayoutSubtreeRoot); |
| + // for each child, if we are going to size ourselves around them: |
| + child.layout(... relayoutSubtreeRoot: relayoutSubtreeRoot); |
| + width = ...; |
| + height = ...; |
| + } else { |
| + saveRelayoutSubtreeRoot(null); // you can skip this if there's no way you would ever have called saveRelayoutSubtreeRoot() before |
| + // we're the root of the relayout subtree |
| + // for each child, if we are going to size ourselves around them: |
| + child.layout(... relayoutSubtreeRoot: this); |
| + width = ...; |
| + height = ...; |
| + } |
| + } else { |
| + // we're sizing ourselves exclusively on input from the parent (arguments to this function) |
| + // ignore relayoutSubtreeRoot |
| + saveRelayoutSubtreeRoot(null); // you can skip this if there's no way you would ever have called saveRelayoutSubtreeRoot() before |
| + width = ...; // based on input from arguments only |
| + height = ...; // based on input from arguments only |
| + } |
| + // for each child whose size we'll ignore when deciding ours: |
| + child.layout(... relayoutSubtreeRoot: null); // or just omit relayoutSubtreeRoot |
| + layoutDone(); |
| + return result; |
| + } |
| + */ |
| + void relayout() { |
| + // Override this to perform relayout without your parent's |
| + // involvement. |
| + // |
| + // This is what is called after the first layout(), if you mark |
| + // yourself dirty and don't have a _relayoutSubtreeRoot set; in |
| + // other words, either if your parent doesn't care what size you |
| + // are (and thus didn't pass a relayoutSubtreeRoot to your |
| + // layout() method) or if you sized yourself entirely based on |
| + // what your parents told you, and not based on your children (and |
| + // thus you never called saveRelayoutSubtreeRoot()). |
| + // |
| + // In the former case, you can resize yourself here at will. In |
| + // the latter case, just leave your dimensions unchanged. |
| + // |
| + // If _relayoutSubtreeRoot is set (i.e. you called saveRelayout- |
| + // SubtreeRoot() in your layout(), with a relayoutSubtreeRoot |
| + // argument that was non-null), then if you mark yourself as dirty |
| + // then we'll tell that subtree root instead, and the layout will |
| + // occur via the layout() tree rather than starting from this |
| + // relayout() method. |
| + assert(_relayoutSubtreeRoot == null); |
| + layoutDone(); |
| + } |
| + void layoutDone({bool needsPaint: true}) { |
| + // make sure to call this at the end of your layout() or relayout() |
| + _needsLayout = false; |
| + if (needsPaint) |
| + markNeedsPaint(); |
| + } |
| + |
| + // 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 |
| + }) { } |
| + |
| + |
| + // HIT TESTING |
| + |
| + void hitTest(double x, double y, List<RenderNode> targets) { |
| + // override this if you have children |
| + // if any of your children cover x,y, call the top-most such |
| + // child's hitTest(). |
| + // then, call this superclass hitTest(). |
| + targets.add(this); |
| + } |
| + |
| + |
| + // PAINTING |
| + |
| + PaintOptions paintOptions; |
| + DisplayList _cachedPaint; |
| + // only set this to a subclass of PaintOptions that this class of RenderNode knows how to deal with |
| + |
| + static List<RenderNode> _nodesNeedingPaint = new List<RenderNode>(); |
| + static bool _debugDoingPaint = false; |
| + bool _needsPaint = true; |
| + bool get needsPaint => _needsPaint; |
| + void markNeedsPaint() { |
| + assert(!_debugDoingPaint); |
| + if (_needsPaint) return; |
| + _needsPaint = true; |
| + _nodesNeedingPaint.add(this); |
| + } |
| + static void flushPaint() { |
| + List<RenderNode> dirtyNodes = _nodesNeedingPaint; |
| + _nodesNeedingPaint = new List<RenderNode>(); |
| + dirtyNodes..sort((a, b) => a.order - b.order)..forEach((node) { |
| + if (node._needsPaint && node.attached) |
| + node._doPaint(); |
| + }); |
| + } |
| + void _doPaint() { |
| + assert(!_needsLayout); |
| + DisplayList newPaint = new DisplayList(); |
| + _needsPaint = false; |
| + try { |
| + paint(newPaint); |
| + } catch (e, stack) { |
| + print('Exception raised during paint of ${this}: ${e}'); |
| + print(stack); |
| + 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 |
| + if (newPaint._children != null) |
| + newPaint._children.forEach((node) { |
| + assert(node.attached == attached); |
| + if (node._needsPaint) |
| + node._doPaint(); |
| + }); |
| + _cachedPaint = newPaint; |
| + } |
| + |
| + void paint(DisplayList canvas) { } |
| + |
| +} |
| + |
| + |
| +// GENERIC MIXIN FOR RENDER NODES THAT TAKE A LIST OF CHILDREN |
| + |
| +abstract class ContainerParentDataMixin<ChildType extends RenderNode> { |
| + ChildType previousSibling; |
| + ChildType nextSibling; |
| + void detachSiblings() { |
| + if (previousSibling != null) { |
| + assert(previousSibling.pos is ContainerParentDataMixin<ChildType>); |
| + previousSibling.pos.nextSibling = nextSibling; |
| + } |
| + if (nextSibling != null) { |
| + assert(nextSibling.pos is ContainerParentDataMixin<ChildType>); |
| + nextSibling.pos.previousSibling = previousSibling; |
| + } |
| + previousSibling = null; |
| + nextSibling = null; |
| + } |
| +} |
| + |
| +abstract class ContainerRenderNodeMixin<ChildType extends RenderNode, ParentDataType extends ContainerParentDataMixin<ChildType>> { |
| + // abstract class that has only InlineNode children |
| + |
| + bool _debugUltimatePreviousSiblingOf(ChildType child, { ChildType equals }) { |
| + assert(child.pos is ParentDataType); |
| + while (child.pos.previousSibling != null) { |
| + child = child.pos.previousSibling; |
| + assert(child.pos is ParentDataType); |
| + } |
| + return child == equals; |
| + } |
| + bool _debugUltimateNextSiblingOf(ChildType child, { ChildType equals }) { |
| + assert(child.pos is ParentDataType); |
| + while (child.pos.nextSibling != null) { |
| + child = child.pos.nextSibling; |
| + assert(child.pos is ParentDataType); |
| + } |
| + return child == equals; |
| + } |
| + |
| + ChildType _firstChild; |
| + ChildType _lastChild; |
| + void add(ChildType child, { ChildType before }) { |
| + setAsChild(child); |
| + assert(child.pos is ParentDataType); |
| + if (before == null) { |
| + // append at the end (_lastChild) |
| + child.pos.previousSibling = _lastChild; |
| + if (_lastChild != null) { |
| + assert(_lastChild.pos is ParentDataType); |
| + _lastChild.pos.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.pos is ParentDataType); |
| + if (before.pos.previousSibling == null) { |
| + // insert at the start (_firstChild); we'll end up with two or more children |
| + assert(before == _firstChild); |
| + child.pos.nextSibling = before; |
| + before.pos.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.pos.previousSibling = before.pos.previousSibling; |
| + child.pos.nextSibling = before; |
| + // set up links from siblings to child |
| + assert(child.pos.previousSibling.pos is ParentDataType); |
| + assert(child.pos.nextSibling.pos is ParentDataType); |
| + child.pos.previousSibling.pos.nextSibling = child; |
| + child.pos.nextSibling.pos.previousSibling = child; |
| + } |
| + } |
| + markNeedsLayout(); |
| + } |
| + void remove(ChildType child) { |
| + assert(child.pos is ParentDataType); |
| + assert(_debugUltimatePreviousSiblingOf(child, equals: _firstChild)); |
| + assert(_debugUltimateNextSiblingOf(child, equals: _lastChild)); |
| + if (child.pos.previousSibling == null) { |
| + _firstChild = child.pos.nextSibling; |
| + } else { |
| + assert(child.pos.previousSibling.pos is ParentDataType); |
| + child.pos.previousSibling.pos.nextSibling = child.pos.nextSibling; |
| + } |
| + if (child.pos.nextSibling == null) { |
| + _lastChild = child.pos.previousSibling; |
| + } else { |
| + assert(child.pos.nextSibling.pos is ParentDataType); |
| + child.pos.nextSibling.pos.previousSibling = child.pos.previousSibling; |
| + } |
| + dropChild(child); |
| + markNeedsLayout(); |
| + } |
| + void reorderChildren() { |
| + ChildType child = _firstChild; |
| + while (child != null) { |
| + reorderChild(child); |
| + assert(child.pos is ParentDataType); |
| + child = child.pos.nextSibling; |
| + } |
| + } |
| + void attachChildren() { |
| + ChildType child = _firstChild; |
| + while (child != null) { |
| + child.attach(); |
| + assert(child.pos is ParentDataType); |
| + child = child.pos.nextSibling; |
| + } |
| + } |
| + void detachChildren() { |
| + ChildType child = _firstChild; |
| + while (child != null) { |
| + child.detach(); |
| + assert(child.pos is ParentDataType); |
| + child = child.pos.nextSibling; |
| + } |
| + } |
| + |
| +} |
| + |
| + |
| +// GENERIC BOX RENDERING |
| +// Anything that has a concept of x, y, width, height is going to derive from this |
| + |
| +class BoxDimensions { |
| + const BoxDimensions({this.width, this.height}); |
| + final double width; |
| + final double height; |
| +} |
| + |
| +class BoxParentData extends ParentData { |
| + double x = 0.0; |
| + double y = 0.0; |
| +} |
| + |
| +abstract class RenderBox extends RenderNode { |
| + |
| + void setupPos(RenderNode child) { |
| + if (child.pos is! BoxParentData) |
| + child.pos = new BoxParentData(); |
| + } |
| + |
| + // override this to report what dimensions you would have if you |
| + // were laid out with the given constraints this can walk the tree |
| + // if it must, but it should be as cheap as possible; just get the |
| + // dimensions and nothing else (e.g. don't calculate hypothetical |
| + // child positions if they're not needed to determine dimensions) |
| + BoxDimensions getIntrinsicDimensions({ |
| + double minWidth: 0.0, |
| + double maxWidth: double.INFINITY, |
| + double minHeight: 0.0, |
| + double maxHeight: double.INFINITY |
| + }) { |
| + return new BoxDimensions( |
| + width: clamp(min: minWidth, max: maxWidth), |
| + height: clamp(min: minHeight, max: maxHeight) |
| + ); |
| + } |
| + |
| + void layout({ |
| + double minWidth: 0.0, |
| + double maxWidth: double.INFINITY, |
| + double minHeight: 0.0, |
| + double maxHeight: double.INFINITY, |
| + RenderNode relayoutSubtreeRoot |
| + }) { |
| + width = clamp(min: minWidth, max: maxWidth); |
| + height = clamp(min: minHeight, max: maxHeight); |
| + layoutDone(); |
| + } |
| + |
| + double width; |
| + double height; |
| + |
| + void rotate({ |
| + int oldAngle, // 0..3 |
| + int newAngle, // 0..3 |
| + Duration time |
| + }) { } |
| + |
| +} |
| + |
| + |
| +// SCREEN LAYOUT MANAGER |
| + |
| +class Screen extends RenderNode { |
| + |
| + Screen({ |
| + RenderBox root, |
| + this.timeForRotation: const Duration(microseconds: 83333) |
| + }) { |
| + assert(root != null); |
| + this.root = root; |
| + } |
| + |
| + double _width; |
| + double get width => _width; |
| + double _height; |
| + double get height => _height; |
| + |
| + int _orientation; // 0..3 |
| + int get orientation => _orientation; |
| + Duration timeForRotation; |
| + |
| + RenderBox _root; |
| + RenderBox get root => _root; |
| + void set root (RenderBox value) { |
| + assert(root != null); |
| + _root = value; |
| + setAsChild(_root); |
| + markNeedsLayout(); |
| + } |
| + |
| + void layout({ |
| + double newWidth, |
| + double newHeight, |
| + int newOrientation |
| + }) { |
| + assert(root != null); |
| + if (newOrientation != orientation) { |
| + if (orientation != null) |
| + root.rotate(oldAngle: orientation, newAngle: newOrientation, time: timeForRotation); |
| + _orientation = newOrientation; |
| + } |
| + if ((newWidth != width) || (newHeight != height)) { |
| + _width = newWidth; |
| + _height = newHeight; |
| + relayout(); |
| + } |
| + } |
| + |
| + void relayout() { |
| + assert(root != null); |
| + root.layout( |
| + minWidth: width, |
| + maxWidth: width, |
| + minHeight: height, |
| + maxHeight: height |
| + ); |
| + assert(root.width == width); |
| + assert(root.height == height); |
| + } |
| + |
| + void rotate({ int oldAngle, int newAngle, Duration time }) { |
| + assert(false); // nobody tells the screen to rotate, the whole rotate() dance is started from our layout() |
| + } |
| + |
| + void paint(DisplayList canvas) { |
| + canvas.paintChild(root, 0.0, 0.0); |
| + } |
| + |
| +} |
| + |
| + |
| +// BLOCK LAYOUT MANAGER |
| + |
| +class BlockParentData extends BoxParentData with ContainerParentDataMixin<RenderBox> { } |
| + |
| +class BlockBox extends RenderBox with ContainerRenderNodeMixin<RenderBox, BlockParentData> { |
| + // lays out RenderBox children in a vertical stack |
| + // uses the maximum width provided by the parent |
| + // sizes itself to the height of its child stack |
| + |
| + BlockBox({ |
| + EdgeDims padding: const EdgeDims(0.0, 0.0, 0.0, 0.0) |
| + }) { |
| + _padding = padding; |
| + } |
| + |
| + EdgeDims _padding; |
| + EdgeDims get padding => _padding; |
| + void set padding(EdgeDims value) { |
| + assert(value != null); |
| + if (_padding != value) { |
| + _padding = value; |
| + markNeedsLayout(); |
| + } |
| + } |
| + |
| + void setupPos(RenderBox child) { |
| + if (child.pos is! BlockParentData) |
| + child.pos = new BlockParentData(); |
| + } |
| + |
| + // override this to report what dimensions you would have if you |
| + // were laid out with the given constraints this can walk the tree |
| + // if it must, but it should be as cheap as possible; just get the |
| + // dimensions and nothing else (e.g. don't calculate hypothetical |
| + // child positions if they're not needed to determine dimensions) |
| + BoxDimensions getIntrinsicDimensions({ |
| + double minWidth: 0.0, |
| + double maxWidth: double.INFINITY, |
| + double minHeight: 0.0, |
| + double maxHeight: double.INFINITY |
| + }) { |
| + double outerHeight = _padding.top + _padding.bottom; |
| + double outerWidth = clamp(min: minWidth, max: maxWidth); |
| + double innerWidth = outerWidth - (_padding.left + _padding.right); |
| + RenderBox child = _firstChild; |
| + while (child != null) { |
| + outerHeight += child.getIntrinsicDimensions(minWidth: innerWidth, maxWidth: innerWidth).height; |
| + assert(child.pos is BlockParentData); |
| + child = child.pos.nextSibling; |
| + } |
| + return new BoxDimensions( |
| + width: outerWidth, |
| + height: clamp(min: minHeight, max: maxHeight, value: outerHeight) |
| + ); |
| + } |
| + |
| + double _minHeight; // value cached from parent for relayout call |
| + double _maxHeight; // value cached from parent for relayout call |
| + void layout({ |
| + double minWidth: 0.0, |
| + double maxWidth: double.INFINITY, |
| + double minHeight: 0.0, |
| + double maxHeight: double.INFINITY, |
| + RenderNode relayoutSubtreeRoot |
| + }) { |
| + if (relayoutSubtreeRoot != null) |
| + saveRelayoutSubtreeRoot(relayoutSubtreeRoot); |
| + relayoutSubtreeRoot = relayoutSubtreeRoot == null ? this : relayoutSubtreeRoot; |
| + width = clamp(min: minWidth, max: maxWidth); |
| + _minHeight = minHeight; |
| + _maxHeight = maxHeight; |
| + internalLayout(relayoutSubtreeRoot); |
| + } |
| + |
| + void relayout() { |
| + internalLayout(this); |
| + } |
| + |
| + void internalLayout(RenderNode relayoutSubtreeRoot) { |
| + assert(_minHeight != null); |
| + assert(_maxHeight != null); |
| + double y = _padding.top; |
| + double innerWidth = width - (_padding.left + _padding.right); |
| + RenderBox child = _firstChild; |
| + while (child != null) { |
| + child.layout(minWidth: innerWidth, maxWidth: innerWidth, relayoutSubtreeRoot: relayoutSubtreeRoot); |
| + assert(child.pos is BlockParentData); |
| + child.pos.x = 0.0; |
| + child.pos.y = y; |
| + y += child.height; |
| + child = child.pos.nextSibling; |
| + } |
| + height = clamp(min: _minHeight, value: y + _padding.bottom, max: _maxHeight); |
| + layoutDone(); |
| + } |
| + |
| + void hitTest(double x, double y, List<RenderNode> targets) { |
| + RenderBox child = _lastChild; |
| + while (child != null) { |
| + assert(child.pos is BlockParentData); |
| + if ((x >= child.pos.x) && (x < child.pos.x + child.width) && |
| + (y >= child.pos.y) && (y < child.pos.y + child.height)) { |
| + child.hitTest(x, y, targets); |
| + break; |
| + } |
| + child = child.pos.previousSibling; |
| + } |
| + super.hitTest(x, y, targets); |
| + } |
| + |
| + void paint(DisplayList canvas) { |
| + RenderBox child = _firstChild; |
| + while (child != null) { |
| + assert(child.pos is BlockParentData); |
| + canvas.paintChild(child, child.pos.x, child.pos.y); |
| + child = child.pos.nextSibling; |
| + } |
| + } |
| + |
| +} |
| + |
| + |
| +// PARAGRAPH LAYOUT MANAGER |
| + |
| +class InlineParentData extends BoxParentData with ContainerParentDataMixin<InlineNode> { } |
| + |
| +class ParagraphBox extends RenderBox with ContainerRenderNodeMixin<InlineNode, InlineParentData> { |
| + |
| + void setupPos(InlineNode child) { |
| + if (child.pos is! InlineParentData) |
| + child.pos = new InlineParentData(); |
| + } |
| + |
| + BoxDimensions getIntrinsicDimensions({ |
| + double minWidth: 0.0, |
| + double maxWidth: double.INFINITY, |
| + double minHeight: 0.0, |
| + double maxHeight: double.INFINITY |
| + }) { |
| + // ...compute intrinsic dimension given these constraints... |
| + } |
| + |
| + double _minWidth; // value cached from parent for relayout call |
| + double _maxWidth; // value cached from parent for relayout call |
| + double _minHeight; // value cached from parent for relayout call |
| + double _maxHeight; // value cached from parent for relayout call |
| + void layout({ |
| + double minWidth: 0.0, |
| + double maxWidth: double.INFINITY, |
| + double minHeight: 0.0, |
| + double maxHeight: double.INFINITY, |
| + RenderNode relayoutSubtreeRoot |
| + }) { |
| + if (relayoutSubtreeRoot != null) |
| + saveRelayoutSubtreeRoot(relayoutSubtreeRoot); |
| + relayoutSubtreeRoot = relayoutSubtreeRoot == null ? this : relayoutSubtreeRoot; |
| + _minWidth = minWidth; |
| + _maxWidth = maxWidth; |
| + _minHeight = minHeight; |
| + _maxHeight = maxHeight; |
| + internalLayout(relayoutSubtreeRoot); |
| + layoutDone(); |
| + } |
| + |
| + void relayout() { |
| + internalLayout(this); |
| + layoutDone(); |
| + } |
| + |
| + external void internalLayout(RenderNode relayoutSubtreeRoot); |
| + external void hitTest(double x, double y, List<RenderNode> targets); |
| + external void paint(DisplayList canvas); |
| + |
| +} |
| + |
| +class InlineNode extends RenderNode with ContainerRenderNodeMixin<InlineNode, InlineParentData> { |
| + // ... |
| +} |
| + |
| + |
| +// InlineBox wraps a RenderBox, i.e. it's a box that fits into inline |
| +// layout without breaking into multiple lines. This allows you to put |
| +// images, form controls, inline blocks, etc, into paragraphs |
| +class InlineBox extends InlineNode { |
| + InlineBox(this.child); |
| + final RenderBox child; |
| + |
| + // ... |
| +} |
| + |
| + |
| +// SCAFFOLD LAYOUT MANAGER |
| + |
| +// a sample special-purpose layout manager |
| + |
| +class ScaffoldBox extends RenderBox { |
| + |
| + ScaffoldBox(this.toolbar, this.body, this.statusbar, this.drawer) { |
| + assert(body != null); |
| + } |
| + |
| + final RenderBox toolbar; |
| + final RenderBox body; |
| + final RenderBox statusbar; |
| + final RenderBox drawer; |
| + |
| + void layout({ |
| + double minWidth: 0.0, |
| + double maxWidth: double.INFINITY, |
| + double minHeight: 0.0, |
| + double maxHeight: double.INFINITY, |
| + RenderNode relayoutSubtreeRoot |
| + }) { |
| + width = clamp(min: minWidth, max: maxWidth); |
| + height = clamp(min: minHeight, max: maxHeight); |
| + relayout(); |
| + } |
| + |
| + static const kToolbarHeight = 100.0; |
| + static const kStatusbarHeight = 50.0; |
| + |
| + void relayout() { |
| + double bodyHeight = height; |
| + if (toolbar != null) { |
| + toolbar.layout(minWidth: width, maxWidth: width, minHeight: kToolbarHeight, maxHeight: kToolbarHeight); |
| + assert(toolbar.pos is BoxParentData); |
| + toolbar.pos.x = 0.0; |
| + toolbar.pos.y = 0.0; |
| + bodyHeight -= kToolbarHeight; |
| + } |
| + if (statusbar != null) { |
| + statusbar.layout(minWidth: width, maxWidth: width, minHeight: kStatusbarHeight, maxHeight: kStatusbarHeight); |
| + assert(statusbar.pos is BoxParentData); |
| + statusbar.pos.x = 0.0; |
| + statusbar.pos.y = height - kStatusbarHeight; |
| + bodyHeight -= kStatusbarHeight; |
| + } |
| + body.layout(minWidth: width, maxWidth: width, minHeight: bodyHeight, maxHeight: bodyHeight); |
| + if (drawer != null) |
| + drawer.layout(minWidth: 0.0, maxWidth: width, minHeight: height, maxHeight: height); |
| + layoutDone(); |
| + } |
| + |
| + void hitTest(double x, double y, List<RenderNode> targets) { |
| + if ((drawer != null) && (x < drawer.width)) { |
| + drawer.hitTest(x, y, targets); |
| + } else if ((toolbar != null) && (y < toolbar.height)) { |
| + toolbar.hitTest(x, y, targets); |
| + } else if ((statusbar != null) && (y > (statusbar.pos as BoxParentData).y)) { |
| + statusbar.hitTest(x, y, targets); |
| + } else { |
| + body.hitTest(x, y, targets); |
| + } |
| + super.hitTest(x, y, targets); |
| + } |
| + |
| + void paint(DisplayList canvas) { |
| + canvas.paintChild(body, (body.pos as BoxParentData).x, (body.pos as BoxParentData).y); |
| + if (statusbar != null) |
| + canvas.paintChild(statusbar, (statusbar.pos as BoxParentData).x, (statusbar.pos as BoxParentData).y); |
| + if (toolbar != null) |
| + canvas.paintChild(toolbar, (toolbar.pos as BoxParentData).x, (toolbar.pos as BoxParentData).y); |
| + if (drawer != null) |
| + canvas.paintChild(drawer, (drawer.pos as BoxParentData).x, (drawer.pos as BoxParentData).y); |
| + } |
| + |
| +} |