| 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'; |
| |
| // 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._construct(className); |
| }); |
| } |
| |
| factory Style(String styles) { |
| assert(!styles.contains(new RegExp('\\b(display|flex|flex-direction)\\b'))); |
| return new Style._addToCache(styles); |
| } |
| |
| factory Style._addToCache(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._construct(className); |
| }); |
| } |
| |
| Style._construct(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 |
| void merge(ParentData other) { |
| // override this in subclasses to merge in data from other into this |
| assert(other.runtimeType == this.runtimeType); |
| } |
| } |
| |
| 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 setParentData(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); |
| setParentData(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); |
| } |
| |
| } |
| |
| 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); |
| 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; |
| } |
| |
| } |
| |
| |
| // 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', stylesToClasses(styles)); |
| } |
| |
| String stylesToClasses(List<Style> styles) { |
| return styles.map((s) => s._className).join(' '); |
| } |
| |
| String _inlineStyles = ''; |
| String _additionalStylesFromParent = ''; // used internally to propagate parentData settings to the child |
| |
| void updateInlineStyle(String newStyle) { |
| assert(newStyle == null || !newStyle.contains(new RegExp('\\b(display|flex|flex-direction)\\b'))); |
| _inlineStyles = newStyle != null ? newStyle : ''; |
| _updateInlineStyleAttribute(); |
| } |
| |
| void _updateInlineStyleAttribute() { |
| if ((_inlineStyles != '') && (_additionalStylesFromParent != '')) |
| _skyElement.setAttribute('style', "$_inlineStyles;$_additionalStylesFromParent"); |
| else |
| _skyElement.setAttribute('style', "$_inlineStyles$_additionalStylesFromParent"); |
| } |
| |
| 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 setParentData(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 FlexBoxParentData extends CSSParentData { |
| int flex; |
| void merge(FlexBoxParentData other) { |
| if (other.flex != null) |
| flex = other.flex; |
| super.merge(other); |
| } |
| } |
| |
| enum FlexDirection { Row, Column } |
| |
| class RenderCSSFlex extends RenderCSSContainer { |
| |
| RenderCSSFlex(debug, FlexDirection direction) : _direction = direction, super(debug); |
| |
| FlexDirection _direction; |
| FlexDirection get direction => _direction; |
| void set direction (FlexDirection value) { |
| _direction = value; |
| markNeedsLayout(); |
| } |
| |
| void setParentData(RenderNode child) { |
| if (child.parentData is! FlexBoxParentData) |
| child.parentData = new FlexBoxParentData(); |
| } |
| |
| static final Style _displayFlex = new Style._addToCache('display:flex'); |
| static final Style _displayFlexRow = new Style._addToCache('flex-direction:row'); |
| static final Style _displayFlexColumn = new Style._addToCache('flex-direction:column'); |
| |
| String stylesToClasses(List<Style> styles) { |
| var settings = _displayFlex._className; |
| switch (_direction) { |
| case FlexDirection.Row: settings += ' ' + _displayFlexRow._className; break; |
| case FlexDirection.Column: settings += ' ' + _displayFlexColumn._className; break; |
| } |
| return super.stylesToClasses(styles) + ' ' + settings; |
| } |
| |
| void markNeedsLayout() { |
| super.markNeedsLayout(); |
| |
| // pretend we did the layout: |
| RenderCSS child = _firstChild; |
| while (child != null) { |
| assert(child.parentData is FlexBoxParentData); |
| if (child.parentData.flex != null) { |
| child._additionalStylesFromParent = 'flex:${child.parentData.flex}'; |
| child._updateInlineStyleAttribute(); |
| } |
| child = child.parentData.nextSibling; |
| } |
| } |
| |
| } |
| |
| class StackParentData extends CSSParentData { |
| double top; |
| double left; |
| double right; |
| double bottom; |
| void merge(StackParentData other) { |
| if (other.top != null) |
| top = other.top; |
| if (other.left != null) |
| left = other.left; |
| if (other.right != null) |
| right = other.right; |
| if (other.bottom != null) |
| bottom = other.bottom; |
| super.merge(other); |
| } |
| } |
| |
| class RenderCSSStack extends RenderCSSContainer { |
| |
| RenderCSSStack(debug) : super(debug); |
| |
| void setParentData(RenderNode child) { |
| if (child.parentData is! StackParentData) |
| child.parentData = new StackParentData(); |
| } |
| |
| static final Style _displayPosition = new Style._addToCache('transform:translateX(0);position:relative'); |
| |
| String stylesToClasses(List<Style> styles) { |
| return super.stylesToClasses(styles) + ' ' + _displayPosition._className; |
| } |
| |
| void markNeedsLayout() { |
| super.markNeedsLayout(); |
| |
| // pretend we did the layout: |
| RenderCSS child = _firstChild; |
| while (child != null) { |
| assert(child.parentData is StackParentData); |
| var style = 'position:absolute;'; |
| if (child.parentData.top != null) |
| style += 'top:${child.parentData.top};'; |
| if (child.parentData.left != null) |
| style += 'left:${child.parentData.left};'; |
| if (child.parentData.right != null) |
| style += 'right:${child.parentData.right};'; |
| if (child.parentData.bottom != null) |
| style += 'bottom:${child.parentData.bottom};'; |
| child._additionalStylesFromParent = style; |
| child._updateInlineStyleAttribute(); |
| child = child.parentData.nextSibling; |
| } |
| } |
| |
| } |
| |
| class RenderCSSParagraph extends RenderCSSContainer { |
| |
| RenderCSSParagraph(debug) : super(debug); |
| |
| static final Style _displayParagraph = new Style._addToCache('display:paragraph'); |
| |
| String stylesToClasses(List<Style> styles) { |
| return super.stylesToClasses(styles) + ' ' + _displayParagraph._className; |
| } |
| |
| } |
| |
| class RenderCSSInline extends RenderCSS { |
| |
| RenderCSSInline(debug, String newData) : super(debug) { |
| data = newData; |
| } |
| |
| static final Style _displayInline = new Style._addToCache('display:inline'); |
| |
| String stylesToClasses(List<Style> styles) { |
| return super.stylesToClasses(styles) + ' ' + _displayInline._className; |
| } |
| |
| sky.Element createSkyElement() { |
| return sky.document.createElement('div') |
| ..setChild(new sky.Text()) |
| ..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); |
| } |