Files
sdk/client/layout/GridLayout.dart
T
jimhug@google.com 42c584b7c4 Revert "Example showing alternate async measurement solution"
This reverts r3473 which was causing several buildbots to go red due to
breaks in swarm tests.

The issue appears to be minor - but the buildbots have been red for too long.

Review URL: https://chromiumcodereview.appspot.com//9145004

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@3474 260f80e4-7a28-3924-810f-c04153c831b5
2012-01-21 13:53:40 +00:00

521 lines
18 KiB
Dart

// 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);
}
}