blob: 2b4ba213071f73d71aafe69320844eb762977a6f [file] [log] [blame]
// 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/base/hit_test.dart';
import 'package:sky/base/scheduler.dart' as scheduler;
import 'package:sky/mojo/activity.dart' as activity;
import 'package:sky/rendering/box.dart';
import 'package:sky/rendering/object.dart';
import 'package:sky/rendering/sky_binding.dart';
export 'package:sky/base/hit_test.dart' show EventDisposition;
export 'package:sky/rendering/box.dart' show BoxConstraints, BoxDecoration, Border, BorderSide, EdgeDims;
export 'package:sky/rendering/flex.dart' show FlexDirection;
export 'package:sky/rendering/object.dart' show Point, Offset, Size, Rect, Color, Paint, Path;
final bool _shouldLogRenderDuration = false;
typedef Widget Builder();
typedef void WidgetTreeWalker(Widget);
abstract class Key {
Key.constructor(); // so that subclasses can call us, since the Key() factory constructor shadows the implicit constructor
factory Key(String value) => new StringKey(value);
factory Key.stringify(Object value) => new StringKey(value.toString());
factory Key.fromObjectIdentity(Object value) => new ObjectKey(value);
}
class StringKey extends Key {
StringKey(this.value) : super.constructor();
final String value;
String toString() => '[\'${value}\']';
bool operator==(other) => other is StringKey && other.value == value;
int get hashCode => value.hashCode;
}
class ObjectKey extends Key {
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 GlobalKeyRemovalListener(GlobalKey key);
abstract class GlobalKey extends Key {
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);
factory GlobalKey.fromObjectIdentity(Object value) => new GlobalObjectKey(value);
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<GlobalKeyRemovalListener>> _removalListeners = new Map<GlobalKey, Set<GlobalKeyRemovalListener>>();
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);
}
}
static bool _notifyingListeners = false;
static void registerRemovalListener(GlobalKey key, GlobalKeyRemovalListener listener) {
assert(!_notifyingListeners);
assert(key != null);
if (!_removalListeners.containsKey(key))
_removalListeners[key] = new Set<GlobalKeyRemovalListener>();
bool added = _removalListeners[key].add(listener);
assert(added);
}
static void unregisterRemovalListener(GlobalKey key, GlobalKeyRemovalListener listener) {
assert(!_notifyingListeners);
assert(key != null);
assert(_removalListeners.containsKey(key));
bool removed = _removalListeners[key].remove(listener);
if (_removalListeners[key].isEmpty)
_removalListeners.remove(key);
assert(removed);
}
static void _notifyListeners() {
assert(!_inRenderDirtyComponents);
assert(!Widget._notifyingMountStatus);
assert(_debugDuplicates.isEmpty);
_notifyingListeners = true;
for (GlobalKey key in _removedKeys) {
if (!_registry.containsKey(key) && _removalListeners.containsKey(key)) {
for (GlobalKeyRemovalListener listener in _removalListeners[key])
listener(key);
_removalListeners.remove(key);
}
}
_removedKeys.clear();
_notifyingListeners = false;
}
}
class LabeledGlobalKey extends GlobalKey {
// the label is purely for documentary purposes and does not affect the key
LabeledGlobalKey(this._label) : super.constructor();
final String _label;
String toString() => '[GlobalKey ${_label != null ? _label : hashCode}]';
}
class GlobalObjectKey extends GlobalKey {
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 || _inRenderDirtyComponents || _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;
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) { }
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 _root;
/// The underlying [RenderObject] associated with this [Widget].
RenderObject get root => _root;
// 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 syncFields().
bool retainStatefulNodeIfPossible(Widget newNode) => false;
void _sync(Widget old, dynamic slot);
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.
Widget findAncestorRenderObjectWrapper() {
var ancestor = _parent;
while (ancestor != null && ancestor is! RenderObjectWrapper)
ancestor = ancestor._parent;
return ancestor;
}
void remove() {
_root = null;
setParent(null);
}
void removeChild(Widget node) {
// Call this when we no longer have a child equivalent to node.
// For example, when our child has changed type, or has been set to null.
// Do not call this when our child has been replaced by an equivalent but
// newer instance that will sync() with the old one, since in that case
// the subtree starting from the old node, as well as the render tree that
// belonged to the old node, continue to live on in the replacement node.
node.remove();
assert(node.parent == null);
}
void detachRoot();
// Returns the child which should be retained as the child of this node.
Widget syncChild(Widget newNode, Widget oldNode, dynamic slot) {
if (newNode == oldNode) {
assert(newNode == null || newNode.mounted);
assert(newNode is! RenderObjectWrapper ||
(newNode is RenderObjectWrapper && newNode._ancestor != null)); // TODO(ianh): Simplify this once the analyzer is cleverer
if (newNode != null)
newNode.setParent(this);
return newNode; // Nothing to do. Subtrees must be identical.
}
if (newNode == null) {
// the child in this slot has gone away
assert(oldNode.mounted);
oldNode.detachRoot();
removeChild(oldNode);
assert(!oldNode.mounted);
return null;
}
if (oldNode != null) {
if (oldNode.runtimeType == newNode.runtimeType && oldNode.key == newNode.key) {
if (oldNode.retainStatefulNodeIfPossible(newNode)) {
assert(oldNode.mounted);
assert(!newNode.mounted);
oldNode.setParent(this);
oldNode._sync(newNode, slot);
assert(oldNode.root is RenderObject);
return oldNode;
} else {
oldNode.setParent(null);
}
} else {
assert(oldNode.mounted);
oldNode.detachRoot();
removeChild(oldNode);
oldNode = null;
}
}
assert(oldNode == null || (oldNode.mounted == false && oldNode.parent == null));
assert(!newNode.mounted);
newNode.setParent(this);
newNode._sync(oldNode, slot);
assert(newNode.root is RenderObject);
return newNode;
}
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.toStringName()}]] ';
}
String toString([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.toString(nextPrefix, _adjustPrefixWithParentCheck(child, nextStartPrefix));
nextStartPrefix = prefix + ' \'-';
nextPrefix = prefix + ' ';
childrenString += lastChild.toString(nextPrefix, _adjustPrefixWithParentCheck(lastChild, nextStartPrefix));
}
return '$startPrefix${toStringName()}\n$childrenString';
}
String toStringName() {
if (key == null)
return '$runtimeType(unkeyed; hashCode=$hashCode)';
return '$runtimeType($key; hashCode=$hashCode)';
}
}
// 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(Widget child, { Key key })
: this.child = child, super(key: key);
// TODO(jackson): Remove this workaround for limitation of Dart mixins
TagNode._withKey(Widget child, Key key)
: this.child = child, super._withKey(key);
Widget child;
void walkChildren(WidgetTreeWalker walker) {
if (child != null)
walker(child);
}
void _sync(Widget old, dynamic slot) {
Widget oldChild = old == null ? null : (old as TagNode).child;
child = syncChild(child, oldChild, slot);
assert(child.parent == this);
assert(child.root != null);
_root = child.root;
assert(_root == root); // in case a subclass reintroduces it
}
void updateSlot(dynamic newSlot) {
child.updateSlot(newSlot);
}
void remove() {
if (child != null)
removeChild(child);
super.remove();
}
void detachRoot() {
if (child != null)
child.detachRoot();
}
}
class ParentDataNode extends TagNode {
ParentDataNode(Widget child, this.parentData, { Key key })
: super(child, key: key);
final ParentData parentData;
}
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 is Component &&
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,
EventListener onWheel,
GestureEventListener onGestureFlingCancel,
GestureEventListener onGestureFlingStart,
GestureEventListener onGestureScrollStart,
GestureEventListener onGestureScrollUpdate,
GestureEventListener onGestureTap,
GestureEventListener onGestureTapDown,
PointerEventListener onPointerCancel,
PointerEventListener onPointerDown,
PointerEventListener onPointerMove,
PointerEventListener onPointerUp,
Map<String, EventListener> custom
}) : listeners = _createListeners(
onWheel: onWheel,
onGestureFlingCancel: onGestureFlingCancel,
onGestureFlingStart: onGestureFlingStart,
onGestureScrollUpdate: onGestureScrollUpdate,
onGestureScrollStart: onGestureScrollStart,
onGestureTap: onGestureTap,
onGestureTapDown: onGestureTapDown,
onPointerCancel: onPointerCancel,
onPointerDown: onPointerDown,
onPointerMove: onPointerMove,
onPointerUp: onPointerUp,
custom: custom
),
super(child, key: key);
final Map<String, EventListener> listeners;
static Map<String, EventListener> _createListeners({
EventListener onWheel,
GestureEventListener onGestureFlingCancel,
GestureEventListener onGestureFlingStart,
GestureEventListener onGestureScrollStart,
GestureEventListener onGestureScrollUpdate,
GestureEventListener onGestureTap,
GestureEventListener onGestureTapDown,
PointerEventListener onPointerCancel,
PointerEventListener onPointerDown,
PointerEventListener onPointerMove,
PointerEventListener onPointerUp,
Map<String, EventListener> custom
}) {
var listeners = custom != null ?
new HashMap<String, EventListener>.from(custom) :
new HashMap<String, EventListener>();
if (onWheel != null)
listeners['wheel'] = onWheel;
if (onGestureFlingCancel != null)
listeners['gestureflingcancel'] = onGestureFlingCancel;
if (onGestureFlingStart != null)
listeners['gestureflingstart'] = onGestureFlingStart;
if (onGestureScrollStart != null)
listeners['gesturescrollstart'] = onGestureScrollStart;
if (onGestureScrollUpdate != null)
listeners['gesturescrollupdate'] = onGestureScrollUpdate;
if (onGestureTap != null)
listeners['gesturetap'] = onGestureTap;
if (onGestureTapDown != null)
listeners['gesturetapdown'] = onGestureTapDown;
if (onPointerCancel != null)
listeners['pointercancel'] = onPointerCancel;
if (onPointerDown != null)
listeners['pointerdown'] = onPointerDown;
if (onPointerMove != null)
listeners['pointermove'] = onPointerMove;
if (onPointerUp != null)
listeners['pointerup'] = onPointerUp;
return listeners;
}
EventDisposition _handleEvent(sky.Event e) {
EventListener listener = listeners[e.type];
if (listener != null) {
return listener(e);
}
return EventDisposition.ignored;
}
}
abstract class Component extends Widget {
Component({ Key key })
: _order = _currentOrder + 1,
super._withKey(key);
bool _isBuilding = false;
bool _dirty = true;
Widget _built;
dynamic _slot; // cached slot from the last time we were synced
void updateSlot(dynamic newSlot) {
_slot = newSlot;
if (_built != null)
_built.updateSlot(newSlot);
}
void walkChildren(WidgetTreeWalker walker) {
if (_built != null)
walker(_built);
}
void remove() {
assert(_built != null);
assert(root != null);
removeChild(_built);
_built = null;
super.remove();
}
void detachRoot() {
assert(_built != null);
assert(root != null);
_built.detachRoot();
}
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() {
// called by Inherited.sync()
_scheduleBuild();
}
// 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(_built == null && old == null)
// 2) Re-building (because a dirty flag got set):
// assert(_built != null && old == null)
// 3) Syncing against an old version
// assert(_built == null && old != null)
void _sync(Component old, dynamic slot) {
assert(_built == null || old == null);
updateSlot(slot);
var oldBuilt;
if (old == null) {
oldBuilt = _built;
} else {
assert(_built == null);
oldBuilt = old._built;
}
_isBuilding = true;
int lastOrder = _currentOrder;
_currentOrder = _order;
_built = build();
_currentOrder = lastOrder;
assert(_built != null);
_built = syncChild(_built, oldBuilt, slot);
assert(_built != null);
assert(_built.parent == this);
_isBuilding = false;
_dirty = false;
_root = _built.root;
assert(_root == root); // in case a subclass reintroduces it
assert(root != null);
}
void _buildIfDirty() {
if (!_dirty || !_mounted)
return;
assert(root != null);
_sync(null, _slot);
}
void _scheduleBuild() {
assert(!_isBuilding);
if (_dirty || !_mounted)
return;
_dirty = true;
_scheduleComponentForRender(this);
}
static void flushBuild() {
if (!_dirtyComponents.isEmpty)
_buildDirtyComponents();
}
Widget build();
}
abstract class StatefulComponent extends Component {
StatefulComponent({ Key key }) : super(key: key);
bool _disqualifiedFromEverAppearingAgain = false;
bool _isStateInitialized = false;
void didMount() {
assert(!_disqualifiedFromEverAppearingAgain);
super.didMount();
}
void _buildIfDirty() {
assert(!_disqualifiedFromEverAppearingAgain);
super._buildIfDirty();
}
bool retainStatefulNodeIfPossible(StatefulComponent newNode) {
assert(!_disqualifiedFromEverAppearingAgain);
assert(newNode != null);
assert(runtimeType == newNode.runtimeType);
assert(key == newNode.key);
assert(_built != null);
newNode._disqualifiedFromEverAppearingAgain = true;
newNode._built = _built;
_built = 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(Widget old, dynamic slot) {
assert(!_disqualifiedFromEverAppearingAgain);
// TODO(ianh): _sync should only be called once when old == null
if (old == null && !_isStateInitialized) {
initState();
_isStateInitialized = true;
}
if (old != null)
syncFields(old);
super._sync(old, slot);
}
// 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.syncFields(source)
// unless you are extending StatefulComponent directly.
void syncFields(Component source);
Widget syncChild(Widget node, Widget oldNode, dynamic slot) {
assert(!_disqualifiedFromEverAppearingAgain);
return super.syncChild(node, oldNode, slot);
}
void setState(void fn()) {
assert(!_disqualifiedFromEverAppearingAgain);
fn();
_scheduleBuild();
}
}
Set<Component> _dirtyComponents = new Set<Component>();
bool _buildScheduled = false;
bool _inRenderDirtyComponents = 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;
});
Widget._notifyMountStatusChanged();
}
List<int> _debugFrameTimes = <int>[];
void _absorbDirtyComponents(List<Component> list) {
list.addAll(_dirtyComponents);
_dirtyComponents.clear();
list.sort((Component a, Component b) => a._order - b._order);
}
void _buildDirtyComponents() {
assert(!_dirtyComponents.isEmpty);
Stopwatch sw;
if (_shouldLogRenderDuration)
sw = new Stopwatch()..start();
_inRenderDirtyComponents = 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;
_inRenderDirtyComponents = false;
sky.tracing.end('Component.flushBuild');
}
Widget._notifyMountStatusChanged();
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();
}
}
}
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, with one exception: A Widget which is a
// Component which lives within an MultiChildRenderObjectWrapper's
// children list, may be 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];
RenderObjectWrapper _ancestor;
void insertChildRoot(RenderObjectWrapper child, dynamic slot);
void detachChildRoot(RenderObjectWrapper child);
void retainStatefulRenderObjectWrapper(RenderObjectWrapper newNode) {
newNode._root = _root;
newNode._ancestor = _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);
if (old == null) {
_root = createNode();
assert(_root != null);
_ancestor = findAncestorRenderObjectWrapper();
if (_ancestor is RenderObjectWrapper)
_ancestor.insertChildRoot(this, slot);
} else {
assert(old is RenderObjectWrapper);
_root = old.root;
_ancestor = old._ancestor;
assert(_root != null);
}
assert(_root == root); // in case a subclass reintroduces it
assert(root != null);
assert(mounted);
_nodeMap[root] = this;
syncRenderObject(old);
}
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.
}
void syncRenderObject(RenderObjectWrapper old) {
ParentData parentData = null;
Widget ancestor = parent;
while (ancestor != null && ancestor is! RenderObjectWrapper) {
if (ancestor is ParentDataNode && ancestor.parentData != null) {
if (parentData != null)
parentData.merge(ancestor.parentData); // this will throw if the types aren't the same
else
parentData = ancestor.parentData;
}
ancestor = ancestor.parent;
}
if (parentData != null) {
assert(root.parentData != null);
root.parentData.merge(parentData); // this will throw if the types aren't appropriate
if (ancestor != null && ancestor.root != null)
ancestor.root.markNeedsLayout();
}
}
void remove() {
assert(root != null);
_nodeMap.remove(root);
super.remove();
}
void detachRoot() {
assert(_ancestor != null);
assert(root != null);
_ancestor.detachChildRoot(this);
}
}
abstract class LeafRenderObjectWrapper extends RenderObjectWrapper {
LeafRenderObjectWrapper({ Key key }) : super(key: key);
void insertChildRoot(RenderObjectWrapper child, dynamic slot) {
assert(false);
}
void detachChildRoot(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);
}
void syncRenderObject(RenderObjectWrapper old) {
super.syncRenderObject(old);
Widget oldChild = old == null ? null : (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 insertChildRoot(RenderObjectWrapper child, dynamic slot) {
final root = this.root; // TODO(ianh): Remove this once the analyzer is cleverer
assert(root is RenderObjectWithChildMixin);
assert(slot == null);
root.child = child.root;
assert(root == this.root); // TODO(ianh): Remove this once the analyzer is cleverer
}
void detachChildRoot(RenderObjectWrapper child) {
final root = this.root; // TODO(ianh): Remove this once the analyzer is cleverer
assert(root is RenderObjectWithChildMixin);
assert(root.child == child.root);
root.child = null;
assert(root == this.root); // TODO(ianh): Remove this once the analyzer is cleverer
}
void remove() {
if (child != null)
removeChild(child);
super.remove();
}
}
abstract class MultiChildRenderObjectWrapper extends RenderObjectWrapper {
// In MultiChildRenderObjectWrapper subclasses, slots are RenderObject nodes
// to use 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 insertChildRoot(RenderObjectWrapper child, dynamic slot) {
final root = this.root; // TODO(ianh): Remove this once the analyzer is cleverer
assert(slot == null || slot is RenderObject);
assert(root is ContainerRenderObjectMixin);
root.add(child.root, before: slot);
assert(root == this.root); // TODO(ianh): Remove this once the analyzer is cleverer
}
void detachChildRoot(RenderObjectWrapper child) {
final root = this.root; // TODO(ianh): Remove this once the analyzer is cleverer
assert(root is ContainerRenderObjectMixin);
assert(child.root.parent == root);
root.remove(child.root);
assert(root == this.root); // TODO(ianh): Remove this once the analyzer is cleverer
}
void remove() {
assert(children != null);
for (var child in children) {
assert(child != null);
removeChild(child);
}
super.remove();
}
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 duplicate child key "${child.key}".''';
}
}
return false;
}
void syncRenderObject(MultiChildRenderObjectWrapper old) {
super.syncRenderObject(old);
final root = this.root; // TODO(ianh): Remove this once the analyzer is cleverer
if (root is! ContainerRenderObjectMixin)
return;
var startIndex = 0;
var endIndex = children.length;
var oldChildren = old == null ? [] : old.children;
var oldStartIndex = 0;
var oldEndIndex = oldChildren.length;
RenderObject nextSibling = null;
Widget currentNode = null;
Widget oldNode = null;
void sync(int atIndex) {
children[atIndex] = syncChild(currentNode, oldNode, nextSibling);
assert(children[atIndex] != null);
assert(children[atIndex].parent == this);
if (atIndex > 0)
children[atIndex-1].updateSlot(children[atIndex].root);
}
// Scan backwards from end of list while nodes can be directly synced
// without reordering.
while (endIndex > startIndex && oldEndIndex > oldStartIndex) {
currentNode = children[endIndex - 1];
oldNode = oldChildren[oldEndIndex - 1];
if (currentNode.runtimeType != oldNode.runtimeType || currentNode.key != oldNode.key) {
break;
}
endIndex--;
oldEndIndex--;
sync(endIndex);
nextSibling = children[endIndex].root;
}
HashMap<Key, Widget> oldNodeIdMap = null;
bool oldNodeReordered(Key key) {
return oldNodeIdMap != null &&
oldNodeIdMap.containsKey(key) &&
oldNodeIdMap[key] == null;
}
void advanceOldStartIndex() {
oldStartIndex++;
while (oldStartIndex < oldEndIndex &&
oldNodeReordered(oldChildren[oldStartIndex].key)) {
oldStartIndex++;
}
}
void ensureOldIdMap() {
if (oldNodeIdMap != null)
return;
oldNodeIdMap = new HashMap<Key, Widget>();
for (int i = oldStartIndex; i < oldEndIndex; i++) {
var node = oldChildren[i];
if (node.key != null)
oldNodeIdMap.putIfAbsent(node.key, () => node);
}
}
bool searchForOldNode() {
if (currentNode.key == null)
return false; // never re-order these nodes
ensureOldIdMap();
oldNode = oldNodeIdMap[currentNode.key];
if (oldNode == null)
return false;
oldNodeIdMap[currentNode.key] = null; // mark it reordered
assert(root is ContainerRenderObjectMixin);
assert(old.root is ContainerRenderObjectMixin);
assert(oldNode.root != null);
if (old.root == root) {
root.move(oldNode.root, before: nextSibling);
} else {
(old.root as ContainerRenderObjectMixin).remove(oldNode.root); // TODO(ianh): Remove cast once the analyzer is cleverer
root.add(oldNode.root, before: nextSibling);
}
return true;
}
// Scan forwards, this time we may re-order;
nextSibling = root.firstChild;
while (startIndex < endIndex && oldStartIndex < oldEndIndex) {
currentNode = children[startIndex];
oldNode = oldChildren[oldStartIndex];
if (currentNode.runtimeType == oldNode.runtimeType && currentNode.key == oldNode.key) {
nextSibling = root.childAfter(nextSibling);
sync(startIndex);
startIndex++;
advanceOldStartIndex();
continue;
}
oldNode = null;
searchForOldNode();
sync(startIndex);
startIndex++;
}
// New insertions
oldNode = null;
while (startIndex < endIndex) {
currentNode = children[startIndex];
sync(startIndex);
startIndex++;
}
// Removals
currentNode = null;
while (oldStartIndex < oldEndIndex) {
oldNode = oldChildren[oldStartIndex];
syncChild(null, oldNode, null);
assert(oldNode.parent == null);
advanceOldStartIndex();
}
assert(root == this.root); // TODO(ianh): Remove this once the analyzer is cleverer
}
}
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 dispatchEvent(sky.Event event, HitTestResult result) {
assert(SkyBinding.instance == this);
EventDisposition disposition = super.dispatchEvent(event, result);
if (disposition == EventDisposition.consumed)
return EventDisposition.consumed;
for (HitTestEntry entry in result.path.reversed) {
Widget target = RenderObjectWrapper._getMounted(entry.target);
if (target == null)
continue;
RenderObject targetRoot = target.root;
while (target != null && target.root == targetRoot) {
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 disposition;
}
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 syncFields(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 syncFields(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 root => super.root;
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();
});
}
}
void debugDumpApp() {
if (_container != null)
_container.toString().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 (root != null) {
assert(_container.child == root);
_container.child = null;
}
_container = value;
if (root != null) {
_container.child = root;
assert(_container.child == root);
}
}
}
final Builder builder;
void _buildIfDirty() {
super._buildIfDirty();
if (root.parent == null) {
// we haven't attached it yet
assert(_container.child == null);
_container.child = root;
}
assert(root.parent == _container);
}
Widget build() => builder();
}