blob: 7d450c98b11bdc330f7d7a1c48fb41004364fb4f [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:mirrors';
import 'dart:sky' as sky;
import '../app/view.dart';
import '../rendering/box.dart';
import '../rendering/object.dart';
export '../rendering/box.dart' show BoxConstraints, BoxDecoration, Border, BorderSide, EdgeDims;
export '../rendering/flex.dart' show FlexDirection;
export '../rendering/object.dart' show Point, Size, Rect, Color, Paint, Path;
final bool _shouldLogRenderDuration = false;
// All Effen nodes derive from Widget. All nodes have a _parent, a _key and
// can be sync'd.
abstract class Widget {
Widget({ String key }) {
_key = key != null ? key : runtimeType.toString();
assert(this is AbstractWidgetRoot || _inRenderDirtyComponents); // you should not build the UI tree ahead of time, build it only during build()
}
String _key;
String get key => _key;
Widget _parent;
Widget get parent => _parent;
bool _mounted = false;
bool _wasMounted = false;
bool get mounted => _mounted;
static bool _notifyingMountStatus = false;
static Set<Widget> _mountedChanged = new HashSet<Widget>();
void setParent(Widget newParent) {
assert(!_notifyingMountStatus);
_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);
}
}
}
static void _notifyMountStatusChanged() {
try {
_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;
}
}
void didMount() { }
void didUnmount() { }
RenderObject _root;
RenderObject get root => _root;
// Subclasses which implements Nodes that become stateful may return true
// if the |old| node has become stateful and should be retained.
// This is called immediately before _sync().
// Component._retainStatefulNodeIfPossible() calls syncFields().
bool _retainStatefulNodeIfPossible(Widget old) => false;
bool get interchangeable => false; // if true, then keys can be duplicated
void _sync(Widget old, dynamic slot);
// 'slot' is the identifier that the parent RenderObjectWrapper uses to know
// where to put this descendant
void remove() {
_root = null;
setParent(null);
}
Widget findAncestor(Type targetType) {
var ancestor = _parent;
while (ancestor != null && !reflectClass(ancestor.runtimeType).isSubtypeOf(reflectClass(targetType)))
ancestor = ancestor._parent;
return ancestor;
}
void removeChild(Widget node) {
node.remove();
}
// Returns the child which should be retained as the child of this node.
Widget syncChild(Widget node, Widget oldNode, dynamic slot) {
assert(oldNode is! Component || !oldNode._disqualifiedFromEverAppearingAgain);
if (node == oldNode) {
assert(node == null || node.mounted);
return node; // Nothing to do. Subtrees must be identical.
}
if (node == null) {
// the child in this slot has gone away
assert(oldNode.mounted);
removeChild(oldNode);
assert(!oldNode.mounted);
return null;
}
if (oldNode != null &&
oldNode.runtimeType == node.runtimeType &&
oldNode.key == node.key &&
node._retainStatefulNodeIfPossible(oldNode)) {
assert(oldNode.mounted);
assert(!node.mounted);
oldNode._sync(node, slot);
assert(oldNode.root is RenderObject);
return oldNode;
}
if (oldNode != null &&
(oldNode.runtimeType != node.runtimeType || oldNode.key != node.key)) {
assert(oldNode.mounted);
removeChild(oldNode);
oldNode = null;
}
assert(!node.mounted);
node.setParent(this);
node._sync(oldNode, slot);
assert(node.root is RenderObject);
return node;
}
}
// 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 content, { String key })
: this.content = content, super(key: key);
Widget content;
void _sync(Widget old, dynamic slot) {
Widget oldContent = old == null ? null : (old as TagNode).content;
content = syncChild(content, oldContent, slot);
assert(content.root != null);
_root = content.root;
assert(_root == root); // in case a subclass reintroduces it
}
void remove() {
if (content != null)
removeChild(content);
super.remove();
}
}
class ParentDataNode extends TagNode {
ParentDataNode(Widget content, this.parentData, { String key })
: super(content, key: key);
final ParentData parentData;
}
typedef void GestureEventListener(sky.GestureEvent e);
typedef void PointerEventListener(sky.PointerEvent e);
typedef void EventListener(sky.Event e);
class Listener extends TagNode {
Listener({
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, sky.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);
final Map<String, sky.EventListener> listeners;
static Map<String, sky.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, sky.EventListener> custom
}) {
var listeners = custom != null ?
new HashMap<String, sky.EventListener>.from(custom) :
new HashMap<String, sky.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;
}
void _handleEvent(sky.Event e) {
sky.EventListener listener = listeners[e.type];
if (listener != null) {
listener(e);
}
}
}
abstract class Component extends Widget {
Component({ String key, bool stateful })
: _stateful = stateful != null ? stateful : false,
_order = _currentOrder + 1,
super(key: key);
static Component _currentlyBuilding;
bool get _isBuilding => _currentlyBuilding == this;
bool _stateful;
bool _dirty = true;
bool _disqualifiedFromEverAppearingAgain = false;
Widget _built;
dynamic _slot; // cached slot from the last time we were synced
void didMount() {
assert(!_disqualifiedFromEverAppearingAgain);
super.didMount();
}
void remove() {
assert(_built != null);
assert(root != null);
removeChild(_built);
_built = null;
super.remove();
}
bool _retainStatefulNodeIfPossible(Widget old) {
assert(!_disqualifiedFromEverAppearingAgain);
Component oldComponent = old as Component;
if (oldComponent == null || !oldComponent._stateful)
return false;
assert(runtimeType == oldComponent.runtimeType);
assert(key == oldComponent.key);
// Make |this|, the newly-created object, into the "old" Component, and kill it
_stateful = false;
_built = oldComponent._built;
assert(_built != null);
_disqualifiedFromEverAppearingAgain = true;
// Make |oldComponent| the "new" component
oldComponent._built = null;
oldComponent._dirty = true;
oldComponent.syncFields(this);
return true;
}
// This is called by _retainStatefulNodeIfPossible(), during
// syncChild(), just before _sync() is called.
// This must be implemented on any subclass that can become stateful
// (but don't call super.syncFields() if you inherit directly from
// Component, since that'll fire an assert).
// If you don't ever become stateful, then don't override this.
void syncFields(Component source) {
assert(false);
}
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(Widget old, dynamic slot) {
assert(_built == null || old == null);
assert(!_disqualifiedFromEverAppearingAgain);
Component oldComponent = old as Component;
_slot = slot;
var oldBuilt;
if (oldComponent == null) {
oldBuilt = _built;
} else {
assert(_built == null);
oldBuilt = oldComponent._built;
}
int lastOrder = _currentOrder;
_currentOrder = _order;
_currentlyBuilding = this;
_built = build();
assert(_built != null);
_currentlyBuilding = null;
_currentOrder = lastOrder;
_built = syncChild(_built, oldBuilt, slot);
assert(_built != null);
_dirty = false;
_root = _built.root;
assert(_root == root); // in case a subclass reintroduces it
assert(root != null);
}
void _buildIfDirty() {
assert(!_disqualifiedFromEverAppearingAgain);
if (!_dirty || !_mounted)
return;
assert(root != null);
_sync(null, _slot);
}
void scheduleBuild() {
setState(() {});
}
void setState(Function fn()) {
assert(!_disqualifiedFromEverAppearingAgain);
_stateful = true;
fn();
if (_isBuilding || _dirty || !_mounted)
return;
_dirty = true;
_scheduleComponentForRender(this);
}
Widget build();
}
Set<Component> _dirtyComponents = new Set<Component>();
bool _buildScheduled = false;
bool _inRenderDirtyComponents = false;
void _buildDirtyComponents() {
//_tracing.begin('fn::_buildDirtyComponents');
Stopwatch sw;
if (_shouldLogRenderDuration)
sw = new Stopwatch()..start();
try {
_inRenderDirtyComponents = true;
List<Component> sortedDirtyComponents = _dirtyComponents.toList();
sortedDirtyComponents.sort((Component a, Component b) => a._order - b._order);
for (var comp in sortedDirtyComponents) {
comp._buildIfDirty();
}
_dirtyComponents.clear();
_buildScheduled = false;
} finally {
_inRenderDirtyComponents = false;
}
Widget._notifyMountStatusChanged();
if (_shouldLogRenderDuration) {
sw.stop();
print('Render took ${sw.elapsedMicroseconds} microseconds');
}
//_tracing.end('fn::_buildDirtyComponents');
}
void _scheduleComponentForRender(Component c) {
assert(!_inRenderDirtyComponents);
_dirtyComponents.add(c);
if (!_buildScheduled) {
_buildScheduled = true;
new Future.microtask(_buildDirtyComponents);
}
}
// 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({ String key }) : super(key: key);
RenderObject createNode();
void insert(RenderObjectWrapper child, dynamic slot);
static final Map<RenderObject, RenderObjectWrapper> _nodeMap =
new HashMap<RenderObject, RenderObjectWrapper>();
static RenderObjectWrapper _getMounted(RenderObject node) => _nodeMap[node];
void _sync(Widget old, dynamic slot) {
assert(parent != null);
if (old == null) {
_root = createNode();
var ancestor = findAncestor(RenderObjectWrapper);
if (ancestor is RenderObjectWrapper)
ancestor.insert(this, slot);
} else {
_root = old.root;
}
assert(_root == root); // in case a subclass reintroduces it
assert(root != null);
assert(mounted);
_nodeMap[root] = this;
syncRenderObject(old);
}
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 (parent.root != null)
parent.root.markNeedsLayout();
}
}
void remove() {
assert(root != null);
_nodeMap.remove(root);
super.remove();
}
}
abstract class OneChildRenderObjectWrapper extends RenderObjectWrapper {
OneChildRenderObjectWrapper({ String key, Widget child })
: _child = child, super(key: key);
Widget _child;
Widget get child => _child;
void syncRenderObject(RenderObjectWrapper old) {
super.syncRenderObject(old);
Widget oldChild = old == null ? null : (old as OneChildRenderObjectWrapper).child;
_child = syncChild(child, oldChild, null);
}
void insert(RenderObjectWrapper child, dynamic slot) {
final root = this.root; // TODO(ianh): Remove this once the analyzer is cleverer
assert(slot == null);
assert(root is RenderObjectWithChildMixin);
root.child = child.root;
assert(root == this.root); // TODO(ianh): Remove this once the analyzer is cleverer
}
void removeChild(Widget node) {
final root = this.root; // TODO(ianh): Remove this once the analyzer is cleverer
assert(root is RenderObjectWithChildMixin);
root.child = null;
super.removeChild(node);
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({ String key, List<Widget> children })
: this.children = children == null ? const [] : children,
super(key: key) {
assert(!_debugHasDuplicateIds());
}
final List<Widget> children;
void insert(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 removeChild(Widget node) {
final root = this.root; // TODO(ianh): Remove this once the analyzer is cleverer
assert(root is ContainerRenderObjectMixin);
assert(node.root.parent == root);
root.remove(node.root);
super.removeChild(node);
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<String>();
for (var child in children) {
assert(child != null);
if (child.interchangeable)
continue; // when these nodes are reordered, we just reassign the data
if (!idSet.add(child.key)) {
throw '''If multiple non-interchangeable nodes exist as children of another node, they must have unique keys. Duplicate: "${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);
}
// 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);
}
HashMap<String, Widget> oldNodeIdMap = null;
bool oldNodeReordered(String 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<String, Widget>();
for (int i = oldStartIndex; i < oldEndIndex; i++) {
var node = oldChildren[i];
if (!node.interchangeable)
oldNodeIdMap.putIfAbsent(node.key, () => node);
}
}
bool searchForOldNode() {
if (currentNode.interchangeable)
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);
(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];
removeChild(oldNode);
advanceOldStartIndex();
}
assert(root == this.root); // TODO(ianh): Remove this once the analyzer is cleverer
}
}
class WidgetAppView extends AppView {
WidgetAppView({ RenderView renderViewOverride: null })
: super(renderViewOverride: renderViewOverride) {
assert(_appView == null);
}
static WidgetAppView _appView;
static AppView get appView => _appView;
static void initWidgetAppView({ RenderView renderViewOverride: null }) {
if (_appView == null)
_appView = new WidgetAppView(renderViewOverride: renderViewOverride);
}
void dispatchEvent(sky.Event event, HitTestResult result) {
assert(_appView == this);
super.dispatchEvent(event, result);
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)
target._handleEvent(event);
target = target._parent;
}
}
}
}
abstract class AbstractWidgetRoot extends Component {
AbstractWidgetRoot({ RenderView renderViewOverride }) : super(stateful: true) {
WidgetAppView.initWidgetAppView(renderViewOverride: renderViewOverride);
_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);
}
}
abstract class App extends AbstractWidgetRoot {
App({ RenderView renderViewOverride }) : super(renderViewOverride: renderViewOverride);
void _buildIfDirty() {
super._buildIfDirty();
if (root.parent == null) {
// we haven't attached it yet
WidgetAppView._appView.root = root;
}
assert(root.parent is RenderView);
}
}
typedef Widget Builder();
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();
}