711e50389f
Doesn't change anything in `front_end/*testcases/primary_constructors/`. (Would have skipped any other file with `test` in its path and an explicit language version marker, but there weren't any outside of those `front_end` directories). Almost no files used as test input were affected, and none testing the actual syntax changed. The `.../nnbd/required_2.dart` test case was split into a legacy version retaining the `var`/`final` with a language marker, and a new version without the `var`/`final` cases. The `pkg/analyzer/` tests, and any other tests that have source code in strings, are not migrated by this CL. Tested: No change to behavior. One test split into legacy and new. Change-Id: I7f5aa4cc98001a9adecacd106c0b3be14f96be1c Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/480542 Reviewed-by: Johnni Winther <johnniwinther@google.com> Reviewed-by: Nate Bosch <nbosch@google.com> Reviewed-by: Ryan Macnak <rmacnak@google.com> Commit-Queue: Lasse Nielsen <lrn@google.com> Reviewed-by: Nicholas Shahan <nshahan@google.com>
1065 lines
29 KiB
Dart
1065 lines
29 KiB
Dart
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
part of swarmlib;
|
|
|
|
// This file contains View framework classes.
|
|
// As it grows, it may need to be split into multiple files.
|
|
|
|
/// A factory that creates a view from a data model. */
|
|
abstract class ViewFactory<D> {
|
|
View newView(D item);
|
|
|
|
/// The width of the created view or null if the width is not fixed. */
|
|
int? get width;
|
|
|
|
/// The height of the created view or null if the height is not fixed. */
|
|
int? get height;
|
|
}
|
|
|
|
abstract class VariableSizeViewFactory<D> {
|
|
View newView(D item);
|
|
|
|
/// The width of the created view for a specific data model. */
|
|
int getWidth(D? item);
|
|
|
|
/// The height of the created view for a specific data model. */
|
|
int getHeight(D? item);
|
|
}
|
|
|
|
/// A collection of event listeners. */
|
|
class EventListeners {
|
|
var listeners;
|
|
EventListeners() {
|
|
listeners = [];
|
|
}
|
|
|
|
void addListener(listener) {
|
|
listeners.add(listener);
|
|
}
|
|
|
|
void fire(event) {
|
|
for (final listener in listeners) {
|
|
listener(event);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Private view class used to store placeholder views for detached ListView
|
|
/// elements.
|
|
class _PlaceholderView extends View {
|
|
_PlaceholderView();
|
|
|
|
@override
|
|
Element render() => Element.tag('div');
|
|
}
|
|
|
|
/// Class providing all metrics required to layout a data driven list view.
|
|
abstract class ListViewLayout<D> {
|
|
void onDataChange();
|
|
|
|
// TODO(jacobr): placing the newView member function on this class seems like
|
|
// the wrong design.
|
|
View newView(int index);
|
|
|
|
/// Get the height of the view. Possibly expensive to compute. */
|
|
int? getHeight(int viewLength);
|
|
|
|
/// Get the width of the view. Possibly expensive to compute. */
|
|
int? getWidth(int viewLength);
|
|
|
|
/// Get the length of the view. Possible expensive to compute. */
|
|
int getLength(int viewLength);
|
|
|
|
/// Estimated height of the view. Guaranteed to be fast to compute. */
|
|
int? getEstimatedHeight(int viewLength);
|
|
|
|
/// Estimated with of the view. Guaranteed to be fast to compute. */
|
|
int? getEstimatedWidth(int viewLength);
|
|
|
|
/// Returns the offset in px that the ith item in the view should be placed
|
|
/// at.
|
|
int getOffset(int index);
|
|
|
|
/// The page the ith item in the view should be placed in.
|
|
int getPage(int index, int viewLength);
|
|
int getPageStartIndex(int index, int viewLength);
|
|
|
|
int getEstimatedLength(int viewLength);
|
|
|
|
/// Snap a specified index to the nearest visible view given the [viewLength].
|
|
int getSnapIndex(num offset, int viewLength);
|
|
|
|
/// Returns an interval specifying what views are currently visible given a
|
|
/// particular [:offset:].
|
|
Interval<int> computeVisibleInterval(
|
|
num offset,
|
|
num viewLength,
|
|
num bufferLength,
|
|
);
|
|
}
|
|
|
|
/// Base class used for the simple fixed size item [:ListView:] classes and more
|
|
/// complex list view classes such as [:VariableSizeListView:] using a
|
|
/// [:ListViewLayout:] class to drive the actual layout.
|
|
class GenericListView<D> extends View {
|
|
/// Minimum throw distance in pixels to trigger snapping to the next item. */
|
|
static const SNAP_TO_NEXT_THROW_THRESHOLD = 15;
|
|
|
|
static const INDEX_DATA_ATTRIBUTE = 'data-index';
|
|
|
|
final bool _scrollable;
|
|
final bool _showScrollbar;
|
|
final bool _snapToItems;
|
|
Scroller? scroller;
|
|
Scrollbar? _scrollbar;
|
|
final List<D> _data;
|
|
final ObservableValue<D?>? _selectedItem;
|
|
final Map<int, View> _itemViews;
|
|
late Element _containerElem;
|
|
final bool _vertical;
|
|
|
|
/// Length of the scrollable dimension of the view in px. */
|
|
int _viewLength = 0;
|
|
Interval<int> _activeInterval;
|
|
final bool _paginate;
|
|
final bool _removeClippedViews;
|
|
final ListViewLayout<D> _layout;
|
|
D? _lastSelectedItem;
|
|
final PageState? _pages;
|
|
|
|
/// Creates a new GenericListView with the given layout and data. If [:_data:]
|
|
/// is an [:ObservableList<T>:] then it will listen to changes to the list
|
|
/// and update the view appropriately.
|
|
GenericListView(
|
|
this._layout,
|
|
this._data,
|
|
this._scrollable,
|
|
this._vertical,
|
|
this._selectedItem,
|
|
this._snapToItems,
|
|
this._paginate,
|
|
this._removeClippedViews,
|
|
this._showScrollbar,
|
|
this._pages,
|
|
) : _activeInterval = Interval(0, 0),
|
|
_itemViews = <int, View>{} {
|
|
// TODO(rnystrom): Move this into enterDocument once we have an exitDocument
|
|
// that we can use to unregister it.
|
|
if (_scrollable) {
|
|
window.onResize.listen((Event event) {
|
|
if (isInDocument) {
|
|
onResize();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void onSelectedItemChange() {
|
|
// TODO(rnystrom): use Observable to track the last value of _selectedItem
|
|
// rather than tracking it ourselves.
|
|
_select(findIndex(_lastSelectedItem), false);
|
|
_select(findIndex(_selectedItem!.value), true);
|
|
_lastSelectedItem = _selectedItem.value;
|
|
}
|
|
|
|
@override
|
|
Iterable<View> get childViews {
|
|
return _itemViews.values.toList();
|
|
}
|
|
|
|
void _onClick(MouseEvent e) {
|
|
int? index = _findAssociatedIndex(e.target as Node?);
|
|
if (index != null) {
|
|
_selectedItem!.value = _data[index];
|
|
}
|
|
}
|
|
|
|
int? _findAssociatedIndex(Node? leafNode) {
|
|
Node? node = leafNode;
|
|
while (node != null && node != _containerElem) {
|
|
if (node.parent == _containerElem) {
|
|
return _nodeToIndex(node as Element);
|
|
}
|
|
node = node.parent;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
int? _nodeToIndex(Element node) {
|
|
// TODO(jacobr): use data attributes when available.
|
|
String? index = node.attributes[INDEX_DATA_ATTRIBUTE];
|
|
if (index != null && index.isNotEmpty) {
|
|
return int.parse(index);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
Element render() {
|
|
final node = Element.tag('div');
|
|
if (_scrollable) {
|
|
_containerElem = Element.tag('div');
|
|
_containerElem.tabIndex = -1;
|
|
node.nodes.add(_containerElem);
|
|
} else {
|
|
_containerElem = node;
|
|
}
|
|
|
|
if (_scrollable) {
|
|
scroller = Scroller(
|
|
_containerElem,
|
|
_vertical /* verticalScrollEnabled */,
|
|
!_vertical /* horizontalScrollEnabled */,
|
|
true /* momentumEnabled */,
|
|
() {
|
|
num? width = _layout.getWidth(_viewLength);
|
|
num? height = _layout.getHeight(_viewLength);
|
|
width = width ?? 0;
|
|
height = height ?? 0;
|
|
return Size(width, height);
|
|
},
|
|
_paginate && _snapToItems ? Scroller.FAST_SNAP_DECELERATION_FACTOR : 1,
|
|
);
|
|
scroller!.onContentMoved.listen((e) => renderVisibleItems(false));
|
|
if (_pages != null) {
|
|
watch(_pages.target, (s) => _onPageSelected());
|
|
}
|
|
|
|
if (_snapToItems) {
|
|
scroller!.onDecelStart.listen((e) => _decelStart());
|
|
scroller!.onScrollerDragEnd.listen((e) => _decelStart());
|
|
}
|
|
if (_showScrollbar) {
|
|
_scrollbar = Scrollbar(scroller!, true);
|
|
}
|
|
} else {
|
|
_reserveArea();
|
|
renderVisibleItems(true);
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
@override
|
|
void afterRender(Element node) {
|
|
// If our data source is observable, observe it.
|
|
final data = _data;
|
|
if (data is ObservableList<D>) {
|
|
attachWatch(data, (EventSummary e) {
|
|
if (e.target == data) {
|
|
onDataChange();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (_selectedItem != null) {
|
|
addOnClick((Event e) {
|
|
_onClick(e as MouseEvent);
|
|
});
|
|
}
|
|
|
|
if (_selectedItem != null) {
|
|
watch(_selectedItem, (EventSummary summary) => onSelectedItemChange());
|
|
}
|
|
}
|
|
|
|
void onDataChange() {
|
|
_layout.onDataChange();
|
|
_renderItems();
|
|
}
|
|
|
|
void _reserveArea() {
|
|
final style = _containerElem.style;
|
|
int? width = _layout.getWidth(_viewLength);
|
|
int? height = _layout.getHeight(_viewLength);
|
|
if (width != null) {
|
|
style.width = '${width}px';
|
|
}
|
|
if (height != null) {
|
|
style.height = '${height}px';
|
|
}
|
|
// TODO(jacobr): this should be specified by the default CSS for a
|
|
// GenericListView.
|
|
style.overflow = 'hidden';
|
|
}
|
|
|
|
void onResize() {
|
|
int lastViewLength = _viewLength;
|
|
scheduleMicrotask(() {
|
|
_viewLength = (_vertical ? node.offset.height : node.offset.width) as int;
|
|
if (_viewLength != lastViewLength) {
|
|
if (_scrollbar != null) {
|
|
_scrollbar!.refresh();
|
|
}
|
|
renderVisibleItems(true);
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void enterDocument() {
|
|
if (scroller != null) {
|
|
onResize();
|
|
|
|
if (_scrollbar != null) {
|
|
_scrollbar!.initialize();
|
|
}
|
|
}
|
|
}
|
|
|
|
int getNextIndex(int index, bool forward) {
|
|
int delta = forward ? 1 : -1;
|
|
if (_paginate) {
|
|
int newPage = Math.max(0, _layout.getPage(index, _viewLength) + delta);
|
|
index = _layout.getPageStartIndex(newPage, _viewLength);
|
|
} else {
|
|
index += delta;
|
|
}
|
|
return GoogleMath.clamp(index, 0, _data.length - 1);
|
|
}
|
|
|
|
void _decelStart() {
|
|
final scroll = scroller!;
|
|
num currentTarget = scroll.verticalEnabled
|
|
? scroll.currentTarget.y
|
|
: scroll.currentTarget.x;
|
|
num current = scroll.verticalEnabled
|
|
? scroll.contentOffset.y
|
|
: scroll.contentOffset.x;
|
|
int targetIndex = _layout.getSnapIndex(currentTarget, _viewLength);
|
|
if (current != currentTarget) {
|
|
// The user is throwing rather than statically releasing.
|
|
// For this case, we want to move them to the next snap interval
|
|
// as long as they made at least a minimal throw gesture.
|
|
num currentIndex = _layout.getSnapIndex(current, _viewLength);
|
|
if (currentIndex == targetIndex &&
|
|
(currentTarget - current).abs() > SNAP_TO_NEXT_THROW_THRESHOLD &&
|
|
-_layout.getOffset(targetIndex) != currentTarget) {
|
|
num snappedCurrentPosition = -_layout.getOffset(targetIndex);
|
|
targetIndex = getNextIndex(targetIndex, currentTarget < current);
|
|
}
|
|
}
|
|
num targetPosition = -_layout.getOffset(targetIndex);
|
|
if (currentTarget != targetPosition) {
|
|
if (scroll.verticalEnabled) {
|
|
scroll.throwTo(scroll.contentOffset.x, targetPosition);
|
|
} else {
|
|
scroll.throwTo(targetPosition, scroll.contentOffset.y);
|
|
}
|
|
} else {
|
|
// Update the target page only after we are all done animating.
|
|
if (_pages != null) {
|
|
_pages.target.value = _layout.getPage(targetIndex, _viewLength);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _renderItems() {
|
|
for (int i = _activeInterval.start; i < _activeInterval.end; i++) {
|
|
_removeView(i);
|
|
}
|
|
_itemViews.clear();
|
|
_activeInterval = Interval(0, 0);
|
|
if (scroller == null) {
|
|
_reserveArea();
|
|
}
|
|
renderVisibleItems(false);
|
|
}
|
|
|
|
void _onPageSelected() {
|
|
if (_pages!.target != _layout.getPage(_activeInterval.start, _viewLength)) {
|
|
_throwTo(
|
|
_layout.getOffset(
|
|
_layout.getPageStartIndex(_pages.target.value, _viewLength),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
num get _offset {
|
|
return scroller!.verticalEnabled
|
|
? scroller!.getVerticalOffset()
|
|
: scroller!.getHorizontalOffset();
|
|
}
|
|
|
|
/// Calculates visible interval, based on the scroller position.
|
|
Interval<int> getVisibleInterval() {
|
|
return _layout.computeVisibleInterval(_offset, _viewLength, 0);
|
|
}
|
|
|
|
void renderVisibleItems(bool lengthChanged) {
|
|
Interval<int> targetInterval;
|
|
if (scroller != null) {
|
|
targetInterval = getVisibleInterval();
|
|
} else {
|
|
// If the view is not scrollable, render all elements.
|
|
targetInterval = Interval(0, _data.length);
|
|
}
|
|
|
|
if (_pages != null) {
|
|
_pages.current.value = _layout.getPage(targetInterval.start, _viewLength);
|
|
}
|
|
if (_pages != null) {
|
|
_pages.length.value = _data.isNotEmpty
|
|
? _layout.getPage(_data.length - 1, _viewLength) + 1
|
|
: 0;
|
|
}
|
|
|
|
if (!_removeClippedViews) {
|
|
// Avoid removing clipped views by extending the target interval to
|
|
// include the existing interval of rendered views.
|
|
targetInterval = targetInterval.union(_activeInterval);
|
|
}
|
|
|
|
if (lengthChanged == false && targetInterval == _activeInterval) {
|
|
return;
|
|
}
|
|
|
|
// TODO(jacobr): add unittests that this code behaves correctly.
|
|
|
|
// Remove views that are not needed anymore
|
|
for (
|
|
int i = _activeInterval.start,
|
|
end = Math.min(targetInterval.start, _activeInterval.end);
|
|
i < end;
|
|
i++
|
|
) {
|
|
_removeView(i);
|
|
}
|
|
for (
|
|
int i = Math.max(targetInterval.end, _activeInterval.start);
|
|
i < _activeInterval.end;
|
|
i++
|
|
) {
|
|
_removeView(i);
|
|
}
|
|
|
|
// Add new views
|
|
for (
|
|
int i = targetInterval.start,
|
|
end = Math.min(_activeInterval.start, targetInterval.end);
|
|
i < end;
|
|
i++
|
|
) {
|
|
_addView(i);
|
|
}
|
|
for (
|
|
int i = Math.max(_activeInterval.end, targetInterval.start);
|
|
i < targetInterval.end;
|
|
i++
|
|
) {
|
|
_addView(i);
|
|
}
|
|
|
|
_activeInterval = targetInterval;
|
|
}
|
|
|
|
void _removeView(int index) {
|
|
// Do not remove placeholder views as they need to stay present in case
|
|
// they scroll out of view and then back into view.
|
|
final nodeView = _itemViews[index]!;
|
|
if (nodeView is! _PlaceholderView) {
|
|
// Remove from the active DOM but don't destroy.
|
|
nodeView.node.remove();
|
|
childViewRemoved(nodeView);
|
|
}
|
|
}
|
|
|
|
View _newView(int index) {
|
|
final view = _layout.newView(index);
|
|
view.node.attributes[INDEX_DATA_ATTRIBUTE] = index.toString();
|
|
return view;
|
|
}
|
|
|
|
View _addView(int index) {
|
|
if (_itemViews.containsKey(index)) {
|
|
final view = _itemViews[index]!;
|
|
_addViewHelper(view, index);
|
|
childViewAdded(view);
|
|
return view;
|
|
}
|
|
|
|
final view = _newView(index);
|
|
_itemViews[index] = view;
|
|
// TODO(jacobr): its ugly to put this here... but its needed
|
|
// as typical even-odd css queries won't work as we only display some
|
|
// children at a time.
|
|
if (index == 0) {
|
|
view.addClass('first-child');
|
|
}
|
|
_selectHelper(view, _data[index] == _lastSelectedItem);
|
|
// The order of the child elements doesn't matter as we use absolute
|
|
// positioning.
|
|
_addViewHelper(view, index);
|
|
childViewAdded(view);
|
|
return view;
|
|
}
|
|
|
|
void _addViewHelper(View view, int index) {
|
|
_positionSubview(view.node, index);
|
|
// The view might already be attached.
|
|
if (view.node.parent != _containerElem) {
|
|
_containerElem.nodes.add(view.node);
|
|
}
|
|
}
|
|
|
|
/// Detach a subview from the view replacing it with an empty placeholder view.
|
|
/// The detached subview can be safely reparented.
|
|
View detachSubview(D itemData) {
|
|
int index = findIndex(itemData)!;
|
|
View? view = _itemViews[index];
|
|
if (view == null) {
|
|
// Edge case: add the view so we can detach as the view is currently
|
|
// outside but might soon be inside the visible area.
|
|
assert(!_activeInterval.contains(index));
|
|
_addView(index);
|
|
view = _itemViews[index];
|
|
}
|
|
final placeholder = _PlaceholderView();
|
|
view!.node.replaceWith(placeholder.node);
|
|
_itemViews[index] = placeholder;
|
|
return view;
|
|
}
|
|
|
|
/// Reattach a subview from the view that was detached from the view
|
|
/// by calling detachSubview. [callback] is called once the subview is
|
|
/// reattached and done animating into position.
|
|
void reattachSubview(D data, View view, bool animate) {
|
|
int index = findIndex(data)!;
|
|
// TODO(jacobr): perform some validation that the view is
|
|
// really detached.
|
|
late Coordinate currentPosition;
|
|
if (animate) {
|
|
currentPosition = FxUtil.computeRelativePosition(
|
|
view.node,
|
|
_containerElem,
|
|
);
|
|
}
|
|
assert(_itemViews[index] is _PlaceholderView);
|
|
view.enterDocument();
|
|
_itemViews[index]!.node.replaceWith(view.node);
|
|
_itemViews[index] = view;
|
|
if (animate) {
|
|
FxUtil.setTranslate(view.node, currentPosition.x, currentPosition.y, 0);
|
|
// The view's position is unchanged except now re-parented to
|
|
// the list view.
|
|
Timer.run(() {
|
|
_positionSubview(view.node, index);
|
|
});
|
|
} else {
|
|
_positionSubview(view.node, index);
|
|
}
|
|
}
|
|
|
|
int? findIndex(D? targetItem) {
|
|
// TODO(jacobr): move this to a util library or modify this class so that
|
|
// the data is an List not a Collection.
|
|
int i = 0;
|
|
for (D item in _data) {
|
|
if (item == targetItem) {
|
|
return i;
|
|
}
|
|
i++;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
void _positionSubview(Element node, int index) {
|
|
if (_vertical) {
|
|
FxUtil.setTranslate(node, 0, _layout.getOffset(index), 0);
|
|
} else {
|
|
FxUtil.setTranslate(node, _layout.getOffset(index), 0, 0);
|
|
}
|
|
node.style.zIndex = index.toString();
|
|
}
|
|
|
|
void _select(int? index, bool selected) {
|
|
if (index != null) {
|
|
final subview = getSubview(index);
|
|
if (subview != null) {
|
|
_selectHelper(subview, selected);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _selectHelper(View view, bool selected) {
|
|
if (selected) {
|
|
view.addClass('sel');
|
|
} else {
|
|
view.removeClass('sel');
|
|
}
|
|
}
|
|
|
|
View? getSubview(int index) {
|
|
return _itemViews[index];
|
|
}
|
|
|
|
void showView(D targetItem) {
|
|
int? index = findIndex(targetItem);
|
|
if (index != null) {
|
|
if (_layout.getOffset(index) < -_offset) {
|
|
_throwTo(_layout.getOffset(index));
|
|
} else if (_layout.getOffset(index + 1) > (-_offset + _viewLength)) {
|
|
// TODO(jacobr): for completeness we should check whether
|
|
// the current view is longer than _viewLength in which case
|
|
// there are some nasty edge cases.
|
|
_throwTo(_layout.getOffset(index + 1) - _viewLength);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _throwTo(num offset) {
|
|
if (_vertical) {
|
|
scroller!.throwTo(0, -offset);
|
|
} else {
|
|
scroller!.throwTo(-offset, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
class FixedSizeListViewLayout<D> implements ListViewLayout<D> {
|
|
final ViewFactory<D> itemViewFactory;
|
|
final bool _vertical;
|
|
final List<D> _data;
|
|
final bool _paginate;
|
|
|
|
FixedSizeListViewLayout(
|
|
this.itemViewFactory,
|
|
this._data,
|
|
this._vertical,
|
|
this._paginate,
|
|
);
|
|
|
|
@override
|
|
void onDataChange() {}
|
|
|
|
@override
|
|
View newView(int index) {
|
|
return itemViewFactory.newView(_data[index]);
|
|
}
|
|
|
|
int get _itemLength {
|
|
return (_vertical ? itemViewFactory.height : itemViewFactory.width)!;
|
|
}
|
|
|
|
@override
|
|
int? getWidth(int viewLength) {
|
|
return _vertical ? itemViewFactory.width : getLength(viewLength);
|
|
}
|
|
|
|
@override
|
|
int? getHeight(int viewLength) {
|
|
return _vertical ? getLength(viewLength) : itemViewFactory.height;
|
|
}
|
|
|
|
@override
|
|
int? getEstimatedHeight(int viewLength) {
|
|
// Returns the exact height as it is trivial to compute for this layout.
|
|
return getHeight(viewLength);
|
|
}
|
|
|
|
@override
|
|
int? getEstimatedWidth(int viewLength) {
|
|
// Returns the exact height as it is trivial to compute for this layout.
|
|
return getWidth(viewLength);
|
|
}
|
|
|
|
@override
|
|
int getEstimatedLength(int viewLength) {
|
|
// Returns the exact length as it is trivial to compute for this layout.
|
|
return getLength(viewLength);
|
|
}
|
|
|
|
@override
|
|
int getLength(int viewLength) {
|
|
int itemLength = (_vertical
|
|
? itemViewFactory.height
|
|
: itemViewFactory.width)!;
|
|
if (viewLength == null || viewLength == 0) {
|
|
return itemLength * _data.length;
|
|
} else if (_paginate) {
|
|
if (_data.isNotEmpty) {
|
|
final pageLength = getPageLength(viewLength);
|
|
return getPage(_data.length - 1, viewLength) * pageLength +
|
|
Math.max(viewLength, pageLength);
|
|
} else {
|
|
return 0;
|
|
}
|
|
} else {
|
|
return itemLength * (_data.length - 1) + Math.max(viewLength, itemLength);
|
|
}
|
|
}
|
|
|
|
@override
|
|
int getOffset(int index) {
|
|
return index * _itemLength;
|
|
}
|
|
|
|
int getPageLength(int viewLength) {
|
|
final itemsPerPage = viewLength ~/ _itemLength;
|
|
return Math.max(1, itemsPerPage) * _itemLength;
|
|
}
|
|
|
|
@override
|
|
int getPage(int index, int viewLength) {
|
|
return getOffset(index) ~/ getPageLength(viewLength);
|
|
}
|
|
|
|
@override
|
|
int getPageStartIndex(int page, int viewLength) {
|
|
return getPageLength(viewLength) ~/ _itemLength * page;
|
|
}
|
|
|
|
@override
|
|
int getSnapIndex(num offset, int viewLength) {
|
|
int index = (-offset / _itemLength).round();
|
|
if (_paginate) {
|
|
index = getPageStartIndex(getPage(index, viewLength), viewLength);
|
|
}
|
|
return GoogleMath.clamp(index, 0, _data.length - 1);
|
|
}
|
|
|
|
@override
|
|
Interval<int> computeVisibleInterval(
|
|
num offset,
|
|
num viewLength,
|
|
num bufferLength,
|
|
) {
|
|
int targetIntervalStart = Math.max(
|
|
0,
|
|
(-offset - bufferLength) ~/ _itemLength,
|
|
);
|
|
num targetIntervalEnd = GoogleMath.clamp(
|
|
((-offset + viewLength + bufferLength) / _itemLength).ceil(),
|
|
targetIntervalStart,
|
|
_data.length,
|
|
);
|
|
return Interval(targetIntervalStart, targetIntervalEnd.toInt());
|
|
}
|
|
}
|
|
|
|
/// Simple list view class where each item has fixed width and height.
|
|
class ListView<D> extends GenericListView<D> {
|
|
/// Creates a new ListView for the given data. If [:_data:] is an
|
|
/// [:ObservableList<T>:] then it will listen to changes to the list and
|
|
/// update the view appropriately.
|
|
ListView(
|
|
List<D> data,
|
|
ViewFactory<D> itemViewFactory,
|
|
bool scrollable,
|
|
bool vertical,
|
|
ObservableValue<D>? selectedItem, [
|
|
bool snapToItems = false,
|
|
bool paginate = false,
|
|
bool removeClippedViews = false,
|
|
bool showScrollbar = false,
|
|
PageState? pages,
|
|
]) : super(
|
|
FixedSizeListViewLayout<D>(itemViewFactory, data, vertical, paginate),
|
|
data,
|
|
scrollable,
|
|
vertical,
|
|
selectedItem,
|
|
snapToItems,
|
|
paginate,
|
|
removeClippedViews,
|
|
showScrollbar,
|
|
pages,
|
|
);
|
|
}
|
|
|
|
/// Layout where each item may have variable size along the axis the list view
|
|
/// extends.
|
|
class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
|
|
final List<D> _data;
|
|
List<int> _itemOffsets;
|
|
List<int> _lengths;
|
|
int _lastOffset = 0;
|
|
final bool _vertical;
|
|
final bool _paginate;
|
|
VariableSizeViewFactory<D> itemViewFactory;
|
|
Interval<int> _lastVisibleInterval;
|
|
|
|
VariableSizeListViewLayout(
|
|
this.itemViewFactory,
|
|
data,
|
|
this._vertical,
|
|
this._paginate,
|
|
) : _data = data,
|
|
_lastVisibleInterval = Interval(0, 0),
|
|
_itemOffsets = <int>[0],
|
|
_lengths = <int>[];
|
|
|
|
@override
|
|
void onDataChange() {
|
|
_itemOffsets.clear();
|
|
_itemOffsets.add(0);
|
|
_lengths.clear();
|
|
}
|
|
|
|
@override
|
|
View newView(int index) => itemViewFactory.newView(_data[index]);
|
|
|
|
@override
|
|
int getWidth(int viewLength) {
|
|
if (_vertical) {
|
|
return itemViewFactory.getWidth(null);
|
|
} else {
|
|
return getLength(viewLength);
|
|
}
|
|
}
|
|
|
|
@override
|
|
int getHeight(int viewLength) {
|
|
if (_vertical) {
|
|
return getLength(viewLength);
|
|
} else {
|
|
return itemViewFactory.getHeight(null);
|
|
}
|
|
}
|
|
|
|
@override
|
|
int getEstimatedHeight(int viewLength) {
|
|
if (_vertical) {
|
|
return getEstimatedLength(viewLength);
|
|
} else {
|
|
return itemViewFactory.getHeight(null);
|
|
}
|
|
}
|
|
|
|
@override
|
|
int getEstimatedWidth(int viewLength) {
|
|
if (_vertical) {
|
|
return itemViewFactory.getWidth(null);
|
|
} else {
|
|
return getEstimatedLength(viewLength);
|
|
}
|
|
}
|
|
|
|
// TODO(jacobr): this logic is overly complicated. Replace with something
|
|
// simpler.
|
|
@override
|
|
int getEstimatedLength(int viewLength) {
|
|
if (_lengths.length == _data.length) {
|
|
// No need to estimate... we have all the data already.
|
|
return getLength(viewLength);
|
|
}
|
|
if (_itemOffsets.length > 1 && _lengths.isNotEmpty) {
|
|
// Estimate length by taking the average of the lengths
|
|
// of the known views.
|
|
num lengthFromAllButLastElement = 0;
|
|
if (_itemOffsets.length > 2) {
|
|
lengthFromAllButLastElement =
|
|
(getOffset(_itemOffsets.length - 2) - getOffset(0)) *
|
|
(_data.length / (_itemOffsets.length - 2));
|
|
}
|
|
return (lengthFromAllButLastElement +
|
|
Math.max(viewLength, _lengths[_lengths.length - 1]))
|
|
.toInt();
|
|
} else {
|
|
if (_lengths.length == 1) {
|
|
return Math.max(viewLength, _lengths[0]);
|
|
} else {
|
|
return viewLength;
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
int getLength(int viewLength) {
|
|
if (_data.isEmpty) {
|
|
return viewLength;
|
|
} else {
|
|
// Hack so that _lengths[length - 1] is available.
|
|
getOffset(_data.length);
|
|
return (getOffset(_data.length - 1) - getOffset(0)) +
|
|
Math.max(_lengths[_lengths.length - 1], viewLength);
|
|
}
|
|
}
|
|
|
|
@override
|
|
int getOffset(int index) {
|
|
if (index >= _itemOffsets.length) {
|
|
int offset = _itemOffsets[_itemOffsets.length - 1];
|
|
for (int i = _itemOffsets.length; i <= index; i++) {
|
|
int length = _vertical
|
|
? itemViewFactory.getHeight(_data[i - 1])
|
|
: itemViewFactory.getWidth(_data[i - 1]);
|
|
offset += length;
|
|
_itemOffsets.add(offset);
|
|
_lengths.add(length);
|
|
}
|
|
}
|
|
return _itemOffsets[index];
|
|
}
|
|
|
|
@override
|
|
int getPage(int index, int viewLength) {
|
|
// TODO(jacobr): implement.
|
|
throw 'Not implemented';
|
|
}
|
|
|
|
@override
|
|
int getPageStartIndex(int page, int viewLength) {
|
|
// TODO(jacobr): implement.
|
|
throw 'Not implemented';
|
|
}
|
|
|
|
@override
|
|
int getSnapIndex(num offset, num viewLength) {
|
|
for (int i = 1; i < _data.length; i++) {
|
|
if (getOffset(i) + getOffset(i - 1) > -offset * 2) {
|
|
return i - 1;
|
|
}
|
|
}
|
|
return _data.length - 1;
|
|
}
|
|
|
|
@override
|
|
Interval<int> computeVisibleInterval(
|
|
num offset,
|
|
num viewLength,
|
|
num bufferLength,
|
|
) {
|
|
final offsetInt = offset.toInt();
|
|
int start = _findFirstItemBefore(
|
|
-offsetInt - bufferLength,
|
|
// ignore: dead_code
|
|
_lastVisibleInterval != null ? _lastVisibleInterval.start : 0,
|
|
);
|
|
int end = _findFirstItemAfter(
|
|
-offsetInt + viewLength + bufferLength,
|
|
// ignore: dead_code
|
|
_lastVisibleInterval != null ? _lastVisibleInterval.end : 0,
|
|
);
|
|
_lastVisibleInterval = Interval(start, Math.max(start, end));
|
|
_lastOffset = offsetInt;
|
|
return _lastVisibleInterval;
|
|
}
|
|
|
|
int _findFirstItemAfter(num target, int hint) {
|
|
for (int i = 0; i < _data.length; i++) {
|
|
if (getOffset(i) > target) {
|
|
return i;
|
|
}
|
|
}
|
|
return _data.length;
|
|
}
|
|
|
|
// TODO(jacobr): use hint.
|
|
int _findFirstItemBefore(num target, int hint) {
|
|
// We go search this direction delaying computing the actual view size
|
|
// as long as possible.
|
|
for (int i = 1; i < _data.length; i++) {
|
|
if (getOffset(i) >= target) {
|
|
return i - 1;
|
|
}
|
|
}
|
|
return Math.max(_data.length - 1, 0);
|
|
}
|
|
}
|
|
|
|
class VariableSizeListView<D> extends GenericListView<D> {
|
|
VariableSizeListView(
|
|
List<D> data,
|
|
VariableSizeViewFactory<D> itemViewFactory,
|
|
bool scrollable,
|
|
bool vertical,
|
|
ObservableValue<D?> selectedItem, [
|
|
bool snapToItems = false,
|
|
bool paginate = false,
|
|
bool removeClippedViews = false,
|
|
bool showScrollbar = false,
|
|
PageState? pages,
|
|
]) : super(
|
|
VariableSizeListViewLayout(itemViewFactory, data, vertical, paginate),
|
|
data,
|
|
scrollable,
|
|
vertical,
|
|
selectedItem,
|
|
snapToItems,
|
|
paginate,
|
|
removeClippedViews,
|
|
showScrollbar,
|
|
pages,
|
|
);
|
|
}
|
|
|
|
/// A back button that is equivalent to clicking "back" in the browser. */
|
|
class BackButton extends View {
|
|
BackButton();
|
|
|
|
@override
|
|
Element render() => Element.html('<div class="back-arrow button"></div>');
|
|
|
|
@override
|
|
void afterRender(Element node) {
|
|
addOnClick((e) => window.history.back());
|
|
}
|
|
}
|
|
|
|
// TODO(terry): Maybe should be part of ButtonView class in appstack/view?
|
|
/// OS button. */
|
|
class PushButtonView extends View {
|
|
final String _text;
|
|
final String _cssClass;
|
|
final _clickHandler;
|
|
|
|
PushButtonView(this._text, this._cssClass, this._clickHandler);
|
|
|
|
@override
|
|
Element render() {
|
|
return Element.html('<button class="$_cssClass">$_text</button>');
|
|
}
|
|
|
|
@override
|
|
void afterRender(Element node) {
|
|
addOnClick(_clickHandler);
|
|
}
|
|
}
|
|
|
|
// TODO(terry): Add a drop shadow around edge and corners need to be rounded.
|
|
// Need to support conveyor for contents of dialog so it's not
|
|
// larger than the parent window.
|
|
/// A generic dialog view supports title, done button and dialog content. */
|
|
class DialogView extends View {
|
|
final String _title;
|
|
final String _cssName;
|
|
final View _content;
|
|
late Element container;
|
|
late PushButtonView _done;
|
|
|
|
DialogView(this._title, this._cssName, this._content);
|
|
|
|
@override
|
|
Element render() {
|
|
final node = Element.html('''
|
|
<div class="dialog-modal">
|
|
<div class="dialog $_cssName">
|
|
<div class="dialog-title-area">
|
|
<span class="dialog-title">$_title</span>
|
|
</div>
|
|
<div class="dialog-body"></div>
|
|
</div>
|
|
</div>''');
|
|
|
|
_done = PushButtonView(
|
|
'Done',
|
|
'done-button',
|
|
EventBatch.wrap((e) => onDone()),
|
|
);
|
|
final titleArea = node.querySelector('.dialog-title-area')!;
|
|
titleArea.nodes.add(_done.node);
|
|
|
|
container = node.querySelector('.dialog-body')!;
|
|
container.nodes.add(_content.node);
|
|
|
|
return node;
|
|
}
|
|
|
|
/// Override to handle dialog done. */
|
|
void onDone() {}
|
|
}
|