// 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/framework.dart'; // return null if index is greater than index of last entry typedef Widget IndexedBuilder(int index); class _Key { const _Key(this.type, this.key); factory _Key.fromWidget(Widget widget) => new _Key(widget.runtimeType, widget.key); final Type type; final Key key; bool operator ==(other) => other is _Key && other.type == type && other.key == key; int get hashCode => 373 * 37 * type.hashCode + key.hashCode; String toString() => "_Key(type: $type, key: $key)"; } typedef void LayoutChangedCallback(); class BlockViewportLayoutState { BlockViewportLayoutState() : _childOffsets = <double>[0.0], _firstVisibleChildIndex = 0, _visibleChildCount = 0, _didReachLastChild = false { _readOnlyChildOffsets = new UnmodifiableListView<double>(_childOffsets); } Map<_Key, Widget> _childrenByKey = new Map<_Key, Widget>(); bool _dirty = true; int _firstVisibleChildIndex; int get firstVisibleChildIndex => _firstVisibleChildIndex; int _visibleChildCount; int get visibleChildCount => _visibleChildCount; // childOffsets 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 are no children, then the only offset // is 0.0. List<double> _childOffsets; UnmodifiableListView<double> _readOnlyChildOffsets; UnmodifiableListView<double> get childOffsets => _readOnlyChildOffsets; double get contentsSize => _childOffsets.last; bool _didReachLastChild; bool get didReachLastChild => _didReachLastChild; Set<int> _invalidIndices = new Set<int>(); bool get isValid => _invalidIndices.length == 0; // Notify the BlockViewport that the children at indices have either // changed size and/or changed type. void invalidate(Iterable<int> indices) { _invalidIndices.addAll(indices); } final List<Function> _listeners = new List<Function>(); void addListener(Function listener) { _listeners.add(listener); } void removeListener(Function listener) { _listeners.remove(listener); } void _notifyListeners() { List<Function> localListeners = new List<Function>.from(_listeners); for (Function listener in localListeners) listener(); } } class BlockViewport extends RenderObjectWrapper { BlockViewport({ this.builder, this.startOffset, this.token, this.layoutState, Key key }) : super(key: key) { assert(this.layoutState != null); } IndexedBuilder builder; double startOffset; Object token; BlockViewportLayoutState layoutState; RenderBlockViewport get root => super.root; RenderBlockViewport createNode() => new RenderBlockViewport(); void walkChildren(WidgetTreeWalker walker) { for (Widget child in layoutState._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 layoutState._childrenByKey.values) { assert(child != null); removeChild(child); } super.remove(); } void didMount() { root.callback = layout; super.didMount(); } void didUnmount() { root.callback = null; super.didUnmount(); } int _findIndexForOffsetBeforeOrAt(double offset) { final List<double> offsets = layoutState._childOffsets; 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 retainStatefulNodeIfPossible(BlockViewport newNode) { assert(layoutState == newNode.layoutState); retainStatefulRenderObjectWrapper(newNode); if (startOffset != newNode.startOffset) { layoutState._dirty = true; startOffset = newNode.startOffset; } if (token != newNode.token || builder != newNode.builder) { layoutState._dirty = true; builder = newNode.builder; token = newNode.token; layoutState._didReachLastChild = false; layoutState._childOffsets = <double>[0.0]; layoutState._invalidIndices = new Set<int>(); } return true; } void syncRenderObject(BlockViewport old) { super.syncRenderObject(old); if (layoutState._dirty || !layoutState.isValid) { root.markNeedsLayout(); } else { if (layoutState._visibleChildCount > 0) { assert(layoutState.firstVisibleChildIndex >= 0); assert(builder != null); assert(root != null); final int startIndex = layoutState._firstVisibleChildIndex; int lastIndex = startIndex + layoutState._visibleChildCount - 1; for (int index = startIndex; index <= lastIndex; index += 1) { Widget widget = builder(index); assert(widget != null); assert(widget.key != null); _Key key = new _Key.fromWidget(widget); Widget oldWidget = layoutState._childrenByKey[key]; assert(oldWidget != null); assert(oldWidget.root.parent == root); widget = syncChild(widget, oldWidget, root.childAfter(oldWidget.root)); assert(widget != null); layoutState._childrenByKey[key] = widget; } } } } // Build the widget at index, and use its maxIntrinsicHeight to fix up // the offsets from index+1 to endIndex. Return the newWidget. Widget _getWidgetAndRecomputeOffsets(int index, int endIndex, BoxConstraints innerConstraints) { final List<double> offsets = layoutState._childOffsets; // Create the newWidget at index. assert(index >= 0); assert(endIndex > index); assert(endIndex < offsets.length); assert(builder != null); Widget newWidget = builder(index); assert(newWidget != null); assert(newWidget.key != null); final _Key key = new _Key.fromWidget(newWidget); Widget oldWidget = layoutState._childrenByKey[key]; newWidget = syncChild(newWidget, oldWidget, _omit); assert(newWidget != null); // Update the offsets based on the newWidget's height. RenderBox widgetRoot = newWidget.root; assert(widgetRoot is RenderBox); double newHeight = widgetRoot.getMaxIntrinsicHeight(innerConstraints); double oldHeight = offsets[index + 1] - offsets[index]; double heightDelta = newHeight - oldHeight; for (int i = index + 1; i <= endIndex; i++) offsets[i] += heightDelta; return newWidget; } Widget _getWidget(int index, BoxConstraints innerConstraints) { final List<double> offsets = layoutState._childOffsets; 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 = layoutState._childrenByKey[key]; widget = syncChild(widget, oldWidget, _omit); 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; } void layout(BoxConstraints constraints) { if (!layoutState._dirty && layoutState.isValid) return; layoutState._dirty = false; LayoutCallbackBuilderHandle handle = enterLayoutCallbackBuilder(); try { _doLayout(constraints); } finally { exitLayoutCallbackBuilder(handle); } layoutState._notifyListeners(); } void _doLayout(BoxConstraints constraints) { Map<_Key, Widget> newChildren = new Map<_Key, Widget>(); Map<int, Widget> builtChildren = new Map<int, Widget>(); final List<double> offsets = layoutState._childOffsets; final Map<_Key, Widget> childrenByKey = layoutState._childrenByKey; final double height = root.size.height; final double endOffset = startOffset + height; BoxConstraints innerConstraints = new BoxConstraints.tightFor(width: constraints.constrainWidth()); // Before doing the actual layout, fix the offsets for the widgets // whose size or type has changed. if (!layoutState.isValid && offsets.length > 0) { List<int> invalidIndices = layoutState._invalidIndices.toList(); invalidIndices.sort(); // Ensure all of the offsets after invalidIndices[0] are updated. if (invalidIndices.last < offsets.length - 1) invalidIndices.add(offsets.length - 1); for (int i = 0; i < invalidIndices.length - 1; i += 1) { int index = invalidIndices[i]; int endIndex = invalidIndices[i + 1]; Widget widget = _getWidgetAndRecomputeOffsets(index, endIndex, innerConstraints); _Key widgetKey = new _Key.fromWidget(widget); bool isVisible = offsets[index] < endOffset && offsets[index + 1] >= startOffset; if (isVisible) { newChildren[widgetKey] = widget; builtChildren[index] = widget; } else { childrenByKey.remove(widgetKey); syncChild(null, widget, null); } } } layoutState._invalidIndices.clear(); 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; layoutState._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) { layoutState._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 || layoutState._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) { layoutState._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; } } layoutState._childrenByKey = newChildren; layoutState._firstVisibleChildIndex = startIndex; layoutState._visibleChildCount = newChildren.length; } }