| // 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. |
| |
| // @dart = 2.9 |
| |
| |
| part of layout; |
| |
| /** |
| * 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. |
| // Because 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) |
| : 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']), |
| super(view) { |
| _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(); |
| size.then((value) { |
| _gridWidth = value.width; |
| _gridHeight = 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 = view.childViews.map((view_) => view_.layout).toList(); |
| 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(); |
| tracks[i + 1].start = endEdge; |
| } else { |
| endEdge = finalPosition.round(); |
| } |
| 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.where((t) => t.maxSizing.isFraction).toList(); |
| |
| // 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 |
| .where((item) => |
| _hasContentSizedTracks(_getTracks(item), sizeMode, breadth)) |
| .toList(); |
| |
| 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(Iterable<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.any((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 = view.childViews.map((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 = view.childViews.map((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() { |
| if (_dimension == Dimension.WIDTH) { |
| return _gridWidth; |
| } else if (_dimension == 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; |
| if (_dimension == Dimension.WIDTH) { |
| start = childLayout.column - 1; |
| span = childLayout.columnSpan; |
| tracks = _columnTracks; |
| } else if (_dimension == 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); |
| } |
| } |