1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 /**
  9   @type String
 10   @constant
 11 */
 12 SC.ALIGN_JUSTIFY = "justify";
 13 
 14 /**
 15   @namespace
 16 
 17   Normal SproutCore views are absolutely positioned--parent views have relatively
 18   little input on where their child views are placed.
 19 
 20   This mixin makes a view layout its child views itself, flowing left-to-right
 21   or up-to-down, and, optionally, wrapping.
 22 
 23   Child views with useAbsoluteLayout===YES will be ignored in the layout process.
 24   This mixin detects when child views have changed their size, and will adjust accordingly.
 25   It also observes child views' isVisible and calculatedWidth/Height properties, and, as a
 26   flowedlayout-specific extension, isHidden.
 27 
 28   These properties are observed through `#js:observeChildLayout` and `#js:unobserveChildLayout`;
 29   you can override the method to add your own properties. To customize isVisible behavior,
 30   you will also want to override shouldIncludeChildInFlow.
 31 
 32   This relies on the children's frames or, if specified, calculatedWidth and calculatedHeight
 33   properties.
 34 
 35   This view mixes very well with animation. Further, it is able to automatically mix
 36   in to child views it manages, created or not yet created, allowing you to specify
 37   settings such as animation once only, and have everything "just work".
 38 
 39   Like normal views, you simply specify child views--everything will "just work."
 40 
 41   @since SproutCore 1.0
 42 */
 43 SC.FlowedLayout = {
 44   isFlowedLayout: YES,
 45   /**
 46     The direction of flow. Possible values:
 47 
 48       - SC.LAYOUT_HORIZONTAL
 49       - SC.LAYOUT_VERTICAL
 50 
 51     @type String
 52     @default SC.LAYOUT_HORIZONTAL
 53   */
 54   layoutDirection: SC.LAYOUT_HORIZONTAL,
 55 
 56   /**
 57     Whether the view should automatically resize (to allow scrolling, for instance)
 58 
 59     @type Boolean
 60     @default YES
 61   */
 62   autoResize: YES,
 63 
 64   /**
 65     @type Boolean
 66     @default YES
 67   */
 68   shouldResizeWidth: YES,
 69 
 70   /**
 71     @type Boolean
 72     @default YES
 73   */
 74   shouldResizeHeight: YES,
 75 
 76   /**
 77     The alignment of items within rows or columns. Possible values:
 78 
 79       - SC.ALIGN_LEFT
 80       - SC.ALIGN_CENTER
 81       - SC.ALIGN_RIGHT
 82       - SC.ALIGN_JUSTIFY
 83 
 84     @type String
 85     @default SC.ALIGN_LEFT
 86   */
 87   align: SC.ALIGN_LEFT,
 88 
 89   /**
 90     If YES, flowing child views are allowed to wrap to new rows or columns.
 91 
 92     @type Boolean
 93     @default YES
 94   */
 95   canWrap: YES,
 96 
 97   /**
 98     A set of spacings (left, top, right, bottom) for subviews. Defaults to 0s all around.
 99     This is the amount of space that will be before, after, above, and below the view. These
100     spacings do not collapse into each other.
101 
102     You can also set flowSpacing on any child view, or implement flowSpacingForView.
103 
104     @type Hash
105     @default `{ left: 0, bottom: 0, top: 0, right: 0 }`
106   */
107   defaultFlowSpacing: { left: 0, bottom: 0, top: 0, right: 0 },
108 
109   /**
110     @type Hash
111 
112     Padding around the edges of this flow layout view. This is useful for
113     situations where you don't control the layout of the FlowedLayout view;
114     for instance, when the view is the contentView for a SC.ScrollView.
115 
116     @type Hash
117     @default `{ left: 0, bottom: 0, top: 0, right: 0 }`
118   */
119   flowPadding: { left: 0, bottom: 0, right: 0, top: 0 },
120 
121   /**
122     @private
123 
124     If the flowPadding somehow misses a property (one of the sides),
125     we need to make sure a default value of 0 is still there.
126    */
127   _scfl_validFlowPadding: function() {
128     var padding = this.get('flowPadding') || {}, ret = {};
129     ret.left = padding.left || 0;
130     ret.top = padding.top || 0;
131     ret.bottom = padding.bottom || 0;
132     ret.right = padding.right || 0;
133     return ret;
134   }.property('flowPadding').cacheable(),
135 
136   concatenatedProperties: ['childMixins'],
137 
138   /** @private */
139   initMixin: function() {
140     this._scfl_tileOnce();
141     // register observer to detect the childViews changes
142     this.addObserver( 'childViews.[]', this, this._scfl_childViewsDidChange );
143   },
144 
145   /** @private
146     Detects when the child views change.
147   */
148   _scfl_childViewsDidChange: function(c) {
149     this._scfl_tileOnce();
150   },
151 
152   /** @private */
153   _scfl_layoutPropertyDidChange: function(childView) {
154     this._scfl_tileOnce();
155   }.observes('layoutDirection', 'align', 'flowPadding', 'canWrap', 'defaultFlowSpacing', 'isVisibleInWindow'),
156 
157   /** @private
158     Overridden to only update if it is a view we do not manage, or the width or height has changed
159     since our last record of it.
160   */
161   layoutDidChangeFor: function(c) {
162     // now, check if anything has changed
163     var l = c._scfl_lastLayout, cl = c.get('layout'), f = c.get('frame');
164     if (!l) return sc_super();
165 
166     var same = YES;
167 
168     // in short, if anything interfered with the layout, we need to
169     // do something about it.
170     if (l.left && l.left !== cl.left) same = NO;
171     else if (l.top && l.top !== cl.top) same = NO;
172     else if (!c.get('fillWidth') && l.width && l.width !== cl.width) same = NO;
173     else if (!l.width && !c.get('fillWidth') && f.width !== c._scfl_lastFrame.width) same = NO;
174     else if (!c.get('fillHeight') && l.height && l.height !== cl.height) same = NO;
175     else if (!l.height && !c.get('fillHeight') && f.height !== c._scfl_lastFrame.height) same = NO;
176 
177     if (same) {
178       return sc_super();
179     }
180 
181     // nothing has changed. This is where we do something
182     this._scfl_tileOnce();
183     sc_super();
184   },
185 
186   /** @private
187     Sets up layout observers on child view. We observe three things:
188     - isVisible
189     - calculatedWidth
190     - calculatedHeight
191 
192     Actual layout changes are detected through layoutDidChangeFor.
193   */
194   observeChildLayout: function(c) {
195     if (c._scfl_isBeingObserved) return;
196     c._scfl_isBeingObserved = YES;
197     c.addObserver('flowSpacing', this, '_scfl_tileOnce');
198     c.addObserver('isVisible', this, '_scfl_tileOnce');
199     c.addObserver('useAbsoluteLayout', this, '_scfl_tileOnce');
200     c.addObserver('calculatedWidth', this, '_scfl_tileOnce');
201     c.addObserver('calculatedHeight', this, '_scfl_tileOnce');
202     c.addObserver('startsNewRow', this, '_scfl_tileOnce');
203     c.addObserver('isSpacer', this, '_scfl_tileOnce');
204     c.addObserver('maxSpacerLength', this, '_scfl_tileOnce');
205     c.addObserver('fillWidth', this, '_scfl_tileOnce');
206     c.addObserver('fillHeight', this, '_scfl_tileOnce');
207   },
208 
209   /** @private
210     Removes observers on child view.
211   */
212   unobserveChildLayout: function(c) {
213     c._scfl_isBeingObserved = NO;
214     c.removeObserver('flowSpacing', this, '_scfl_tileOnce');
215     c.removeObserver('isVisible', this, '_scfl_tileOnce');
216     c.removeObserver('useAbsoluteLayout', this, '_scfl_tileOnce');
217     c.removeObserver('calculatedWidth', this, '_scfl_tileOnce');
218     c.removeObserver('calculatedHeight', this, '_scfl_tileOnce');
219     c.removeObserver('startsNewRow', this, '_scfl_tileOnce');
220     c.removeObserver('isSpacer', this, '_scfl_tileOnce');
221     c.removeObserver('maxSpacerLength', this, '_scfl_tileOnce');
222     c.removeObserver('fillWidth', this, '_scfl_tileOnce');
223     c.removeObserver('fillHeight', this, '_scfl_tileOnce');
224   },
225 
226   /**
227     Determines whether the specified child view should be included in the flow layout.
228     By default, if it has isVisible: NO or useAbsoluteLayout: YES, it will not be included.
229 
230     @field
231     @type Boolean
232     @default NO
233   */
234   shouldIncludeChildInFlow: function(idx, c) {
235     return c.get('isVisible') && !c.get('useAbsoluteLayout');
236   },
237 
238   /**
239     Returns the flow spacings for a given view. By default, returns the view's flowSpacing,
240     and if they don't exist, the defaultFlowSpacing for this view.
241 
242     @field
243     @type Hash
244   */
245   flowSpacingForChild: function(idx, view) {
246     var spacing = view.get('flowSpacing');
247     if (SC.none(spacing)) spacing = this.get('defaultFlowSpacing');
248     if (SC.none(spacing)) spacing = 0;
249 
250     if (SC.typeOf(spacing) === SC.T_NUMBER) {
251       spacing = { left: spacing, right: spacing, bottom: spacing, top: spacing };
252     } else {
253       spacing['left'] = spacing['left'] || 0;
254       spacing['right'] = spacing['right'] || 0;
255       spacing['top'] = spacing['top'] || 0;
256       spacing['bottom'] = spacing['bottom'] || 0;
257     }
258 
259     return spacing;
260   },
261 
262   /**
263     Returns the flow size for a given view, excluding spacing. The default version
264     checks the view's calculatedWidth/Height, then its frame.
265 
266     For spacers, this returns an empty size.
267 
268     @field
269     @type Hash
270     @default {width: 0, height: 0}
271   */
272   flowSizeForChild: function(idx, view) {
273     var cw = view.get('calculatedWidth'), ch = view.get('calculatedHeight'),
274     layoutDirection = this.get('layoutDirection'),
275     calc = {}, f = view.get('frame'), l = view.get('layout');
276     view._scfl_lastFrame = f;
277 
278     // if there is a calculated width, use that. NOTE: if calculatedWidth === 0,
279     // it is invalid. This is the practice in other views.
280     if (cw) {
281       calc.width = cw;
282     } else {
283       // we should use the layout width if available to avoid breaking layouts
284       // that have borders
285       calc.width = l.width || f.width;
286     }
287 
288     // same for calculated height
289     if (ch) {
290       calc.height = ch;
291     } else {
292       // we should use the layout width if available to avoid breaking layouts
293       // that have borders
294       calc.height = l.height || f.height;
295     }
296 
297     // if it is a spacer, we must set the dimension that it
298     // expands in to 0.
299     if (view.get('isSpacer')) {
300       calc.maxSpacerLength = view.get('maxSpacerLength');
301 
302       if (layoutDirection === SC.LAYOUT_HORIZONTAL) {
303         calc.width = l.minWidth || 0;
304       } else {
305         calc.height = l.minHeight || 0;
306       }
307     }
308 
309     // if it has a fillWidth/Height, clear it for later
310     if (layoutDirection === SC.LAYOUT_HORIZONTAL && view.get('fillHeight')) {
311       calc.height = l.minHeight || 0;
312     } else if (layoutDirection === SC.LAYOUT_VERTICAL && view.get('fillWidth')) {
313       calc.width = l.minWidth || 0;
314     }
315 
316     return calc;
317   },
318 
319   /** @private */
320   clippingFrame: function() {
321     return { left: 0, top: 0, width: this.get('calculatedWidth'), height: this.get('calculatedHeight') };
322   }.property('calculatedWidth', 'calculatedHeight'),
323 
324   /** @private */
325 
326   // the maximum row length when all flexible items are collapsed.
327   _scfl_maxCollapsedRowLength: 0,
328 
329   // the total row size when all flexible rows are collapsed.
330   _scfl_totalCollapsedRowSize: 0,
331 
332 
333   _scfl_calculatedSizeDidChange: function() {
334     if(this.get('autoResize')) {
335       if (this.get('layoutDirection') == SC.LAYOUT_VERTICAL) {
336         if (this.get('shouldResizeHeight')) {
337           this.adjust('minHeight', this.get('_scfl_maximumCollapsedRowLength'));
338         }
339 
340         if (this.get('shouldResizeWidth')) {
341           this.adjust('minWidth', this.get('_scfl_totalCollapsedRowSize'));
342         }
343       } else {
344         if (this.get('shouldResizeWidth')) {
345           this.adjust('minWidth', this.get('_scfl_maximumCollapsedRowLength'));
346         }
347         if (this.get('shouldResizeHeight')) {
348           this.adjust('minHeight', this.get('_scfl_totalCollapsedRowSize'));
349         }
350       }
351     }
352   }.observes('autoResize', 'shouldResizeWidth', '_scfl_maximumCollapsedRowLength', '_scfl_totalCollapsedRowSize', 'shouldResizeHeight'),
353 
354   /**
355     @private
356     Creates a plan, initializing all of the basic properties in it, but not
357     doing anything further.
358 
359     Other methods should be called to do this:
360 
361     - _scfl_distributeChildrenIntoRows distributes children into rows.
362     - _scfl_positionChildrenInRows positions the children within the rows.
363       - this calls _scfl_positionChildrenInRow
364     - _scfl_positionRows positions and sizes rows within the plan.
365 
366     The plan's structure is defined inside the method.
367 
368     Some of these methods may eventually be made public and/or delegate methods.
369   */
370   _scfl_createPlan: function() {
371     var layoutDirection = this.get('layoutDirection'),
372         flowPadding = this.get('_scfl_validFlowPadding'),
373         frame = this.get('frame');
374 
375     var isVertical = (layoutDirection === SC.LAYOUT_VERTICAL);
376 
377     // A plan hash contains general information about the layout, and also,
378     // the collection of rows.
379     //
380     // This method only fills out a subset of the properties in a plan.
381     //
382     var plan = {
383       // The rows array starts empty. It will get filled out by the method
384       // _scfl_distributeChildrenIntoRows.
385       rows: undefined,
386 
387 
388       // the maximum row length where all collapsible items are collapsed.
389       maximumCollapsedRowLength: 0,
390 
391       // the total sizes of all rows when collapsed (With flex-height rows
392       // at minimum size)
393       totalCollapsedRowSize: 0,
394 
395       // These properties are calculated once here, but later used by
396       // the various methods.
397       isVertical: layoutDirection === SC.LAYOUT_VERTICAL,
398       isHorizontal: layoutDirection === SC.LAYOUT_HORIZONTAL,
399 
400       flowPadding: flowPadding,
401 
402       planStartPadding: flowPadding[isVertical ? 'left' : 'top'],
403       planEndPadding: flowPadding[isVertical ? 'right' : 'bottom'],
404 
405       rowStartPadding: flowPadding[isVertical ? 'top' : 'left'],
406       rowEndPadding: flowPadding[isVertical ? 'bottom' : 'right'],
407 
408       maximumRowLength: undefined, // to be calculated below
409 
410       // if any rows need to fit to fill, this is the size to fill
411       fitToPlanSize: undefined,
412 
413 
414       align: this.get('align')
415     };
416 
417     if (isVertical) {
418       plan.maximumRowLength = frame.height - plan.rowStartPadding - plan.rowEndPadding;
419       plan.fitToPlanSize = frame.width - plan.planStartPadding - plan.planEndPadding;
420     } else {
421       plan.maximumRowLength = frame.width - plan.rowStartPadding - plan.rowEndPadding;
422       plan.fitToPlanSize = frame.height - plan.planStartPadding - plan.planEndPadding;
423     }
424 
425     return plan;
426   },
427 
428   /** @private */
429   _scfl_distributeChildrenIntoRows: function(plan) {
430     var children = this.get('childViews'), child, idx, len = children.length,
431         isVertical = plan.isVertical, rows = [], lastIdx;
432 
433     lastIdx = -1; idx = 0;
434     while (idx < len && idx !== lastIdx) {
435       lastIdx = idx;
436 
437       var row = {
438         // always a reference to the plan
439         plan: plan,
440 
441         // the combined size of the items in the row. This is used, for instance,
442         // in justification or right-alignment.
443         rowLength: undefined,
444 
445         // the size of the row. When flowing horizontally, this is the height;
446         // it is the opposite dimension of rowLength. It is calculated
447         // both while positioning items in the row and while positioning the rows
448         // themselves.
449         rowSize: undefined,
450 
451         // whether this row should expand to fit any available space. In this case,
452         // the size is the row's minimum size.
453         shouldExpand: undefined,
454 
455         // to be decided by _scfl_distributeItemsIntoRows
456         items: undefined,
457 
458         // to be decided by _scfl_positionRows
459         position: undefined
460       };
461 
462       idx = this._scfl_distributeChildrenIntoRow(children, idx, row);
463       rows.push(row);
464     }
465 
466     plan.rows = rows;
467   },
468 
469   /**
470     @private
471     Distributes as many children as possible into a single row, stating
472     at the given index, and returning the index of the next item, if any.
473   */
474   _scfl_distributeChildrenIntoRow: function(children, startingAt, row) {
475     var idx, len = children.length, plan = row.plan, child, childSize, spacing,
476         items = [], itemOffset = 0, isVertical = plan.isVertical, itemSize, itemLength,
477         maxSpacerLength,
478         canWrap = this.get('canWrap'),
479         newRowPending = NO,
480         maxItemLength = 0,
481         max = row.plan.maximumRowLength;
482 
483     for (idx = startingAt; idx < len; idx++) {
484       child = children[idx];
485 
486       // this must be set before we check if the child is included because even
487       // if it isn't included, we need to remember that there is a line break
488       // for later
489       newRowPending = newRowPending || (items.length > 0 && child.get('startsNewRow'));
490 
491       if (!this.shouldIncludeChildInFlow(idx, child)) continue;
492 
493       childSize = this.flowSizeForChild(idx, child);
494       spacing = this.flowSpacingForChild(idx, child);
495 
496       childSize.width += spacing.left + spacing.right;
497       childSize.height += spacing.top + spacing.bottom;
498 
499       itemLength = childSize[isVertical ? 'height' : 'width'];
500       if(!SC.none(childSize.maxSpacerLength)) maxSpacerLength = childSize.maxSpacerLength + (isVertical ? spacing.top + spacing.bottom : spacing.left + spacing.right);
501       itemSize = childSize[isVertical ? 'width' : 'height'];
502 
503       // there are two cases where we must start a new row: if the child or a
504       // previous child in the row that wasn't included has
505       // startsNewRow === YES, and if the item cannot fit. Neither applies if there
506       // is nothing in the row yet.
507       if ((newRowPending || (canWrap && itemOffset + itemLength > max)) && items.length > 0) {
508         break;
509       }
510 
511       var item = {
512         child: child,
513 
514         itemLength: itemLength,
515         maxSpacerLength: maxSpacerLength,
516         itemSize: itemSize,
517 
518         spacing: spacing,
519 
520         // The position in the row.
521         //
522         // note: in one process or another, this becomes left or top.
523         // but before that, it is calculated.
524         position: undefined,
525 
526         // whether this item should attempt to fill to the row's size
527         fillRow: isVertical ? child.get('fillWidth') : child.get('fillHeight'),
528 
529         // whether this item is a spacer, and thus should be resized to its itemLength
530         isSpacer: child.get('isSpacer'),
531 
532         // these will get set if necessary during the positioning code
533         left: undefined, top: undefined,
534         width: undefined, height: undefined
535       };
536 
537 
538       items.push(item);
539       itemOffset += itemLength;
540       maxItemLength = Math.max(itemLength, maxItemLength);
541     }
542 
543     row.rowLength = itemOffset;
544 
545     // if the row cannot wrap, then the minimum size for the row (and therefore collapsed size)
546     // is the same as the current row length: it consists of the minimum size of all items.
547     //
548     // If the row can wrap, then the longest item will determine the size of a fully
549     // collapsed (one item per row) layout.
550     var minRowLength = canWrap ? maxItemLength : row.rowLength;
551     row.plan.maximumCollapsedRowLength = Math.max(minRowLength, row.plan.maximumCollapsedRowLength);
552     row.items = items;
553     return idx;
554   },
555 
556   /** @private */
557   _scfl_positionChildrenInRows: function(plan) {
558     var rows = plan.rows, len = rows.length, idx;
559 
560     for (idx = 0; idx < len; idx++) {
561       this._scfl_positionChildrenInRow(rows[idx]);
562     }
563   },
564 
565   /**
566     @private
567     Positions items within a row. The items are already in the row, this just
568     modifies the 'position' property.
569 
570     This also marks a tentative size of the row, and whether it should be expanded
571     to fit in any available extra space. Note the term 'size' rather than 'length'...
572   */
573   _scfl_positionChildrenInRow: function(row) {
574     var items = row.items, len = items.length, idx, item, position, rowSize = 0,
575         spacerCount = 0, spacerSize, align = row.plan.align, shouldExpand = YES,
576         leftOver = 0, noMaxWidth = NO;
577 
578     //
579     // STEP ONE: DETERMINE SPACER SIZE + COUNT
580     //
581     for (idx = 0; idx < len; idx++) {
582       item = items[idx];
583       if (item.isSpacer) {
584         spacerCount += item.child.get('spaceUnits') || 1;
585       }
586     }
587 
588     // justification is like adding a spacer between every item. We'll actually account for
589     // that later, but for now...
590     if (align === SC.ALIGN_JUSTIFY) spacerCount += len - 1;
591 
592     // calculate spacer size
593     spacerSize = Math.max(0, row.plan.maximumRowLength - row.rowLength) / spacerCount;
594 
595     // determine individual spacer sizes using spacerSize and limited by
596     // each spacer's maxWidth (if they have one)
597     while(spacerSize > 0) {
598       for (idx = 0; idx < len; idx++) {
599         item = items[idx];
600 
601         if (item.isSpacer) {
602           item.itemLength += spacerSize * (item.child.get('spaceUnits') || 1);
603           if(item.itemLength > item.maxSpacerLength) {
604             leftOver +=  item.itemLength - item.maxSpacerLength;
605             item.itemLength = item.maxSpacerLength;
606           }
607           else {
608             noMaxWidth = YES;
609           }
610         }
611       }
612 
613       // if none of the spacers can expand further, stop
614       if(!noMaxWidth) break;
615 
616       spacerSize = Math.round(leftOver / spacerCount);
617       leftOver = 0;
618     }
619 
620     //
621     // STEP TWO: ADJUST FOR ALIGNMENT
622     // Note: if there are spacers, this has no effect, because they fill all available
623     // space.
624     //
625     position = 0;
626     if (spacerCount === 0 && (align === SC.ALIGN_RIGHT || align === SC.ALIGN_BOTTOM)) {
627       position = row.plan.maximumRowLength - row.rowLength;
628     } else if (spacerCount === 0 && (align === SC.ALIGN_CENTER || align === SC.ALIGN_MIDDLE)) {
629       position = (row.plan.maximumRowLength / 2) - (row.rowLength / 2);
630     }
631 
632     position += row.plan.rowStartPadding;
633     //
634     // STEP TWO: LOOP + POSITION
635     //
636     for (idx = 0; idx < len; idx++) {
637       item = items[idx];
638 
639       // if this item has fillWidth or fillHeight set, the row should expand
640       // laterally
641       if(!item.fillRow) shouldExpand = NO;
642 
643       // if the item is not a fill-row item, this row has a size that all fill-row
644       // items should expand to
645       rowSize = Math.max(item.itemSize, rowSize);
646 
647       item.position = position;
648 
649       position += item.itemLength;
650 
651       // if justification is on, we have one more spacer
652       // note that we check idx because position is used to determine the new rowLength.
653       if (align === SC.ALIGN_JUSTIFY && idx < len - 1) position += spacerSize;
654     }
655 
656     row.shouldExpand = len > 0 ? shouldExpand : NO;
657     row.rowLength = position - row.plan.rowStartPadding; // row length does not include padding
658     row.rowSize = rowSize;
659 
660     row.plan.totalCollapsedRowSize += row.rowSize;
661 
662   },
663 
664   /** @private */
665   _scfl_positionRows: function(plan) {
666     var rows = plan.rows, len = rows.length, idx, row, position,
667         fillRowCount = 0, planSize = 0, fillSpace;
668 
669     // first, we need a count of rows that need to fill, and the size they
670     // are filling to (the combined size of all _other_ rows).
671     for (idx = 0; idx < len; idx++) {
672       if (rows[idx].shouldExpand) fillRowCount++;
673       planSize += rows[idx].rowSize;
674     }
675 
676     fillSpace = plan.fitToPlanSize - planSize;
677 
678     // now, position+size the rows
679     position = plan.planStartPadding;
680     for (idx = 0; idx < len; idx++) {
681       row = rows[idx];
682 
683       if (row.shouldExpand && fillSpace > 0) {
684         row.rowSize += fillSpace / fillRowCount;
685         fillRowCount--;
686       }
687 
688       row.position = position;
689       position += row.rowSize;
690     }
691   },
692 
693   /**
694     @private
695     Positions all of the child views according to the plan.
696   */
697   _scfl_applyPlan: function(plan) {
698     var rows = plan.rows, rowIdx, rowsLen, row, longestRow = 0, totalSize = 0,
699         items, itemIdx, itemsLen, item, layout, itemSize,
700 
701         isVertical = plan.isVertical;
702 
703     rowsLen = rows.length;
704     for (rowIdx = 0; rowIdx < rowsLen; rowIdx++) {
705       row = rows[rowIdx];
706       longestRow = Math.max(longestRow, row.rowLength);
707       totalSize += row.rowSize;
708 
709       items = row.items; itemsLen = items.length;
710 
711       for (itemIdx = 0; itemIdx < itemsLen; itemIdx++) {
712         item = items[itemIdx];
713         item.child.beginPropertyChanges();
714 
715         itemSize = item.fillRow ? row.rowSize : item.itemSize;
716 
717         layout = {
718           left: item.spacing.left + (isVertical ? row.position : item.position),
719           top: item.spacing.top + (isVertical ? item.position : row.position),
720           width: isVertical ? itemSize : item.itemLength,
721           height: isVertical ? item.itemLength : itemSize
722         };
723 
724         layout.width -= item.spacing.left + item.spacing.right;
725         layout.height -= item.spacing.top + item.spacing.bottom;
726 
727         this.applyPlanToView(item.child, layout);
728         item.child._scfl_lastLayout = layout;
729 
730         item.child.endPropertyChanges();
731       }
732     }
733 
734     totalSize += plan.planStartPadding + plan.planEndPadding;
735     longestRow += plan.rowStartPadding + plan.rowEndPadding;
736 
737     this.beginPropertyChanges();
738 
739     this.set('calculatedHeight', isVertical ? longestRow : totalSize);
740     this.set('calculatedWidth', isVertical ? totalSize : longestRow);
741     this.set('_scfl_maximumCollapsedRowLength', plan.maximumCollapsedRowLength);
742     this.set('_scfl_totalCollapsedRowSize', plan.totalCollapsedRowSize);
743 
744     this.endPropertyChanges();
745   },
746 
747   /**
748     Applies the given layout to the view.
749     Override this if you would like your view to, for example, animate to a new position.
750   */
751   applyPlanToView: function(view, layout) {
752     view.adjust(layout);
753   },
754 
755   /** @private */
756   _scfl_tileOnce: function() {
757     this.invokeLast(this._scfl_tile);
758   },
759 
760   _scfl_tile: function() {
761     // short circuit when hidden
762     if(!this.get('isVisibleInWindow')) return;
763 
764     // first, do the plan
765     var plan = this._scfl_createPlan();
766     this._scfl_distributeChildrenIntoRows(plan);
767     this._scfl_positionChildrenInRows(plan);
768     this._scfl_positionRows(plan);
769     this._scfl_applyPlan(plan);
770 
771     // save so it can be observed
772     this.setIfChanged('numberOfRows', plan.rows.length);
773 
774     // second, observe all children, and stop observing any children we no longer
775     // should be observing.
776     var previouslyObserving = this._scfl_isObserving || SC.CoreSet.create(),
777         nowObserving = this._scfl_isObserving = SC.CoreSet.create();
778 
779     var children = this.get('childViews'), len = children.length, idx, child;
780     for (idx = 0; idx < len; idx++) {
781       child = children[idx];
782 
783       if (!previouslyObserving.contains(child)) {
784         this.observeChildLayout(child);
785       } else {
786         previouslyObserving.remove(child);
787       }
788 
789       nowObserving.add(child);
790     }
791 
792     len = previouslyObserving.length;
793     for (idx = 0; idx < len; idx++) {
794       this.unobserveChildLayout(previouslyObserving[idx]);
795     }
796   },
797 
798   /** @private */
799   _scfl_frameDidChange: function() {
800     var frame = this.get("frame"), lf = this._scfl_lastFrameSize || {};
801     this._scfl_lastFrameSize = SC.clone(frame);
802 
803     if (lf.width == frame.width && lf.height == frame.height) {
804       return;
805     }
806 
807     this._scfl_tileOnce();
808   }.observes('frame'),
809 
810   /** @private */
811   destroyMixin: function() {
812     this.removeObserver( 'childViews.[]', this, this._scfl_childViewsDidChange );
813 
814     var isObserving = this._scfl_isObserving;
815     if (!isObserving) return;
816 
817     var len = isObserving.length, idx;
818     for (idx = 0; idx < len; idx++) {
819       this.unobserveChildLayout(isObserving[idx]);
820     }
821   },
822 
823   /** @private
824     Reorders childViews so that the passed views are at the beginning in the order they are passed. Needed because childViews are laid out in the order they appear in childViews.
825   */
826   reorder: function(views) {
827     if(!SC.typeOf(views) === SC.T_ARRAY) views = arguments;
828 
829     var i = views.length, childViews = this.childViews, view;
830 
831     // childViews.[] should be observed
832     this.beginPropertyChanges();
833 
834     while(i-- > 0) {
835       view = views[i];
836 
837       if(SC.typeOf(view) === SC.T_STRING) view = this.get(view);
838 
839       childViews.removeObject(view);
840       childViews.unshiftObject(view);
841     }
842 
843     this.endPropertyChanges();
844 
845     this._scfl_childViewsDidChange();
846 
847     return this;
848   }
849 };
850 
851