// 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: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"; } // 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, RenderCSSContainer host, RenderCSS insertBefore); void _remove() { _defunct = true; _root = null; handleRemoved(); } void handleRemoved() { } 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, RenderCSSContainer host, RenderCSS insertBefore) { 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, host, insertBefore); node._defunct = true; assert(oldNode._root is RenderCSS); return oldNode; } node._parent = this; if (oldNode == null) { _traceSync(_SyncOperation.INSERTION, node._key); } else { _traceSync(_SyncOperation.STATELESS, node._key); } node._sync(oldNode, host, insertBefore); 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, RenderCSSContainer host, RenderCSS insertBefore) { UINode oldContent = old == null ? null : (old as ContentNode).content; content = _syncChild(content, oldContent, host, insertBefore); 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 listeners; static final Set _registeredEvents = new HashSet(); 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; } 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); 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, RenderCSSContainer host, RenderCSS insertBefore) { for (var type in listeners.keys) { _ensureDocumentListener(type); } super._sync(old, host, insertBefore); } } /* * 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 _nodeMap = new HashMap(); static RenderNodeWrapper _getMounted(RenderCSS node) => _nodeMap[node]; RenderNodeWrapper({ Object key }) : super(key: key); RenderNodeWrapper get _emptyNode; RenderCSS _createNode(); void _sync(UINode old, RenderCSSContainer host, RenderCSS insertBefore) { if (old == null) { _root = _createNode(); assert(_root != null); host.add(_root, before: insertBefore); old = _emptyNode; } else { _root = old._root; assert(_root != null); } _nodeMap[_root] = this; _syncNode(old); } void _syncNode(RenderNodeWrapper old); 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 _emptyList = new List(); abstract class OneChildListRenderNodeWrapper extends RenderNodeWrapper { final List children; final Style style; final String inlineStyle; OneChildListRenderNodeWrapper({ Object key, List children, this.style, this.inlineStyle }) : this.children = children == null ? _emptyList : children, super(key: key) { assert(!_debugHasDuplicateIds()); } 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 _syncNode(RenderNodeWrapper old) { OneChildListRenderNodeWrapper oldOneChildListRenderNodeWrapper = old as OneChildListRenderNodeWrapper; List