blob: aacebb58b8ff8f3fb8a6d51a6cae22063c09973d [file] [log] [blame]
// 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 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);
}
}