Hixie 7cb2b7114e [Effen] Move responsibility for inserting nodes to the OneChildListRenderNodeWrapper.
Before, OneChildListRenderNodeWrappers were responsible for removing
the child nodes' RenderCSS nodes from their containers, and for moving
those around when nodes were reordered. Now, they're also responsible
for adding them.

R=abarth@chromium.org

Review URL: https://codereview.chromium.org/1145953003
2015-05-20 15:56:10 -07:00

962 lines
25 KiB
Dart

// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library fn;
import 'dart:async';
import 'dart:collection';
import 'dart:mirrors';
import 'dart:sky' as sky;
import 'reflect.dart' as reflect;
import 'layout.dart';
export 'layout.dart' show Style;
final sky.Tracing _tracing = sky.window.tracing;
final bool _shouldLogRenderDuration = false;
final bool _shouldTrace = false;
enum _SyncOperation { IDENTICAL, INSERTION, STATEFUL, STATELESS, REMOVAL }
/*
* All Effen nodes derive from UINode. All nodes have a _parent, a _key and
* can be sync'd.
*/
abstract class UINode {
String _key;
UINode _parent;
UINode get parent => _parent;
RenderCSS _root;
bool _defunct = false;
UINode({ Object key }) {
_key = key == null ? "$runtimeType" : "$runtimeType-$key";
assert(this is App || _inRenderDirtyComponents); // you should not build the UI tree ahead of time, build it only during build()
}
// Subclasses which implements Nodes that become stateful may return true
// if the |old| node has become stateful and should be retained.
bool _willSync(UINode old) => false;
bool get interchangeable => false; // if true, then keys can be duplicated
void _sync(UINode old, dynamic slot);
// 'slot' is the identifier that the parent RenderNodeWrapper uses to know
// where to put this descendant
void _remove() {
_defunct = true;
_root = null;
handleRemoved();
}
void handleRemoved() { }
UINode findAncestor(Type targetType) {
var ancestor = _parent;
while (ancestor != null && !reflectClass(ancestor.runtimeType).isSubtypeOf(reflectClass(targetType)))
ancestor = ancestor._parent;
return ancestor;
}
int _nodeDepth;
void _ensureDepth() {
if (_nodeDepth == null) {
if (_parent != null) {
_parent._ensureDepth();
_nodeDepth = _parent._nodeDepth + 1;
} else {
_nodeDepth = 0;
}
}
}
void _trace(String message) {
if (!_shouldTrace)
return;
_ensureDepth();
print((' ' * _nodeDepth) + message);
}
void _traceSync(_SyncOperation op, String key) {
if (!_shouldTrace)
return;
String opString = op.toString().toLowerCase();
String outString = opString.substring(opString.indexOf('.') + 1);
_trace('_sync($outString) $key');
}
void _removeChild(UINode node) {
_traceSync(_SyncOperation.REMOVAL, node._key);
node._remove();
}
// Returns the child which should be retained as the child of this node.
UINode _syncChild(UINode node, UINode oldNode, dynamic slot) {
assert(node != null);
assert(oldNode == null || node._key == oldNode._key);
if (node == oldNode) {
_traceSync(_SyncOperation.IDENTICAL, node._key);
return node; // Nothing to do. Subtrees must be identical.
}
// TODO(rafaelw): This eagerly removes the old DOM. It may be that a
// new component was built that could re-use some of it. Consider
// syncing the new VDOM against the old one.
if (oldNode != null && node._key != oldNode._key) {
_removeChild(oldNode);
}
if (node._willSync(oldNode)) {
_traceSync(_SyncOperation.STATEFUL, node._key);
oldNode._sync(node, slot);
node._defunct = true;
assert(oldNode._root is RenderCSS);
return oldNode;
}
assert(!node._defunct);
node._parent = this;
if (oldNode == null) {
_traceSync(_SyncOperation.INSERTION, node._key);
} else {
_traceSync(_SyncOperation.STATELESS, node._key);
}
node._sync(oldNode, slot);
if (oldNode != null)
oldNode._defunct = true;
assert(node._root is RenderCSS);
return node;
}
}
abstract class ContentNode extends UINode {
UINode content;
ContentNode(UINode content) : this.content = content, super(key: content._key);
void _sync(UINode old, dynamic slot) {
UINode oldContent = old == null ? null : (old as ContentNode).content;
content = _syncChild(content, oldContent, slot);
assert(content._root != null);
_root = content._root;
}
void _remove() {
if (content != null)
_removeChild(content);
super._remove();
}
}
class StyleNode extends ContentNode {
final Style style;
StyleNode(UINode content, this.style): super(content);
}
class ParentDataNode extends ContentNode {
final ParentData parentData;
ParentDataNode(UINode content, this.parentData): super(content);
}
typedef GestureEventListener(sky.GestureEvent e);
typedef PointerEventListener(sky.PointerEvent e);
typedef EventListener(sky.Event e);
class EventListenerNode extends ContentNode {
final Map<String, sky.EventListener> listeners;
static final Set<String> _registeredEvents = new HashSet<String>();
static Map<String, sky.EventListener> _createListeners({
EventListener onWheel,
GestureEventListener onGestureFlingCancel,
GestureEventListener onGestureFlingStart,
GestureEventListener onGestureScrollStart,
GestureEventListener onGestureScrollUpdate,
GestureEventListener onGestureTap,
GestureEventListener onGestureTapDown,
PointerEventListener onPointerCancel,
PointerEventListener onPointerDown,
PointerEventListener onPointerMove,
PointerEventListener onPointerUp,
Map<String, sky.EventListener> custom
}) {
var listeners = custom != null ?
new HashMap<String, sky.EventListener>.from(custom) :
new HashMap<String, sky.EventListener>();
if (onWheel != null)
listeners['wheel'] = onWheel;
if (onGestureFlingCancel != null)
listeners['gestureflingcancel'] = onGestureFlingCancel;
if (onGestureFlingStart != null)
listeners['gestureflingstart'] = onGestureFlingStart;
if (onGestureScrollStart != null)
listeners['gesturescrollstart'] = onGestureScrollStart;
if (onGestureScrollUpdate != null)
listeners['gesturescrollupdate'] = onGestureScrollUpdate;
if (onGestureTap != null)
listeners['gesturetap'] = onGestureTap;
if (onGestureTapDown != null)
listeners['gesturetapdown'] = onGestureTapDown;
if (onPointerCancel != null)
listeners['pointercancel'] = onPointerCancel;
if (onPointerDown != null)
listeners['pointerdown'] = onPointerDown;
if (onPointerMove != null)
listeners['pointermove'] = onPointerMove;
if (onPointerUp != null)
listeners['pointerup'] = onPointerUp;
return listeners;
}
EventListenerNode(UINode content, {
EventListener onWheel,
GestureEventListener onGestureFlingCancel,
GestureEventListener onGestureFlingStart,
GestureEventListener onGestureScrollStart,
GestureEventListener onGestureScrollUpdate,
GestureEventListener onGestureTap,
GestureEventListener onGestureTapDown,
PointerEventListener onPointerCancel,
PointerEventListener onPointerDown,
PointerEventListener onPointerMove,
PointerEventListener onPointerUp,
Map<String, sky.EventListener> custom
}) : listeners = _createListeners(
onWheel: onWheel,
onGestureFlingCancel: onGestureFlingCancel,
onGestureFlingStart: onGestureFlingStart,
onGestureScrollUpdate: onGestureScrollUpdate,
onGestureScrollStart: onGestureScrollStart,
onGestureTap: onGestureTap,
onGestureTapDown: onGestureTapDown,
onPointerCancel: onPointerCancel,
onPointerDown: onPointerDown,
onPointerMove: onPointerMove,
onPointerUp: onPointerUp,
custom: custom
),
super(content);
void _handleEvent(sky.Event e) {
sky.EventListener listener = listeners[e.type];
if (listener != null) {
listener(e);
}
}
static void _dispatchEvent(sky.Event e) {
UINode target = RenderNodeWrapper._getMounted(bridgeEventTargetToRenderNode(e.target));
// TODO(rafaelw): StopPropagation?
while (target != null) {
if (target is EventListenerNode) {
target._handleEvent(e);
}
target = target._parent;
}
}
static void _ensureDocumentListener(String eventType) {
if (_registeredEvents.add(eventType)) {
sky.document.addEventListener(eventType, _dispatchEvent);
}
}
void _sync(UINode old, dynamic slot) {
for (var type in listeners.keys) {
_ensureDocumentListener(type);
}
super._sync(old, slot);
}
}
/*
* RenderNodeWrappers correspond to a desired state of a RenderCSS.
* They are fully immutable, with one exception: A UINode which is a
* Component which lives within an OneChildListRenderNodeWrapper's
* children list, may be replaced with the "old" instance if it has
* become stateful.
*/
abstract class RenderNodeWrapper extends UINode {
static final Map<RenderCSS, RenderNodeWrapper> _nodeMap =
new HashMap<RenderCSS, RenderNodeWrapper>();
static RenderNodeWrapper _getMounted(RenderCSS node) => _nodeMap[node];
RenderNodeWrapper({
Object key,
this.style,
this.inlineStyle
}) : super(key: key);
final Style style;
final String inlineStyle;
RenderCSS _createNode();
RenderNodeWrapper get _emptyNode;
void insert(RenderNodeWrapper child, dynamic slot);
void _sync(UINode old, dynamic slot) {
if (old == null) {
_root = _createNode();
assert(_root != null);
var ancestor = findAncestor(RenderNodeWrapper);
if (ancestor is RenderNodeWrapper)
ancestor.insert(this, slot);
old = _emptyNode;
} else {
_root = old._root;
assert(_root != null);
}
_nodeMap[_root] = this;
_syncRenderNode(old);
}
void _syncRenderNode(RenderNodeWrapper old) {
RenderNodeWrapper oldRenderNodeWrapper = old as RenderNodeWrapper;
List<Style> styles = new List<Style>();
if (style != null)
styles.add(style);
ParentData parentData = null;
UINode parent = _parent;
while (parent != null && parent is! RenderNodeWrapper) {
if (parent is StyleNode && parent.style != null)
styles.add(parent.style);
else
if (parent is ParentDataNode && parent.parentData != null) {
if (parentData != null)
parentData.merge(parent.parentData); // this will throw if the types aren't the same
else
parentData = parent.parentData;
}
parent = parent._parent;
}
_root.updateStyles(styles);
if (parentData != null) {
assert(_root.parentData != null);
_root.parentData.merge(parentData); // this will throw if the types aren't approriate
assert(parent != null);
assert(parent._root != null);
parent._root.markNeedsLayout();
}
_root.updateInlineStyle(inlineStyle);
}
void _removeChild(UINode node) {
assert(_root is RenderCSSContainer);
_root.remove(node._root);
super._removeChild(node);
}
void _remove() {
assert(_root != null);
_nodeMap.remove(_root);
super._remove();
}
}
final List<UINode> _emptyList = new List<UINode>();
abstract class OneChildListRenderNodeWrapper extends RenderNodeWrapper {
// In OneChildListRenderNodeWrapper subclasses, slots are RenderCSS nodes
// to use as the "insert before" sibling in RenderCSSContainer.add() calls
final List<UINode> children;
OneChildListRenderNodeWrapper({
Object key,
List<UINode> children,
Style style,
String inlineStyle
}) : this.children = children == null ? _emptyList : children,
super(
key: key,
style: style,
inlineStyle: inlineStyle
) {
assert(!_debugHasDuplicateIds());
}
void insert(RenderNodeWrapper child, dynamic slot) {
assert(slot == null || slot is RenderCSS);
_root.add(child._root, before: slot);
}
void _remove() {
assert(children != null);
for (var child in children) {
assert(child != null);
_removeChild(child);
}
super._remove();
}
bool _debugHasDuplicateIds() {
var idSet = new HashSet<String>();
for (var child in children) {
assert(child != null);
if (child.interchangeable)
continue; // when these nodes are reordered, we just reassign the data
if (!idSet.add(child._key)) {
throw '''If multiple non-interchangeable nodes of the same type exist as children
of another node, they must have unique keys.
Duplicate: "${child._key}"''';
}
}
return false;
}
void _syncRenderNode(OneChildListRenderNodeWrapper old) {
super._syncRenderNode(old);
if (_root is! RenderCSSContainer)
return;
var startIndex = 0;
var endIndex = children.length;
var oldChildren = old.children;
var oldStartIndex = 0;
var oldEndIndex = oldChildren.length;
RenderCSS nextSibling = null;
UINode currentNode = null;
UINode oldNode = null;
void sync(int atIndex) {
children[atIndex] = _syncChild(currentNode, oldNode, nextSibling);
assert(children[atIndex] != null);
}
// Scan backwards from end of list while nodes can be directly synced
// without reordering.
while (endIndex > startIndex && oldEndIndex > oldStartIndex) {
currentNode = children[endIndex - 1];
oldNode = oldChildren[oldEndIndex - 1];
if (currentNode._key != oldNode._key) {
break;
}
endIndex--;
oldEndIndex--;
sync(endIndex);
}
HashMap<String, UINode> oldNodeIdMap = null;
bool oldNodeReordered(String key) {
return oldNodeIdMap != null &&
oldNodeIdMap.containsKey(key) &&
oldNodeIdMap[key] == null;
}
void advanceOldStartIndex() {
oldStartIndex++;
while (oldStartIndex < oldEndIndex &&
oldNodeReordered(oldChildren[oldStartIndex]._key)) {
oldStartIndex++;
}
}
void ensureOldIdMap() {
if (oldNodeIdMap != null)
return;
oldNodeIdMap = new HashMap<String, UINode>();
for (int i = oldStartIndex; i < oldEndIndex; i++) {
var node = oldChildren[i];
if (!node.interchangeable)
oldNodeIdMap.putIfAbsent(node._key, () => node);
}
}
bool searchForOldNode() {
if (currentNode.interchangeable)
return false; // never re-order these nodes
ensureOldIdMap();
oldNode = oldNodeIdMap[currentNode._key];
if (oldNode == null)
return false;
oldNodeIdMap[currentNode._key] = null; // mark it reordered
assert(_root is RenderCSSContainer);
assert(oldNode._root is RenderCSSContainer);
old._root.remove(oldNode._root);
_root.add(oldNode._root, before: nextSibling);
return true;
}
// Scan forwards, this time we may re-order;
nextSibling = _root.firstChild;
while (startIndex < endIndex && oldStartIndex < oldEndIndex) {
currentNode = children[startIndex];
oldNode = oldChildren[oldStartIndex];
if (currentNode._key == oldNode._key) {
assert(currentNode.runtimeType == oldNode.runtimeType);
nextSibling = _root.childAfter(nextSibling);
sync(startIndex);
startIndex++;
advanceOldStartIndex();
continue;
}
oldNode = null;
searchForOldNode();
sync(startIndex);
startIndex++;
}
// New insertions
oldNode = null;
while (startIndex < endIndex) {
currentNode = children[startIndex];
sync(startIndex);
startIndex++;
}
// Removals
currentNode = null;
while (oldStartIndex < oldEndIndex) {
oldNode = oldChildren[oldStartIndex];
_removeChild(oldNode);
advanceOldStartIndex();
}
}
}
class Container extends OneChildListRenderNodeWrapper {
RenderCSSContainer _root;
RenderCSSContainer _createNode() => new RenderCSSContainer(this);
static final Container _emptyContainer = new Container();
RenderNodeWrapper get _emptyNode => _emptyContainer;
Container({
Object key,
List<UINode> children,
Style style,
String inlineStyle
}) : super(
key: key,
children: children,
style: style,
inlineStyle: inlineStyle
);
}
class Paragraph extends OneChildListRenderNodeWrapper {
RenderCSSParagraph _root;
RenderCSSParagraph _createNode() => new RenderCSSParagraph(this);
static final Paragraph _emptyContainer = new Paragraph();
RenderNodeWrapper get _emptyNode => _emptyContainer;
Paragraph({
Object key,
List<UINode> children,
Style style,
String inlineStyle
}) : super(
key: key,
children: children,
style: style,
inlineStyle: inlineStyle
);
}
class FlexContainer extends OneChildListRenderNodeWrapper {
RenderCSSFlex _root;
RenderCSSFlex _createNode() => new RenderCSSFlex(this, this.direction);
static final FlexContainer _emptyContainer = new FlexContainer();
// direction doesn't matter if it's empty
RenderNodeWrapper get _emptyNode => _emptyContainer;
final FlexDirection direction;
FlexContainer({
Object key,
List<UINode> children,
Style style,
String inlineStyle,
this.direction: FlexDirection.Row
}) : super(
key: key,
children: children,
style: style,
inlineStyle: inlineStyle
);
void _syncRenderNode(UINode old) {
super._syncRenderNode(old);
_root.direction = direction;
}
}
class FillStackContainer extends OneChildListRenderNodeWrapper {
RenderCSSStack _root;
RenderCSSStack _createNode() => new RenderCSSStack(this);
static final FillStackContainer _emptyContainer = new FillStackContainer();
RenderNodeWrapper get _emptyNode => _emptyContainer;
FillStackContainer({
Object key,
List<UINode> children,
Style style,
String inlineStyle
}) : super(
key: key,
children: _positionNodesToFill(children),
style: style,
inlineStyle: inlineStyle
);
static StackParentData _fillParentData = new StackParentData()
..top = 0.0
..left = 0.0
..right = 0.0
..bottom = 0.0;
static List<UINode> _positionNodesToFill(List<UINode> input) {
if (input == null)
return null;
return input.map((node) {
return new ParentDataNode(node, _fillParentData);
}).toList();
}
}
class TextFragment extends RenderNodeWrapper {
RenderCSSInline _root;
RenderCSSInline _createNode() => new RenderCSSInline(this, this.data);
static final TextFragment _emptyText = new TextFragment('');
RenderNodeWrapper get _emptyNode => _emptyText;
final String data;
TextFragment(this.data, {
Object key,
Style style,
String inlineStyle
}) : super(
key: key,
style: style,
inlineStyle: inlineStyle
);
void _syncRenderNode(UINode old) {
super._syncRenderNode(old);
_root.data = data;
}
}
class Image extends RenderNodeWrapper {
RenderCSSImage _root;
RenderCSSImage _createNode() => new RenderCSSImage(this, this.src, this.width, this.height);
static final Image _emptyImage = new Image();
RenderNodeWrapper get _emptyNode => _emptyImage;
final String src;
final int width;
final int height;
Image({
Object key,
Style style,
String inlineStyle,
this.width,
this.height,
this.src
}) : super(
key: key,
style: style,
inlineStyle: inlineStyle
);
void _syncRenderNode(UINode old) {
super._syncRenderNode(old);
_root.configure(this.src, this.width, this.height);
}
}
Set<Component> _mountedComponents = new HashSet<Component>();
Set<Component> _unmountedComponents = new HashSet<Component>();
void _enqueueDidMount(Component c) {
assert(!_notifingMountStatus);
_mountedComponents.add(c);
}
void _enqueueDidUnmount(Component c) {
assert(!_notifingMountStatus);
_unmountedComponents.add(c);
}
bool _notifingMountStatus = false;
void _notifyMountStatusChanged() {
try {
_notifingMountStatus = true;
_unmountedComponents.forEach((c) => c._didUnmount());
_mountedComponents.forEach((c) => c._didMount());
_mountedComponents.clear();
_unmountedComponents.clear();
} finally {
_notifingMountStatus = false;
}
}
List<Component> _dirtyComponents = new List<Component>();
bool _buildScheduled = false;
bool _inRenderDirtyComponents = false;
void _buildDirtyComponents() {
_tracing.begin('fn::_buildDirtyComponents');
Stopwatch sw;
if (_shouldLogRenderDuration)
sw = new Stopwatch()..start();
try {
_inRenderDirtyComponents = true;
_dirtyComponents.sort((a, b) => a._order - b._order);
for (var comp in _dirtyComponents) {
comp._buildIfDirty();
}
_dirtyComponents.clear();
_buildScheduled = false;
} finally {
_inRenderDirtyComponents = false;
}
_notifyMountStatusChanged();
if (_shouldLogRenderDuration) {
sw.stop();
print('Render took ${sw.elapsedMicroseconds} microseconds');
}
_tracing.end('fn::_buildDirtyComponents');
}
void _scheduleComponentForRender(Component c) {
assert(!_inRenderDirtyComponents);
_dirtyComponents.add(c);
if (!_buildScheduled) {
_buildScheduled = true;
new Future.microtask(_buildDirtyComponents);
}
}
abstract class Component extends UINode {
bool get _isBuilding => _currentlyBuilding == this;
bool _dirty = true;
UINode _built;
final int _order;
static int _currentOrder = 0;
bool _stateful;
static Component _currentlyBuilding;
List<Function> _mountCallbacks;
List<Function> _unmountCallbacks;
dynamic _slot; // cached slot from the last time we were synced
void onDidMount(Function fn) {
if (_mountCallbacks == null)
_mountCallbacks = new List<Function>();
_mountCallbacks.add(fn);
}
void onDidUnmount(Function fn) {
if (_unmountCallbacks == null)
_unmountCallbacks = new List<Function>();
_unmountCallbacks.add(fn);
}
Component({ Object key, bool stateful })
: _stateful = stateful != null ? stateful : false,
_order = _currentOrder + 1,
super(key: key);
Component.fromArgs(Object key, bool stateful)
: this(key: key, stateful: stateful);
void _didMount() {
if (_mountCallbacks != null)
_mountCallbacks.forEach((fn) => fn());
}
void _didUnmount() {
if (_unmountCallbacks != null)
_unmountCallbacks.forEach((fn) => fn());
}
// TODO(rafaelw): It seems wrong to expose DOM at all. This is presently
// needed to get sizing info.
RenderCSS getRoot() => _root;
void _remove() {
assert(_built != null);
assert(_root != null);
_removeChild(_built);
_built = null;
_enqueueDidUnmount(this);
super._remove();
}
bool _willSync(UINode old) {
Component oldComponent = old as Component;
if (oldComponent == null || !oldComponent._stateful)
return false;
// Make |this| the "old" Component
_stateful = false;
_built = oldComponent._built;
assert(_built != null);
// Make |oldComponent| the "new" component
reflect.copyPublicFields(this, oldComponent);
oldComponent._built = null;
oldComponent._dirty = true;
return true;
}
/* There are three cases here:
* 1) Building for the first time:
* assert(_built == null && old == null)
* 2) Re-building (because a dirty flag got set):
* assert(_built != null && old == null)
* 3) Syncing against an old version
* assert(_built == null && old != null)
*/
void _sync(UINode old, dynamic slot) {
assert(!_defunct);
assert(_built == null || old == null);
Component oldComponent = old as Component;
_slot = slot;
var oldBuilt;
if (oldComponent == null) {
oldBuilt = _built;
} else {
assert(_built == null);
oldBuilt = oldComponent._built;
}
if (oldBuilt == null)
_enqueueDidMount(this);
int lastOrder = _currentOrder;
_currentOrder = _order;
_currentlyBuilding = this;
_built = build();
_currentlyBuilding = null;
_currentOrder = lastOrder;
_built = _syncChild(_built, oldBuilt, slot);
_dirty = false;
_root = _built._root;
assert(_root != null);
}
void _buildIfDirty() {
if (!_dirty || _defunct)
return;
_trace('$_key rebuilding...');
assert(_root != null);
_sync(null, _slot);
}
void scheduleBuild() {
setState(() {});
}
void setState(Function fn()) {
_stateful = true;
fn();
if (_isBuilding || _dirty || _defunct)
return;
_dirty = true;
_scheduleComponentForRender(this);
}
UINode build();
}
abstract class App extends Component {
RenderCSS _host;
App() : super(stateful: true) {
_host = new RenderCSSRoot(this);
_scheduleComponentForRender(this);
}
void _buildIfDirty() {
if (!_dirty || _defunct)
return;
_trace('$_key rebuilding...');
_sync(null, null);
if (_root.parent == null)
_host.add(_root);
assert(_root.parent == _host);
}
}
class Text extends Component {
Text(this.data) : super(key: '*text*');
final String data;
bool get interchangeable => true;
UINode build() => new Paragraph(children: [new TextFragment(data)]);
}