| Index: client/layout/GridLayout.dart
|
| ===================================================================
|
| --- client/layout/GridLayout.dart (revision 4144)
|
| +++ client/layout/GridLayout.dart (working copy)
|
| @@ -1,520 +0,0 @@
|
| -// 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);
|
| - }
|
| -}
|
|
|