blob: 2ade1e9709024e8dafca300c04ef5369edfdc686 [file] [log] [blame]
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);
}