mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Layout API prototype, for discussion
R=abarth@chromium.org Review URL: https://codereview.chromium.org/1093633002
This commit is contained in:
parent
179d1b2969
commit
edacf2a362
@ -1,5 +1,7 @@
|
||||
library layout;
|
||||
|
||||
// This version of layout.dart is a shim version of layout2.dart that is backed using CSS and <div>s.
|
||||
|
||||
import 'node.dart';
|
||||
import 'dart:sky' as sky;
|
||||
import 'dart:collection';
|
||||
@ -64,7 +66,7 @@ class ParentData {
|
||||
}
|
||||
}
|
||||
|
||||
abstract class RenderNode extends Node {
|
||||
abstract class RenderNode extends AbstractNode {
|
||||
|
||||
// LAYOUT
|
||||
|
||||
|
||||
757
sdk/lib/framework/layout2.dart
Normal file
757
sdk/lib/framework/layout2.dart
Normal file
@ -0,0 +1,757 @@
|
||||
library layout;
|
||||
|
||||
// This version of layout.dart is an update to the other one, this one using new APIs.
|
||||
// It will not work in a stock Sky setup currently.
|
||||
|
||||
import 'node.dart';
|
||||
|
||||
import 'dart:sky' as sky;
|
||||
|
||||
// ABSTRACT LAYOUT
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
class RenderNodeDisplayList extends sky.PictureRecorder {
|
||||
RenderNodeDisplayList(double width, double height) : super(width, height);
|
||||
void paintChild(RenderNode child, double x, double y) {
|
||||
save();
|
||||
translate(x, y);
|
||||
child.paint(this);
|
||||
restore();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class RenderNode extends AbstractNode {
|
||||
|
||||
// LAYOUT
|
||||
|
||||
// parentData 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 parentData;
|
||||
void setupPos(RenderNode child) {
|
||||
// override this to setup .parentData correctly for your class
|
||||
if (child.parentData is! ParentData)
|
||||
child.parentData = new ParentData();
|
||||
}
|
||||
|
||||
void adoptChild(RenderNode child) { // only for use by subclasses
|
||||
// call this whenever you decide a node is a child
|
||||
assert(child != null);
|
||||
setupPos(child);
|
||||
super.adoptChild(child);
|
||||
}
|
||||
void dropChild(RenderNode child) { // only for use by subclasses
|
||||
assert(child != null);
|
||||
assert(child.parentData != null);
|
||||
child.parentData.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) {
|
||||
_relayoutSubtreeRoot = relayoutSubtreeRoot;
|
||||
assert(_relayoutSubtreeRoot == null || _relayoutSubtreeRoot._relayoutSubtreeRoot == null);
|
||||
assert(_relayoutSubtreeRoot == null || _relayoutSubtreeRoot == parent || _relayoutSubtreeRoot == parent._relayoutSubtreeRoot);
|
||||
}
|
||||
bool debugAncestorsAlreadyMarkedNeedsLayout() {
|
||||
if (_relayoutSubtreeRoot == null)
|
||||
return true;
|
||||
RenderNode node = this;
|
||||
while (node != _relayoutSubtreeRoot) {
|
||||
assert(node._relayoutSubtreeRoot == _relayoutSubtreeRoot);
|
||||
assert(node.parent != null);
|
||||
node = node.parent as RenderNode;
|
||||
if (!node._needsLayout)
|
||||
return false;
|
||||
}
|
||||
assert(node._relayoutSubtreeRoot == null);
|
||||
return true;
|
||||
}
|
||||
void markNeedsLayout() {
|
||||
assert(!_debugDoingLayout);
|
||||
assert(!_debugDoingPaint);
|
||||
if (_needsLayout) {
|
||||
assert(debugAncestorsAlreadyMarkedNeedsLayout());
|
||||
return;
|
||||
}
|
||||
_needsLayout = true;
|
||||
if (_relayoutSubtreeRoot != null)
|
||||
parent.markNeedsLayout();
|
||||
else
|
||||
_nodesNeedingLayout.add(this);
|
||||
}
|
||||
static void flushLayout() {
|
||||
_debugDoingLayout = true;
|
||||
List<RenderNode> dirtyNodes = _nodesNeedingLayout;
|
||||
_nodesNeedingLayout = new List<RenderNode>();
|
||||
dirtyNodes..sort((a, b) => a.depth - b.depth)..forEach((node) {
|
||||
if (node._needsLayout && node.attached)
|
||||
node._doLayout();
|
||||
});
|
||||
_debugDoingLayout = false;
|
||||
}
|
||||
void _doLayout() {
|
||||
try {
|
||||
assert(_relayoutSubtreeRoot == null);
|
||||
relayout();
|
||||
} 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}) {
|
||||
bool childArgumentsChanged = ...; // true if arguments we're going to pass to the children are different than last time, false otherwise
|
||||
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:
|
||||
if (child.needsLayout || childArgumentsChanged)
|
||||
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:
|
||||
if (child.needsLayout || childArgumentsChanged)
|
||||
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:
|
||||
if (child.needsLayout || childArgumentsChanged)
|
||||
child.layout(... relayoutSubtreeRoot: null); // or just omit relayoutSubtreeRoot
|
||||
layoutDone();
|
||||
return;
|
||||
}
|
||||
*/
|
||||
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.
|
||||
//
|
||||
// when calling children's layout() methods, skip any children
|
||||
// that have needsLayout == false unless the arguments you are
|
||||
// passing in have changed since the last time
|
||||
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 handlePointer(sky.PointerEvent event) {
|
||||
// override this if you have children, to hand it to the appropriate child
|
||||
// override this if you want to do anything with the pointer event
|
||||
}
|
||||
|
||||
|
||||
// PAINTING
|
||||
|
||||
static bool _debugDoingPaint = false;
|
||||
void markNeedsPaint() {
|
||||
assert(!_debugDoingPaint);
|
||||
var ancestor = this;
|
||||
while (ancestor.parent != null)
|
||||
ancestor = ancestor.parent;
|
||||
assert(ancestor is Screen);
|
||||
ancestor.paintFrame();
|
||||
}
|
||||
void paint(RenderNodeDisplayList 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.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 ContainerRenderNodeMixin<ChildType extends RenderNode, ParentDataType extends ContainerParentDataMixin<ChildType>> implements RenderNode {
|
||||
// abstract class that has only InlineNode children
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
ChildType _firstChild;
|
||||
ChildType _lastChild;
|
||||
void add(ChildType child, { ChildType before }) {
|
||||
assert(child != this);
|
||||
assert(before != this);
|
||||
assert(child != before);
|
||||
assert(child != _firstChild);
|
||||
assert(child != _lastChild);
|
||||
adoptChild(child);
|
||||
assert(child.parentData is ParentDataType);
|
||||
assert(child.parentData.nextSibling == null);
|
||||
assert(child.parentData.previousSibling == null);
|
||||
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);
|
||||
}
|
||||
}
|
||||
markNeedsLayout();
|
||||
}
|
||||
void remove(ChildType child) {
|
||||
assert(child.parentData is ParentDataType);
|
||||
assert(_debugUltimatePreviousSiblingOf(child, equals: _firstChild));
|
||||
assert(_debugUltimateNextSiblingOf(child, equals: _lastChild));
|
||||
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;
|
||||
dropChild(child);
|
||||
markNeedsLayout();
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
ChildType get firstChild => _firstChild;
|
||||
ChildType get lastChild => _lastChild;
|
||||
ChildType childAfter(ChildType child) {
|
||||
assert(child.parentData is ParentDataType);
|
||||
return child.parentData.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.parentData is! BoxParentData)
|
||||
child.parentData = 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;
|
||||
adoptChild(_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(RenderNodeDisplayList canvas) {
|
||||
canvas.paintChild(root, 0.0, 0.0);
|
||||
}
|
||||
|
||||
void paintFrame() {
|
||||
RenderNode._debugDoingPaint = true;
|
||||
var canvas = new RenderNodeDisplayList(sky.view.width, sky.view.height);
|
||||
paint(canvas);
|
||||
sky.view.picture = canvas.endRecording();
|
||||
sky.view.schedulePaint();
|
||||
RenderNode._debugDoingPaint = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// BLOCK LAYOUT MANAGER
|
||||
|
||||
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 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.parentData is! BlockParentData)
|
||||
child.parentData = 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.parentData is BlockParentData);
|
||||
child = child.parentData.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.parentData is BlockParentData);
|
||||
child.parentData.x = 0.0;
|
||||
child.parentData.y = y;
|
||||
y += child.height;
|
||||
child = child.parentData.nextSibling;
|
||||
}
|
||||
height = clamp(min: _minHeight, value: y + _padding.bottom, max: _maxHeight);
|
||||
layoutDone();
|
||||
}
|
||||
|
||||
void handlePointer(sky.PointerEvent event, { double x: 0.0, double y: 0.0 }) {
|
||||
// the x, y parameters have the top left of the node's box as the origin
|
||||
RenderBox child = _lastChild;
|
||||
while (child != null) {
|
||||
assert(child.parentData is BlockParentData);
|
||||
if ((x >= child.parentData.x) && (x < child.parentData.x + child.width) &&
|
||||
(y >= child.parentData.y) && (y < child.parentData.y + child.height)) {
|
||||
child.handlePointer(event, x: x-child.parentData.x, y: y-child.parentData.y);
|
||||
break;
|
||||
}
|
||||
child = child.parentData.previousSibling;
|
||||
}
|
||||
super.handlePointer(event);
|
||||
}
|
||||
|
||||
void paint(RenderNodeDisplayList canvas) {
|
||||
RenderBox child = _firstChild;
|
||||
while (child != null) {
|
||||
assert(child.parentData is BlockParentData);
|
||||
canvas.paintChild(child, child.parentData.x, child.parentData.y);
|
||||
child = child.parentData.nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class FlexBoxParentData extends BoxParentData {
|
||||
int flex;
|
||||
void merge(FlexBoxParentData other) {
|
||||
if (other.flex != null)
|
||||
flex = other.flex;
|
||||
super.merge(other);
|
||||
}
|
||||
}
|
||||
|
||||
enum FlexDirection { Row, Column }
|
||||
|
||||
// TODO(ianh): FlexBox
|
||||
|
||||
|
||||
// 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.parentData is BoxParentData);
|
||||
toolbar.parentData.x = 0.0;
|
||||
toolbar.parentData.y = 0.0;
|
||||
bodyHeight -= kToolbarHeight;
|
||||
}
|
||||
if (statusbar != null) {
|
||||
statusbar.layout(minWidth: width, maxWidth: width, minHeight: kStatusbarHeight, maxHeight: kStatusbarHeight);
|
||||
assert(statusbar.parentData is BoxParentData);
|
||||
statusbar.parentData.x = 0.0;
|
||||
statusbar.parentData.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 handlePointer(sky.PointerEvent event, { double x: 0.0, double y: 0.0 }) {
|
||||
if ((drawer != null) && (x < drawer.width)) {
|
||||
drawer.handlePointer(event, x: x, y: y);
|
||||
} else if ((toolbar != null) && (y < toolbar.height)) {
|
||||
toolbar.handlePointer(event, x: x, y: y);
|
||||
} else if ((statusbar != null) && (y > (statusbar.parentData as BoxParentData).y)) {
|
||||
statusbar.handlePointer(event, x: x, y: y-(statusbar.parentData as BoxParentData).y);
|
||||
} else {
|
||||
body.handlePointer(event, x: x, y: y-(body.parentData as BoxParentData).y);
|
||||
}
|
||||
super.handlePointer(event, x: x, y: y);
|
||||
}
|
||||
|
||||
void paint(RenderNodeDisplayList canvas) {
|
||||
canvas.paintChild(body, (body.parentData as BoxParentData).x, (body.parentData as BoxParentData).y);
|
||||
if (statusbar != null)
|
||||
canvas.paintChild(statusbar, (statusbar.parentData as BoxParentData).x, (statusbar.parentData as BoxParentData).y);
|
||||
if (toolbar != null)
|
||||
canvas.paintChild(toolbar, (toolbar.parentData as BoxParentData).x, (toolbar.parentData as BoxParentData).y);
|
||||
if (drawer != null)
|
||||
canvas.paintChild(drawer, (drawer.parentData as BoxParentData).x, (drawer.parentData as BoxParentData).y);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
library node;
|
||||
|
||||
class Node {
|
||||
class AbstractNode {
|
||||
|
||||
// Nodes always have a 'depth' greater than their ancestors'.
|
||||
// There's no guarantee regarding depth between siblings. The depth
|
||||
@ -15,7 +15,7 @@ class Node {
|
||||
|
||||
int _depth = 0;
|
||||
int get depth => _depth;
|
||||
void redepthChild(Node child) { // internal, do not call
|
||||
void redepthChild(AbstractNode child) { // internal, do not call
|
||||
assert(child._attached == _attached);
|
||||
if (child._depth <= _depth) {
|
||||
child._depth = _depth + 1;
|
||||
@ -44,9 +44,9 @@ class Node {
|
||||
}
|
||||
detachChildren() { } // workaround for lack of inter-class mixins in Dart
|
||||
|
||||
Node _parent;
|
||||
Node get parent => _parent;
|
||||
void adoptChild(Node child) { // only for use by subclasses
|
||||
AbstractNode _parent;
|
||||
AbstractNode get parent => _parent;
|
||||
void adoptChild(AbstractNode child) { // only for use by subclasses
|
||||
assert(child != null);
|
||||
assert(child._parent == null);
|
||||
child._parent = this;
|
||||
@ -54,7 +54,7 @@ class Node {
|
||||
child.attach();
|
||||
redepthChild(child);
|
||||
}
|
||||
void dropChild(Node child) { // only for use by subclasses
|
||||
void dropChild(AbstractNode child) { // only for use by subclasses
|
||||
assert(child != null);
|
||||
assert(child._parent == this);
|
||||
assert(child.attached == attached);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user