| Index: sky/framework/layout.dart
|
| diff --git a/sky/framework/layout.dart b/sky/framework/layout.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..eb5ff2f39ed81309c98ba4695e67cf655e4145ca
|
| --- /dev/null
|
| +++ b/sky/framework/layout.dart
|
| @@ -0,0 +1,406 @@
|
| +library layout;
|
| +
|
| +import 'node.dart';
|
| +import 'dart:sky' as sky;
|
| +import 'dart:collection';
|
| +
|
| +// UTILS
|
| +
|
| +// Bridge to legacy CSS-like style specification
|
| +// Eventually we'll replace this with something else
|
| +class Style {
|
| + final String _className;
|
| + static final Map<String, Style> _cache = new HashMap<String, Style>();
|
| +
|
| + static int _nextStyleId = 1;
|
| +
|
| + static String _getNextClassName() { return "style${_nextStyleId++}"; }
|
| +
|
| + Style extend(Style other) {
|
| + var className = "$_className ${other._className}";
|
| +
|
| + return _cache.putIfAbsent(className, () {
|
| + return new Style._internal(className);
|
| + });
|
| + }
|
| +
|
| + factory Style(String styles) {
|
| + return _cache.putIfAbsent(styles, () {
|
| + var className = _getNextClassName();
|
| + sky.Element styleNode = sky.document.createElement('style');
|
| + styleNode.setChild(new sky.Text(".$className { $styles }"));
|
| + sky.document.appendChild(styleNode);
|
| + return new Style._internal(className);
|
| + });
|
| + }
|
| +
|
| + Style._internal(this._className);
|
| +}
|
| +
|
| +class Rect {
|
| + const Rect(this.x, this.y, this.width, this.height);
|
| + final double x;
|
| + final double y;
|
| + final double width;
|
| + final double height;
|
| +}
|
| +
|
| +
|
| +// ABSTRACT LAYOUT
|
| +
|
| +class ParentData {
|
| + void detach() {
|
| + detachSiblings();
|
| + }
|
| + void detachSiblings() { } // workaround for lack of inter-class mixins in Dart
|
| +}
|
| +
|
| +abstract class RenderNode extends Node {
|
| +
|
| + // 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 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.parentData != null);
|
| + child.parentData.detach();
|
| + super.dropChild(child);
|
| + }
|
| +
|
| +}
|
| +
|
| +abstract class RenderBox extends RenderNode { }
|
| +
|
| +
|
| +// 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);
|
| + setAsChild(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;
|
| + }
|
| +
|
| +}
|
| +
|
| +
|
| +// CSS SHIMS
|
| +
|
| +abstract class RenderCSS extends RenderBox {
|
| +
|
| + dynamic debug;
|
| + sky.Element _skyElement;
|
| +
|
| + RenderCSS(this.debug) {
|
| + _skyElement = createSkyElement();
|
| + registerEventTarget(_skyElement, this);
|
| + }
|
| +
|
| + sky.Element createSkyElement();
|
| +
|
| + void updateStyles(List<Style> styles) {
|
| + _skyElement.setAttribute('class', styles.map((s) => s._className).join(' '));
|
| + }
|
| +
|
| + void updateInlineStyle(String newStyle) {
|
| + _skyElement.setAttribute('style', newStyle);
|
| + }
|
| +
|
| + double get width {
|
| + sky.ClientRect rect = _skyElement.getBoundingClientRect();
|
| + return rect.width;
|
| + }
|
| +
|
| + double get height {
|
| + sky.ClientRect rect = _skyElement.getBoundingClientRect();
|
| + return rect.height;
|
| + }
|
| +
|
| + Rect get rect {
|
| + sky.ClientRect rect = _skyElement.getBoundingClientRect();
|
| + return new Rect(rect.left, rect.top, rect.width, rect.height);
|
| + }
|
| +
|
| +}
|
| +
|
| +class CSSParentData extends ParentData with ContainerParentDataMixin<RenderCSS> { }
|
| +
|
| +class RenderCSSContainer extends RenderCSS with ContainerRenderNodeMixin<RenderCSS, CSSParentData> {
|
| +
|
| + RenderCSSContainer(debug) : super(debug);
|
| +
|
| + void setupPos(RenderNode child) {
|
| + if (child.parentData is! CSSParentData)
|
| + child.parentData = new CSSParentData();
|
| + }
|
| +
|
| + sky.Element createSkyElement() => sky.document.createElement('div')
|
| + ..setAttribute('debug', debug.toString());
|
| +
|
| + void markNeedsLayout() { }
|
| +
|
| + void add(RenderCSS child, { RenderCSS before }) {
|
| + if (before != null) {
|
| + assert(before._skyElement.parentNode != null);
|
| + assert(before._skyElement.parentNode == _skyElement);
|
| + }
|
| + super.add(child, before: before);
|
| + if (before != null) {
|
| + before._skyElement.insertBefore([child._skyElement]);
|
| + assert(child._skyElement.parentNode != null);
|
| + assert(child._skyElement.parentNode == _skyElement);
|
| + assert(child._skyElement.parentNode == before._skyElement.parentNode);
|
| + } else {
|
| + _skyElement.appendChild(child._skyElement);
|
| + }
|
| + }
|
| + void remove(RenderCSS child) {
|
| + child._skyElement.remove();
|
| + super.remove(child);
|
| + }
|
| +
|
| +}
|
| +
|
| +class RenderCSSText extends RenderCSS {
|
| +
|
| + RenderCSSText(debug, String newData) : super(debug) {
|
| + data = newData;
|
| + }
|
| +
|
| + static final Style _displayParagraph = new Style('display:paragraph');
|
| +
|
| + sky.Element createSkyElement() {
|
| + return sky.document.createElement('div')
|
| + ..setChild(new sky.Text())
|
| + ..setAttribute('class', _displayParagraph._className)
|
| + ..setAttribute('debug', debug.toString());
|
| + }
|
| +
|
| + void set data (String value) {
|
| + (_skyElement.firstChild as sky.Text).data = value;
|
| + }
|
| +
|
| +}
|
| +
|
| +class RenderCSSImage extends RenderCSS {
|
| +
|
| + RenderCSSImage(debug, String src, num width, num height) : super(debug) {
|
| + configure(src, width, height);
|
| + }
|
| +
|
| + sky.Element createSkyElement() {
|
| + return sky.document.createElement('img')
|
| + ..setAttribute('debug', debug.toString());
|
| + }
|
| +
|
| + void configure(String src, num width, num height) {
|
| + if (_skyElement.getAttribute('src') != src)
|
| + _skyElement.setAttribute('src', src);
|
| + _skyElement.style['width'] = '${width}px';
|
| + _skyElement.style['height'] = '${height}px';
|
| + }
|
| +
|
| +}
|
| +
|
| +class RenderCSSRoot extends RenderCSSContainer {
|
| + RenderCSSRoot(debug) : super(debug);
|
| + sky.Element createSkyElement() {
|
| + var result = super.createSkyElement();
|
| + assert(result != null);
|
| + sky.document.appendChild(result);
|
| + return result;
|
| + }
|
| +}
|
| +
|
| +
|
| +// legacy tools
|
| +Map<sky.EventTarget, RenderNode> _eventTargetRegistry = {};
|
| +void registerEventTarget(sky.EventTarget e, RenderNode n) {
|
| + _eventTargetRegistry[e] = n;
|
| +}
|
| +RenderNode bridgeEventTargetToRenderNode(sky.EventTarget e) {
|
| + return _eventTargetRegistry[e];
|
| +}
|
| +
|
| +
|
| +
|
| +
|
| +String _attributes(node) {
|
| + if (node is! sky.Element) return '';
|
| + var result = '';
|
| + var attrs = node.getAttributes();
|
| + for (var attr in attrs)
|
| + result += ' ${attr.name}="${attr.value}"';
|
| + return result;
|
| +}
|
| +
|
| +void _serialiseDOM(node, [String prefix = '']) {
|
| + if (node is sky.Text) {
|
| + print(prefix + 'text: "' + node.data.replaceAll('\n', '\\n') + '"');
|
| + return;
|
| + }
|
| + print(prefix + node.toString() + _attributes(node));
|
| + var children = node.getChildNodes();
|
| + prefix = prefix + ' ';
|
| + for (var child in children)
|
| + _serialiseDOM(child, prefix);
|
| +}
|
| +
|
| +void dumpState() {
|
| + _serialiseDOM(sky.document);
|
| +}
|
|
|