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); |
+ } |
+ |
+} |