| OLD | NEW |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 library fn; | 5 library fn; |
| 6 | 6 |
| 7 import 'dart:async'; | 7 import 'dart:async'; |
| 8 import 'dart:collection'; | 8 import 'dart:collection'; |
| 9 import 'dart:sky' as sky; | 9 import 'dart:sky' as sky; |
| 10 import 'reflect.dart' as reflect; | 10 import 'reflect.dart' as reflect; |
| (...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 74 void _parentInsertBefore(sky.ParentNode parent, | 74 void _parentInsertBefore(sky.ParentNode parent, |
| 75 sky.Node node, | 75 sky.Node node, |
| 76 sky.Node ref) { | 76 sky.Node ref) { |
| 77 if (ref != null) { | 77 if (ref != null) { |
| 78 ref.insertBefore([node]); | 78 ref.insertBefore([node]); |
| 79 } else { | 79 } else { |
| 80 parent.appendChild(node); | 80 parent.appendChild(node); |
| 81 } | 81 } |
| 82 } | 82 } |
| 83 | 83 |
| 84 /* |
| 85 * All Effen nodes derive from Node. All nodes have a _parent, a _key and |
| 86 * can be sync'd. |
| 87 */ |
| 84 abstract class Node { | 88 abstract class Node { |
| 85 String _key; | 89 String _key; |
| 86 Node _parent; | 90 Node _parent; |
| 87 sky.Node _root; | 91 sky.Node _root; |
| 88 bool _defunct = false; | 92 bool _defunct = false; |
| 89 | 93 |
| 90 // TODO(abarth): Both Elements and Components have |events| but |Text| | 94 // TODO(abarth): Both Elements and Components have |events| but |Text| |
| 91 // doesn't. Should we add a common base class to contain |events|? | 95 // doesn't. Should we add a common base class to contain |events|? |
| 92 final EventMap events = new EventMap(); | 96 final EventMap events = new EventMap(); |
| 93 | 97 |
| 94 Node({ Object key }) { | 98 Node({ Object key }) { |
| 95 _key = key == null ? "$runtimeType" : "$runtimeType-$key"; | 99 _key = key == null ? "$runtimeType" : "$runtimeType-$key"; |
| 96 } | 100 } |
| 97 | 101 |
| 98 Node get _emptyNode; | 102 // Subclasses which implements Nodes that become stateful may return true |
| 103 // if the |old| node has become stateful and should be retained. |
| 104 bool _willSync(Node old) => false; |
| 105 |
| 106 void _sync(Node old, sky.ParentNode host, sky.Node insertBefore); |
| 107 |
| 108 void _remove() { |
| 109 _defunct = true; |
| 110 _root = null; |
| 111 } |
| 112 |
| 113 // Returns the child which should be retained as the child of this node. |
| 114 Node _syncChild(Node node, Node oldNode, sky.ParentNode host, |
| 115 sky.Node insertBefore) { |
| 116 if (node == oldNode) |
| 117 return node; // Nothing to do. Subtrees must be identical. |
| 118 |
| 119 // TODO(rafaelw): This eagerly removes the old DOM. It may be that a |
| 120 // new component was built that could re-use some of it. Consider |
| 121 // syncing the new VDOM against the old one. |
| 122 if (oldNode != null && node._key != oldNode._key) { |
| 123 oldNode._remove(); |
| 124 } |
| 125 |
| 126 if (node._willSync(oldNode)) { |
| 127 oldNode._sync(node, host, insertBefore); |
| 128 node._defunct = true; |
| 129 assert(oldNode._root is sky.Node); |
| 130 return oldNode; |
| 131 } |
| 132 |
| 133 node._parent = this; |
| 134 node._sync(oldNode, host, insertBefore); |
| 135 if (oldNode != null) |
| 136 oldNode._defunct = true; |
| 137 |
| 138 assert(node._root is sky.Node); |
| 139 return node; |
| 140 } |
| 141 |
| 142 void _syncEvents(EventMap oldEventMap) { |
| 143 List<EventHandler> newHandlers = events._handlers; |
| 144 int newStartIndex = 0; |
| 145 int newEndIndex = newHandlers.length; |
| 146 |
| 147 List<EventHandler> oldHandlers = oldEventMap._handlers; |
| 148 int oldStartIndex = 0; |
| 149 int oldEndIndex = oldHandlers.length; |
| 150 |
| 151 // Skip over leading handlers that match. |
| 152 while (newStartIndex < newEndIndex && oldStartIndex < oldEndIndex) { |
| 153 EventHandler newHandler = newHandlers[newStartIndex]; |
| 154 EventHandler oldHandler = oldHandlers[oldStartIndex]; |
| 155 if (newHandler.type != oldHandler.type |
| 156 || newHandler.listener != oldHandler.listener) |
| 157 break; |
| 158 ++newStartIndex; |
| 159 ++oldStartIndex; |
| 160 } |
| 161 |
| 162 // Skip over trailing handlers that match. |
| 163 while (newStartIndex < newEndIndex && oldStartIndex < oldEndIndex) { |
| 164 EventHandler newHandler = newHandlers[newEndIndex - 1]; |
| 165 EventHandler oldHandler = oldHandlers[oldEndIndex - 1]; |
| 166 if (newHandler.type != oldHandler.type |
| 167 || newHandler.listener != oldHandler.listener) |
| 168 break; |
| 169 --newEndIndex; |
| 170 --oldEndIndex; |
| 171 } |
| 172 |
| 173 sky.Element root = _root as sky.Element; |
| 174 |
| 175 for (int i = oldStartIndex; i < oldEndIndex; ++i) { |
| 176 EventHandler oldHandler = oldHandlers[i]; |
| 177 root.removeEventListener(oldHandler.type, oldHandler.listener); |
| 178 } |
| 179 |
| 180 for (int i = newStartIndex; i < newEndIndex; ++i) { |
| 181 EventHandler newHandler = newHandlers[i]; |
| 182 root.addEventListener(newHandler.type, newHandler.listener); |
| 183 } |
| 184 } |
| 185 |
| 186 } |
| 187 |
| 188 /* |
| 189 * RenderNodes correspond to a desired state of a sky.Node. They are fully |
| 190 * immutable, with one exception: A Node which is a Component which lives within |
| 191 * an Element's children list, may be replaced with the "old" instance if it |
| 192 * has become stateful. |
| 193 */ |
| 194 abstract class RenderNode extends Node { |
| 195 |
| 196 RenderNode({ Object key }) : super(key: key); |
| 197 |
| 198 RenderNode get _emptyNode; |
| 99 | 199 |
| 100 sky.Node _createNode(); | 200 sky.Node _createNode(); |
| 101 | 201 |
| 102 void _mount(Node parent, sky.ParentNode host, sky.Node insertBefore) { | 202 void _sync(Node old, sky.ParentNode host, sky.Node insertBefore) { |
| 103 var node = _emptyNode; | 203 if (old == null) { |
| 104 node._parent = parent; | 204 _root = _createNode(); |
| 205 _parentInsertBefore(host, _root, insertBefore); |
| 206 old = _emptyNode; |
| 207 } else { |
| 208 _root = old._root; |
| 209 } |
| 105 | 210 |
| 106 node._root = _createNode(); | 211 _syncNode(old); |
| 107 assert(node._root != null); | |
| 108 | |
| 109 _parentInsertBefore(host, node._root, insertBefore); | |
| 110 | |
| 111 _syncNode(node); | |
| 112 } | 212 } |
| 113 | 213 |
| 114 bool _sync(Node old, Node parent, sky.ParentNode host, | 214 void _syncNode(RenderNode old); |
| 115 sky.Node insertBefore) { | |
| 116 | 215 |
| 117 if (old == null) { | 216 void _remove() { |
| 118 _mount(parent, host, insertBefore); | |
| 119 return false; | |
| 120 } | |
| 121 | |
| 122 return _syncNode(old); | |
| 123 } | |
| 124 | |
| 125 // Return true IFF the old node has *become* the new node (should be | |
| 126 // retained because it is stateful) | |
| 127 bool _syncNode(Node old) { | |
| 128 assert(!old._defunct); | |
| 129 _root = old._root; | |
| 130 _parent = old._parent; | |
| 131 return false; | |
| 132 } | |
| 133 | |
| 134 void _unmount() { | |
| 135 assert(_root != null); | 217 assert(_root != null); |
| 136 _parent = null; | |
| 137 _root.remove(); | 218 _root.remove(); |
| 138 _defunct = true; | 219 super._remove(); |
| 139 _root = null; | |
| 140 } | 220 } |
| 141 } | 221 } |
| 142 | 222 |
| 143 class Text extends Node { | 223 class Text extends RenderNode { |
| 144 final String data; | 224 final String data; |
| 145 | 225 |
| 146 // Text nodes are special cases of having non-unique keys (which don't need | 226 // Text nodes are special cases of having non-unique keys (which don't need |
| 147 // to be assigned as part of the API). Since they are unique in not having | 227 // to be assigned as part of the API). Since they are unique in not having |
| 148 // children, there's little point to reordering, so we always just re-assign | 228 // children, there's little point to reordering, so we always just re-assign |
| 149 // the data. | 229 // the data. |
| 150 Text(this.data) : super(key:'*text*'); | 230 Text(this.data) : super(key:'*text*'); |
| 151 | 231 |
| 152 static Text _emptyText = new Text(null); | 232 static final Text _emptyText = new Text(null); |
| 153 | 233 |
| 154 Node get _emptyNode => _emptyText; | 234 RenderNode get _emptyNode => _emptyText; |
| 155 | 235 |
| 156 sky.Node _createNode() { | 236 sky.Node _createNode() { |
| 157 return new sky.Text(data); | 237 return new sky.Text(data); |
| 158 } | 238 } |
| 159 | 239 |
| 160 bool _syncNode(Node old) { | 240 void _syncNode(RenderNode old) { |
| 161 super._syncNode(old); | |
| 162 if (old == _emptyText) | 241 if (old == _emptyText) |
| 163 return false; // we set inside _createNode(); | 242 return; // we set inside _createNode(); |
| 164 | 243 |
| 165 (_root as sky.Text).data = data; | 244 (_root as sky.Text).data = data; |
| 166 return false; | |
| 167 } | 245 } |
| 168 } | 246 } |
| 169 | 247 |
| 170 final List<Node> _emptyList = new List<Node>(); | 248 final List<Node> _emptyList = new List<Node>(); |
| 171 | 249 |
| 172 abstract class Element extends Node { | 250 abstract class Element extends RenderNode { |
| 173 | 251 |
| 174 String get _tagName; | 252 String get _tagName; |
| 175 | 253 |
| 176 sky.Node _createNode() => sky.document.createElement(_tagName); | 254 sky.Node _createNode() => sky.document.createElement(_tagName); |
| 177 | 255 |
| 178 final String inlineStyle; | 256 final String inlineStyle; |
| 179 | 257 |
| 180 final List<Node> _children; | 258 final List<Node> _children; |
| 181 final String _class; | 259 final String _class; |
| 182 | 260 |
| 183 Element({ | 261 Element({ |
| 184 Object key, | 262 Object key, |
| 185 List<Node> children, | 263 List<Node> children, |
| 186 Style style, | 264 Style style, |
| 187 this.inlineStyle | 265 this.inlineStyle |
| 188 }) : _class = style == null ? '' : style._className, | 266 }) : _class = style == null ? '' : style._className, |
| 189 _children = children == null ? _emptyList : children, | 267 _children = children == null ? _emptyList : children, |
| 190 super(key:key) { | 268 super(key:key) { |
| 191 | 269 |
| 192 if (_isInCheckedMode) { | 270 if (_isInCheckedMode) { |
| 193 _debugReportDuplicateIds(); | 271 _debugReportDuplicateIds(); |
| 194 } | 272 } |
| 195 } | 273 } |
| 196 | 274 |
| 197 void _unmount() { | 275 void _remove() { |
| 198 super._unmount(); | 276 super._remove(); |
| 199 if (_children != null) { | 277 if (_children != null) { |
| 200 for (var child in _children) { | 278 for (var child in _children) { |
| 201 child._unmount(); | 279 child._remove(); |
| 202 } | 280 } |
| 203 } | 281 } |
| 204 } | 282 } |
| 205 | 283 |
| 206 void _debugReportDuplicateIds() { | 284 void _debugReportDuplicateIds() { |
| 207 var idSet = new HashSet<String>(); | 285 var idSet = new HashSet<String>(); |
| 208 for (var child in _children) { | 286 for (var child in _children) { |
| 209 if (child is Text) { | 287 if (child is Text) { |
| 210 continue; // Text nodes all have the same key and are never reordered. | 288 continue; // Text nodes all have the same key and are never reordered. |
| 211 } | 289 } |
| 212 | 290 |
| 213 if (!idSet.add(child._key)) { | 291 if (!idSet.add(child._key)) { |
| 214 throw '''If multiple (non-Text) nodes of the same type exist as children | 292 throw '''If multiple (non-Text) nodes of the same type exist as children |
| 215 of another node, they must have unique keys.'''; | 293 of another node, they must have unique keys.'''; |
| 216 } | 294 } |
| 217 } | 295 } |
| 218 } | 296 } |
| 219 | 297 |
| 220 void _syncEvents([Element old]) { | 298 void _syncNode(RenderNode old) { |
| 221 List<EventHandler> newHandlers = events._handlers; | |
| 222 int newStartIndex = 0; | |
| 223 int newEndIndex = newHandlers.length; | |
| 224 | |
| 225 List<EventHandler> oldHandlers = old.events._handlers; | |
| 226 int oldStartIndex = 0; | |
| 227 int oldEndIndex = oldHandlers.length; | |
| 228 | |
| 229 // Skip over leading handlers that match. | |
| 230 while (newStartIndex < newEndIndex && oldStartIndex < oldEndIndex) { | |
| 231 EventHandler newHandler = newHandlers[newStartIndex]; | |
| 232 EventHandler oldHandler = oldHandlers[oldStartIndex]; | |
| 233 if (newHandler.type != oldHandler.type | |
| 234 || newHandler.listener != oldHandler.listener) | |
| 235 break; | |
| 236 ++newStartIndex; | |
| 237 ++oldStartIndex; | |
| 238 } | |
| 239 | |
| 240 // Skip over trailing handlers that match. | |
| 241 while (newStartIndex < newEndIndex && oldStartIndex < oldEndIndex) { | |
| 242 EventHandler newHandler = newHandlers[newEndIndex - 1]; | |
| 243 EventHandler oldHandler = oldHandlers[oldEndIndex - 1]; | |
| 244 if (newHandler.type != oldHandler.type | |
| 245 || newHandler.listener != oldHandler.listener) | |
| 246 break; | |
| 247 --newEndIndex; | |
| 248 --oldEndIndex; | |
| 249 } | |
| 250 | |
| 251 sky.Element root = _root as sky.Element; | |
| 252 | |
| 253 for (int i = oldStartIndex; i < oldEndIndex; ++i) { | |
| 254 EventHandler oldHandler = oldHandlers[i]; | |
| 255 root.removeEventListener(oldHandler.type, oldHandler.listener); | |
| 256 } | |
| 257 | |
| 258 for (int i = newStartIndex; i < newEndIndex; ++i) { | |
| 259 EventHandler newHandler = newHandlers[i]; | |
| 260 root.addEventListener(newHandler.type, newHandler.listener); | |
| 261 } | |
| 262 } | |
| 263 | |
| 264 bool _syncNode(Node old) { | |
| 265 super._syncNode(old); | |
| 266 | |
| 267 Element oldElement = old as Element; | 299 Element oldElement = old as Element; |
| 268 sky.Element root = _root as sky.Element; | 300 sky.Element root = _root as sky.Element; |
| 269 | 301 |
| 270 _syncEvents(oldElement); | 302 _syncEvents(oldElement.events); |
| 271 | 303 |
| 272 if (_class != oldElement._class) | 304 if (_class != oldElement._class) |
| 273 root.setAttribute('class', _class); | 305 root.setAttribute('class', _class); |
| 274 | 306 |
| 275 if (inlineStyle != oldElement.inlineStyle) | 307 if (inlineStyle != oldElement.inlineStyle) |
| 276 root.setAttribute('style', inlineStyle); | 308 root.setAttribute('style', inlineStyle); |
| 277 | 309 |
| 278 _syncChildren(oldElement); | 310 _syncChildren(oldElement); |
| 279 | |
| 280 return false; | |
| 281 } | 311 } |
| 282 | 312 |
| 283 void _syncChildren(Element oldElement) { | 313 void _syncChildren(Element oldElement) { |
| 284 // print("---Syncing children of $_key"); | |
| 285 sky.Element root = _root as sky.Element; | 314 sky.Element root = _root as sky.Element; |
| 286 assert(root != null); | 315 assert(root != null); |
| 287 | 316 |
| 288 var startIndex = 0; | 317 var startIndex = 0; |
| 289 var endIndex = _children.length; | 318 var endIndex = _children.length; |
| 290 | 319 |
| 291 var oldChildren = oldElement._children; | 320 var oldChildren = oldElement._children; |
| 292 var oldStartIndex = 0; | 321 var oldStartIndex = 0; |
| 293 var oldEndIndex = oldChildren.length; | 322 var oldEndIndex = oldChildren.length; |
| 294 | 323 |
| 295 sky.Node nextSibling = null; | 324 sky.Node nextSibling = null; |
| 296 Node currentNode = null; | 325 Node currentNode = null; |
| 297 Node oldNode = null; | 326 Node oldNode = null; |
| 298 | 327 |
| 299 void sync(int atIndex) { | 328 void sync(int atIndex) { |
| 300 if (currentNode._sync(oldNode, this, _root, nextSibling)) { | 329 _children[atIndex] = _syncChild(currentNode, oldNode, _root, nextSibling); |
| 301 // oldNode was stateful and must be retained. | |
| 302 currentNode = oldNode; | |
| 303 _children[atIndex] = currentNode; | |
| 304 } | |
| 305 | |
| 306 assert(currentNode._root is sky.Node); | |
| 307 } | 330 } |
| 308 | 331 |
| 309 // Scan backwards from end of list while nodes can be directly synced | 332 // Scan backwards from end of list while nodes can be directly synced |
| 310 // without reordering. | 333 // without reordering. |
| 311 // print("...scanning backwards"); | |
| 312 while (endIndex > startIndex && oldEndIndex > oldStartIndex) { | 334 while (endIndex > startIndex && oldEndIndex > oldStartIndex) { |
| 313 currentNode = _children[endIndex - 1]; | 335 currentNode = _children[endIndex - 1]; |
| 314 oldNode = oldChildren[oldEndIndex - 1]; | 336 oldNode = oldChildren[oldEndIndex - 1]; |
| 315 | 337 |
| 316 if (currentNode._key != oldNode._key) { | 338 if (currentNode._key != oldNode._key) { |
| 317 break; | 339 break; |
| 318 } | 340 } |
| 319 | 341 |
| 320 // print('> syncing matched at: $endIndex : $oldEndIndex'); | |
| 321 endIndex--; | 342 endIndex--; |
| 322 oldEndIndex--; | 343 oldEndIndex--; |
| 323 sync(endIndex); | 344 sync(endIndex); |
| 324 nextSibling = currentNode._root; | 345 nextSibling = currentNode._root; |
| 325 } | 346 } |
| 326 | 347 |
| 327 HashMap<String, Node> oldNodeIdMap = null; | 348 HashMap<String, Node> oldNodeIdMap = null; |
| 328 | 349 |
| 329 bool oldNodeReordered(String key) { | 350 bool oldNodeReordered(String key) { |
| 330 return oldNodeIdMap != null && | 351 return oldNodeIdMap != null && |
| (...skipping 25 matching lines...) Expand all Loading... |
| 356 bool searchForOldNode() { | 377 bool searchForOldNode() { |
| 357 if (currentNode is Text) | 378 if (currentNode is Text) |
| 358 return false; // Never re-order Text nodes. | 379 return false; // Never re-order Text nodes. |
| 359 | 380 |
| 360 ensureOldIdMap(); | 381 ensureOldIdMap(); |
| 361 oldNode = oldNodeIdMap[currentNode._key]; | 382 oldNode = oldNodeIdMap[currentNode._key]; |
| 362 if (oldNode == null) | 383 if (oldNode == null) |
| 363 return false; | 384 return false; |
| 364 | 385 |
| 365 oldNodeIdMap[currentNode._key] = null; // mark it reordered. | 386 oldNodeIdMap[currentNode._key] = null; // mark it reordered. |
| 366 // print("Reparenting ${currentNode._key}"); | |
| 367 _parentInsertBefore(root, oldNode._root, nextSibling); | 387 _parentInsertBefore(root, oldNode._root, nextSibling); |
| 368 return true; | 388 return true; |
| 369 } | 389 } |
| 370 | 390 |
| 371 // Scan forwards, this time we may re-order; | 391 // Scan forwards, this time we may re-order; |
| 372 // print("...scanning forward"); | |
| 373 nextSibling = root.firstChild; | 392 nextSibling = root.firstChild; |
| 374 while (startIndex < endIndex && oldStartIndex < oldEndIndex) { | 393 while (startIndex < endIndex && oldStartIndex < oldEndIndex) { |
| 375 currentNode = _children[startIndex]; | 394 currentNode = _children[startIndex]; |
| 376 oldNode = oldChildren[oldStartIndex]; | 395 oldNode = oldChildren[oldStartIndex]; |
| 377 | 396 |
| 378 if (currentNode._key == oldNode._key) { | 397 if (currentNode._key == oldNode._key) { |
| 379 // print('> syncing matched at: $startIndex : $oldStartIndex'); | |
| 380 assert(currentNode.runtimeType == oldNode.runtimeType); | 398 assert(currentNode.runtimeType == oldNode.runtimeType); |
| 381 nextSibling = nextSibling.nextSibling; | 399 nextSibling = nextSibling.nextSibling; |
| 382 sync(startIndex); | 400 sync(startIndex); |
| 383 startIndex++; | 401 startIndex++; |
| 384 advanceOldStartIndex(); | 402 advanceOldStartIndex(); |
| 385 continue; | 403 continue; |
| 386 } | 404 } |
| 387 | 405 |
| 388 oldNode = null; | 406 oldNode = null; |
| 389 if (searchForOldNode()) { | 407 searchForOldNode(); |
| 390 // print('> reordered to $startIndex'); | |
| 391 } else { | |
| 392 // print('> inserting at $startIndex'); | |
| 393 } | |
| 394 | |
| 395 sync(startIndex); | 408 sync(startIndex); |
| 396 startIndex++; | 409 startIndex++; |
| 397 } | 410 } |
| 398 | 411 |
| 399 // New insertions | 412 // New insertions |
| 400 oldNode = null; | 413 oldNode = null; |
| 401 // print('...processing remaining insertions'); | |
| 402 while (startIndex < endIndex) { | 414 while (startIndex < endIndex) { |
| 403 // print('> inserting at $startIndex'); | |
| 404 currentNode = _children[startIndex]; | 415 currentNode = _children[startIndex]; |
| 405 sync(startIndex); | 416 sync(startIndex); |
| 406 startIndex++; | 417 startIndex++; |
| 407 } | 418 } |
| 408 | 419 |
| 409 // Removals | 420 // Removals |
| 410 // print('...processing remaining removals'); | |
| 411 currentNode = null; | 421 currentNode = null; |
| 412 while (oldStartIndex < oldEndIndex) { | 422 while (oldStartIndex < oldEndIndex) { |
| 413 oldNode = oldChildren[oldStartIndex]; | 423 oldNode = oldChildren[oldStartIndex]; |
| 414 // print('> ${oldNode._key} removing from $oldEndIndex'); | 424 oldNode._remove(); |
| 415 oldNode._unmount(); | |
| 416 advanceOldStartIndex(); | 425 advanceOldStartIndex(); |
| 417 } | 426 } |
| 418 } | 427 } |
| 419 } | 428 } |
| 420 | 429 |
| 421 class Container extends Element { | 430 class Container extends Element { |
| 422 | 431 |
| 423 String get _tagName => 'div'; | 432 String get _tagName => 'div'; |
| 424 | 433 |
| 425 static final Container _emptyContainer = new Container(); | 434 static final Container _emptyContainer = new Container(); |
| 426 | 435 |
| 427 Node get _emptyNode => _emptyContainer; | 436 RenderNode get _emptyNode => _emptyContainer; |
| 428 | 437 |
| 429 Container({ | 438 Container({ |
| 430 Object key, | 439 Object key, |
| 431 List<Node> children, | 440 List<Node> children, |
| 432 Style style, | 441 Style style, |
| 433 String inlineStyle | 442 String inlineStyle |
| 434 }) : super( | 443 }) : super( |
| 435 key: key, | 444 key: key, |
| 436 children: children, | 445 children: children, |
| 437 style: style, | 446 style: style, |
| 438 inlineStyle: inlineStyle | 447 inlineStyle: inlineStyle |
| 439 ); | 448 ); |
| 440 } | 449 } |
| 441 | 450 |
| 442 class Image extends Element { | 451 class Image extends Element { |
| 443 | 452 |
| 444 String get _tagName => 'img'; | 453 String get _tagName => 'img'; |
| 445 | 454 |
| 446 static final Image _emptyImage = new Image(); | 455 static final Image _emptyImage = new Image(); |
| 447 | 456 |
| 448 Node get _emptyNode => _emptyImage; | 457 RenderNode get _emptyNode => _emptyImage; |
| 449 | 458 |
| 450 String src; | 459 final String src; |
| 451 int width; | 460 final int width; |
| 452 int height; | 461 final int height; |
| 453 | 462 |
| 454 Image({ | 463 Image({ |
| 455 Object key, | 464 Object key, |
| 456 List<Node> children, | 465 List<Node> children, |
| 457 Style style, | 466 Style style, |
| 458 String inlineStyle, | 467 String inlineStyle, |
| 459 this.width, | 468 this.width, |
| 460 this.height, | 469 this.height, |
| 461 this.src | 470 this.src |
| 462 }) : super( | 471 }) : super( |
| 463 key: key, | 472 key: key, |
| 464 children: children, | 473 children: children, |
| 465 style: style, | 474 style: style, |
| 466 inlineStyle: inlineStyle | 475 inlineStyle: inlineStyle |
| 467 ); | 476 ); |
| 468 | 477 |
| 469 bool _syncNode(Node old) { | 478 void _syncNode(Node old) { |
| 470 super._syncNode(old); | 479 super._syncNode(old); |
| 471 | 480 |
| 472 Image oldImage = old as Image; | 481 Image oldImage = old as Image; |
| 473 sky.HTMLImageElement skyImage = _root as sky.HTMLImageElement; | 482 sky.HTMLImageElement skyImage = _root as sky.HTMLImageElement; |
| 474 | 483 |
| 475 if (src != oldImage.src) | 484 if (src != oldImage.src) |
| 476 skyImage.src = src; | 485 skyImage.src = src; |
| 477 | 486 |
| 478 if (width != oldImage.width) | 487 if (width != oldImage.width) |
| 479 skyImage.style['width'] = '${width}px'; | 488 skyImage.style['width'] = '${width}px'; |
| 480 | 489 |
| 481 if (height != oldImage.height) | 490 if (height != oldImage.height) |
| 482 skyImage.style['height'] = '${height}px'; | 491 skyImage.style['height'] = '${height}px'; |
| 483 | |
| 484 return false; | |
| 485 } | 492 } |
| 486 } | 493 } |
| 487 | 494 |
| 488 class Anchor extends Element { | 495 class Anchor extends Element { |
| 489 | 496 |
| 490 String get _tagName => 'a'; | 497 String get _tagName => 'a'; |
| 491 | 498 |
| 492 static final Anchor _emptyAnchor = new Anchor(); | 499 static final Anchor _emptyAnchor = new Anchor(); |
| 493 | 500 |
| 494 Node get _emptyNode => _emptyAnchor; | 501 Node get _emptyNode => _emptyAnchor; |
| 495 | 502 |
| 496 String href; | 503 final String href; |
| 497 int width; | 504 final int width; |
| 498 int height; | 505 final int height; |
| 499 | 506 |
| 500 Anchor({ | 507 Anchor({ |
| 501 Object key, | 508 Object key, |
| 502 List<Node> children, | 509 List<Node> children, |
| 503 Style style, | 510 Style style, |
| 504 String inlineStyle, | 511 String inlineStyle, |
| 505 this.width, | 512 this.width, |
| 506 this.height, | 513 this.height, |
| 507 this.href | 514 this.href |
| 508 }) : super( | 515 }) : super( |
| 509 key: key, | 516 key: key, |
| 510 children: children, | 517 children: children, |
| 511 style: style, | 518 style: style, |
| 512 inlineStyle: inlineStyle | 519 inlineStyle: inlineStyle |
| 513 ); | 520 ); |
| 514 | 521 |
| 515 bool _syncNode(Node old) { | 522 void _syncNode(Node old) { |
| 516 super._syncNode(old); | 523 super._syncNode(old); |
| 517 | 524 |
| 518 Anchor oldAnchor = old as Anchor; | 525 Anchor oldAnchor = old as Anchor; |
| 519 sky.HTMLAnchorElement skyAnchor = _root as sky.HTMLAnchorElement; | 526 sky.HTMLAnchorElement skyAnchor = _root as sky.HTMLAnchorElement; |
| 520 | 527 |
| 521 if (href != oldAnchor.href) | 528 if (href != oldAnchor.href) |
| 522 skyAnchor.href = href; | 529 skyAnchor.href = href; |
| 523 | |
| 524 return false; | |
| 525 } | 530 } |
| 526 } | 531 } |
| 527 | 532 |
| 528 List<Component> _dirtyComponents = new List<Component>(); | 533 List<Component> _dirtyComponents = new List<Component>(); |
| 534 Set<Component> _mountedComponents = new HashSet<Component>(); |
| 535 Set<Component> _unmountedComponents = new HashSet<Component>(); |
| 536 |
| 529 bool _buildScheduled = false; | 537 bool _buildScheduled = false; |
| 530 bool _inRenderDirtyComponents = false; | 538 bool _inRenderDirtyComponents = false; |
| 531 | 539 |
| 540 void _notifyMountStatusChanged() { |
| 541 _unmountedComponents.forEach((c) => c.didUnmount()); |
| 542 _mountedComponents.forEach((c) => c.didMount()); |
| 543 _mountedComponents.clear(); |
| 544 _unmountedComponents.clear(); |
| 545 } |
| 546 |
| 532 void _buildDirtyComponents() { | 547 void _buildDirtyComponents() { |
| 548 Stopwatch sw; |
| 549 if (_shouldLogRenderDuration) |
| 550 sw = new Stopwatch()..start(); |
| 551 |
| 533 try { | 552 try { |
| 534 _inRenderDirtyComponents = true; | 553 _inRenderDirtyComponents = true; |
| 535 Stopwatch sw = new Stopwatch()..start(); | |
| 536 | 554 |
| 537 _dirtyComponents.sort((a, b) => a._order - b._order); | 555 _dirtyComponents.sort((a, b) => a._order - b._order); |
| 538 for (var comp in _dirtyComponents) { | 556 for (var comp in _dirtyComponents) { |
| 539 comp._buildIfDirty(); | 557 comp._buildIfDirty(); |
| 540 } | 558 } |
| 541 | 559 |
| 542 _dirtyComponents.clear(); | 560 _dirtyComponents.clear(); |
| 543 _buildScheduled = false; | 561 _buildScheduled = false; |
| 544 | |
| 545 sw.stop(); | |
| 546 if (_shouldLogRenderDuration) | |
| 547 print("Render took ${sw.elapsedMicroseconds} microseconds"); | |
| 548 } finally { | 562 } finally { |
| 549 _inRenderDirtyComponents = false; | 563 _inRenderDirtyComponents = false; |
| 550 } | 564 } |
| 565 |
| 566 _notifyMountStatusChanged(); |
| 567 |
| 568 if (_shouldLogRenderDuration) { |
| 569 sw.stop(); |
| 570 print("Render took ${sw.elapsedMicroseconds} microseconds"); |
| 571 } |
| 551 } | 572 } |
| 552 | 573 |
| 553 void _scheduleComponentForRender(Component c) { | 574 void _scheduleComponentForRender(Component c) { |
| 554 assert(!_inRenderDirtyComponents); | 575 assert(!_inRenderDirtyComponents); |
| 555 _dirtyComponents.add(c); | 576 _dirtyComponents.add(c); |
| 556 | 577 |
| 557 if (!_buildScheduled) { | 578 if (!_buildScheduled) { |
| 558 _buildScheduled = true; | 579 _buildScheduled = true; |
| 559 new Future.microtask(_buildDirtyComponents); | 580 new Future.microtask(_buildDirtyComponents); |
| 560 } | 581 } |
| 561 } | 582 } |
| 562 | 583 |
| 584 EventMap _emptyEventMap = new EventMap(); |
| 585 |
| 563 abstract class Component extends Node { | 586 abstract class Component extends Node { |
| 564 bool _dirty = true; // components begin dirty because they haven't built. | 587 bool get _isBuilding => _currentlyBuilding == this; |
| 565 Node _vdom = null; | 588 bool _dirty = true; |
| 589 |
| 590 Node _built; |
| 566 final int _order; | 591 final int _order; |
| 567 static int _currentOrder = 0; | 592 static int _currentOrder = 0; |
| 568 bool _stateful; | 593 bool _stateful; |
| 569 static Component _currentlyRendering; | 594 static Component _currentlyBuilding; |
| 570 | 595 |
| 571 Component({ Object key, bool stateful }) | 596 Component({ Object key, bool stateful }) |
| 572 : _stateful = stateful != null ? stateful : false, | 597 : _stateful = stateful != null ? stateful : false, |
| 573 _order = _currentOrder + 1, | 598 _order = _currentOrder + 1, |
| 574 super(key:key); | 599 super(key:key); |
| 575 | 600 |
| 576 void didMount() {} | 601 void didMount() {} |
| 577 void didUnmount() {} | 602 void didUnmount() {} |
| 578 | 603 |
| 579 // TODO(rafaelw): It seems wrong to expose DOM at all. This is presently | 604 // TODO(rafaelw): It seems wrong to expose DOM at all. This is presently |
| 580 // needed to get sizing info. | 605 // needed to get sizing info. |
| 581 sky.Node getRoot() => _root; | 606 sky.Node getRoot() => _root; |
| 582 | 607 |
| 583 void _mount(Node parent, sky.ParentNode host, sky.Node insertBefore) { | 608 void _remove() { |
| 584 _parent = parent; | 609 assert(_built != null); |
| 585 | 610 assert(_root != null); |
| 586 _syncInternal(host, insertBefore); | 611 _built._remove(); |
| 587 | 612 _built = null; |
| 588 didMount(); | 613 _unmountedComponents.add(this); |
| 614 super._remove(); |
| 589 } | 615 } |
| 590 | 616 |
| 591 void _unmount() { | 617 bool _willSync(Node old) { |
| 592 assert(_vdom != null); | 618 Component oldComponent = old as Component; |
| 593 assert(_root != null); | 619 if (oldComponent == null || !oldComponent._stateful) |
| 594 _vdom._unmount(); | 620 return false; |
| 595 _vdom = null; | 621 |
| 596 _root = null; | 622 // Make |this| the "old" Component |
| 597 _parent = null; | 623 _stateful = false; |
| 598 _defunct = true; | 624 _built = oldComponent._built; |
| 599 didUnmount(); | 625 assert(_built != null); |
| 626 |
| 627 // Make |oldComponent| the "new" component |
| 628 reflect.copyPublicFields(this, oldComponent); |
| 629 oldComponent._built = null; |
| 630 oldComponent._dirty = true; |
| 631 return true; |
| 600 } | 632 } |
| 601 | 633 |
| 602 bool _syncNode(Node old) { | 634 /* There are three cases here: |
| 635 * 1) Building for the first time: |
| 636 * assert(_built == null && old == null) |
| 637 * 2) Re-building (because a dirty flag got set): |
| 638 * assert(_built != null && old == null) |
| 639 * 3) Syncing against an old version |
| 640 * assert(_built == null && old != null) |
| 641 */ |
| 642 void _sync(Node old, sky.ParentNode host, sky.Node insertBefore) { |
| 643 assert(!_defunct); |
| 644 assert(_built == null || old == null); |
| 645 |
| 603 Component oldComponent = old as Component; | 646 Component oldComponent = old as Component; |
| 604 | 647 |
| 605 if (!oldComponent._stateful) { | 648 var oldBuilt; |
| 606 _vdom = oldComponent._vdom; | 649 if (oldComponent == null) { |
| 607 _syncInternal(); | 650 oldBuilt = _built; |
| 608 | 651 } else { |
| 609 return false; | 652 assert(_built == null); |
| 653 oldBuilt = oldComponent._built; |
| 610 } | 654 } |
| 611 | 655 |
| 612 _stateful = false; // prevent iloop from _syncInternal below. | 656 if (oldBuilt == null) |
| 613 | 657 _mountedComponents.add(this); |
| 614 reflect.copyPublicFields(this, oldComponent); | |
| 615 | |
| 616 oldComponent._dirty = true; | |
| 617 _dirty = false; | |
| 618 | |
| 619 oldComponent._syncInternal(); | |
| 620 return true; // Retain old component | |
| 621 } | |
| 622 | |
| 623 void _syncInternal([sky.Node host, sky.Node insertBefore]) { | |
| 624 if (!_dirty) { | |
| 625 assert(_vdom != null); | |
| 626 return; | |
| 627 } | |
| 628 | |
| 629 var oldRendered = _vdom; | |
| 630 bool mounting = oldRendered == null; | |
| 631 | 658 |
| 632 int lastOrder = _currentOrder; | 659 int lastOrder = _currentOrder; |
| 633 _currentOrder = _order; | 660 _currentOrder = _order; |
| 634 _currentlyRendering = this; | 661 _currentlyBuilding = this; |
| 635 _vdom = build(); | 662 _built = build(); |
| 636 _currentlyRendering = null; | 663 _currentlyBuilding = null; |
| 637 _currentOrder = lastOrder; | 664 _currentOrder = lastOrder; |
| 638 | 665 |
| 639 _vdom.events.addAll(events); | 666 _built = _syncChild(_built, oldBuilt, host, insertBefore); |
| 667 _dirty = false; |
| 668 _root = _built._root; |
| 640 | 669 |
| 641 _dirty = false; | 670 _built.events.addAll(events); |
| 642 | 671 _syncEvents(oldComponent != null ? oldComponent.events : _emptyEventMap); |
| 643 // TODO(rafaelw): This eagerly removes the old DOM. It may be that a | |
| 644 // new component was built that could re-use some of it. Consider | |
| 645 // syncing the new VDOM against the old one. | |
| 646 if (oldRendered != null && | |
| 647 _vdom.runtimeType != oldRendered.runtimeType) { | |
| 648 var oldRoot = oldRendered._root; | |
| 649 host = oldRoot.parentNode; | |
| 650 insertBefore = oldRoot.nextSibling; | |
| 651 oldRendered._unmount(); | |
| 652 oldRendered = null; | |
| 653 } | |
| 654 | |
| 655 if (_vdom._sync(oldRendered, this, host, insertBefore)) { | |
| 656 _vdom = oldRendered; // retain stateful component | |
| 657 } | |
| 658 | |
| 659 _root = _vdom._root; | |
| 660 assert(_vdom._root is sky.Node); | |
| 661 | |
| 662 if (mounting) { | |
| 663 didMount(); | |
| 664 } | |
| 665 } | 672 } |
| 666 | 673 |
| 667 void _buildIfDirty() { | 674 void _buildIfDirty() { |
| 668 if (_defunct) | 675 if (!_dirty || _defunct) |
| 669 return; | 676 return; |
| 670 | 677 |
| 671 assert(_vdom != null); | 678 assert(_root != null); |
| 672 | 679 _sync(null, _root.parentNode, _root.nextSibling); |
| 673 var vdom = _vdom; | |
| 674 while (vdom is Component) { | |
| 675 vdom = vdom._vdom; | |
| 676 } | |
| 677 | |
| 678 assert(vdom._root != null); | |
| 679 _syncInternal(); | |
| 680 } | 680 } |
| 681 | 681 |
| 682 void scheduleBuild() { | 682 void scheduleBuild() { |
| 683 setState(() {}); | 683 setState(() {}); |
| 684 } | 684 } |
| 685 | 685 |
| 686 void setState(Function fn()) { | 686 void setState(Function fn()) { |
| 687 assert(_vdom != null || _defunct); // cannot setState before mounting. | |
| 688 _stateful = true; | 687 _stateful = true; |
| 689 fn(); | 688 fn(); |
| 690 if (!_defunct && _currentlyRendering != this) { | 689 if (_isBuilding || _dirty || _defunct) |
| 691 _dirty = true; | 690 return; |
| 692 _scheduleComponentForRender(this); | 691 |
| 693 } | 692 _dirty = true; |
| 693 _scheduleComponentForRender(this); |
| 694 } | 694 } |
| 695 | 695 |
| 696 Node build(); | 696 Node build(); |
| 697 } | 697 } |
| 698 | 698 |
| 699 abstract class App extends Component { | 699 abstract class App extends Component { |
| 700 sky.Node _host = null; | 700 sky.Node _host = null; |
| 701 App() : super(stateful: true) { | 701 App() : super(stateful: true) { |
| 702 _host = sky.document.createElement('div'); | 702 _host = sky.document.createElement('div'); |
| 703 sky.document.appendChild(_host); | 703 sky.document.appendChild(_host); |
| 704 | 704 |
| 705 new Future.microtask(() { | 705 new Future.microtask(() { |
| 706 Stopwatch sw = new Stopwatch()..start(); | 706 Stopwatch sw = new Stopwatch()..start(); |
| 707 | 707 |
| 708 _mount(null, _host, null); | 708 _sync(null, _host, null); |
| 709 assert(_root is sky.Node); | 709 assert(_root is sky.Node); |
| 710 | 710 |
| 711 sw.stop(); | 711 sw.stop(); |
| 712 if (_shouldLogRenderDuration) | 712 if (_shouldLogRenderDuration) |
| 713 print("Initial build: ${sw.elapsedMicroseconds} microseconds"); | 713 print("Initial build: ${sw.elapsedMicroseconds} microseconds"); |
| 714 }); | 714 }); |
| 715 } | 715 } |
| 716 } | 716 } |
| OLD | NEW |