Adam Barth b4ff5ca6ae Prototype of fn3
This patch contains a prototype of a new widget framework. In this framework,
Components can be reused in the tree as many times as the author desires. Also,
StatefulComponent is split into two pieces, a ComponentConfiguration and a
ComponentState. The ComponentConfiguration is created by the author and can be
reused as many times as desired. When mounted into the tree, the
ComponentConfiguration creates a ComponentState to hold the state for the
component. The state remains in the tree and cannot be reused.
2015-09-18 23:17:52 -07:00

1680 lines
57 KiB
Dart

// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:collection';
import 'dart:sky' as sky;
import 'package:sky/animation.dart';
import 'package:sky/services.dart';
import 'package:sky/src/rendering/box.dart';
import 'package:sky/src/rendering/error.dart';
import 'package:sky/src/rendering/hit_test.dart';
import 'package:sky/src/rendering/object.dart';
import 'package:sky/src/rendering/sky_binding.dart';
import 'package:sky/src/rendering/view.dart';
export 'package:sky/src/rendering/box.dart' show BoxConstraints, BoxDecoration, Border, BorderSide, EdgeDims;
export 'package:sky/src/rendering/hit_test.dart' show EventDisposition, combineEventDispositions;
export 'package:sky/src/rendering/object.dart' show Point, Offset, Size, Rect, Color, Paint, Path;
final bool _shouldLogRenderDuration = false; // see also 'enableProfilingLoop' argument to runApp()
typedef Widget Builder();
typedef void WidgetTreeWalker(Widget widget);
abstract class Key {
const Key.constructor(); // so that subclasses can call us, since the Key() factory constructor shadows the implicit constructor
factory Key(String value) => new ValueKey<String>(value);
}
class ValueKey<T> extends Key {
const ValueKey(this.value) : super.constructor();
final T value;
String toString() => '[\'${value}\']';
bool operator==(other) => other is ValueKey<T> && other.value == value;
int get hashCode => value.hashCode;
}
class ObjectKey extends Key {
const ObjectKey(this.value) : super.constructor();
final Object value;
String toString() => '[${value.runtimeType}(${value.hashCode})]';
bool operator==(other) => other is ObjectKey && identical(other.value, value);
int get hashCode => identityHashCode(value);
}
typedef void GlobalKeySyncListener(GlobalKey key, Widget widget);
typedef void GlobalKeyRemoveListener(GlobalKey key);
abstract class GlobalKey extends Key {
const GlobalKey.constructor() : super.constructor(); // so that subclasses can call us, since the Key() factory constructor shadows the implicit constructor
factory GlobalKey({ String label }) => new LabeledGlobalKey(label); // the label is purely for debugging purposes and is otherwise ignored
static final Map<GlobalKey, Widget> _registry = new Map<GlobalKey, Widget>();
static final Map<GlobalKey, int> _debugDuplicates = new Map<GlobalKey, int>();
static final Map<GlobalKey, Set<GlobalKeySyncListener>> _syncListeners = new Map<GlobalKey, Set<GlobalKeySyncListener>>();
static final Map<GlobalKey, Set<GlobalKeyRemoveListener>> _removeListeners = new Map<GlobalKey, Set<GlobalKeyRemoveListener>>();
static final Set<GlobalKey> _syncedKeys = new Set<GlobalKey>();
static final Set<GlobalKey> _removedKeys = new Set<GlobalKey>();
void _register(Widget widget) {
assert(() {
if (_registry.containsKey(this)) {
int oldCount = _debugDuplicates.putIfAbsent(this, () => 1);
assert(oldCount >= 1);
_debugDuplicates[this] = oldCount + 1;
}
return true;
});
_registry[this] = widget;
}
void _unregister(Widget widget) {
assert(() {
if (_registry.containsKey(this) && _debugDuplicates.containsKey(this)) {
int oldCount = _debugDuplicates[this];
assert(oldCount >= 2);
if (oldCount == 2) {
_debugDuplicates.remove(this);
} else {
_debugDuplicates[this] = oldCount - 1;
}
}
return true;
});
if (_registry[this] == widget) {
_registry.remove(this);
_removedKeys.add(this);
}
}
void _didSync() {
_syncedKeys.add(this);
}
static void registerSyncListener(GlobalKey key, GlobalKeySyncListener listener) {
assert(key != null);
Set<GlobalKeySyncListener> listeners =
_syncListeners.putIfAbsent(key, () => new Set<GlobalKeySyncListener>());
bool added = listeners.add(listener);
assert(added);
}
static void unregisterSyncListener(GlobalKey key, GlobalKeySyncListener listener) {
assert(key != null);
assert(_syncListeners.containsKey(key));
bool removed = _syncListeners[key].remove(listener);
if (_syncListeners[key].isEmpty)
_syncListeners.remove(key);
assert(removed);
}
static void registerRemoveListener(GlobalKey key, GlobalKeyRemoveListener listener) {
assert(key != null);
Set<GlobalKeyRemoveListener> listeners =
_removeListeners.putIfAbsent(key, () => new Set<GlobalKeyRemoveListener>());
bool added = listeners.add(listener);
assert(added);
}
static void unregisterRemoveListener(GlobalKey key, GlobalKeyRemoveListener listener) {
assert(key != null);
assert(_removeListeners.containsKey(key));
bool removed = _removeListeners[key].remove(listener);
if (_removeListeners[key].isEmpty)
_removeListeners.remove(key);
assert(removed);
}
static Widget getWidget(GlobalKey key) {
assert(key != null);
return _registry[key];
}
static void _notifyListeners() {
assert(!_inBuildDirtyComponents);
assert(!Widget._notifyingMountStatus);
assert(() {
String message = '';
for (GlobalKey key in _debugDuplicates.keys) {
message += 'Duplicate GlobalKey found amongst mounted widgets: $key (${_debugDuplicates[key]} instances)\n';
message += 'Most recently registered instance is:\n${_registry[key]}\n';
}
if (!_debugDuplicates.isEmpty)
throw message;
return true;
});
if (_syncedKeys.isEmpty && _removedKeys.isEmpty)
return;
try {
for (GlobalKey key in _syncedKeys) {
Widget widget = _registry[key];
if (widget != null && _syncListeners.containsKey(key)) {
Set<GlobalKeySyncListener> localListeners = new Set<GlobalKeySyncListener>.from(_syncListeners[key]);
for (GlobalKeySyncListener listener in localListeners)
listener(key, widget);
}
}
for (GlobalKey key in _removedKeys) {
if (!_registry.containsKey(key) && _removeListeners.containsKey(key)) {
Set<GlobalKeyRemoveListener> localListeners = new Set<GlobalKeyRemoveListener>.from(_removeListeners[key]);
for (GlobalKeyRemoveListener listener in localListeners)
listener(key);
}
}
} finally {
_removedKeys.clear();
_syncedKeys.clear();
}
}
}
class LabeledGlobalKey extends GlobalKey {
// the label is purely for documentary purposes and does not affect the key
const LabeledGlobalKey(this._label) : super.constructor();
final String _label;
String toString() => '[GlobalKey ${_label != null ? _label : hashCode}]';
}
class GlobalObjectKey extends GlobalKey {
const GlobalObjectKey(this.value) : super.constructor();
final Object value;
String toString() => '[GlobalKey ${value.runtimeType}(${value.hashCode})]';
bool operator==(other) => other is GlobalObjectKey && identical(other.value, value);
int get hashCode => identityHashCode(value);
}
/// A base class for elements of the widget tree
abstract class Widget {
Widget({ Key key }) : _key = key {
assert(_isConstructedDuringBuild());
}
// TODO(jackson): Remove this workaround for limitation of Dart mixins
Widget._withKey(Key key) : _key = key {
assert(_isConstructedDuringBuild());
}
// you should not build the UI tree ahead of time, build it only during build()
bool _isConstructedDuringBuild() => this is AbstractWidgetRoot || this is App || _inBuildDirtyComponents || _inLayoutCallbackBuilder > 0;
Key _key;
/// A semantic identifer for this widget
///
/// Keys are used to find matches when synchronizing two widget trees, for
/// example after a [Component] rebuilds. Without keys, two widgets can match
/// if their runtimeType matches. With keys, the keys must match as well.
/// Assigning a key to a widget can improve performance by causing the
/// framework to sync widgets that share a lot of common structure and can
/// help match stateful components semantically rather than positionally.
Key get key => _key;
Widget _parent;
/// The parent of this widget in the widget tree.
Widget get parent => _parent;
// The "generation" of a Widget is the frame in which it was last
// synced. We use this to tell if an instance of a Widget has moved
// to earlier in the tree so that when we come across where it used
// to be, we pretend it was never there. See syncChild().
static int _currentGeneration = 1;
int _generation = 0;
bool get isFromOldGeneration => _generation < _currentGeneration;
void _markAsFromCurrentGeneration() { _generation = _currentGeneration; }
bool get _hasState => false;
static bool _canSync(Widget a, Widget b) {
assert(a != null);
assert(b != null);
return (a == b) ||
(a.runtimeType == b.runtimeType &&
a.key == b.key &&
(!a._hasState || !b._hasState));
}
bool _mounted = false;
bool _wasMounted = false;
bool get mounted => _mounted;
static bool _notifyingMountStatus = false;
static List<Widget> _mountedChanged = new List<Widget>();
/// Called during the synchronizing process to update the widget's parent.
void setParent(Widget newParent) {
assert(!_notifyingMountStatus);
if (_parent == newParent)
return;
_parent = newParent;
if (newParent == null) {
if (_mounted) {
_mounted = false;
_mountedChanged.add(this);
}
} else {
assert(newParent._mounted);
if (_parent._mounted != _mounted) {
_mounted = _parent._mounted;
_mountedChanged.add(this);
}
}
}
/// Walks the immediate children of this widget.
///
/// Override this if you have children and call walker on each child.
/// Note that you may be called before the child has had its parent
/// pointer set to point to you. Your walker, and any methods it
/// invokes on your descendants, should not rely on the ancestor
/// chain being correctly configured at this point.
void walkChildren(WidgetTreeWalker walker) { }
/// If the object has a single child, return it. Override this if
/// you define a new child model with only one child.
Widget get singleChild => null;
/// Detaches the single child of this object from this object,
/// without calling remove() on that child.
/// Only called when singleChild returns a non-null node.
/// Override this if you override singleChild to return non-null.
void takeChild() {
assert(singleChild != null);
throw '${runtimeType} does not define a "takeChild()" method';
}
static void _notifyMountStatusChanged() {
try {
sky.tracing.begin('Widget._notifyMountStatusChanged');
_notifyingMountStatus = true;
for (Widget node in _mountedChanged) {
if (node._wasMounted != node._mounted) {
if (node._mounted)
node.didMount();
else
node.didUnmount();
node._wasMounted = node._mounted;
}
}
_mountedChanged.clear();
} finally {
_notifyingMountStatus = false;
sky.tracing.end('Widget._notifyMountStatusChanged');
}
GlobalKey._notifyListeners();
}
/// Override this function to learn when this [Widget] enters the widget tree.
void didMount() {
if (key is GlobalKey)
(key as GlobalKey)._register(this); // TODO(ianh): remove cast when analyzer is cleverer
}
/// Override this function to learn when this [Widget] leaves the widget tree.
void didUnmount() {
if (key is GlobalKey)
(key as GlobalKey)._unregister(this); // TODO(ianh): remove cast when analyzer is cleverer
}
RenderObject _renderObject;
/// The underlying [RenderObject] associated with this [Widget].
RenderObject get renderObject => _renderObject;
// Subclasses which implements Nodes that become stateful may return true
// if the node has become stateful and should be retained.
// This is called immediately before _sync().
// Component.retainStatefulNodeIfPossible() calls syncConstructorArguments().
// This is only ever called on a node A with an argument B if _canSync(A, B)
// is true. It's ok if after calling retainStatefulNodeIfPossible(), the two
// nodes no longer return true from _canSync().
bool retainStatefulNodeIfPossible(Widget newNode) => false;
void _sync(Widget old, dynamic slot) {
// When this is called, old must be from an old generation. It's a violation
// of the _sync() contract to call _sync() with an old node that has already
// been synced this generation, as this means that that node is elsewhere in
// the tree by now.
// By the time we get here, though, syncChild() has already been called on
// our children (by the subclasses overriding this). This means 'old' may
// have been moved to somewhere else in the tree and might therefore already
// be from a new generation.
assert(isFromOldGeneration);
_markAsFromCurrentGeneration();
if (old != null && old != this)
old._markAsFromCurrentGeneration();
if (key is GlobalKey)
(key as GlobalKey)._didSync(); // TODO(ianh): Remove the cast once the analyzer is cleverer.
}
void updateSlot(dynamic newSlot);
// 'slot' is the identifier that the ancestor RenderObjectWrapper uses to know
// where to put this descendant. If you just defer to a child, then make sure
// to pass them the slot.
Set<Type> _dependencies;
Inherited inheritedOfType(Type targetType) {
if (_dependencies == null)
_dependencies = new Set<Type>();
_dependencies.add(targetType);
Widget ancestor = parent;
while (ancestor != null && ancestor.runtimeType != targetType)
ancestor = ancestor.parent;
return ancestor;
}
void dependenciesChanged() {
// This is called if you've use inheritedOfType and the Inherited
// ancestor you were provided was changed. For a widget to use Inherited
// it needs an implementation of dependenciesChanged. If your widget extends
// Component or RenderObjectWrapper this is provided for you automatically.
// If you aren't able to extend one of those classes, you need to
// provide your own implementation of dependenciesChanged.
assert(false);
}
void remove() {
walkChildren((Widget child) {
if (child.isFromOldGeneration || !isFromOldGeneration) {
assert(child.parent == this);
child.remove();
} else {
// if it's from the current generation and we're not, it means it's been moved somewhere else in the tree already and isn't really our child anymore
assert(child.parent != this);
}
});
_renderObject = null;
setParent(null);
}
void detachRenderObject();
Widget _getCandidateSingleChildFrom(Widget oldChild) {
Widget candidate = oldChild.singleChild;
if (candidate != null && !candidate.isFromOldGeneration)
candidate = null;
assert(candidate == null || candidate.parent == oldChild);
return candidate;
}
// Returns the child which should be retained as the child of this node.
Widget syncChild(Widget newNode, Widget oldNode, dynamic slot) {
String debugDetails;
assert(() {
// we save this information early because by the time the exception fires we might have changed everything around
debugDetails = ' old child: $oldNode\n new child: $newNode';
return true;
});
try {
// At this point, oldNode might have been moved to a new part in the tree.
// If this has happened, it will already have been synced, in which case
// it will be from a new generation. Our contract with _sync() is that we
// will not pass an old child that has been moved to elsewhere in the
// tree, so it's important that we verify at each step here that we're not
// dealing with such a child. This is why this function keeps checking
// isFromOldGeneration on the candidate "old" children.
if (newNode == oldNode) {
// If our child literally hasn't changed identity, short-circuit the
// work. That subtree hasn't changed. We still need to set the parent
// because _we_ might have changed identity, even if the child hasn't.
assert(newNode is! RenderObjectWrapper ||
(newNode is RenderObjectWrapper && newNode._ancestor != null)); // if the child didn't change, it had better be configured
assert(newNode is! StatefulComponent ||
(newNode is StatefulComponent && newNode._isStateInitialized)); // if the child didn't change, it had better be configured
// TODO(ianh): Simplify the two asserts above once the analyzer is cleverer
if (newNode != null) {
newNode.setParent(this);
newNode._markAsFromCurrentGeneration();
}
return newNode; // Nothing to do. Subtrees must be identical.
}
// if the old node isn't the same as the new node and has already been synced, then
// we must assume that it has been moved elsewhere in the tree and isn't really a
// match for the node we're trying to insert.
if (oldNode != null && !oldNode.isFromOldGeneration)
oldNode = null;
if (newNode == null) {
// the child in this slot has gone away
// remove it if they old one is still here
if (oldNode != null) {
assert(oldNode != null);
assert(oldNode.isFromOldGeneration);
assert(oldNode.mounted);
oldNode.detachRenderObject();
oldNode.remove();
assert(!oldNode.mounted);
// we don't update the generation of oldNode, because there's
// still a chance it could be reused as-is later in the tree.
}
return null;
}
if (oldNode != null) {
assert(newNode != null);
assert(newNode.isFromOldGeneration);
assert(oldNode.isFromOldGeneration);
if (!Widget._canSync(newNode, oldNode)) {
assert(oldNode.mounted);
// We want to handle the case where there is a removal of zero
// or more widgets. In this case, we should be able to sync
// ourselves with a Widget that is a descendant of the
// oldNode, skipping the nodes in between. Let's try that.
Widget deadNode = oldNode;
Widget candidate = _getCandidateSingleChildFrom(oldNode);
oldNode = null;
while (candidate != null) {
if (Widget._canSync(newNode, candidate)) {
assert(candidate.parent != null);
assert(candidate.parent.singleChild == candidate);
if (candidate.renderObject != deadNode.renderObject) {
// TODO(ianh): Handle removal across RenderNode boundaries
} else {
candidate.parent.takeChild();
oldNode = candidate;
}
break;
}
candidate = _getCandidateSingleChildFrom(candidate);
}
// TODO(ianh): Handle insertion, too...
if (oldNode == null)
deadNode.detachRenderObject();
deadNode.remove();
}
if (oldNode != null) {
assert(newNode.isFromOldGeneration);
assert(oldNode.isFromOldGeneration);
assert(Widget._canSync(newNode, oldNode));
if (oldNode == newNode) {
newNode.setParent(this);
newNode._markAsFromCurrentGeneration();
return newNode;
} else if (oldNode.retainStatefulNodeIfPossible(newNode)) {
assert(oldNode.mounted);
assert(!newNode.mounted);
oldNode.setParent(this);
oldNode._sync(newNode, slot);
assert(oldNode.renderObject is RenderObject);
return oldNode;
} else {
oldNode.setParent(null);
}
}
}
assert(oldNode == null || (oldNode.mounted == false && oldNode.parent == null && newNode.mounted == false && oldNode.isFromOldGeneration && newNode.isFromOldGeneration));
assert(oldNode != newNode);
newNode.setParent(this);
newNode._sync(oldNode, slot);
assert(newNode.renderObject is RenderObject);
return newNode;
} catch (e, stack) {
_debugReportException('syncing children of $this\n$debugDetails', e, stack);
return null;
}
}
// This function can be safely called when the layout is valid.
// For example Listener or SizeObserver callbacks can safely call
// globalToLocal().
Point globalToLocal(Point point) {
assert(mounted);
assert(renderObject is RenderBox);
return (renderObject as RenderBox).globalToLocal(point);
}
// See globalToLocal().
Point localToGlobal(Point point) {
assert(mounted);
assert(renderObject is RenderBox);
return (renderObject as RenderBox).localToGlobal(point);
}
String toString() {
List<String> details = <String>[];
debugAddDetails(details);
String detailString = details.join('; ');
return '$runtimeType($detailString})';
}
void debugAddDetails(List<String> details) {
if (key != null)
details.add('$key');
details.add('hashCode=$hashCode');
details.add(mounted ? 'mounted' : 'not mounted');
if (_generation != _currentGeneration) {
if (_generation == 0) {
details.add('never synced');
} else {
int delta = _generation - _currentGeneration;
String sign = delta < 0 ? '' : '+';
details.add('gen$sign$delta');
}
}
}
String toStringDeep([String prefix = '', String startPrefix = '']) {
String childrenString = '';
List<Widget> children = new List<Widget>();
walkChildren(children.add);
if (children.length > 0) {
Widget lastChild = children.removeLast();
String nextStartPrefix = prefix + ' +-';
String nextPrefix = prefix + ' | ';
for (Widget child in children)
childrenString += child.toStringDeep(nextPrefix, _adjustPrefixWithParentCheck(child, nextStartPrefix));
nextStartPrefix = prefix + ' \'-';
nextPrefix = prefix + ' ';
childrenString += lastChild.toStringDeep(nextPrefix, _adjustPrefixWithParentCheck(lastChild, nextStartPrefix));
}
return '$startPrefix$this\n$childrenString';
}
String _adjustPrefixWithParentCheck(Widget child, String prefix) {
if (child.parent == this)
return prefix;
if (child.parent == null)
return '$prefix [[DISCONNECTED]] ';
return '$prefix [[PARENT IS ${child.parent}]] ';
}
}
// Descendants of TagNode provide a way to tag RenderObjectWrapper and
// Component nodes with annotations, such as event listeners,
// stylistic information, etc.
abstract class TagNode extends Widget {
TagNode({ Key key, Widget child })
: child = child, super(key: key) {
assert(child != null);
}
// TODO(jackson): Remove this workaround for limitation of Dart mixins
TagNode._withKey(Widget child, Key key)
: child = child, super._withKey(key);
Widget child;
void walkChildren(WidgetTreeWalker walker) {
if (child != null)
walker(child);
}
Widget get singleChild => child;
bool _debugChildTaken = false;
void takeChild() {
assert(!_debugChildTaken);
assert(singleChild == child);
assert(child != null);
child = null;
_renderObject = null;
assert(() { _debugChildTaken = true; return true; });
}
void _sync(TagNode old, dynamic slot) {
assert(!_debugChildTaken);
child = syncChild(child, old?.child, slot);
if (child != null) {
assert(child.parent == this);
assert(child.renderObject != null);
_renderObject = child.renderObject;
assert(_renderObject == renderObject); // in case a subclass reintroduces it
} else {
_renderObject = null;
}
super._sync(old, slot);
}
void updateSlot(dynamic newSlot) {
child.updateSlot(newSlot);
}
void detachRenderObject() {
assert(!_debugChildTaken);
if (child != null)
child.detachRenderObject();
}
}
abstract class ParentDataNode extends TagNode {
ParentDataNode({ Key key, Widget child })
: super(key: key, child: child);
/// Subclasses should override this function to ensure that they are placed
/// inside widgets that expect them.
///
/// The given ancestor is the first non-component ancestor of this widget.
void debugValidateAncestor(Widget ancestor);
}
abstract class Inherited extends TagNode {
Inherited({ Key key, Widget child }) : super._withKey(child, key);
void _sync(Widget old, dynamic slot) {
if (old != null) {
if (syncShouldNotify(old))
notifyDescendants();
}
super._sync(old, slot);
}
void notifyDescendants() {
final Type ourRuntimeType = runtimeType;
void notifyChildren(Widget child) {
if (child._dependencies != null &&
child._dependencies.contains(ourRuntimeType))
child.dependenciesChanged();
if (child.runtimeType != ourRuntimeType)
child.walkChildren(notifyChildren);
}
walkChildren(notifyChildren);
}
bool syncShouldNotify(Inherited old);
}
typedef EventDisposition GestureEventListener(sky.GestureEvent e);
typedef EventDisposition PointerEventListener(sky.PointerEvent e);
typedef EventDisposition EventListener(sky.Event e);
class Listener extends TagNode {
Listener({
Key key,
Widget child,
PointerEventListener this.onPointerDown,
PointerEventListener this.onPointerMove,
PointerEventListener this.onPointerUp,
PointerEventListener this.onPointerCancel
}) : super(key: key, child: child);
final PointerEventListener onPointerDown;
final PointerEventListener onPointerMove;
final PointerEventListener onPointerUp;
final PointerEventListener onPointerCancel;
EventDisposition _handleEvent(sky.Event event) {
if (onPointerDown != null && event.type == 'pointerdown')
return onPointerDown(event);
if (onPointerMove != null && event.type == 'pointermove')
return onPointerMove(event);
if (onPointerUp != null && event.type == 'pointerup')
return onPointerUp(event);
if (onPointerCancel != null && event.type == 'pointercancel')
return onPointerCancel(event);
return EventDisposition.ignored;
}
}
abstract class Component extends Widget {
Component({ Key key })
: _order = _currentOrder + 1,
super._withKey(key);
bool _debugIsBuilding = false;
static Queue<Component> _debugComponentBuildTree = new Queue<Component>();
bool _dirty = true;
Widget _child;
void walkChildren(WidgetTreeWalker walker) {
if (_child != null)
walker(_child);
}
Widget get singleChild => _child;
bool _debugChildTaken = false;
void takeChild() {
assert(!_debugChildTaken);
assert(singleChild == _child);
assert(_child != null);
_child = null;
_renderObject = null;
assert(() { _debugChildTaken = true; return true; });
}
void remove() {
assert(_debugChildTaken || (_child != null && _renderObject != null));
super.remove();
_child = null;
_renderObject = null;
}
void detachRenderObject() {
assert(!_debugChildTaken);
if (_child != null)
_child.detachRenderObject();
}
void dependenciesChanged() {
// called by Inherited.sync()
_scheduleBuild();
}
dynamic _slot; // cached slot from the last time we were synced
void updateSlot(dynamic newSlot) {
_slot = newSlot;
if (_child != null)
_child.updateSlot(newSlot);
}
// order corresponds to _build_ order, not depth in the tree.
// All the Components built by a particular other Component will have the
// same order, regardless of whether one is subsequently inserted
// into another. The order is used to not tell a Component to
// rebuild if the Component that it built has itself been rebuilt.
final int _order;
static int _currentOrder = 0;
// There are three cases here:
// 1) Building for the first time:
// assert(_child == null && old == null)
// 2) Re-building (because a dirty flag got set):
// assert(_child != null && old == null)
// 3) Syncing against an old version (we are either new, or Stateful and pretending to be new)
// assert(_child == null && old != null)
void _sync(Component old, dynamic slot) {
assert(!_debugChildTaken);
assert(_child == null || old == null);
assert(old == null || old.isFromOldGeneration);
updateSlot(slot);
Widget oldChild;
if (old == null) {
oldChild = _child;
_child = null;
} else {
oldChild = old._child;
old._child = null;
assert(_child == null);
}
assert(() {
_debugIsBuilding = true;
_debugComponentBuildTree.add(this);
return true;
});
int lastOrder = _currentOrder;
_currentOrder = _order;
try {
_child = build();
assert(_child != null);
} catch (e, stack) {
_debugReportException('building $this', e, stack);
}
_currentOrder = lastOrder;
assert(() { _debugChildTaken = false; return true; });
try {
// even if build() failed (i.e. _child == null), we still call syncChild(), to remove the oldChild
_child = syncChild(_child, oldChild, slot);
assert(!_debugChildTaken); // we shouldn't be able to lose our child when we're syncing it!
assert(_child == null || _child.parent == this);
assert(_child == null || _child.renderObject != null);
} catch (e, stack) {
_debugReportException('syncing build output of $this\n old child: $oldChild\n new child: $_child', e, stack);
_child = null;
}
assert(() {
if (_child == null) {
try {
_child = new ErrorWidget()..setParent(this).._sync(null, slot);
} catch (e) {
print('(application is now in an unstable state - ignore any subsequent exceptions)');
}
}
_debugIsBuilding = false;
return identical(_debugComponentBuildTree.removeLast(), this);
});
_dirty = false;
_renderObject = _child.renderObject;
assert(_renderObject == renderObject); // in case a subclass reintroduces it
super._sync(old, slot);
}
void _markAsFromCurrentGeneration() {
if (_dirty)
return;
super._markAsFromCurrentGeneration();
}
void _buildIfDirty() {
if (!_dirty || !_mounted)
return;
assert(isFromOldGeneration);
assert(renderObject != null);
_sync(null, _slot);
}
void _scheduleBuild() {
assert(!_debugIsBuilding);
if (_dirty || !_mounted)
return;
_dirty = true;
_scheduleComponentForRender(this);
}
static void flushBuild() {
if (!_dirtyComponents.isEmpty)
_buildDirtyComponents();
}
Widget build();
void debugAddDetails(List<String> details) {
super.debugAddDetails(details);
if (_dirty)
details.add('dirty');
}
}
abstract class StatefulComponent extends Component {
StatefulComponent({ Key key }) : super(key: key);
bool _isStateInitialized = false;
bool get _hasState => _isStateInitialized;
bool retainStatefulNodeIfPossible(StatefulComponent newNode) {
assert(newNode != null);
assert(!newNode._isStateInitialized);
assert(this != newNode);
assert(Widget._canSync(this, newNode));
assert(_child != null);
newNode._child = _child;
_child = null;
_dirty = true;
return true;
}
// because our retainStatefulNodeIfPossible() method returns true,
// when _sync is called, our 'old' is actually the new instance that
// we are to copy state from.
void _sync(StatefulComponent old, dynamic slot) {
if (old == null) {
if (!_isStateInitialized) {
assert(mounted);
assert(!_wasMounted);
initState();
_isStateInitialized = true;
}
} else {
assert(_isStateInitialized);
assert(!old._isStateInitialized);
assert(old.isFromOldGeneration);
syncConstructorArguments(old);
}
super._sync(old, slot);
assert(_child != null);
}
// Stateful components can override initState if they want
// to do non-trivial work to initialize state. This is
// always called before build().
void initState() { }
// This is called by _sync(). Derived classes should override this
// method to update `this` to account for the new values the parent
// passed to `source`. Make sure to call super.syncConstructorArguments(source)
// unless you are extending StatefulComponent directly.
// A given source can be used multiple times as a source.
// The source must not be mutated.
void syncConstructorArguments(Component source);
// Calls function fn immediately and then schedules another build
// for this Component.
void setState(void fn()) {
assert(!_debugIsBuilding);
assert(_isStateInitialized);
fn();
_scheduleBuild();
}
void debugAddDetails(List<String> details) {
super.debugAddDetails(details);
details.add(_isStateInitialized ? 'stateful' : 'stateless');
}
}
Set<Component> _dirtyComponents = new Set<Component>();
bool _buildScheduled = false;
bool _inBuildDirtyComponents = false;
int _inLayoutCallbackBuilder = 0;
class LayoutCallbackBuilderHandle { bool _active = true; }
LayoutCallbackBuilderHandle enterLayoutCallbackBuilder() {
LayoutCallbackBuilderHandle result;
assert(() {
_inLayoutCallbackBuilder += 1;
result = new LayoutCallbackBuilderHandle();
return true;
});
return result;
}
void exitLayoutCallbackBuilder(LayoutCallbackBuilderHandle handle) {
assert(() {
assert(handle._active);
handle._active = false;
_inLayoutCallbackBuilder -= 1;
return true;
});
_endSyncPhase();
}
List<int> _debugFrameTimes = <int>[];
// TODO(ianh): Move this to Component
void _absorbDirtyComponents(List<Component> list) {
list.addAll(_dirtyComponents);
_dirtyComponents.clear();
list.sort((Component a, Component b) => a._order - b._order);
}
// TODO(ianh): Move this to Component
void _buildDirtyComponents() {
assert(!_dirtyComponents.isEmpty);
Stopwatch sw;
if (_shouldLogRenderDuration)
sw = new Stopwatch()..start();
while (!_dirtyComponents.isEmpty) {
_inBuildDirtyComponents = true;
try {
sky.tracing.begin('Component.flushBuild');
List<Component> sortedDirtyComponents = new List<Component>();
_absorbDirtyComponents(sortedDirtyComponents);
int index = 0;
while (index < sortedDirtyComponents.length) {
Component component = sortedDirtyComponents[index];
component._buildIfDirty();
if (_dirtyComponents.length > 0) {
// the following assert verifies that we're not rebuilding anyone twice in one frame
assert(_dirtyComponents.every((Component component) => !sortedDirtyComponents.contains(component)));
_absorbDirtyComponents(sortedDirtyComponents);
index = 0;
} else {
index += 1;
}
}
} finally {
_buildScheduled = false;
_inBuildDirtyComponents = false;
sky.tracing.end('Component.flushBuild');
}
_endSyncPhase();
}
if (_shouldLogRenderDuration) {
sw.stop();
_debugFrameTimes.add(sw.elapsedMicroseconds);
if (_debugFrameTimes.length >= 1000) {
_debugFrameTimes.sort();
const int i = 99;
print('Component.flushBuild: ${i+1}th fastest frame out of the last ${_debugFrameTimes.length}: ${_debugFrameTimes[i]} microseconds');
_debugFrameTimes.clear();
}
}
}
// TODO(ianh): Move this to Widget
void _endSyncPhase() {
try {
Widget._currentGeneration += 1;
Widget._notifyMountStatusChanged();
} catch (e, stack) {
_debugReportException('sending post-sync notifications', e, stack);
return null;
}
}
// TODO(ianh): Move this to Component
void _scheduleComponentForRender(Component component) {
_dirtyComponents.add(component);
if (!_buildScheduled) {
_buildScheduled = true;
scheduler.ensureVisualUpdate();
}
}
// RenderObjectWrappers correspond to a desired state of a
// RenderObject. They are fully immutable, except that a Widget which
// is a Component which lives within a RenderObjectWrapper's children
// list may be in-place replaced with the "old" instance if it has
// become stateful.
abstract class RenderObjectWrapper extends Widget {
RenderObjectWrapper({ Key key }) : super(key: key);
RenderObject createNode();
static final Map<RenderObject, RenderObjectWrapper> _nodeMap =
new HashMap<RenderObject, RenderObjectWrapper>();
static RenderObjectWrapper _getMounted(RenderObject node) => _nodeMap[node];
static Iterable<Widget> getWidgetsForRenderObject(RenderObject renderObject) sync* {
Widget target = RenderObjectWrapper._getMounted(renderObject);
if (target == null)
return;
RenderObject targetRenderObject = target.renderObject;
while (target != null && target.renderObject == targetRenderObject) {
yield target;
target = target.parent;
}
}
RenderObjectWrapper _ancestor;
void insertChildRenderObject(RenderObjectWrapper child, dynamic slot);
void detachChildRenderObject(RenderObjectWrapper child);
bool get _hasState => _renderObject != null;
void retainStatefulRenderObjectWrapper(RenderObjectWrapper newNode) {
newNode._renderObject = _renderObject;
newNode._ancestor = _ancestor;
}
RenderObjectWrapper findAncestorRenderObjectWrapper() {
Widget ancestor = parent;
while (ancestor != null && ancestor is! RenderObjectWrapper)
ancestor = ancestor.parent;
return ancestor;
}
void _sync(RenderObjectWrapper old, dynamic slot) {
// TODO(abarth): We should split RenderObjectWrapper into two pieces so that
// RenderViewObject doesn't need to inherit all this code it
// doesn't need.
assert(parent != null || this is RenderViewWrapper);
assert(old == null || old.isFromOldGeneration);
if (old == null) {
_renderObject = createNode();
assert(_renderObject != null);
_ancestor = findAncestorRenderObjectWrapper();
if (_ancestor != null)
_ancestor.insertChildRenderObject(this, slot);
} else {
_renderObject = old.renderObject;
_ancestor = old._ancestor;
assert(_renderObject != null);
}
assert(() {
_renderObject.debugExceptionContext = Component._debugComponentBuildTree.fold(' Widget build stack:', (String result, Component component) => result + '\n $component');
return true;
});
assert(_renderObject == renderObject); // in case a subclass reintroduces it
assert(renderObject != null);
assert(mounted);
_nodeMap[renderObject] = this;
syncRenderObject(old);
super._sync(old, slot);
}
void updateSlot(dynamic newSlot) {
// We never use the slot except during sync(), in which
// case our parent is handing it to us anyway.
// We don't need to propagate this to our children, since
// we give them their own slots for them to fit into us.
}
/// Override this function if your RenderObjectWrapper uses a [ParentDataNode]
/// to program parent data into children.
void updateParentData(RenderObject child, ParentDataNode node) { }
void syncRenderObject(RenderObjectWrapper old) {
assert(old == null || old.renderObject == renderObject);
ParentDataNode parentDataNode = null;
for (Widget current = parent; current != null; current = current.parent) {
assert(() {
if (current is ParentDataNode) {
Widget ancestor = current.parent;
while (ancestor is Component)
ancestor = ancestor.parent;
// ancestor might be null in two cases:
// - asking for the ancestor of a Widget that has no non-Component
// ancestors between itself and its AbstractWidgetRoot ancestor
// - if the node is just being synced to get its intrinsic
// dimensions, as e.g. MixedViewport does.
if (ancestor != null)
current.debugValidateAncestor(ancestor);
}
return true;
});
if (current is ParentDataNode) {
assert(parentDataNode == null);
parentDataNode = current;
} else if (current is RenderObjectWrapper) {
current.updateParentData(renderObject, parentDataNode);
break;
}
}
}
// for use by subclasses that manage their children using lists
void syncChildren(List<Widget> newChildren, List<Widget> oldChildren) {
assert(newChildren != null);
assert(oldChildren != null);
assert(!identical(newChildren, oldChildren));
// This attempts to diff the new child list (this.children) with
// the old child list (old.children), and update our renderObject
// accordingly.
// The cases it tries to optimise for are:
// - the old list is empty
// - the lists are identical
// - there is an insertion or removal of one or more widgets in
// only one place in the list
// If a widget with a key is in both lists, it will be synced.
// Widgets without keys might be synced but there is no guarantee.
// The general approach is to sync the entire new list backwards, as follows:
// 1. Walk the lists from the top until you no longer have
// matching nodes. We don't sync these yet, but we now know to
// skip them below. We do this because at each sync we need to
// pass the pointer to the new next widget as the slot, which
// we can't do until we've synced the next child.
// 2. Walk the lists from the bottom, syncing nodes, until you no
// longer have matching nodes.
// At this point we narrowed the old and new lists to the point
// where the nodes no longer match.
// 3. Walk the narrowed part of the old list to get the list of
// keys and sync null with non-keyed items.
// 4. Walk the narrowed part of the new list backwards:
// * Sync unkeyed items with null
// * Sync keyed items with the source if it exists, else with null.
// 5. Walk the top list again but backwards, syncing the nodes.
// 6. Sync null with any items in the list of keys that are still
// mounted.
final ContainerRenderObjectMixin renderObject = this.renderObject; // TODO(ianh): Remove this once the analyzer is cleverer
assert(renderObject is ContainerRenderObjectMixin);
int childrenTop = 0;
int newChildrenBottom = newChildren.length-1;
int oldChildrenBottom = oldChildren.length-1;
// top of the lists
while ((childrenTop <= oldChildrenBottom) && (childrenTop <= newChildrenBottom)) {
Widget oldChild = oldChildren[childrenTop];
Widget newChild = newChildren[childrenTop];
if (oldChild != newChild && !oldChild.isFromOldGeneration) {
// even if the two nodes could in theory be synced, they can't really because
// it would seem that the old node has already been synced elsewhere in the tree.
break;
}
assert(oldChild.mounted);
if (!Widget._canSync(oldChild, newChild))
break;
childrenTop += 1;
}
Widget nextSibling;
// bottom of the lists
while ((childrenTop <= oldChildrenBottom) && (childrenTop <= newChildrenBottom)) {
Widget oldChild = oldChildren[oldChildrenBottom];
Widget newChild = newChildren[newChildrenBottom];
if (oldChild != newChild && !oldChild.isFromOldGeneration) {
// even if the two nodes could in theory be synced, they can't really because
// it would seem that the old node has already been synced elsewhere in the tree.
break;
}
assert(oldChild.mounted);
if (!Widget._canSync(oldChild, newChild))
break;
assert(oldChild == newChild || !newChild.mounted);
newChild = syncChild(newChild, oldChild, nextSibling);
assert(newChild.mounted);
newChildren[newChildrenBottom] = newChild;
nextSibling = newChild;
oldChildrenBottom -= 1;
newChildrenBottom -= 1;
}
// middle of the lists - old list
bool haveOldNodes = childrenTop <= oldChildrenBottom;
Map<Key, Widget> oldKeyedChildren;
if (haveOldNodes) {
oldKeyedChildren = new Map<Key, Widget>();
while (childrenTop <= oldChildrenBottom) {
Widget oldChild = oldChildren[oldChildrenBottom];
assert(oldChild.mounted);
if (oldChild.key != null) {
oldKeyedChildren[oldChild.key] = oldChild;
} else {
// Let's give up trying to sync this node with a new node. If this
// child is from the old generation, then we'll remove it from our
// child list. It it's not, then that means it's already been synced
// elsewhere and we should leave it alone.
if (oldChild.isFromOldGeneration)
syncChild(null, oldChild, null);
}
oldChildrenBottom -= 1;
}
}
// middle of the lists - new list
while (childrenTop <= newChildrenBottom) {
Widget oldChild;
Widget newChild = newChildren[newChildrenBottom];
if (haveOldNodes) {
Key key = newChild.key;
if (key != null) {
oldChild = oldKeyedChildren[newChild.key];
if (oldChild != null) {
if ((Widget._canSync(oldChild, newChild)) &&
(oldChild == newChild || oldChild.isFromOldGeneration)) {
// we found a match!
// remove it from oldKeyedChildren so we don't unsync it later
oldKeyedChildren.remove(key);
} else {
// Either it wasn't a match, or it's already been synced elsewhere in the tree.
// In either case, let's pretend we didn't see it for now.
oldChild = null;
}
}
}
}
assert(oldChild == null || Widget._canSync(oldChild, newChild));
assert(oldChild == newChild || !newChild.mounted);
newChild = syncChild(newChild, oldChild, nextSibling);
assert(newChild.mounted);
assert(oldChild == newChild || oldChild == null || !oldChild.mounted);
newChildren[newChildrenBottom] = newChild;
nextSibling = newChild;
newChildrenBottom -= 1;
}
assert(oldChildrenBottom == newChildrenBottom);
assert(childrenTop == newChildrenBottom+1);
// now sync the top of the list
while (childrenTop > 0) {
childrenTop -= 1;
Widget oldChild = oldChildren[childrenTop];
assert(oldChild.mounted);
Widget newChild = newChildren[childrenTop];
assert(Widget._canSync(oldChild, newChild));
newChild = syncChild(newChild, oldChild, nextSibling);
assert(newChild.mounted);
assert(oldChild == newChild || oldChild == null || !oldChild.mounted);
newChildren[childrenTop] = newChild;
nextSibling = newChild;
}
// clean up any of the remaining middle nodes from the old list
if (haveOldNodes && !oldKeyedChildren.isEmpty) {
for (Widget oldChild in oldKeyedChildren.values)
if (oldChild.isFromOldGeneration)
syncChild(null, oldChild, null);
}
assert(renderObject == this.renderObject); // TODO(ianh): Remove this once the analyzer is cleverer
}
void dependenciesChanged() {
// called by Inherited.sync()
syncRenderObject(this);
}
void remove() {
assert(renderObject != null);
_nodeMap.remove(renderObject);
super.remove();
}
void detachRenderObject() {
assert(renderObject != null);
assert(_ancestor != null);
assert(_ancestor.renderObject != null);
_ancestor.detachChildRenderObject(this);
}
}
abstract class LeafRenderObjectWrapper extends RenderObjectWrapper {
LeafRenderObjectWrapper({ Key key }) : super(key: key);
void insertChildRenderObject(RenderObjectWrapper child, dynamic slot) {
assert(false);
}
void detachChildRenderObject(RenderObjectWrapper child) {
assert(false);
}
}
abstract class OneChildRenderObjectWrapper extends RenderObjectWrapper {
OneChildRenderObjectWrapper({ Key key, Widget child })
: _child = child, super(key: key);
Widget _child;
Widget get child => _child;
void walkChildren(WidgetTreeWalker walker) {
if (child != null)
walker(child);
}
Widget get singleChild => _child;
bool _debugChildTaken = false;
void takeChild() {
assert(!_debugChildTaken);
assert(singleChild == child);
assert(child != null);
_child = null;
assert(() { _debugChildTaken = true; return true; });
}
void syncRenderObject(RenderObjectWrapper old) {
assert(!_debugChildTaken);
super.syncRenderObject(old);
Widget oldChild = (old as OneChildRenderObjectWrapper)?.child;
Widget newChild = child;
_child = syncChild(newChild, oldChild, null);
assert((newChild == null && child == null) || (newChild != null && child.parent == this));
assert(oldChild == null || child == oldChild || oldChild.parent == null);
}
void insertChildRenderObject(RenderObjectWrapper child, dynamic slot) {
final renderObject = this.renderObject; // TODO(ianh): Remove this once the analyzer is cleverer
assert(renderObject is RenderObjectWithChildMixin);
assert(slot == null);
renderObject.child = child.renderObject;
assert(renderObject == this.renderObject); // TODO(ianh): Remove this once the analyzer is cleverer
}
void detachChildRenderObject(RenderObjectWrapper child) {
final renderObject = this.renderObject; // TODO(ianh): Remove this once the analyzer is cleverer
assert(renderObject is RenderObjectWithChildMixin);
assert(renderObject.child == child.renderObject);
renderObject.child = null;
assert(renderObject == this.renderObject); // TODO(ianh): Remove this once the analyzer is cleverer
}
}
abstract class MultiChildRenderObjectWrapper extends RenderObjectWrapper {
// In MultiChildRenderObjectWrapper subclasses, slots are the Widget
// nodes whose RenderObjects are to be used as the "insert before"
// sibling in ContainerRenderObjectMixin.add() calls
MultiChildRenderObjectWrapper({ Key key, List<Widget> children })
: this.children = children == null ? const [] : children,
super(key: key) {
assert(!_debugHasDuplicateIds());
}
final List<Widget> children;
void walkChildren(WidgetTreeWalker walker) {
for (Widget child in children)
walker(child);
}
void insertChildRenderObject(RenderObjectWrapper child, Widget slot) {
final renderObject = this.renderObject; // TODO(ianh): Remove this once the analyzer is cleverer
RenderObject nextSibling = slot?.renderObject;
assert(nextSibling == null || nextSibling is RenderObject);
assert(renderObject is ContainerRenderObjectMixin);
renderObject.add(child.renderObject, before: nextSibling);
assert(renderObject == this.renderObject); // TODO(ianh): Remove this once the analyzer is cleverer
}
void detachChildRenderObject(RenderObjectWrapper child) {
final renderObject = this.renderObject; // TODO(ianh): Remove this once the analyzer is cleverer
assert(renderObject is ContainerRenderObjectMixin);
assert(child.renderObject.parent == renderObject);
renderObject.remove(child.renderObject);
assert(renderObject == this.renderObject); // TODO(ianh): Remove this once the analyzer is cleverer
}
bool _debugHasDuplicateIds() {
var idSet = new HashSet<Key>();
for (var child in children) {
assert(child != null);
if (child.key == null)
continue; // when these nodes are reordered, we just reassign the data
if (!idSet.add(child.key)) {
throw 'If multiple keyed nodes exist as children of another node, they must have unique keys. $this has multiple children with key "${child.key}".';
}
}
return false;
}
void syncRenderObject(MultiChildRenderObjectWrapper old) {
super.syncRenderObject(old);
List<Widget> oldChildren = old == null ? const <Widget>[] : old.children;
if (oldChildren == children) {
int index = children.length;
Widget nextSibling = null;
while (index > 0) {
index -= 1;
Widget child = children[index];
nextSibling = syncChild(child, child, nextSibling);
children[index] = nextSibling;
}
} else {
syncChildren(children, oldChildren);
}
}
}
class WidgetSkyBinding extends SkyBinding {
WidgetSkyBinding({ RenderView renderViewOverride: null })
: super(renderViewOverride: renderViewOverride);
static void initWidgetSkyBinding({ RenderView renderViewOverride: null }) {
if (SkyBinding.instance == null)
new WidgetSkyBinding(renderViewOverride: renderViewOverride);
assert(SkyBinding.instance is WidgetSkyBinding);
}
EventDisposition handleEvent(sky.Event event, BindingHitTestEntry entry) {
EventDisposition disposition = EventDisposition.ignored;
for (HitTestEntry entry in entry.result.path) {
if (entry.target is! RenderObject)
continue;
for (Widget target in RenderObjectWrapper.getWidgetsForRenderObject(entry.target)) {
if (target is Listener) {
EventDisposition targetDisposition = target._handleEvent(event);
if (targetDisposition == EventDisposition.consumed) {
return targetDisposition;
} else if (targetDisposition == EventDisposition.processed) {
disposition = EventDisposition.processed;
}
}
target = target._parent;
}
}
return combineEventDispositions(disposition, super.handleEvent(event, entry));
}
void beginFrame(double timeStamp) {
Component.flushBuild();
super.beginFrame(timeStamp);
}
}
abstract class App extends StatefulComponent {
App({ Key key }) : super(key: key);
void _handleEvent(sky.Event event) {
if (event.type == 'back')
onBack();
}
void didMount() {
super.didMount();
SkyBinding.instance.addEventListener(_handleEvent);
}
void didUnmount() {
super.didUnmount();
SkyBinding.instance.removeEventListener(_handleEvent);
}
void syncConstructorArguments(Component source) { }
// Override this to handle back button behavior in your app
// Call super.onBack() to finish the activity
void onBack() {
activity.finishCurrentActivity();
}
}
abstract class AbstractWidgetRoot extends StatefulComponent {
AbstractWidgetRoot() {
_mounted = true;
_scheduleComponentForRender(this);
}
void syncConstructorArguments(AbstractWidgetRoot source) {
assert(false);
// if we get here, it implies that we have a parent
}
void _buildIfDirty() {
assert(_dirty);
assert(_mounted);
assert(parent == null);
_sync(null, null);
}
}
class RenderViewWrapper extends OneChildRenderObjectWrapper {
RenderViewWrapper({ Key key, Widget child }) : super(key: key, child: child);
RenderView get renderObject => super.renderObject;
RenderView createNode() => SkyBinding.instance.renderView;
}
class AppContainer extends AbstractWidgetRoot {
AppContainer(this.app) {
assert(SkyBinding.instance is WidgetSkyBinding);
}
final App app;
Widget build() => new RenderViewWrapper(child: app);
}
AppContainer _container;
void runApp(App app, { RenderView renderViewOverride, bool enableProfilingLoop: false }) {
WidgetSkyBinding.initWidgetSkyBinding(renderViewOverride: renderViewOverride);
_container = new AppContainer(app);
if (enableProfilingLoop) {
new Timer.periodic(const Duration(milliseconds: 20), (_) {
app._scheduleBuild();
});
}
}
/// Prints a textual representation of the entire widget tree
void debugDumpApp() {
if (_container != null)
_container.toStringDeep().split('\n').forEach(print);
else
print('runApp() not yet called');
}
class RenderBoxToWidgetAdapter extends AbstractWidgetRoot {
RenderBoxToWidgetAdapter(
RenderObjectWithChildMixin<RenderBox> container,
this.builder
) : _container = container, super() {
assert(builder != null);
}
RenderObjectWithChildMixin<RenderBox> _container;
RenderObjectWithChildMixin<RenderBox> get container => _container;
void set container(RenderObjectWithChildMixin<RenderBox> value) {
if (_container != value) {
assert(value.child == null);
if (renderObject != null) {
assert(_container.child == renderObject);
_container.child = null;
}
_container = value;
if (renderObject != null) {
_container.child = renderObject;
assert(_container.child == renderObject);
}
}
}
final Builder builder;
void _buildIfDirty() {
super._buildIfDirty();
if (renderObject.parent == null) {
// we haven't attached it yet
assert(_container.child == null);
_container.child = renderObject;
}
assert(renderObject.parent == _container);
}
Widget build() => builder();
}
class ErrorWidget extends LeafRenderObjectWrapper {
RenderBox createNode() => new RenderErrorBox();
}
typedef void WidgetsExceptionHandler(String context, dynamic exception, StackTrace stack);
/// This callback is invoked whenever an exception is caught by the widget
/// system. The 'context' argument is a description of what was happening when
/// the exception occurred, and may include additional details such as
/// descriptions of the objects involved. The 'exception' argument contains the
/// object that was thrown, and the 'stack' argument contains the stack trace.
/// The callback is invoked after the information is printed to the console, and
/// could be used to print additional information, such as from
/// [debugDumpApp()].
WidgetsExceptionHandler debugWidgetsExceptionHandler;
void _debugReportException(String context, dynamic exception, StackTrace stack) {
print('------------------------------------------------------------------------');
'Exception caught while $context'.split('\n').forEach(print);
print('$exception');
print('Stack trace:');
'$stack'.split('\n').forEach(print);
print('Build stack:');
Component._debugComponentBuildTree.forEach((Component component) { print(' $component'); });
if (debugWidgetsExceptionHandler != null)
debugWidgetsExceptionHandler(context, exception, stack);
print('------------------------------------------------------------------------');
}