OLD | NEW |
| (Empty) |
1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 /** | |
6 * Implements a grid-based layout system based on: | |
7 * [http://dev.w3.org/csswg/css3-grid-align/] | |
8 * | |
9 * This layout is designed to support animations and work on browsers that | |
10 * don't support grid natively. As such, we implement it on top of absolute | |
11 * positioning. | |
12 */ | |
13 // TODO(jmesserly): the DOM integration still needs work: | |
14 // - The grid assumes it is absolutely positioned in its container. | |
15 // Becasue of that, the grid doesn't work right unless it has at least one | |
16 // fractional size in each dimension. In other words, only "top down" grids | |
17 // work at the moment, because the grid can't determine its own size. | |
18 // The core algorithm supports computing min breadth; the issue is about how | |
19 // to integrate it into our View layer. | |
20 // - Unless a child element is "display: inline-block" we can't get its | |
21 // horizontal content size. | |
22 // - Once we set an element's size to "position: absolute", we lose the | |
23 // ability to get its original content size. If the width or height gets | |
24 // set to something other than the content size, we can't recover the | |
25 // original content size. | |
26 // - There's some rounding to ints when we want to set the positions of our | |
27 // tracks. I don't think we necessarily need to do that. | |
28 // | |
29 // TODO(jmesserly): Some features of the spec are unimplemented: | |
30 // - grid-flow & items that have row and column set to 'auto'. | |
31 // - Grid writing modes (right to left languages, etc) | |
32 // - We don't do a second calculation pass if min content size of a grid-item | |
33 // changes due to column width. | |
34 // - The CSS parsing is not 100% complete, see the parser TODOs. | |
35 // - We don't implement error recovery for invalid combinations of CSS | |
36 // properties, or invalid CSS property values. Instead we throw an error. | |
37 // | |
38 // TODO(jmesserly): high level performance optimizations we could do: | |
39 // - Optimize for the common case of spanCount = 1 | |
40 // - Optimize for the vbox/hbox case (1 row or 1 column) | |
41 // - Optimize for the case of no content sized tracks | |
42 // - Optimize for the "incremental update" cases | |
43 class GridLayout extends ViewLayout { | |
44 | |
45 /** Configuration parameters defined in CSS. */ | |
46 final GridTrackList rows; | |
47 final GridTrackList columns; | |
48 final GridTemplate template; | |
49 | |
50 /** The default sizing for new rows. */ | |
51 final TrackSizing rowSizing; | |
52 | |
53 /** The default sizing for new columns. */ | |
54 final TrackSizing columnSizing; | |
55 | |
56 /** | |
57 * This stores the grid's size during a layout. | |
58 * Used for rows/columns with % or fr units. | |
59 */ | |
60 int _gridWidth, _gridHeight; | |
61 | |
62 /** | |
63 * During a layout, this stores all row/column size information. | |
64 * Because grid-items can implicitly specify their own rows/columns, we can't | |
65 * compute this until we know the set of items. | |
66 */ | |
67 List<GridTrack> _rowTracks, _columnTracks; | |
68 | |
69 /** During a layout, tracks which dimension we're processing. */ | |
70 Dimension _dimension; | |
71 | |
72 GridLayout(Positionable view) | |
73 : super(view), | |
74 rows = _GridTrackParser.parse(view.customStyle['grid-rows']), | |
75 columns = _GridTrackParser.parse(view.customStyle['grid-columns']), | |
76 template = _GridTemplateParser.parse(view.customStyle['grid-template']), | |
77 | |
78 rowSizing = _GridTrackParser.parseTrackSizing( | |
79 view.customStyle['grid-row-sizing']), | |
80 | |
81 columnSizing = _GridTrackParser.parseTrackSizing( | |
82 view.customStyle['grid-column-sizing']) { | |
83 | |
84 _rowTracks = rows != null ? rows.tracks : new List<GridTrack>(); | |
85 _columnTracks = columns != null ? columns.tracks : new List<GridTrack>(); | |
86 } | |
87 | |
88 | |
89 int get currentWidth() => _gridWidth; | |
90 int get currentHeight() => _gridHeight; | |
91 | |
92 void cacheExistingBrowserLayout() { | |
93 // We don't need to do anything as we don't rely on the _cachedViewRect | |
94 // when the grid layout is used. | |
95 } | |
96 | |
97 // TODO(jacobr): cleanup this method so that it returns a Future | |
98 // rather than taking a Completer as an argument. | |
99 /** The main entry point for layout computation. */ | |
100 void measureLayout(Future<Size> size, Completer<bool> changed) { | |
101 _ensureAllTracks(); | |
102 window.requestLayoutFrame(() { | |
103 _gridWidth = size.value.width; | |
104 _gridHeight = size.value.height; | |
105 | |
106 if (_rowTracks.length > 0 && _columnTracks.length > 0) { | |
107 _measureTracks(); | |
108 _setBoundsOfChildren(); | |
109 if (changed != null) { | |
110 changed.complete(true); | |
111 } | |
112 } | |
113 }); | |
114 } | |
115 | |
116 /** | |
117 * The top level measurement function. | |
118 * [http://dev.w3.org/csswg/css3-grid-align/#calculating-size-of-grid-tracks] | |
119 */ | |
120 void _measureTracks() { | |
121 // Resolve logical width, then height. Width comes first so we can use | |
122 // the width when determining the content-sized height. | |
123 try { | |
124 _dimension = Dimension.WIDTH; | |
125 _computeUsedBreadthOfTracks(_columnTracks); | |
126 _dimension = Dimension.HEIGHT; | |
127 _computeUsedBreadthOfTracks(_rowTracks); | |
128 } finally { | |
129 _dimension = null; | |
130 } | |
131 | |
132 // TODO(jmesserly): we're supposed to detect a min-content size change | |
133 // due to our computed width and trigger a new layout. | |
134 // How do we implement that? | |
135 } | |
136 | |
137 num _getRemainingSpace(List<GridTrack> tracks) { | |
138 num remaining = _getGridContentSize(); | |
139 remaining -= CollectionUtils.sum(tracks, (t) => t.usedBreadth); | |
140 return Math.max(0, remaining); | |
141 } | |
142 | |
143 /** | |
144 * This is the core Grid Track sizing algorithm. It is run for Grid columns | |
145 * and Grid rows. The goal of the function is to ensure: | |
146 * 1. That each Grid Track satisfies its minSizing | |
147 * 2. That each Grid Track grows from the breadth which satisfied its | |
148 * minSizing to a breadth which satifies its | |
149 * maxSizing, subject to RemainingSpace. | |
150 */ | |
151 // Note: spec does not correctly doc all the parameters to this function. | |
152 void _computeUsedBreadthOfTracks(List<GridTrack> tracks) { | |
153 | |
154 // TODO(jmesserly): as a performance optimization we could cache this | |
155 final items = CollectionUtils.map(view.childViews, (view_) => view_.layout); | |
156 CollectionUtils.sortBy(items, (item) => _getSpanCount(item)); | |
157 | |
158 // 1. Initialize per Grid Track variables | |
159 for (final t in tracks) { | |
160 // percentage or length sizing functions will return a value | |
161 // min-content, max-content, or a fraction will be set to 0 | |
162 t.usedBreadth = t.minSizing.resolveLength(_getGridContentSize()); | |
163 t.maxBreadth = t.maxSizing.resolveLength(_getGridContentSize()); | |
164 t.updatedBreadth = 0; | |
165 } | |
166 | |
167 // 2. Resolve content-based MinTrackSizingFunctions | |
168 final USED_BREADTH = const _UsedBreadthAccumulator(); | |
169 final MAX_BREADTH = const _MaxBreadthAccumulator(); | |
170 | |
171 _distributeSpaceBySpanCount(items, ContentSizeMode.MIN, USED_BREADTH); | |
172 | |
173 _distributeSpaceBySpanCount(items, ContentSizeMode.MAX, USED_BREADTH); | |
174 | |
175 // 3. Ensure that maxBreadth is as big as usedBreadth for each track | |
176 for (final t in tracks) { | |
177 if (t.maxBreadth < t.usedBreadth) { | |
178 t.maxBreadth = t.usedBreadth; | |
179 } | |
180 } | |
181 | |
182 // 4. Resolve content-based MaxTrackSizingFunctions | |
183 _distributeSpaceBySpanCount(items, ContentSizeMode.MIN, MAX_BREADTH); | |
184 | |
185 _distributeSpaceBySpanCount(items, ContentSizeMode.MAX, MAX_BREADTH); | |
186 | |
187 // 5. Grow all Grid Tracks in GridTracks from their usedBreadth up to their | |
188 // maxBreadth value until RemainingSpace is exhausted. | |
189 // Note: it's not spec'd what to pass as the accumulator, but usedBreadth | |
190 // seems right. | |
191 _distributeSpaceToTracks(tracks, _getRemainingSpace(tracks), | |
192 USED_BREADTH, false); | |
193 | |
194 // Spec wording is confusing about which direction this assignment happens, | |
195 // but this is the way that makes sense. | |
196 for (final t in tracks) { | |
197 t.usedBreadth = t.updatedBreadth; | |
198 } | |
199 | |
200 // 6. Grow all Grid Tracks having a fraction as their maxSizing | |
201 final tempBreadth = _calcNormalizedFractionBreadth(tracks); | |
202 for (final t in tracks) { | |
203 t.usedBreadth = Math.max(t.usedBreadth, | |
204 tempBreadth * t.maxSizing.fractionValue); | |
205 } | |
206 | |
207 _computeTrackPositions(tracks); | |
208 } | |
209 | |
210 /** | |
211 * Final steps to finish positioning tracks. Takes the track size and uses | |
212 * it to get start and end positions. Also rounds the positions to integers. | |
213 */ | |
214 void _computeTrackPositions(List<GridTrack> tracks) { | |
215 // Compute start positions of tracks, as well as the final position | |
216 | |
217 num position = 0; | |
218 for (final t in tracks) { | |
219 t.start = position; | |
220 position += t.usedBreadth; | |
221 } | |
222 | |
223 // Now, go through and round each position to an integer. Then | |
224 // compute the sizes based on those integers. | |
225 num finalPosition = position; | |
226 | |
227 for (int i = 0; i < tracks.length; i++) { | |
228 int startEdge = tracks[i].start; | |
229 int endEdge; | |
230 if (i < tracks.length - 1) { | |
231 endEdge = tracks[i + 1].start.round().toInt(); | |
232 tracks[i + 1].start = endEdge; | |
233 } else { | |
234 endEdge = finalPosition.round().toInt(); | |
235 } | |
236 int breadth = endEdge - startEdge; | |
237 | |
238 // check that we're not off by >= 1px. | |
239 assert((endEdge - startEdge - tracks[i].usedBreadth).abs() < 1); | |
240 | |
241 tracks[i].usedBreadth = breadth; | |
242 } | |
243 } | |
244 | |
245 /** | |
246 * This method computes a '1fr' value, referred to as the | |
247 * tempBreadth, for a set of Grid Tracks. The value computed | |
248 * will ensure that when the tempBreadth is multiplied by the | |
249 * fractions associated with tracks, that the UsedBreadths of tracks | |
250 * will increase by an amount equal to the maximum of zero and the specified | |
251 * freeSpace less the sum of the current UsedBreadths. | |
252 */ | |
253 num _calcNormalizedFractionBreadth(List<GridTrack> tracks) { | |
254 | |
255 final fractionTracks = tracks.filter((t) => t.maxSizing.isFraction); | |
256 | |
257 // Note: the spec has various bugs in this function, such as mismatched | |
258 // identifiers and names that aren't defined. For the most part it's | |
259 // possible to figure out the meaning. It's also a bit confused about | |
260 // how to compute spaceNeededFromFractionTracks, but that should just be the | |
261 // set to the remaining free space after usedBreadth is accounted for. | |
262 | |
263 // We use the tempBreadth field to store the normalized fraction breadth | |
264 for (final t in fractionTracks) { | |
265 t.tempBreadth = t.usedBreadth / t.maxSizing.fractionValue; | |
266 } | |
267 | |
268 CollectionUtils.sortBy(fractionTracks, (t) => t.tempBreadth); | |
269 | |
270 num spaceNeededFromFractionTracks = _getRemainingSpace(tracks); | |
271 num currentBandFractionBreadth = 0; | |
272 num accumulatedFractions = 0; | |
273 for (final t in fractionTracks) { | |
274 if (t.tempBreadth != currentBandFractionBreadth) { | |
275 if (t.tempBreadth * accumulatedFractions > | |
276 spaceNeededFromFractionTracks) { | |
277 break; | |
278 } | |
279 currentBandFractionBreadth = t.tempBreadth; | |
280 } | |
281 accumulatedFractions += t.maxSizing.fractionValue; | |
282 spaceNeededFromFractionTracks += t.usedBreadth; | |
283 } | |
284 return spaceNeededFromFractionTracks / accumulatedFractions; | |
285 } | |
286 | |
287 /** | |
288 * Ensures that for each Grid Track in tracks, a value will be | |
289 * computed, updatedBreadth, that represents the Grid Track's share of | |
290 * freeSpace. | |
291 */ | |
292 void _distributeSpaceToTracks(List<GridTrack> tracks, num freeSpace, | |
293 _BreadthAccumulator breadth, bool ignoreMaxBreadth) { | |
294 | |
295 // TODO(jmesserly): in some cases it would be safe to sort the passed in | |
296 // list in place. Not always though. | |
297 tracks = CollectionUtils.orderBy(tracks, | |
298 (t) => t.maxBreadth - breadth.getSize(t)); | |
299 | |
300 // Give each Grid Track an equal share of the space, but without exceeding | |
301 // their maxBreadth values. Because there are different MaxBreadths | |
302 // assigned to the different Grid Tracks, this can result in uneven growth. | |
303 for (int i = 0; i < tracks.length; i++) { | |
304 num share = freeSpace / (tracks.length - i); | |
305 share = Math.min(share, tracks[i].maxBreadth); | |
306 tracks[i].tempBreadth = share; | |
307 freeSpace -= share; | |
308 } | |
309 | |
310 // If the first loop completed having grown every Grid Track to its | |
311 // maxBreadth, and there is still freeSpace, then divide that space | |
312 // evenly and assign it to each Grid Track without regard for its | |
313 // maxBreadth. This phase of growth will always be even, but only occurs | |
314 // when the ignoreMaxBreadth flag is true. | |
315 if (freeSpace > 0 && ignoreMaxBreadth) { | |
316 for (int i = 0; i < tracks.length; i++) { | |
317 final share = freeSpace / (tracks.length - i); | |
318 tracks[i].tempBreadth += share; | |
319 freeSpace -= share; | |
320 } | |
321 } | |
322 | |
323 // Note: the spec has us updating all grid tracks, not just the passed in | |
324 // tracks, but I think that's a spec bug. | |
325 for (final t in tracks) { | |
326 t.updatedBreadth = Math.max(t.updatedBreadth, t.tempBreadth); | |
327 } | |
328 } | |
329 | |
330 /** | |
331 * This function prioritizes the distribution of space driven by Grid Items | |
332 * in content-sized Grid Tracks by the Grid Item's spanCount. That is, Grid | |
333 * Items having a lower spanCount have an opportunity to increase the size of | |
334 * the Grid Tracks they cover before those with larger SpanCounts. | |
335 * | |
336 * Note: items are assumed to be already sorted in increasing span count | |
337 */ | |
338 void _distributeSpaceBySpanCount(List<ViewLayout> items, | |
339 ContentSizeMode sizeMode, _BreadthAccumulator breadth) { | |
340 | |
341 items = items.filter((item) => | |
342 _hasContentSizedTracks(_getTracks(item), sizeMode, breadth)); | |
343 | |
344 var tracks = []; | |
345 | |
346 for (int i = 0; i < items.length; i++) { | |
347 final item = items[i]; | |
348 | |
349 final itemTargetSize = item.measureContent(this, _dimension, sizeMode); | |
350 | |
351 final spannedTracks = _getTracks(item); | |
352 _distributeSpaceToTracks(spannedTracks, itemTargetSize, breadth, true); | |
353 | |
354 // Remember that we need to update the sizes on these tracks | |
355 tracks.addAll(spannedTracks); | |
356 | |
357 // Each time we transition to a new spanCount, update any modified tracks | |
358 bool spanCountFinished = false; | |
359 if (i + 1 == items.length) { | |
360 spanCountFinished = true; | |
361 } else if (_getSpanCount(item) != _getSpanCount(items[i + 1])) { | |
362 spanCountFinished = true; | |
363 } | |
364 | |
365 if (spanCountFinished) { | |
366 for (final t in tracks) { | |
367 breadth.setSize(t, | |
368 Math.max(breadth.getSize(t), t.updatedBreadth)); | |
369 } | |
370 tracks = []; | |
371 } | |
372 } | |
373 } | |
374 | |
375 /** | |
376 * Returns true if we have an appropriate content sized dimension, and don't | |
377 * cross a fractional track. | |
378 */ | |
379 static bool _hasContentSizedTracks(Collection<GridTrack> tracks, | |
380 ContentSizeMode sizeMode, _BreadthAccumulator breadth) { | |
381 | |
382 for (final t in tracks) { | |
383 final fn = breadth.getSizingFunction(t); | |
384 if (sizeMode == ContentSizeMode.MAX && fn.isMaxContentSized || | |
385 sizeMode == ContentSizeMode.MIN && fn.isContentSized) { | |
386 | |
387 // Make sure we don't cross a fractional track | |
388 return tracks.length == 1 || !tracks.some((t_) => t_.isFractional); | |
389 } | |
390 } | |
391 return false; | |
392 } | |
393 | |
394 /** Ensures that the numbered track exists. */ | |
395 void _ensureTrack(List<GridTrack> tracks, TrackSizing sizing, | |
396 int start, int span) { | |
397 // Start is 1-based. Make it 0-based. | |
398 start -= 1; | |
399 | |
400 // Grow the list if needed | |
401 int length = start + span; | |
402 int first = Math.min(start, tracks.length); | |
403 tracks.length = Math.max(tracks.length, length); | |
404 | |
405 // Fill in tracks | |
406 for (int i = first; i < length; i++) { | |
407 if (tracks[i] == null) { | |
408 tracks[i] = new GridTrack(sizing); | |
409 } | |
410 } | |
411 } | |
412 | |
413 /** | |
414 * Scans children creating GridLayoutParams as needed, and creates all of the | |
415 * rows and columns that we will need. | |
416 * | |
417 * Note: this can potentially create new rows/columns, so this needs to be | |
418 * run before the track sizing algorithm. | |
419 */ | |
420 void _ensureAllTracks() { | |
421 final items = CollectionUtils.map(view.childViews, (view_) => view_.layout); | |
422 | |
423 for (final child in items) { | |
424 if (child.layoutParams == null) { | |
425 final p = new GridLayoutParams(child.view, this); | |
426 _ensureTrack(_rowTracks, rowSizing, p.row, p.rowSpan); | |
427 _ensureTrack(_columnTracks, columnSizing, p.column, p.columnSpan); | |
428 child.layoutParams = p; | |
429 } | |
430 child.cacheExistingBrowserLayout(); | |
431 } | |
432 } | |
433 | |
434 /** | |
435 * Given the track sizes that were computed, position children in the grid. | |
436 */ | |
437 void _setBoundsOfChildren() { | |
438 final items = CollectionUtils.map(view.childViews, (view_) => view_.layout); | |
439 | |
440 for (final item in items) { | |
441 GridLayoutParams childLayout = item.layoutParams; | |
442 var xPos = _getTrackLocationX(childLayout); | |
443 var yPos = _getTrackLocationY(childLayout); | |
444 | |
445 int left = xPos.start, width = xPos.length; | |
446 int top = yPos.start, height = yPos.length; | |
447 | |
448 // Somewhat counterintuitively (at least to me): | |
449 // grid-col-align is the horizontal alignment | |
450 // grid-row-align is the vertical alignment | |
451 xPos = childLayout.columnAlign.align(xPos, item.currentWidth); | |
452 yPos = childLayout.rowAlign.align(yPos, item.currentHeight); | |
453 | |
454 item.setBounds(xPos.start, yPos.start, xPos.length, yPos.length); | |
455 } | |
456 } | |
457 | |
458 num _getGridContentSize() { | |
459 switch (_dimension) { | |
460 case Dimension.WIDTH: | |
461 return _gridWidth; | |
462 case Dimension.HEIGHT: | |
463 return _gridHeight; | |
464 } | |
465 } | |
466 | |
467 _GridLocation _getTrackLocationX(GridLayoutParams childLayout) { | |
468 int start = childLayout.column - 1; | |
469 int end = start + childLayout.columnSpan - 1; | |
470 | |
471 start = _columnTracks[start].start; | |
472 end = _columnTracks[end].end; | |
473 | |
474 return new _GridLocation(start, end - start); | |
475 } | |
476 | |
477 _GridLocation _getTrackLocationY(GridLayoutParams childLayout) { | |
478 int start = childLayout.row - 1; | |
479 int end = start + childLayout.rowSpan - 1; | |
480 | |
481 start = _rowTracks[start].start; | |
482 end = _rowTracks[end].end; | |
483 | |
484 return new _GridLocation(start, end - start); | |
485 } | |
486 | |
487 /** Gets the tracks that this item crosses. */ | |
488 // TODO(jmesserly): might be better to return an iterable | |
489 List<GridTrack> _getTracks(ViewLayout item) { | |
490 GridLayoutParams childLayout = item.layoutParams; | |
491 | |
492 int start, span; | |
493 List<GridTrack> tracks; | |
494 switch (_dimension) { | |
495 case Dimension.WIDTH: | |
496 start = childLayout.column - 1; | |
497 span = childLayout.columnSpan; | |
498 tracks = _columnTracks; | |
499 break; | |
500 case Dimension.HEIGHT: | |
501 start = childLayout.row - 1; | |
502 span = childLayout.rowSpan; | |
503 tracks = _rowTracks; | |
504 } | |
505 | |
506 assert(start >= 0 && span >= 1); | |
507 | |
508 final result = new List<GridTrack>(span); | |
509 for (int i = 0; i < span; i++) { | |
510 result[i] = tracks[start + i]; | |
511 } | |
512 return result; | |
513 } | |
514 | |
515 int _getSpanCount(ViewLayout item) { | |
516 GridLayoutParams childLayout = item.layoutParams; | |
517 return (_dimension == Dimension.WIDTH ? | |
518 childLayout.columnSpan : childLayout.rowSpan); | |
519 } | |
520 } | |
OLD | NEW |