OLD | NEW |
| (Empty) |
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 | |
3 // found in the LICENSE file. | |
4 | |
5 library fn; | |
6 | |
7 import 'dart:async'; | |
8 import 'dart:collection'; | |
9 import 'dart:sky' as sky; | |
10 import 'reflect.dart' as reflect; | |
11 import 'layout.dart'; | |
12 | |
13 export 'layout.dart' show Style; | |
14 | |
15 final sky.Tracing _tracing = sky.window.tracing; | |
16 | |
17 final bool _shouldLogRenderDuration = false; | |
18 final bool _shouldTrace = false; | |
19 | |
20 enum _SyncOperation { IDENTICAL, INSERTION, STATEFUL, STATELESS, REMOVAL } | |
21 | |
22 /* | |
23 * All Effen nodes derive from UINode. All nodes have a _parent, a _key and | |
24 * can be sync'd. | |
25 */ | |
26 abstract class UINode { | |
27 String _key; | |
28 UINode _parent; | |
29 UINode get parent => _parent; | |
30 RenderCSS _root; | |
31 bool _defunct = false; | |
32 | |
33 UINode({ Object key }) { | |
34 _key = key == null ? "$runtimeType" : "$runtimeType-$key"; | |
35 } | |
36 | |
37 // Subclasses which implements Nodes that become stateful may return true | |
38 // if the |old| node has become stateful and should be retained. | |
39 bool _willSync(UINode old) => false; | |
40 | |
41 bool get interchangeable => false; // if true, then keys can be duplicated | |
42 | |
43 void _sync(UINode old, RenderCSSContainer host, RenderCSS insertBefore); | |
44 | |
45 void _remove() { | |
46 _defunct = true; | |
47 _root = null; | |
48 handleRemoved(); | |
49 } | |
50 void handleRemoved() { } | |
51 | |
52 int _nodeDepth; | |
53 void _ensureDepth() { | |
54 if (_nodeDepth == null) { | |
55 if (_parent != null) { | |
56 _parent._ensureDepth(); | |
57 _nodeDepth = _parent._nodeDepth + 1; | |
58 } else { | |
59 _nodeDepth = 0; | |
60 } | |
61 } | |
62 } | |
63 | |
64 void _trace(String message) { | |
65 if (!_shouldTrace) | |
66 return; | |
67 | |
68 _ensureDepth(); | |
69 print((' ' * _nodeDepth) + message); | |
70 } | |
71 | |
72 void _traceSync(_SyncOperation op, String key) { | |
73 if (!_shouldTrace) | |
74 return; | |
75 | |
76 String opString = op.toString().toLowerCase(); | |
77 String outString = opString.substring(opString.indexOf('.') + 1); | |
78 _trace('_sync($outString) $key'); | |
79 } | |
80 | |
81 void _removeChild(UINode node) { | |
82 _traceSync(_SyncOperation.REMOVAL, node._key); | |
83 node._remove(); | |
84 } | |
85 | |
86 // Returns the child which should be retained as the child of this node. | |
87 UINode _syncChild(UINode node, UINode oldNode, RenderCSSContainer host, | |
88 RenderCSS insertBefore) { | |
89 | |
90 assert(oldNode == null || node._key == oldNode._key); | |
91 | |
92 if (node == oldNode) { | |
93 _traceSync(_SyncOperation.IDENTICAL, node._key); | |
94 return node; // Nothing to do. Subtrees must be identical. | |
95 } | |
96 | |
97 // TODO(rafaelw): This eagerly removes the old DOM. It may be that a | |
98 // new component was built that could re-use some of it. Consider | |
99 // syncing the new VDOM against the old one. | |
100 if (oldNode != null && node._key != oldNode._key) { | |
101 _removeChild(oldNode); | |
102 } | |
103 | |
104 if (node._willSync(oldNode)) { | |
105 _traceSync(_SyncOperation.STATEFUL, node._key); | |
106 oldNode._sync(node, host, insertBefore); | |
107 node._defunct = true; | |
108 assert(oldNode._root is RenderCSS); | |
109 return oldNode; | |
110 } | |
111 | |
112 node._parent = this; | |
113 | |
114 if (oldNode == null) { | |
115 _traceSync(_SyncOperation.INSERTION, node._key); | |
116 } else { | |
117 _traceSync(_SyncOperation.STATELESS, node._key); | |
118 } | |
119 node._sync(oldNode, host, insertBefore); | |
120 if (oldNode != null) | |
121 oldNode._defunct = true; | |
122 | |
123 assert(node._root is RenderCSS); | |
124 return node; | |
125 } | |
126 } | |
127 | |
128 abstract class ContentNode extends UINode { | |
129 UINode content; | |
130 | |
131 ContentNode(UINode content) : this.content = content, super(key: content._key)
; | |
132 | |
133 void _sync(UINode old, RenderCSSContainer host, RenderCSS insertBefore) { | |
134 UINode oldContent = old == null ? null : (old as ContentNode).content; | |
135 content = _syncChild(content, oldContent, host, insertBefore); | |
136 assert(content._root != null); | |
137 _root = content._root; | |
138 } | |
139 | |
140 void _remove() { | |
141 if (content != null) | |
142 _removeChild(content); | |
143 super._remove(); | |
144 } | |
145 } | |
146 | |
147 class StyleNode extends ContentNode { | |
148 final Style style; | |
149 | |
150 StyleNode(UINode content, this.style): super(content); | |
151 } | |
152 | |
153 class ParentDataNode extends ContentNode { | |
154 final ParentData parentData; | |
155 | |
156 ParentDataNode(UINode content, this.parentData): super(content); | |
157 } | |
158 | |
159 /* | |
160 * SkyNodeWrappers correspond to a desired state of a RenderCSS. They are fully | |
161 * immutable, with one exception: A UINode which is a Component which lives with
in | |
162 * an SkyElementWrapper's children list, may be replaced with the "old" instance
if it | |
163 * has become stateful. | |
164 */ | |
165 abstract class SkyNodeWrapper extends UINode { | |
166 | |
167 static final Map<RenderCSS, SkyNodeWrapper> _nodeMap = | |
168 new HashMap<RenderCSS, SkyNodeWrapper>(); | |
169 | |
170 static SkyNodeWrapper _getMounted(RenderCSS node) => _nodeMap[node]; | |
171 | |
172 SkyNodeWrapper({ Object key }) : super(key: key); | |
173 | |
174 SkyNodeWrapper get _emptyNode; | |
175 | |
176 RenderCSS _createNode(); | |
177 | |
178 void _sync(UINode old, RenderCSSContainer host, RenderCSS insertBefore) { | |
179 if (old == null) { | |
180 _root = _createNode(); | |
181 assert(_root != null); | |
182 host.add(_root, before: insertBefore); | |
183 old = _emptyNode; | |
184 } else { | |
185 _root = old._root; | |
186 assert(_root != null); | |
187 } | |
188 | |
189 _nodeMap[_root] = this; | |
190 _syncNode(old); | |
191 } | |
192 | |
193 void _syncNode(SkyNodeWrapper old); | |
194 | |
195 void _removeChild(UINode node) { | |
196 assert(_root is RenderCSSContainer); | |
197 _root.remove(node._root); | |
198 super._removeChild(node); | |
199 } | |
200 | |
201 void _remove() { | |
202 assert(_root != null); | |
203 _nodeMap.remove(_root); | |
204 super._remove(); | |
205 } | |
206 } | |
207 | |
208 typedef GestureEventListener(sky.GestureEvent e); | |
209 typedef PointerEventListener(sky.PointerEvent e); | |
210 typedef EventListener(sky.Event e); | |
211 | |
212 class EventListenerNode extends ContentNode { | |
213 final Map<String, sky.EventListener> listeners; | |
214 | |
215 static final Set<String> _registeredEvents = new HashSet<String>(); | |
216 | |
217 static Map<String, sky.EventListener> _createListeners({ | |
218 EventListener onWheel, | |
219 GestureEventListener onGestureFlingCancel, | |
220 GestureEventListener onGestureFlingStart, | |
221 GestureEventListener onGestureScrollStart, | |
222 GestureEventListener onGestureScrollUpdate, | |
223 GestureEventListener onGestureTap, | |
224 GestureEventListener onGestureTapDown, | |
225 PointerEventListener onPointerCancel, | |
226 PointerEventListener onPointerDown, | |
227 PointerEventListener onPointerMove, | |
228 PointerEventListener onPointerUp, | |
229 Map<String, sky.EventListener> custom | |
230 }) { | |
231 var listeners = custom != null ? | |
232 new HashMap<String, sky.EventListener>.from(custom) : | |
233 new HashMap<String, sky.EventListener>(); | |
234 | |
235 if (onWheel != null) | |
236 listeners['wheel'] = onWheel; | |
237 if (onGestureFlingCancel != null) | |
238 listeners['gestureflingcancel'] = onGestureFlingCancel; | |
239 if (onGestureFlingStart != null) | |
240 listeners['gestureflingstart'] = onGestureFlingStart; | |
241 if (onGestureScrollStart != null) | |
242 listeners['gesturescrollstart'] = onGestureScrollStart; | |
243 if (onGestureScrollUpdate != null) | |
244 listeners['gesturescrollupdate'] = onGestureScrollUpdate; | |
245 if (onGestureTap != null) | |
246 listeners['gesturetap'] = onGestureTap; | |
247 if (onGestureTapDown != null) | |
248 listeners['gesturetapdown'] = onGestureTapDown; | |
249 if (onPointerCancel != null) | |
250 listeners['pointercancel'] = onPointerCancel; | |
251 if (onPointerDown != null) | |
252 listeners['pointerdown'] = onPointerDown; | |
253 if (onPointerMove != null) | |
254 listeners['pointermove'] = onPointerMove; | |
255 if (onPointerUp != null) | |
256 listeners['pointerup'] = onPointerUp; | |
257 | |
258 return listeners; | |
259 } | |
260 | |
261 EventListenerNode(UINode content, { | |
262 EventListener onWheel, | |
263 GestureEventListener onGestureFlingCancel, | |
264 GestureEventListener onGestureFlingStart, | |
265 GestureEventListener onGestureScrollStart, | |
266 GestureEventListener onGestureScrollUpdate, | |
267 GestureEventListener onGestureTap, | |
268 GestureEventListener onGestureTapDown, | |
269 PointerEventListener onPointerCancel, | |
270 PointerEventListener onPointerDown, | |
271 PointerEventListener onPointerMove, | |
272 PointerEventListener onPointerUp, | |
273 Map<String, sky.EventListener> custom | |
274 }) : listeners = _createListeners( | |
275 onWheel: onWheel, | |
276 onGestureFlingCancel: onGestureFlingCancel, | |
277 onGestureFlingStart: onGestureFlingStart, | |
278 onGestureScrollUpdate: onGestureScrollUpdate, | |
279 onGestureScrollStart: onGestureScrollStart, | |
280 onGestureTap: onGestureTap, | |
281 onGestureTapDown: onGestureTapDown, | |
282 onPointerCancel: onPointerCancel, | |
283 onPointerDown: onPointerDown, | |
284 onPointerMove: onPointerMove, | |
285 onPointerUp: onPointerUp, | |
286 custom: custom | |
287 ), | |
288 super(content); | |
289 | |
290 void _handleEvent(sky.Event e) { | |
291 sky.EventListener listener = listeners[e.type]; | |
292 if (listener != null) { | |
293 listener(e); | |
294 } | |
295 } | |
296 | |
297 static void _dispatchEvent(sky.Event e) { | |
298 UINode target = SkyNodeWrapper._getMounted(bridgeEventTargetToRenderNode(e.t
arget)); | |
299 | |
300 // TODO(rafaelw): StopPropagation? | |
301 while (target != null) { | |
302 if (target is EventListenerNode) { | |
303 target._handleEvent(e); | |
304 } | |
305 | |
306 target = target._parent; | |
307 } | |
308 } | |
309 | |
310 static void _ensureDocumentListener(String eventType) { | |
311 if (_registeredEvents.add(eventType)) { | |
312 sky.document.addEventListener(eventType, _dispatchEvent); | |
313 } | |
314 } | |
315 | |
316 void _sync(UINode old, RenderCSSContainer host, RenderCSS insertBefore) { | |
317 for (var type in listeners.keys) { | |
318 _ensureDocumentListener(type); | |
319 } | |
320 | |
321 super._sync(old, host, insertBefore); | |
322 } | |
323 } | |
324 | |
325 final List<UINode> _emptyList = new List<UINode>(); | |
326 | |
327 abstract class SkyElementWrapper extends SkyNodeWrapper { | |
328 | |
329 final List<UINode> children; | |
330 final Style style; | |
331 final String inlineStyle; | |
332 | |
333 SkyElementWrapper({ | |
334 Object key, | |
335 List<UINode> children, | |
336 this.style, | |
337 this.inlineStyle | |
338 }) : this.children = children == null ? _emptyList : children, | |
339 super(key: key) { | |
340 | |
341 assert(!_debugHasDuplicateIds()); | |
342 } | |
343 | |
344 void _remove() { | |
345 assert(children != null); | |
346 for (var child in children) { | |
347 assert(child != null); | |
348 _removeChild(child); | |
349 } | |
350 super._remove(); | |
351 } | |
352 | |
353 bool _debugHasDuplicateIds() { | |
354 var idSet = new HashSet<String>(); | |
355 for (var child in children) { | |
356 assert(child != null); | |
357 if (child.interchangeable) | |
358 continue; // when these nodes are reordered, we just reassign the data | |
359 | |
360 if (!idSet.add(child._key)) { | |
361 throw '''If multiple non-interchangeable nodes of the same type exist as
children | |
362 of another node, they must have unique keys. | |
363 Duplicate: "${child._key}"'''; | |
364 } | |
365 } | |
366 return false; | |
367 } | |
368 | |
369 void _syncNode(SkyNodeWrapper old) { | |
370 SkyElementWrapper oldSkyElementWrapper = old as SkyElementWrapper; | |
371 | |
372 List<Style> styles = new List<Style>(); | |
373 if (style != null) | |
374 styles.add(style); | |
375 ParentData parentData = null; | |
376 UINode parent = _parent; | |
377 while (parent != null && parent is! SkyNodeWrapper) { | |
378 if (parent is StyleNode && parent.style != null) | |
379 styles.add(parent.style); | |
380 else | |
381 if (parent is ParentDataNode && parent.parentData != null) { | |
382 if (parentData != null) | |
383 parentData.merge(parent.parentData); // this will throw if the types a
ren't the same | |
384 else | |
385 parentData = parent.parentData; | |
386 } | |
387 parent = parent._parent; | |
388 } | |
389 _root.updateStyles(styles); | |
390 if (parentData != null) { | |
391 assert(_root.parentData != null); | |
392 _root.parentData.merge(parentData); // this will throw if the types aren't
approriate | |
393 assert(parent != null); | |
394 assert(parent._root != null); | |
395 parent._root.markNeedsLayout(); | |
396 } | |
397 _root.updateInlineStyle(inlineStyle); | |
398 | |
399 _syncChildren(oldSkyElementWrapper); | |
400 } | |
401 | |
402 void _syncChildren(SkyElementWrapper oldSkyElementWrapper) { | |
403 if (_root is! RenderCSSContainer) | |
404 return; | |
405 | |
406 var startIndex = 0; | |
407 var endIndex = children.length; | |
408 | |
409 var oldChildren = oldSkyElementWrapper.children; | |
410 var oldStartIndex = 0; | |
411 var oldEndIndex = oldChildren.length; | |
412 | |
413 RenderCSS nextSibling = null; | |
414 UINode currentNode = null; | |
415 UINode oldNode = null; | |
416 | |
417 void sync(int atIndex) { | |
418 children[atIndex] = _syncChild(currentNode, oldNode, _root, nextSibling); | |
419 assert(children[atIndex] != null); | |
420 } | |
421 | |
422 // Scan backwards from end of list while nodes can be directly synced | |
423 // without reordering. | |
424 while (endIndex > startIndex && oldEndIndex > oldStartIndex) { | |
425 currentNode = children[endIndex - 1]; | |
426 oldNode = oldChildren[oldEndIndex - 1]; | |
427 | |
428 if (currentNode._key != oldNode._key) { | |
429 break; | |
430 } | |
431 | |
432 endIndex--; | |
433 oldEndIndex--; | |
434 sync(endIndex); | |
435 } | |
436 | |
437 HashMap<String, UINode> oldNodeIdMap = null; | |
438 | |
439 bool oldNodeReordered(String key) { | |
440 return oldNodeIdMap != null && | |
441 oldNodeIdMap.containsKey(key) && | |
442 oldNodeIdMap[key] == null; | |
443 } | |
444 | |
445 void advanceOldStartIndex() { | |
446 oldStartIndex++; | |
447 while (oldStartIndex < oldEndIndex && | |
448 oldNodeReordered(oldChildren[oldStartIndex]._key)) { | |
449 oldStartIndex++; | |
450 } | |
451 } | |
452 | |
453 void ensureOldIdMap() { | |
454 if (oldNodeIdMap != null) | |
455 return; | |
456 | |
457 oldNodeIdMap = new HashMap<String, UINode>(); | |
458 for (int i = oldStartIndex; i < oldEndIndex; i++) { | |
459 var node = oldChildren[i]; | |
460 if (!node.interchangeable) | |
461 oldNodeIdMap.putIfAbsent(node._key, () => node); | |
462 } | |
463 } | |
464 | |
465 bool searchForOldNode() { | |
466 if (currentNode.interchangeable) | |
467 return false; // never re-order these nodes | |
468 | |
469 ensureOldIdMap(); | |
470 oldNode = oldNodeIdMap[currentNode._key]; | |
471 if (oldNode == null) | |
472 return false; | |
473 | |
474 oldNodeIdMap[currentNode._key] = null; // mark it reordered | |
475 assert(_root is RenderCSSContainer); | |
476 assert(oldNode._root is RenderCSSContainer); | |
477 oldSkyElementWrapper._root.remove(oldNode._root); | |
478 _root.add(oldNode._root, before: nextSibling); | |
479 return true; | |
480 } | |
481 | |
482 // Scan forwards, this time we may re-order; | |
483 nextSibling = _root.firstChild; | |
484 while (startIndex < endIndex && oldStartIndex < oldEndIndex) { | |
485 currentNode = children[startIndex]; | |
486 oldNode = oldChildren[oldStartIndex]; | |
487 | |
488 if (currentNode._key == oldNode._key) { | |
489 assert(currentNode.runtimeType == oldNode.runtimeType); | |
490 nextSibling = _root.childAfter(nextSibling); | |
491 sync(startIndex); | |
492 startIndex++; | |
493 advanceOldStartIndex(); | |
494 continue; | |
495 } | |
496 | |
497 oldNode = null; | |
498 searchForOldNode(); | |
499 sync(startIndex); | |
500 startIndex++; | |
501 } | |
502 | |
503 // New insertions | |
504 oldNode = null; | |
505 while (startIndex < endIndex) { | |
506 currentNode = children[startIndex]; | |
507 sync(startIndex); | |
508 startIndex++; | |
509 } | |
510 | |
511 // Removals | |
512 currentNode = null; | |
513 while (oldStartIndex < oldEndIndex) { | |
514 oldNode = oldChildren[oldStartIndex]; | |
515 _removeChild(oldNode); | |
516 advanceOldStartIndex(); | |
517 } | |
518 } | |
519 } | |
520 | |
521 class Container extends SkyElementWrapper { | |
522 | |
523 RenderCSSContainer _root; | |
524 RenderCSSContainer _createNode() => new RenderCSSContainer(this); | |
525 | |
526 static final Container _emptyContainer = new Container(); | |
527 | |
528 SkyNodeWrapper get _emptyNode => _emptyContainer; | |
529 | |
530 Container({ | |
531 Object key, | |
532 List<UINode> children, | |
533 Style style, | |
534 String inlineStyle | |
535 }) : super( | |
536 key: key, | |
537 children: children, | |
538 style: style, | |
539 inlineStyle: inlineStyle | |
540 ); | |
541 } | |
542 | |
543 class Paragraph extends SkyElementWrapper { | |
544 | |
545 RenderCSSParagraph _root; | |
546 RenderCSSParagraph _createNode() => new RenderCSSParagraph(this); | |
547 | |
548 static final Paragraph _emptyContainer = new Paragraph(); | |
549 | |
550 SkyNodeWrapper get _emptyNode => _emptyContainer; | |
551 | |
552 Paragraph({ | |
553 Object key, | |
554 List<UINode> children, | |
555 Style style, | |
556 String inlineStyle | |
557 }) : super( | |
558 key: key, | |
559 children: children, | |
560 style: style, | |
561 inlineStyle: inlineStyle | |
562 ); | |
563 } | |
564 | |
565 class FlexContainer extends SkyElementWrapper { | |
566 | |
567 RenderCSSFlex _root; | |
568 RenderCSSFlex _createNode() => new RenderCSSFlex(this, this.direction); | |
569 | |
570 static final FlexContainer _emptyContainer = new FlexContainer(); | |
571 // direction doesn't matter if it's empty | |
572 | |
573 SkyNodeWrapper get _emptyNode => _emptyContainer; | |
574 | |
575 final FlexDirection direction; | |
576 | |
577 FlexContainer({ | |
578 Object key, | |
579 List<UINode> children, | |
580 Style style, | |
581 String inlineStyle, | |
582 this.direction: FlexDirection.Row | |
583 }) : super( | |
584 key: key, | |
585 children: children, | |
586 style: style, | |
587 inlineStyle: inlineStyle | |
588 ); | |
589 | |
590 void _syncNode(UINode old) { | |
591 super._syncNode(old); | |
592 _root.direction = direction; | |
593 } | |
594 } | |
595 | |
596 class FillStackContainer extends SkyElementWrapper { | |
597 | |
598 RenderCSSStack _root; | |
599 RenderCSSStack _createNode() => new RenderCSSStack(this); | |
600 | |
601 static final FillStackContainer _emptyContainer = new FillStackContainer(); | |
602 | |
603 SkyNodeWrapper get _emptyNode => _emptyContainer; | |
604 | |
605 FillStackContainer({ | |
606 Object key, | |
607 List<UINode> children, | |
608 Style style, | |
609 String inlineStyle | |
610 }) : super( | |
611 key: key, | |
612 children: _positionNodesToFill(children), | |
613 style: style, | |
614 inlineStyle: inlineStyle | |
615 ); | |
616 | |
617 static StackParentData _fillParentData = new StackParentData() | |
618 ..top = 0.0 | |
619 ..left = 0.0 | |
620 ..right = 0.0 | |
621 ..bottom = 0.0; | |
622 | |
623 static List<UINode> _positionNodesToFill(List<UINode> input) { | |
624 if (input == null) | |
625 return null; | |
626 return input.map((node) { | |
627 return new ParentDataNode(node, _fillParentData); | |
628 }).toList(); | |
629 } | |
630 } | |
631 | |
632 class TextFragment extends SkyElementWrapper { | |
633 | |
634 RenderCSSInline _root; | |
635 RenderCSSInline _createNode() => new RenderCSSInline(this, this.data); | |
636 | |
637 static final TextFragment _emptyText = new TextFragment(''); | |
638 | |
639 SkyNodeWrapper get _emptyNode => _emptyText; | |
640 | |
641 final String data; | |
642 | |
643 TextFragment(this.data, { | |
644 Object key, | |
645 Style style, | |
646 String inlineStyle | |
647 }) : super( | |
648 key: key, | |
649 style: style, | |
650 inlineStyle: inlineStyle | |
651 ); | |
652 | |
653 void _syncNode(UINode old) { | |
654 super._syncNode(old); | |
655 _root.data = data; | |
656 } | |
657 } | |
658 | |
659 class Image extends SkyElementWrapper { | |
660 | |
661 RenderCSSImage _root; | |
662 RenderCSSImage _createNode() => new RenderCSSImage(this, this.src, this.width,
this.height); | |
663 | |
664 static final Image _emptyImage = new Image(); | |
665 | |
666 SkyNodeWrapper get _emptyNode => _emptyImage; | |
667 | |
668 final String src; | |
669 final int width; | |
670 final int height; | |
671 | |
672 Image({ | |
673 Object key, | |
674 Style style, | |
675 String inlineStyle, | |
676 this.width, | |
677 this.height, | |
678 this.src | |
679 }) : super( | |
680 key: key, | |
681 style: style, | |
682 inlineStyle: inlineStyle | |
683 ); | |
684 | |
685 void _syncNode(UINode old) { | |
686 super._syncNode(old); | |
687 _root.configure(this.src, this.width, this.height); | |
688 } | |
689 } | |
690 | |
691 | |
692 Set<Component> _mountedComponents = new HashSet<Component>(); | |
693 Set<Component> _unmountedComponents = new HashSet<Component>(); | |
694 | |
695 void _enqueueDidMount(Component c) { | |
696 assert(!_notifingMountStatus); | |
697 _mountedComponents.add(c); | |
698 } | |
699 | |
700 void _enqueueDidUnmount(Component c) { | |
701 assert(!_notifingMountStatus); | |
702 _unmountedComponents.add(c); | |
703 } | |
704 | |
705 bool _notifingMountStatus = false; | |
706 | |
707 void _notifyMountStatusChanged() { | |
708 try { | |
709 _notifingMountStatus = true; | |
710 _unmountedComponents.forEach((c) => c._didUnmount()); | |
711 _mountedComponents.forEach((c) => c._didMount()); | |
712 _mountedComponents.clear(); | |
713 _unmountedComponents.clear(); | |
714 } finally { | |
715 _notifingMountStatus = false; | |
716 } | |
717 } | |
718 | |
719 List<Component> _dirtyComponents = new List<Component>(); | |
720 bool _buildScheduled = false; | |
721 bool _inRenderDirtyComponents = false; | |
722 | |
723 void _buildDirtyComponents() { | |
724 _tracing.begin('fn::_buildDirtyComponents'); | |
725 | |
726 Stopwatch sw; | |
727 if (_shouldLogRenderDuration) | |
728 sw = new Stopwatch()..start(); | |
729 | |
730 try { | |
731 _inRenderDirtyComponents = true; | |
732 | |
733 _dirtyComponents.sort((a, b) => a._order - b._order); | |
734 for (var comp in _dirtyComponents) { | |
735 comp._buildIfDirty(); | |
736 } | |
737 | |
738 _dirtyComponents.clear(); | |
739 _buildScheduled = false; | |
740 } finally { | |
741 _inRenderDirtyComponents = false; | |
742 } | |
743 | |
744 _notifyMountStatusChanged(); | |
745 | |
746 if (_shouldLogRenderDuration) { | |
747 sw.stop(); | |
748 print('Render took ${sw.elapsedMicroseconds} microseconds'); | |
749 } | |
750 | |
751 _tracing.end('fn::_buildDirtyComponents'); | |
752 } | |
753 | |
754 void _scheduleComponentForRender(Component c) { | |
755 assert(!_inRenderDirtyComponents); | |
756 _dirtyComponents.add(c); | |
757 | |
758 if (!_buildScheduled) { | |
759 _buildScheduled = true; | |
760 new Future.microtask(_buildDirtyComponents); | |
761 } | |
762 } | |
763 | |
764 abstract class Component extends UINode { | |
765 bool get _isBuilding => _currentlyBuilding == this; | |
766 bool _dirty = true; | |
767 | |
768 UINode _built; | |
769 final int _order; | |
770 static int _currentOrder = 0; | |
771 bool _stateful; | |
772 static Component _currentlyBuilding; | |
773 List<Function> _mountCallbacks; | |
774 List<Function> _unmountCallbacks; | |
775 | |
776 void onDidMount(Function fn) { | |
777 if (_mountCallbacks == null) | |
778 _mountCallbacks = new List<Function>(); | |
779 | |
780 _mountCallbacks.add(fn); | |
781 } | |
782 | |
783 void onDidUnmount(Function fn) { | |
784 if (_unmountCallbacks == null) | |
785 _unmountCallbacks = new List<Function>(); | |
786 | |
787 _unmountCallbacks.add(fn); | |
788 } | |
789 | |
790 | |
791 Component({ Object key, bool stateful }) | |
792 : _stateful = stateful != null ? stateful : false, | |
793 _order = _currentOrder + 1, | |
794 super(key: key); | |
795 | |
796 Component.fromArgs(Object key, bool stateful) | |
797 : this(key: key, stateful: stateful); | |
798 | |
799 void _didMount() { | |
800 if (_mountCallbacks != null) | |
801 _mountCallbacks.forEach((fn) => fn()); | |
802 } | |
803 | |
804 void _didUnmount() { | |
805 if (_unmountCallbacks != null) | |
806 _unmountCallbacks.forEach((fn) => fn()); | |
807 } | |
808 | |
809 // TODO(rafaelw): It seems wrong to expose DOM at all. This is presently | |
810 // needed to get sizing info. | |
811 RenderCSS getRoot() => _root; | |
812 | |
813 void _remove() { | |
814 assert(_built != null); | |
815 assert(_root != null); | |
816 _removeChild(_built); | |
817 _built = null; | |
818 _enqueueDidUnmount(this); | |
819 super._remove(); | |
820 } | |
821 | |
822 bool _willSync(UINode old) { | |
823 Component oldComponent = old as Component; | |
824 if (oldComponent == null || !oldComponent._stateful) | |
825 return false; | |
826 | |
827 // Make |this| the "old" Component | |
828 _stateful = false; | |
829 _built = oldComponent._built; | |
830 assert(_built != null); | |
831 | |
832 // Make |oldComponent| the "new" component | |
833 reflect.copyPublicFields(this, oldComponent); | |
834 oldComponent._built = null; | |
835 oldComponent._dirty = true; | |
836 return true; | |
837 } | |
838 | |
839 /* There are three cases here: | |
840 * 1) Building for the first time: | |
841 * assert(_built == null && old == null) | |
842 * 2) Re-building (because a dirty flag got set): | |
843 * assert(_built != null && old == null) | |
844 * 3) Syncing against an old version | |
845 * assert(_built == null && old != null) | |
846 */ | |
847 void _sync(UINode old, RenderCSSContainer host, RenderCSS insertBefore) { | |
848 assert(!_defunct); | |
849 assert(_built == null || old == null); | |
850 | |
851 Component oldComponent = old as Component; | |
852 | |
853 var oldBuilt; | |
854 if (oldComponent == null) { | |
855 oldBuilt = _built; | |
856 } else { | |
857 assert(_built == null); | |
858 oldBuilt = oldComponent._built; | |
859 } | |
860 | |
861 if (oldBuilt == null) | |
862 _enqueueDidMount(this); | |
863 | |
864 int lastOrder = _currentOrder; | |
865 _currentOrder = _order; | |
866 _currentlyBuilding = this; | |
867 _built = build(); | |
868 _currentlyBuilding = null; | |
869 _currentOrder = lastOrder; | |
870 | |
871 _built = _syncChild(_built, oldBuilt, host, insertBefore); | |
872 _dirty = false; | |
873 _root = _built._root; | |
874 assert(_root != null); | |
875 } | |
876 | |
877 void _buildIfDirty() { | |
878 if (!_dirty || _defunct) | |
879 return; | |
880 | |
881 _trace('$_key rebuilding...'); | |
882 _sync(null, null, null); // TODO(ianh): figure out how passing "null, null,
null" here is ok | |
883 } | |
884 | |
885 void scheduleBuild() { | |
886 setState(() {}); | |
887 } | |
888 | |
889 void setState(Function fn()) { | |
890 _stateful = true; | |
891 fn(); | |
892 if (_isBuilding || _dirty || _defunct) | |
893 return; | |
894 | |
895 _dirty = true; | |
896 _scheduleComponentForRender(this); | |
897 } | |
898 | |
899 UINode build(); | |
900 } | |
901 | |
902 abstract class App extends Component { | |
903 RenderCSS _host; | |
904 | |
905 App() : super(stateful: true) { | |
906 _host = new RenderCSSRoot(this); | |
907 _scheduleComponentForRender(this); | |
908 } | |
909 | |
910 void _buildIfDirty() { | |
911 if (!_dirty || _defunct) | |
912 return; | |
913 | |
914 _trace('$_key rebuilding...'); | |
915 _sync(null, _host, _root); | |
916 } | |
917 } | |
918 | |
919 class Text extends Component { | |
920 Text(this.data) : super(key: '*text*'); | |
921 final String data; | |
922 bool get interchangeable => true; | |
923 UINode build() => new Paragraph(children: [new TextFragment(data)]); | |
924 } | |
OLD | NEW |