blob: 65380a5d2c4e07d5d7064102125aee7e71d7c9c2 [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:collection';
import 'package:sky/rendering/block.dart';
import 'package:sky/rendering/box.dart';
import 'package:sky/rendering/object.dart';
import 'package:sky/widgets/widget.dart';
// return null if index is greater than index of last entry
typedef Widget IndexedBuilder(int index);
typedef void LayoutChangedCallback(
int firstVisibleChildIndex,
int visibleChildCount,
UnmodifiableListView<double> childOffsets,
bool didReachLastChild
);
class _Key {
const _Key(this.type, this.key);
factory _Key.fromWidget(Widget widget) => new _Key(widget.runtimeType, widget.key);
final Type type;
final String key;
bool operator ==(other) => other is _Key && other.type == type && other.key == key;
int get hashCode => 373 * 37 * type.hashCode + key.hashCode;
}
class BlockViewport extends RenderObjectWrapper {
BlockViewport({ this.builder, this.startOffset, this.token, this.onLayoutChanged, String key })
: super(key: key);
IndexedBuilder builder;
double startOffset;
Object token;
LayoutChangedCallback onLayoutChanged;
RenderBlockViewport get root => super.root;
RenderBlockViewport createNode() => new RenderBlockViewport();
Map<_Key, Widget> _childrenByKey = new Map<_Key, Widget>();
void walkChildren(WidgetTreeWalker walker) {
for (Widget child in _childrenByKey.values)
walker(child);
}
static const _omit = const Object(); // used as a slot when it's not yet time to attach the child
void insertChildRoot(RenderObjectWrapper child, dynamic slot) {
if (slot == _omit)
return;
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);
if (child.root.parent != root)
return; // probably had slot == _omit when inserted
root.remove(child.root);
assert(root == this.root); // TODO(ianh): Remove this once the analyzer is cleverer
}
void remove() {
for (Widget child in _childrenByKey.values) {
assert(child != null);
removeChild(child);
}
super.remove();
}
void didMount() {
root.callback = layout;
super.didMount();
}
void didUnmount() {
root.callback = null;
super.didUnmount();
}
// _offsets contains the offsets of each child from the top of the
// list up to the last one we've ever created, and the offset of the
// end of the last one. If there's no children, then the only offset
// is 0.0.
List<double> _offsets = <double>[0.0];
int _currentStartIndex = 0;
int _currentChildCount = 0;
bool _didReachLastChild = false;
int _findIndexForOffsetBeforeOrAt(double offset) {
int left = 0;
int right = _offsets.length - 1;
while (right >= left) {
int middle = left + ((right - left) ~/ 2);
if (_offsets[middle] < offset) {
left = middle + 1;
} else if (_offsets[middle] > offset) {
right = middle - 1;
} else {
return middle;
}
}
return right;
}
bool _dirty = true;
bool retainStatefulNodeIfPossible(BlockViewport newNode) {
retainStatefulRenderObjectWrapper(newNode);
if (startOffset != newNode.startOffset) {
_dirty = true;
startOffset = newNode.startOffset;
}
if (token != newNode.token || builder != newNode.builder) {
_dirty = true;
builder = newNode.builder;
token = newNode.token;
_offsets = <double>[0.0];
_didReachLastChild = false;
}
return true;
}
void syncRenderObject(BlockViewport old) {
super.syncRenderObject(old);
if (_dirty) {
root.markNeedsLayout();
} else {
if (_currentChildCount > 0) {
assert(_currentStartIndex >= 0);
assert(builder != null);
assert(root != null);
int lastIndex = _currentStartIndex + _currentChildCount - 1;
for (int index = _currentStartIndex; index <= lastIndex; index += 1) {
Widget widget = builder(index);
assert(widget != null);
assert(widget.key != null);
_Key key = new _Key.fromWidget(widget);
Widget oldWidget = _childrenByKey[key];
assert(oldWidget != null);
assert(oldWidget.root.parent == root);
widget = syncChild(widget, oldWidget, root.childAfter(oldWidget.root));
assert(widget != null);
_childrenByKey[key] = widget;
}
}
}
}
Widget _getWidget(int index, BoxConstraints innerConstraints) {
LayoutCallbackBuilderHandle handle = enterLayoutCallbackBuilder();
try {
assert(index >= 0);
Widget widget = builder == null ? null : builder(index);
if (widget == null)
return null;
assert(widget.key != null); // items in lists must have keys
final _Key key = new _Key.fromWidget(widget);
Widget oldWidget = _childrenByKey[key];
widget = syncChild(widget, oldWidget, _omit);
if (oldWidget != null)
_childrenByKey[key] = widget;
if (index >= _offsets.length - 1) {
assert(index == _offsets.length - 1);
final double widgetStartOffset = _offsets[index];
RenderBox widgetRoot = widget.root;
assert(widgetRoot is RenderBox);
final double widgetEndOffset = widgetStartOffset + widgetRoot.getMaxIntrinsicHeight(innerConstraints);
_offsets.add(widgetEndOffset);
}
return widget;
} finally {
exitLayoutCallbackBuilder(handle);
}
}
void layout(BoxConstraints constraints) {
if (!_dirty)
return;
_dirty = false;
Map<_Key, Widget> newChildren = new Map<_Key, Widget>();
Map<int, Widget> builtChildren = new Map<int, Widget>();
final double height = root.size.height;
final double endOffset = startOffset + height;
BoxConstraints innerConstraints = new BoxConstraints.tightFor(width: constraints.constrainWidth());
int startIndex;
bool haveChildren;
if (startOffset <= 0.0) {
startIndex = 0;
if (_offsets.length > 1) {
haveChildren = true;
} else {
Widget widget = _getWidget(startIndex, innerConstraints);
if (widget != null) {
newChildren[new _Key.fromWidget(widget)] = widget;
builtChildren[startIndex] = widget;
haveChildren = true;
} else {
haveChildren = false;
_didReachLastChild = true;
}
}
} else {
startIndex = _findIndexForOffsetBeforeOrAt(startOffset);
if (startIndex == _offsets.length - 1) {
// We don't have an offset on the list that is beyond the start offset.
assert(_offsets.last <= startOffset);
// Fill the list until this isn't true or until we know that the
// list is complete (and thus we are overscrolled).
while (true) {
Widget widget = _getWidget(startIndex, innerConstraints);
if (widget == null) {
_didReachLastChild = true;
break;
}
_Key widgetKey = new _Key.fromWidget(widget);
if (_offsets.last > startOffset) {
newChildren[widgetKey] = widget;
builtChildren[startIndex] = widget;
break;
}
if (!_childrenByKey.containsKey(widgetKey)) {
// we don't actually need this one, release it
syncChild(null, widget, null);
} // else we'll get rid of it later, when we remove old children
startIndex += 1;
assert(startIndex == _offsets.length - 1);
}
if (_offsets.last > startOffset) {
// If we're here, we have at least one child, so our list has
// at least two offsets, the top of the child and the bottom
// of the child.
assert(_offsets.length >= 2);
assert(startIndex == _offsets.length - 2);
haveChildren = true;
} else {
// If we're here, there are no children to show.
haveChildren = false;
}
} else {
haveChildren = true;
}
}
assert(haveChildren != null);
assert(haveChildren || _didReachLastChild);
assert(startIndex >= 0);
assert(startIndex < _offsets.length);
int index = startIndex;
if (haveChildren) {
// Build all the widgets we need.
root.startOffset = _offsets[index] - startOffset;
while (_offsets[index] < endOffset) {
if (!builtChildren.containsKey(index)) {
Widget widget = _getWidget(index, innerConstraints);
if (widget == null) {
_didReachLastChild = true;
break;
}
newChildren[new _Key.fromWidget(widget)] = widget;
builtChildren[index] = widget;
}
assert(builtChildren[index] != null);
index += 1;
}
}
// Remove any old children.
for (_Key oldChildKey in _childrenByKey.keys) {
if (!newChildren.containsKey(oldChildKey))
syncChild(null, _childrenByKey[oldChildKey], null); // calls detachChildRoot()
}
if (haveChildren) {
// Place all our children in our RenderObject.
// All the children we are placing are in builtChildren and newChildren.
// We will walk them backwards so we can set the siblings at the same time.
RenderBox nextSibling = null;
while (index > startIndex) {
index -= 1;
Widget widget = builtChildren[index];
if (widget.root.parent == root) {
root.move(widget.root, before: nextSibling);
} else {
assert(widget.root.parent == null);
root.add(widget.root, before: nextSibling);
}
widget.updateSlot(nextSibling);
nextSibling = widget.root;
}
}
_childrenByKey = newChildren;
_currentStartIndex = startIndex;
_currentChildCount = _childrenByKey.length;
if (onLayoutChanged != null) {
onLayoutChanged(
_currentStartIndex,
_currentChildCount,
new UnmodifiableListView<double>(_offsets),
_didReachLastChild
);
}
}
}