// 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 _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(); _columnTracks = columns != null ? columns.tracks : new List(); } 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, Completer 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 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 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 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 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 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 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 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 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 _getTracks(ViewLayout item) { GridLayoutParams childLayout = item.layoutParams; int start, span; List 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(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); } }