Index: client/layout/GridLayout.dart |
=================================================================== |
--- client/layout/GridLayout.dart (revision 4144) |
+++ client/layout/GridLayout.dart (working copy) |
@@ -1,520 +0,0 @@ |
-// 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. |
- |
-/** |
- * Implements a grid-based layout system based on: |
- * [http://dev.w3.org/csswg/css3-grid-align/] |
- * |
- * This layout is designed to support animations and work on browsers that |
- * don't support grid natively. As such, we implement it on top of absolute |
- * positioning. |
- */ |
-// TODO(jmesserly): the DOM integration still needs work: |
-// - The grid assumes it is absolutely positioned in its container. |
-// Becasue of that, the grid doesn't work right unless it has at least one |
-// fractional size in each dimension. In other words, only "top down" grids |
-// work at the moment, because the grid can't determine its own size. |
-// The core algorithm supports computing min breadth; the issue is about how |
-// to integrate it into our View layer. |
-// - Unless a child element is "display: inline-block" we can't get its |
-// horizontal content size. |
-// - Once we set an element's size to "position: absolute", we lose the |
-// ability to get its original content size. If the width or height gets |
-// set to something other than the content size, we can't recover the |
-// original content size. |
-// - There's some rounding to ints when we want to set the positions of our |
-// tracks. I don't think we necessarily need to do that. |
-// |
-// TODO(jmesserly): Some features of the spec are unimplemented: |
-// - grid-flow & items that have row and column set to 'auto'. |
-// - Grid writing modes (right to left languages, etc) |
-// - We don't do a second calculation pass if min content size of a grid-item |
-// changes due to column width. |
-// - The CSS parsing is not 100% complete, see the parser TODOs. |
-// - We don't implement error recovery for invalid combinations of CSS |
-// properties, or invalid CSS property values. Instead we throw an error. |
-// |
-// TODO(jmesserly): high level performance optimizations we could do: |
-// - Optimize for the common case of spanCount = 1 |
-// - Optimize for the vbox/hbox case (1 row or 1 column) |
-// - Optimize for the case of no content sized tracks |
-// - Optimize for the "incremental update" cases |
-class GridLayout extends ViewLayout { |
- |
- /** Configuration parameters defined in CSS. */ |
- final GridTrackList rows; |
- final GridTrackList columns; |
- final GridTemplate template; |
- |
- /** The default sizing for new rows. */ |
- final TrackSizing rowSizing; |
- |
- /** The default sizing for new columns. */ |
- final TrackSizing columnSizing; |
- |
- /** |
- * This stores the grid's size during a layout. |
- * Used for rows/columns with % or fr units. |
- */ |
- int _gridWidth, _gridHeight; |
- |
- /** |
- * During a layout, this stores all row/column size information. |
- * Because grid-items can implicitly specify their own rows/columns, we can't |
- * compute this until we know the set of items. |
- */ |
- List<GridTrack> _rowTracks, _columnTracks; |
- |
- /** During a layout, tracks which dimension we're processing. */ |
- Dimension _dimension; |
- |
- GridLayout(Positionable view) |
- : super(view), |
- rows = _GridTrackParser.parse(view.customStyle['grid-rows']), |
- columns = _GridTrackParser.parse(view.customStyle['grid-columns']), |
- template = _GridTemplateParser.parse(view.customStyle['grid-template']), |
- |
- rowSizing = _GridTrackParser.parseTrackSizing( |
- view.customStyle['grid-row-sizing']), |
- |
- columnSizing = _GridTrackParser.parseTrackSizing( |
- view.customStyle['grid-column-sizing']) { |
- |
- _rowTracks = rows != null ? rows.tracks : new List<GridTrack>(); |
- _columnTracks = columns != null ? columns.tracks : new List<GridTrack>(); |
- } |
- |
- |
- int get currentWidth() => _gridWidth; |
- int get currentHeight() => _gridHeight; |
- |
- void cacheExistingBrowserLayout() { |
- // We don't need to do anything as we don't rely on the _cachedViewRect |
- // when the grid layout is used. |
- } |
- |
- // TODO(jacobr): cleanup this method so that it returns a Future |
- // rather than taking a Completer as an argument. |
- /** The main entry point for layout computation. */ |
- void measureLayout(Future<Size> size, Completer<bool> changed) { |
- _ensureAllTracks(); |
- window.requestLayoutFrame(() { |
- _gridWidth = size.value.width; |
- _gridHeight = size.value.height; |
- |
- if (_rowTracks.length > 0 && _columnTracks.length > 0) { |
- _measureTracks(); |
- _setBoundsOfChildren(); |
- if (changed != null) { |
- changed.complete(true); |
- } |
- } |
- }); |
- } |
- |
- /** |
- * The top level measurement function. |
- * [http://dev.w3.org/csswg/css3-grid-align/#calculating-size-of-grid-tracks] |
- */ |
- void _measureTracks() { |
- // Resolve logical width, then height. Width comes first so we can use |
- // the width when determining the content-sized height. |
- try { |
- _dimension = Dimension.WIDTH; |
- _computeUsedBreadthOfTracks(_columnTracks); |
- _dimension = Dimension.HEIGHT; |
- _computeUsedBreadthOfTracks(_rowTracks); |
- } finally { |
- _dimension = null; |
- } |
- |
- // TODO(jmesserly): we're supposed to detect a min-content size change |
- // due to our computed width and trigger a new layout. |
- // How do we implement that? |
- } |
- |
- num _getRemainingSpace(List<GridTrack> tracks) { |
- num remaining = _getGridContentSize(); |
- remaining -= CollectionUtils.sum(tracks, (t) => t.usedBreadth); |
- return Math.max(0, remaining); |
- } |
- |
- /** |
- * This is the core Grid Track sizing algorithm. It is run for Grid columns |
- * and Grid rows. The goal of the function is to ensure: |
- * 1. That each Grid Track satisfies its minSizing |
- * 2. That each Grid Track grows from the breadth which satisfied its |
- * minSizing to a breadth which satifies its |
- * maxSizing, subject to RemainingSpace. |
- */ |
- // Note: spec does not correctly doc all the parameters to this function. |
- void _computeUsedBreadthOfTracks(List<GridTrack> tracks) { |
- |
- // TODO(jmesserly): as a performance optimization we could cache this |
- final items = CollectionUtils.map(view.childViews, (view_) => view_.layout); |
- CollectionUtils.sortBy(items, (item) => _getSpanCount(item)); |
- |
- // 1. Initialize per Grid Track variables |
- for (final t in tracks) { |
- // percentage or length sizing functions will return a value |
- // min-content, max-content, or a fraction will be set to 0 |
- t.usedBreadth = t.minSizing.resolveLength(_getGridContentSize()); |
- t.maxBreadth = t.maxSizing.resolveLength(_getGridContentSize()); |
- t.updatedBreadth = 0; |
- } |
- |
- // 2. Resolve content-based MinTrackSizingFunctions |
- final USED_BREADTH = const _UsedBreadthAccumulator(); |
- final MAX_BREADTH = const _MaxBreadthAccumulator(); |
- |
- _distributeSpaceBySpanCount(items, ContentSizeMode.MIN, USED_BREADTH); |
- |
- _distributeSpaceBySpanCount(items, ContentSizeMode.MAX, USED_BREADTH); |
- |
- // 3. Ensure that maxBreadth is as big as usedBreadth for each track |
- for (final t in tracks) { |
- if (t.maxBreadth < t.usedBreadth) { |
- t.maxBreadth = t.usedBreadth; |
- } |
- } |
- |
- // 4. Resolve content-based MaxTrackSizingFunctions |
- _distributeSpaceBySpanCount(items, ContentSizeMode.MIN, MAX_BREADTH); |
- |
- _distributeSpaceBySpanCount(items, ContentSizeMode.MAX, MAX_BREADTH); |
- |
- // 5. Grow all Grid Tracks in GridTracks from their usedBreadth up to their |
- // maxBreadth value until RemainingSpace is exhausted. |
- // Note: it's not spec'd what to pass as the accumulator, but usedBreadth |
- // seems right. |
- _distributeSpaceToTracks(tracks, _getRemainingSpace(tracks), |
- USED_BREADTH, false); |
- |
- // Spec wording is confusing about which direction this assignment happens, |
- // but this is the way that makes sense. |
- for (final t in tracks) { |
- t.usedBreadth = t.updatedBreadth; |
- } |
- |
- // 6. Grow all Grid Tracks having a fraction as their maxSizing |
- final tempBreadth = _calcNormalizedFractionBreadth(tracks); |
- for (final t in tracks) { |
- t.usedBreadth = Math.max(t.usedBreadth, |
- tempBreadth * t.maxSizing.fractionValue); |
- } |
- |
- _computeTrackPositions(tracks); |
- } |
- |
- /** |
- * Final steps to finish positioning tracks. Takes the track size and uses |
- * it to get start and end positions. Also rounds the positions to integers. |
- */ |
- void _computeTrackPositions(List<GridTrack> tracks) { |
- // Compute start positions of tracks, as well as the final position |
- |
- num position = 0; |
- for (final t in tracks) { |
- t.start = position; |
- position += t.usedBreadth; |
- } |
- |
- // Now, go through and round each position to an integer. Then |
- // compute the sizes based on those integers. |
- num finalPosition = position; |
- |
- for (int i = 0; i < tracks.length; i++) { |
- int startEdge = tracks[i].start; |
- int endEdge; |
- if (i < tracks.length - 1) { |
- endEdge = tracks[i + 1].start.round().toInt(); |
- tracks[i + 1].start = endEdge; |
- } else { |
- endEdge = finalPosition.round().toInt(); |
- } |
- int breadth = endEdge - startEdge; |
- |
- // check that we're not off by >= 1px. |
- assert((endEdge - startEdge - tracks[i].usedBreadth).abs() < 1); |
- |
- tracks[i].usedBreadth = breadth; |
- } |
- } |
- |
- /** |
- * This method computes a '1fr' value, referred to as the |
- * tempBreadth, for a set of Grid Tracks. The value computed |
- * will ensure that when the tempBreadth is multiplied by the |
- * fractions associated with tracks, that the UsedBreadths of tracks |
- * will increase by an amount equal to the maximum of zero and the specified |
- * freeSpace less the sum of the current UsedBreadths. |
- */ |
- num _calcNormalizedFractionBreadth(List<GridTrack> tracks) { |
- |
- final fractionTracks = tracks.filter((t) => t.maxSizing.isFraction); |
- |
- // Note: the spec has various bugs in this function, such as mismatched |
- // identifiers and names that aren't defined. For the most part it's |
- // possible to figure out the meaning. It's also a bit confused about |
- // how to compute spaceNeededFromFractionTracks, but that should just be the |
- // set to the remaining free space after usedBreadth is accounted for. |
- |
- // We use the tempBreadth field to store the normalized fraction breadth |
- for (final t in fractionTracks) { |
- t.tempBreadth = t.usedBreadth / t.maxSizing.fractionValue; |
- } |
- |
- CollectionUtils.sortBy(fractionTracks, (t) => t.tempBreadth); |
- |
- num spaceNeededFromFractionTracks = _getRemainingSpace(tracks); |
- num currentBandFractionBreadth = 0; |
- num accumulatedFractions = 0; |
- for (final t in fractionTracks) { |
- if (t.tempBreadth != currentBandFractionBreadth) { |
- if (t.tempBreadth * accumulatedFractions > |
- spaceNeededFromFractionTracks) { |
- break; |
- } |
- currentBandFractionBreadth = t.tempBreadth; |
- } |
- accumulatedFractions += t.maxSizing.fractionValue; |
- spaceNeededFromFractionTracks += t.usedBreadth; |
- } |
- return spaceNeededFromFractionTracks / accumulatedFractions; |
- } |
- |
- /** |
- * Ensures that for each Grid Track in tracks, a value will be |
- * computed, updatedBreadth, that represents the Grid Track's share of |
- * freeSpace. |
- */ |
- void _distributeSpaceToTracks(List<GridTrack> tracks, num freeSpace, |
- _BreadthAccumulator breadth, bool ignoreMaxBreadth) { |
- |
- // TODO(jmesserly): in some cases it would be safe to sort the passed in |
- // list in place. Not always though. |
- tracks = CollectionUtils.orderBy(tracks, |
- (t) => t.maxBreadth - breadth.getSize(t)); |
- |
- // Give each Grid Track an equal share of the space, but without exceeding |
- // their maxBreadth values. Because there are different MaxBreadths |
- // assigned to the different Grid Tracks, this can result in uneven growth. |
- for (int i = 0; i < tracks.length; i++) { |
- num share = freeSpace / (tracks.length - i); |
- share = Math.min(share, tracks[i].maxBreadth); |
- tracks[i].tempBreadth = share; |
- freeSpace -= share; |
- } |
- |
- // If the first loop completed having grown every Grid Track to its |
- // maxBreadth, and there is still freeSpace, then divide that space |
- // evenly and assign it to each Grid Track without regard for its |
- // maxBreadth. This phase of growth will always be even, but only occurs |
- // when the ignoreMaxBreadth flag is true. |
- if (freeSpace > 0 && ignoreMaxBreadth) { |
- for (int i = 0; i < tracks.length; i++) { |
- final share = freeSpace / (tracks.length - i); |
- tracks[i].tempBreadth += share; |
- freeSpace -= share; |
- } |
- } |
- |
- // Note: the spec has us updating all grid tracks, not just the passed in |
- // tracks, but I think that's a spec bug. |
- for (final t in tracks) { |
- t.updatedBreadth = Math.max(t.updatedBreadth, t.tempBreadth); |
- } |
- } |
- |
- /** |
- * This function prioritizes the distribution of space driven by Grid Items |
- * in content-sized Grid Tracks by the Grid Item's spanCount. That is, Grid |
- * Items having a lower spanCount have an opportunity to increase the size of |
- * the Grid Tracks they cover before those with larger SpanCounts. |
- * |
- * Note: items are assumed to be already sorted in increasing span count |
- */ |
- void _distributeSpaceBySpanCount(List<ViewLayout> items, |
- ContentSizeMode sizeMode, _BreadthAccumulator breadth) { |
- |
- items = items.filter((item) => |
- _hasContentSizedTracks(_getTracks(item), sizeMode, breadth)); |
- |
- var tracks = []; |
- |
- for (int i = 0; i < items.length; i++) { |
- final item = items[i]; |
- |
- final itemTargetSize = item.measureContent(this, _dimension, sizeMode); |
- |
- final spannedTracks = _getTracks(item); |
- _distributeSpaceToTracks(spannedTracks, itemTargetSize, breadth, true); |
- |
- // Remember that we need to update the sizes on these tracks |
- tracks.addAll(spannedTracks); |
- |
- // Each time we transition to a new spanCount, update any modified tracks |
- bool spanCountFinished = false; |
- if (i + 1 == items.length) { |
- spanCountFinished = true; |
- } else if (_getSpanCount(item) != _getSpanCount(items[i + 1])) { |
- spanCountFinished = true; |
- } |
- |
- if (spanCountFinished) { |
- for (final t in tracks) { |
- breadth.setSize(t, |
- Math.max(breadth.getSize(t), t.updatedBreadth)); |
- } |
- tracks = []; |
- } |
- } |
- } |
- |
- /** |
- * Returns true if we have an appropriate content sized dimension, and don't |
- * cross a fractional track. |
- */ |
- static bool _hasContentSizedTracks(Collection<GridTrack> tracks, |
- ContentSizeMode sizeMode, _BreadthAccumulator breadth) { |
- |
- for (final t in tracks) { |
- final fn = breadth.getSizingFunction(t); |
- if (sizeMode == ContentSizeMode.MAX && fn.isMaxContentSized || |
- sizeMode == ContentSizeMode.MIN && fn.isContentSized) { |
- |
- // Make sure we don't cross a fractional track |
- return tracks.length == 1 || !tracks.some((t_) => t_.isFractional); |
- } |
- } |
- return false; |
- } |
- |
- /** Ensures that the numbered track exists. */ |
- void _ensureTrack(List<GridTrack> tracks, TrackSizing sizing, |
- int start, int span) { |
- // Start is 1-based. Make it 0-based. |
- start -= 1; |
- |
- // Grow the list if needed |
- int length = start + span; |
- int first = Math.min(start, tracks.length); |
- tracks.length = Math.max(tracks.length, length); |
- |
- // Fill in tracks |
- for (int i = first; i < length; i++) { |
- if (tracks[i] == null) { |
- tracks[i] = new GridTrack(sizing); |
- } |
- } |
- } |
- |
- /** |
- * Scans children creating GridLayoutParams as needed, and creates all of the |
- * rows and columns that we will need. |
- * |
- * Note: this can potentially create new rows/columns, so this needs to be |
- * run before the track sizing algorithm. |
- */ |
- void _ensureAllTracks() { |
- final items = CollectionUtils.map(view.childViews, (view_) => view_.layout); |
- |
- for (final child in items) { |
- if (child.layoutParams == null) { |
- final p = new GridLayoutParams(child.view, this); |
- _ensureTrack(_rowTracks, rowSizing, p.row, p.rowSpan); |
- _ensureTrack(_columnTracks, columnSizing, p.column, p.columnSpan); |
- child.layoutParams = p; |
- } |
- child.cacheExistingBrowserLayout(); |
- } |
- } |
- |
- /** |
- * Given the track sizes that were computed, position children in the grid. |
- */ |
- void _setBoundsOfChildren() { |
- final items = CollectionUtils.map(view.childViews, (view_) => view_.layout); |
- |
- for (final item in items) { |
- GridLayoutParams childLayout = item.layoutParams; |
- var xPos = _getTrackLocationX(childLayout); |
- var yPos = _getTrackLocationY(childLayout); |
- |
- int left = xPos.start, width = xPos.length; |
- int top = yPos.start, height = yPos.length; |
- |
- // Somewhat counterintuitively (at least to me): |
- // grid-col-align is the horizontal alignment |
- // grid-row-align is the vertical alignment |
- xPos = childLayout.columnAlign.align(xPos, item.currentWidth); |
- yPos = childLayout.rowAlign.align(yPos, item.currentHeight); |
- |
- item.setBounds(xPos.start, yPos.start, xPos.length, yPos.length); |
- } |
- } |
- |
- num _getGridContentSize() { |
- switch (_dimension) { |
- case Dimension.WIDTH: |
- return _gridWidth; |
- case Dimension.HEIGHT: |
- return _gridHeight; |
- } |
- } |
- |
- _GridLocation _getTrackLocationX(GridLayoutParams childLayout) { |
- int start = childLayout.column - 1; |
- int end = start + childLayout.columnSpan - 1; |
- |
- start = _columnTracks[start].start; |
- end = _columnTracks[end].end; |
- |
- return new _GridLocation(start, end - start); |
- } |
- |
- _GridLocation _getTrackLocationY(GridLayoutParams childLayout) { |
- int start = childLayout.row - 1; |
- int end = start + childLayout.rowSpan - 1; |
- |
- start = _rowTracks[start].start; |
- end = _rowTracks[end].end; |
- |
- return new _GridLocation(start, end - start); |
- } |
- |
- /** Gets the tracks that this item crosses. */ |
- // TODO(jmesserly): might be better to return an iterable |
- List<GridTrack> _getTracks(ViewLayout item) { |
- GridLayoutParams childLayout = item.layoutParams; |
- |
- int start, span; |
- List<GridTrack> tracks; |
- switch (_dimension) { |
- case Dimension.WIDTH: |
- start = childLayout.column - 1; |
- span = childLayout.columnSpan; |
- tracks = _columnTracks; |
- break; |
- case Dimension.HEIGHT: |
- start = childLayout.row - 1; |
- span = childLayout.rowSpan; |
- tracks = _rowTracks; |
- } |
- |
- assert(start >= 0 && span >= 1); |
- |
- final result = new List<GridTrack>(span); |
- for (int i = 0; i < span; i++) { |
- result[i] = tracks[start + i]; |
- } |
- return result; |
- } |
- |
- int _getSpanCount(ViewLayout item) { |
- GridLayoutParams childLayout = item.layoutParams; |
- return (_dimension == Dimension.WIDTH ? |
- childLayout.columnSpan : childLayout.rowSpan); |
- } |
-} |