Chromium Code Reviews| Index: runtime/observatory/lib/src/app/view_model.dart |
| diff --git a/runtime/observatory/lib/src/app/view_model.dart b/runtime/observatory/lib/src/app/view_model.dart |
| index 94edb496bac325876f8b946aac51a476a80b54c7..2f63db8a505e76d8efb84627389b5c84bd485dbd 100644 |
| --- a/runtime/observatory/lib/src/app/view_model.dart |
| +++ b/runtime/observatory/lib/src/app/view_model.dart |
| @@ -4,6 +4,386 @@ |
| part of app; |
| +abstract class VirtualTreeRow { |
| + // Number of ems each subtree is indented. |
| + static const subtreeIndent = 2; |
| + |
| + static const redColor = '#F44336'; |
| + static const blueColor = '#3F51B5'; |
| + static const purpleColor = '#673AB7'; |
| + static const greenColor = '#4CAF50'; |
| + static const orangeColor = '#FF9800'; |
| + static const lightGrayColor = '#FAFAFA'; |
| + |
| + List backgroundColors = const [ |
| + purpleColor, |
| + redColor, |
| + greenColor, |
| + blueColor, |
| + orangeColor, |
| + ]; |
| + |
| + final VirtualTree tree; |
| + final List<VirtualTreeRow> children = []; |
| + final List<StreamSubscription> _listeners = []; |
| + final int depth; |
| + bool _expanded = false; |
| + |
| + VirtualTreeRow(this.tree, this.depth); |
| + |
| + bool get expanded => _expanded; |
| + |
| + set expanded(bool expanded) { |
| + var changed = _expanded != expanded; |
| + _expanded = expanded; |
| + if (!changed) { |
| + return; |
| + } |
| + if (_expanded) { |
| + _expand(); |
| + } else { |
| + _collapse(); |
| + } |
| + } |
| + |
| + Element makeColorBar() { |
| + var element = new SpanElement(); |
| + element.style.paddingLeft = '2px'; |
| + element.style.paddingRight = '2px'; |
| + var flexBasis = '2px'; |
| + element.style.flexBasis = flexBasis; |
|
rmacnak
2016/03/24 23:20:36
Odd for this one to have a variable when the rest
Cutch
2016/03/25 03:04:08
Done here and elsewhere.
|
| + element.style.height = '${tree.rowHeight}px'; |
| + element.style.minHeight = '${tree.rowHeight}px'; |
| + if (depth > 0) { |
| + var colorIndex = (depth - 1) % backgroundColors.length; |
| + element.style.backgroundColor = backgroundColors[colorIndex]; |
| + } |
| + return element; |
| + } |
| + |
| + Element makeExpander() { |
| + SpanElement element = new SpanElement(); |
| + var flexBasis = '2em'; |
| + element.style.flexBasis = flexBasis; |
| + if (!hasChildren()) { |
| + element.style.visibility = 'hidden'; |
| + } else { |
| + element.style.visibility = 'visible'; |
| + element.children.add(expanded ? |
| + new Element.tag('icon-expand-more') : |
| + new Element.tag('icon-chevron-right')); |
| + } |
| + _listeners.add(element.onClick.listen((e) { |
| + e.stopPropagation(); |
| + toggle(); |
| + })); |
| + return element; |
| + } |
| + |
| + Element makeIndenter() { |
| + SpanElement element = new SpanElement(); |
| + var flexBasis = '${subtreeIndent * depth}em'; |
| + element.style.flexBasis = flexBasis; |
| + return element; |
| + } |
| + |
| + Element makeText(String text, {String toolTip, String flexBasis: '7em'}) { |
| + SpanElement element = new SpanElement(); |
| + element.text = text; |
| + if (toolTip != null) { |
| + element.title = toolTip; |
| + } |
| + if (flexBasis != null) { |
| + element.style.flexBasis = flexBasis; |
| + } |
| + return element; |
| + } |
| + |
| + Element makeGap([int ems = 1]) { |
| + SpanElement element = new SpanElement(); |
| + var flexBasis = '${ems}em'; |
| + element.style.flexBasis = flexBasis; |
| + return element; |
| + } |
| + |
| + void _cleanupListeners() { |
| + for (var listener in _listeners) { |
| + listener.cancel(); |
| + } |
| + _listeners.clear(); |
| + } |
| + |
| + void _expand() { |
| + tree._onExpand(this); |
| + } |
| + |
| + void _collapse() { |
| + if (children.length == 0) { |
| + // Nothing to do. |
| + return; |
| + } |
| + for (var i = 0; i < children.length; i++) { |
| + if (children[i].expanded) { |
| + children[i]._collapse(); |
| + } |
| + } |
| + _expanded = false; |
| + tree._onCollapse(this); |
| + } |
| + |
| + void toggle() { |
| + expanded = !expanded; |
| + } |
| + |
| + void _render(DivElement rowDiv) { |
| + rowDiv.style.display = 'flex'; |
| + rowDiv.style.alignItems = 'center'; |
| + _cleanupListeners(); |
| + onShow(); |
| + onRender(rowDiv); |
| + } |
| + |
| + /// Called when you should render into [rowDiv]. |
| + void onRender(DivElement rowDiv); |
| + |
| + // Called when this row is visible. |
| + void onShow(); |
| + |
| + // Return true if this node can be expanded. |
| + bool hasChildren() { |
| + return false; |
| + } |
| + |
| + // Called when this row is not visible. |
| + void onHide() { |
| + _cleanupListeners(); |
| + } |
| +} |
| + |
| +class VirtualTree { |
| + final int rowHeight; |
| + final List<VirtualTreeRow> rows = []; |
| + final DivElement root; |
| + final Stopwatch _clock = new Stopwatch(); |
| + |
| + DivElement _treeHeightElement; |
| + DivElement _tree; |
| + |
| + StreamSubscription _scrollSubscription; |
| + StreamSubscription _resizeSubscription; |
| + Timer _sweeperTimer; |
| + |
| + // Height of [root] in pixels. |
| + int viewHeight; |
| + |
| + // Number of pixels view can be scrolled before a redraw occurs. |
| + int redrawThresholdPixels; |
| + |
| + // Number of rows visible at any given time. |
| + int numVisibleRows; |
| + // Number of rows above the current view that are in the dom. |
| + int extraRowsAbove; |
| + // Number of rows below the current view that are in the dom. |
| + int extraRowsBelow; |
| + |
| + // The time of the last scroll event. |
| + int lastScrollTimeMilliseconds; |
| + |
| + // The scroll top of the last scroll event. |
| + int lastPaintScrollTop; |
| + |
| + // The starting row of the last paint. |
| + int lastPaintStartingRow; |
| + |
| + static const scrollStopThresholdMilliseconds = 100; |
| + |
| + VirtualTree(this.rowHeight, this.root) { |
| + _clock.start(); |
| + _install(); |
| + _resize(); |
| + _paint(0); |
| + } |
| + |
| + void uninstall() => _uninstall(); |
| + |
| + void refresh() { |
| + _resize(); |
| + _paint(lastPaintStartingRow); |
| + } |
| + |
| + // Clear the tree. |
| + void clear() { |
| + rows.clear(); |
| + _resize(); |
| + } |
| + |
| + void _onExpand(VirtualTreeRow parent) { |
| + int index = rows.indexOf(parent); |
| + if (index == -1) { |
| + return; |
| + } |
| + rows.insertAll(index + 1, parent.children); |
| + refresh(); |
| + } |
| + |
| + void _onCollapse(VirtualTreeRow parent) { |
| + int index = rows.indexOf(parent); |
| + if (index == -1) { |
| + return; |
| + } |
| + int start = index + 1; |
| + int end = start + parent.children.length; |
| + rows.removeRange(start, end); |
| + refresh(); |
| + } |
| + |
| + void _resize() { |
| + viewHeight = root.offsetHeight; |
| + numVisibleRows = (viewHeight ~/ rowHeight) + 1; |
| + extraRowsAbove = numVisibleRows ~/ 2; |
| + extraRowsBelow = numVisibleRows - extraRowsAbove; |
| + redrawThresholdPixels = |
| + math.min(extraRowsAbove, extraRowsBelow) * rowHeight; |
| + _treeHeightElement.style.height = '${_treeHeight()}px'; |
| + } |
| + |
| + int _treeHeight() { |
| + return rows.length * rowHeight; |
| + } |
| + |
| + int _now() => _clock.elapsedMilliseconds; |
| + |
| + int _millisecondsSinceLastScroll() { |
| + int now = _now(); |
| + if (lastScrollTimeMilliseconds == null) { |
| + return now; |
| + } |
| + return now - lastScrollTimeMilliseconds; |
| + } |
| + |
| + int _pixelsFromLastScroll(int currentScrollTop) { |
| + if (lastPaintScrollTop == null) { |
| + return currentScrollTop; |
| + } |
| + |
| + return (currentScrollTop - lastPaintScrollTop).abs(); |
| + } |
| + |
| + int _pixelToRow(int pixelY) { |
| + int result = pixelY ~/ rowHeight; |
| + return result; |
| + } |
| + |
| + void _install() { |
| + // This element controls the height of the tree's scrollable region. |
| + // It is one pixel wide and the height is set to rowHeight * numRows. |
| + _treeHeightElement = new DivElement(); |
| + _treeHeightElement.style.position = 'absolute'; |
| + _treeHeightElement.style.top = '0'; |
| + _treeHeightElement.style.left = '0'; |
| + _treeHeightElement.style.width = '1px'; |
| + |
| + // This element holds the visible tree rows and the height controlling |
| + // element. It takes the full width and height of its parent element. |
| + _tree = new DivElement(); |
| + _tree.children.add(_treeHeightElement); |
| + _tree.style.width = '100%'; |
| + _tree.style.height = '100%'; |
| + _tree.style.position = 'relative'; |
| + _tree.style.overflow = 'auto'; |
| + |
| + // Listen for scroll events on the tree. |
| + _scrollSubscription = _tree.onScroll.listen(_onScroll); |
| + |
| + root.children.add(_tree); |
| + |
| + // Listen for resize events. |
| + _resizeSubscription = window.onResize.listen((_) { |
| + _resize(); |
| + int row = |
| + lastPaintStartingRow != null ? lastPaintStartingRow : 0; |
| + _paint(row); |
| + }); |
| + |
| + // Regularly sweep non-visible rows. |
| + _sweeperTimer = |
| + new Timer.periodic(const Duration(milliseconds: 300), _sweepRows); |
| + } |
| + |
| + void _uninstall() { |
| + root.children.clear(); |
| + _scrollSubscription?.cancel(); |
| + _scrollSubscription = null; |
| + _resizeSubscription?.cancel(); |
| + _resizeSubscription = null; |
| + _sweeperTimer?.cancel(); |
| + _sweeperTimer = null; |
| + } |
| + |
| + void _onScroll(Event scrollEvent) { |
| + Element target = scrollEvent.target; |
| + |
| + int scrollTop = target.scrollTop; |
| + |
| + if (_pixelsFromLastScroll(scrollTop) > redrawThresholdPixels) { |
| + int startingRow = math.max(_pixelToRow(scrollTop), 0); |
| + _paint(startingRow); |
| + lastPaintScrollTop = scrollTop; |
| + } |
| + lastScrollTimeMilliseconds = _now(); |
| + scrollEvent.preventDefault(); |
| + } |
| + |
| + void _sweepRows(Timer timer) { |
| + // It hasn't been long enough since the last scroll. |
| + if (_millisecondsSinceLastScroll() <= scrollStopThresholdMilliseconds) { |
| + return; |
| + } |
| + // Find all rows marked with the garbage class. |
| + var garbageRows = _tree.querySelectorAll('.garbage'); |
| + // Remove them from the tree. |
| + for (var row in garbageRows) { |
| + _tree.children.remove(row); |
| + } |
| + } |
| + |
| + void _paint(int startingRow) { |
| + lastPaintStartingRow = startingRow; |
| + |
| + // Hide all existing rows and mark them for collection. The first child |
| + // is the height control element, so we skip that. |
| + for (int i = 1; i < _tree.children.length; i++) { |
| + _tree.children[i].style.display = 'none'; |
| + _tree.children[i].classes.add('garbage'); |
| + } |
| + |
| + int endingRow = |
| + math.min(rows.length, startingRow + numVisibleRows + extraRowsBelow); |
| + |
| + startingRow = |
| + math.max(0, startingRow - extraRowsAbove); |
| + |
| + print('PAINT $startingRow $endingRow'); |
| + |
| + // Create visible rows and insert them into a document fragment. |
| + DocumentFragment fragment = new DocumentFragment(); |
| + for (int i = startingRow; i < endingRow; i++) { |
| + DivElement row = new DivElement(); |
| + row.style.position = 'absolute'; |
| + row.style.height = '${rowHeight}px'; |
| + row.style.maxHeight = '${rowHeight}px'; |
| + row.style.margin = '0'; |
| + row.style.width = '100%'; |
| + row.style.top = '${(i * rowHeight)}px'; |
| + // Render the row. |
| + rows[i]._render(row); |
| + fragment.children.add(row); |
| + } |
| + // Append the fragment to the DOM. |
| + _tree.append(fragment); |
| + } |
| +} |
| + |
| abstract class TableTreeRow extends Observable { |
| static const arrowRight = '\u2192'; |
| static const arrowDownRight = '\u21b3'; |