745f112ef6
Opt an initial batch of files under samples, samples-dev, utils, and runtime/tools/dartfuzz out of null safety in preparation for switching the flag on by default. Change-Id: Icdfd52a5a969e678a7205903332f73fe3841c223 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/166960 Reviewed-by: Daco Harkes <dacoharkes@google.com> Commit-Queue: Leaf Petersen <leafp@google.com>
514 lines
18 KiB
Dart
514 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.
|
|
|
|
// @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);
|
|
}
|
|
}
|