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