// 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. import 'dart:async'; import 'dart:collection'; import 'dart:mirrors'; import 'dart:sky' as sky; import '../app.dart'; import '../rendering/box.dart'; import '../rendering/object.dart'; export '../rendering/box.dart' show BoxConstraints, BoxDecoration, Border, BorderSide, EdgeDims; export '../rendering/flex.dart' show FlexDirection; export '../rendering/object.dart' show Point, Size, Rect, Color, Paint, Path; // final sky.Tracing _tracing = sky.window.tracing; final bool _shouldLogRenderDuration = false; /* * All Effen nodes derive from UINode. All nodes have a _parent, a _key and * can be sync'd. */ abstract class UINode { 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() } String _key; String get key => _key; UINode _parent; UINode get parent => _parent; bool _mounted = false; bool _wasMounted = false; bool get mounted => _mounted; static bool _notifyingMountStatus = false; static Set _mountedChanged = new HashSet(); void setParent(UINode newParent) { assert(!_notifyingMountStatus); _parent = newParent; if (newParent == null) { if (_mounted) { _mounted = false; _mountedChanged.add(this); } } else { assert(newParent._mounted); if (_parent._mounted != _mounted) { _mounted = _parent._mounted; _mountedChanged.add(this); } } } static void _notifyMountStatusChanged() { try { _notifyingMountStatus = true; for (UINode node in _mountedChanged) { if (node._wasMounted != node._mounted) { if (node._mounted) node.didMount(); else node.didUnmount(); node._wasMounted = node._mounted; } } _mountedChanged.clear(); } finally { _notifyingMountStatus = false; } } void didMount() { } void didUnmount() { } RenderObject _root; RenderObject get root => _root; // Subclasses which implements Nodes that become stateful may return true // if the |old| node has become stateful and should be retained. // This is called immediately before _sync(). // Component._retainStatefulNodeIfPossible() calls syncFields(). bool _retainStatefulNodeIfPossible(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 RenderObjectWrapper uses to know // where to put this descendant void remove() { _root = null; setParent(null); } UINode findAncestor(Type targetType) { var ancestor = _parent; while (ancestor != null && !reflectClass(ancestor.runtimeType).isSubtypeOf(reflectClass(targetType))) ancestor = ancestor._parent; return ancestor; } void removeChild(UINode node) { node.remove(); } // Returns the child which should be retained as the child of this node. UINode syncChild(UINode node, UINode oldNode, dynamic slot) { assert(oldNode is! Component || !oldNode._disqualifiedFromEverAppearingAgain); if (node == oldNode) { assert(node == null || node.mounted); return node; // Nothing to do. Subtrees must be identical. } if (node == null) { // the child in this slot has gone away assert(oldNode.mounted); removeChild(oldNode); assert(!oldNode.mounted); return null; } if (oldNode != null && node._key == oldNode._key && node._retainStatefulNodeIfPossible(oldNode)) { assert(oldNode.mounted); assert(!node.mounted); oldNode._sync(node, slot); assert(oldNode.root is RenderObject); return oldNode; } if (oldNode != null && node._key != oldNode._key) { assert(oldNode.mounted); removeChild(oldNode); oldNode = null; } assert(!node.mounted); node.setParent(this); node._sync(oldNode, slot); assert(node.root is RenderObject); return node; } } // Descendants of TagNode provide a way to tag RenderObjectWrapper and // Component nodes with annotations, such as event listeners, // stylistic information, etc. abstract class TagNode extends UINode { TagNode(UINode content, { Object key }) : this.content = content, super(key: key); UINode content; void _sync(UINode old, dynamic slot) { UINode oldContent = old == null ? null : (old as TagNode).content; content = syncChild(content, oldContent, slot); assert(content.root != null); _root = content.root; assert(_root == root); // in case a subclass reintroduces it } void remove() { if (content != null) removeChild(content); super.remove(); } } class ParentDataNode extends TagNode { ParentDataNode(UINode content, this.parentData, { Object key }): super(content, key: key); final ParentData parentData; } typedef void GestureEventListener(sky.GestureEvent e); typedef void PointerEventListener(sky.PointerEvent e); typedef void EventListener(sky.Event e); class EventListenerNode extends TagNode { 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 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); final Map listeners; static Map _createListeners({ EventListener onWheel, GestureEventListener onGestureFlingCancel, GestureEventListener onGestureFlingStart, GestureEventListener onGestureScrollStart, GestureEventListener onGestureScrollUpdate, GestureEventListener onGestureTap, GestureEventListener onGestureTapDown, PointerEventListener onPointerCancel, PointerEventListener onPointerDown, PointerEventListener onPointerMove, PointerEventListener onPointerUp, Map custom }) { var listeners = custom != null ? new HashMap.from(custom) : new HashMap(); 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; } void _handleEvent(sky.Event e) { sky.EventListener listener = listeners[e.type]; if (listener != null) { listener(e); } } } abstract class Component extends UINode { 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); static Component _currentlyBuilding; bool get _isBuilding => _currentlyBuilding == this; bool _stateful; bool _dirty = true; bool _disqualifiedFromEverAppearingAgain = false; UINode _built; dynamic _slot; // cached slot from the last time we were synced void didMount() { assert(!_disqualifiedFromEverAppearingAgain); super.didMount(); } void remove() { assert(_built != null); assert(root != null); removeChild(_built); _built = null; super.remove(); } bool _retainStatefulNodeIfPossible(UINode old) { assert(!_disqualifiedFromEverAppearingAgain); Component oldComponent = old as Component; if (oldComponent == null || !oldComponent._stateful) return false; assert(key == oldComponent.key); // Make |this|, the newly-created object, into the "old" Component, and kill it _stateful = false; _built = oldComponent._built; assert(_built != null); _disqualifiedFromEverAppearingAgain = true; // Make |oldComponent| the "new" component oldComponent._built = null; oldComponent._dirty = true; oldComponent.syncFields(this); return true; } // This is called by _retainStatefulNodeIfPossible(), during // syncChild(), just before _sync() is called. // This must be implemented on any subclass that can become stateful // (but don't call super.syncFields() if you inherit directly from // Component, since that'll fire an assert). // If you don't ever become stateful, then don't override this. void syncFields(Component source) { assert(false); } final int _order; static int _currentOrder = 0; /* 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(_built == null || old == null); assert(!_disqualifiedFromEverAppearingAgain); Component oldComponent = old as Component; _slot = slot; var oldBuilt; if (oldComponent == null) { oldBuilt = _built; } else { assert(_built == null); oldBuilt = oldComponent._built; } int lastOrder = _currentOrder; _currentOrder = _order; _currentlyBuilding = this; _built = build(); assert(_built != null); _currentlyBuilding = null; _currentOrder = lastOrder; _built = syncChild(_built, oldBuilt, slot); assert(_built != null); _dirty = false; _root = _built.root; assert(_root == root); // in case a subclass reintroduces it assert(root != null); } void _buildIfDirty() { assert(!_disqualifiedFromEverAppearingAgain); if (!_dirty || !_mounted) return; assert(root != null); _sync(null, _slot); } void scheduleBuild() { setState(() {}); } void setState(Function fn()) { assert(!_disqualifiedFromEverAppearingAgain); _stateful = true; fn(); if (_isBuilding || _dirty || !_mounted) return; _dirty = true; _scheduleComponentForRender(this); } UINode build(); } Set _dirtyComponents = new Set(); bool _buildScheduled = false; bool _inRenderDirtyComponents = false; void _buildDirtyComponents() { //_tracing.begin('fn::_buildDirtyComponents'); Stopwatch sw; if (_shouldLogRenderDuration) sw = new Stopwatch()..start(); try { _inRenderDirtyComponents = true; List sortedDirtyComponents = _dirtyComponents.toList(); sortedDirtyComponents.sort((Component a, Component b) => a._order - b._order); for (var comp in sortedDirtyComponents) { comp._buildIfDirty(); } _dirtyComponents.clear(); _buildScheduled = false; } finally { _inRenderDirtyComponents = false; } UINode._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); } } /* * RenderObjectWrappers correspond to a desired state of a RenderObject. * They are fully immutable, with one exception: A UINode which is a * Component which lives within an MultiChildRenderObjectWrapper's * children list, may be replaced with the "old" instance if it has * become stateful. */ abstract class RenderObjectWrapper extends UINode { RenderObjectWrapper({ Object key }) : super(key: key); RenderObject createNode(); void insert(RenderObjectWrapper child, dynamic slot); static final Map _nodeMap = new HashMap(); static RenderObjectWrapper _getMounted(RenderObject node) => _nodeMap[node]; void _sync(UINode old, dynamic slot) { assert(parent != null); if (old == null) { _root = createNode(); var ancestor = findAncestor(RenderObjectWrapper); if (ancestor is RenderObjectWrapper) ancestor.insert(this, slot); } else { _root = old.root; } assert(_root == root); // in case a subclass reintroduces it assert(root != null); assert(mounted); _nodeMap[root] = this; syncRenderObject(old); } void syncRenderObject(RenderObjectWrapper old) { ParentData parentData = null; UINode ancestor = parent; while (ancestor != null && ancestor is! RenderObjectWrapper) { if (ancestor is ParentDataNode && ancestor.parentData != null) { if (parentData != null) parentData.merge(ancestor.parentData); // this will throw if the types aren't the same else parentData = ancestor.parentData; } ancestor = ancestor.parent; } if (parentData != null) { assert(root.parentData != null); root.parentData.merge(parentData); // this will throw if the types aren't appropriate if (parent.root != null) parent.root.markNeedsLayout(); } } void remove() { assert(root != null); _nodeMap.remove(root); super.remove(); } } abstract class OneChildRenderObjectWrapper extends RenderObjectWrapper { OneChildRenderObjectWrapper({ UINode child, Object key }) : _child = child, super(key: key); UINode _child; UINode get child => _child; void syncRenderObject(RenderObjectWrapper old) { super.syncRenderObject(old); UINode oldChild = old == null ? null : (old as OneChildRenderObjectWrapper).child; _child = syncChild(child, oldChild, null); } void insert(RenderObjectWrapper child, dynamic slot) { final root = this.root; // TODO(ianh): Remove this once the analyzer is cleverer assert(slot == null); assert(root is RenderObjectWithChildMixin); root.child = child.root; assert(root == this.root); // TODO(ianh): Remove this once the analyzer is cleverer } void removeChild(UINode node) { final root = this.root; // TODO(ianh): Remove this once the analyzer is cleverer assert(root is RenderObjectWithChildMixin); root.child = null; super.removeChild(node); assert(root == this.root); // TODO(ianh): Remove this once the analyzer is cleverer } void remove() { if (child != null) removeChild(child); super.remove(); } } abstract class MultiChildRenderObjectWrapper extends RenderObjectWrapper { // In MultiChildRenderObjectWrapper subclasses, slots are RenderObject nodes // to use as the "insert before" sibling in ContainerRenderObjectMixin.add() calls MultiChildRenderObjectWrapper({ Object key, List children }) : this.children = children == null ? const [] : children, super(key: key) { assert(!_debugHasDuplicateIds()); } final List children; void insert(RenderObjectWrapper child, dynamic slot) { final root = this.root; // TODO(ianh): Remove this once the analyzer is cleverer assert(slot == null || slot is RenderObject); assert(root is ContainerRenderObjectMixin); root.add(child.root, before: slot); assert(root == this.root); // TODO(ianh): Remove this once the analyzer is cleverer } void removeChild(UINode node) { final root = this.root; // TODO(ianh): Remove this once the analyzer is cleverer assert(root is ContainerRenderObjectMixin); assert(node.root.parent == root); root.remove(node.root); super.removeChild(node); assert(root == this.root); // TODO(ianh): Remove this once the analyzer is cleverer } void remove() { assert(children != null); for (var child in children) { assert(child != null); removeChild(child); } super.remove(); } bool _debugHasDuplicateIds() { var idSet = new HashSet(); 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 syncRenderObject(MultiChildRenderObjectWrapper old) { super.syncRenderObject(old); final root = this.root; // TODO(ianh): Remove this once the analyzer is cleverer if (root is! ContainerRenderObjectMixin) return; var startIndex = 0; var endIndex = children.length; var oldChildren = old == null ? [] : old.children; var oldStartIndex = 0; var oldEndIndex = oldChildren.length; RenderObject 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 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(); 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 ContainerRenderObjectMixin); assert(old.root is ContainerRenderObjectMixin); assert(oldNode.root != null); (old.root as ContainerRenderObjectMixin).remove(oldNode.root); // TODO(ianh): Remove cast once the analyzer is cleverer 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(); } assert(root == this.root); // TODO(ianh): Remove this once the analyzer is cleverer } } class UINodeAppView extends AppView { UINodeAppView() { assert(_appView == null); } static UINodeAppView _appView; static void initUINodeAppView() { if (_appView == null) _appView = new UINodeAppView(); } void dispatchEvent(sky.Event event, HitTestResult result) { assert(_appView == this); super.dispatchEvent(event, result); for (HitTestEntry entry in result.path.reversed) { UINode target = RenderObjectWrapper._getMounted(entry.target); if (target == null) continue; RenderObject targetRoot = target.root; while (target != null && target.root == targetRoot) { if (target is EventListenerNode) target._handleEvent(event); target = target._parent; } } } } abstract class AbstractUINodeRoot extends Component { AbstractUINodeRoot() : super(stateful: true) { UINodeAppView.initUINodeAppView(); _mounted = true; _scheduleComponentForRender(this); } void syncFields(AbstractUINodeRoot source) { assert(false); // if we get here, it implies that we have a parent } void _buildIfDirty() { assert(_dirty); assert(_mounted); assert(parent == null); _sync(null, null); } } abstract class App extends AbstractUINodeRoot { App(); AppView get appView => UINodeAppView._appView; void _buildIfDirty() { super._buildIfDirty(); if (root.parent == null) { // we haven't attached it yet UINodeAppView._appView.root = root; } assert(root.parent is RenderView); } } typedef UINode Builder(); class RenderObjectToUINodeAdapter extends AbstractUINodeRoot { RenderObjectToUINodeAdapter( RenderObjectWithChildMixin container, this.builder ) : _container = container { assert(builder != null); } RenderObjectWithChildMixin _container; RenderObjectWithChildMixin get container => _container; void set container(RenderObjectWithChildMixin value) { if (_container != value) { assert(value.child == null); if (root != null) { assert(_container.child == root); _container.child = null; } _container = value; if (root != null) { _container.child = root; assert(_container.child == root); } } } final Builder builder; void _buildIfDirty() { super._buildIfDirty(); if (root.parent == null) { // we haven't attached it yet assert(_container.child == null); _container.child = root; } assert(root.parent == _container); } UINode build() => builder(); }