foam-framework
Version:
MVC metaprogramming framework
329 lines (296 loc) • 13.6 kB
JavaScript
/**
* @license
* Copyright 2015 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
CLASS({
package: 'foam.patterns.layout',
name: 'LinearLayoutTrait',
documentation: function() {/*
A linear layout for row or column alignment. Only the main axis is laid out.
This layout assumes the trait owner has a <code>this.children</code> array, and the
items inside implement $$DOC{ref:'foam.patterns.layout.LayoutItemHorizontalTrait'} or
$$DOC{ref:'foam.patterns.layout.LayoutItemVerticalTrait'},
depending on $$DOC{ref:'.orientation'}.
*/},
properties: [
{
name: 'orientation',
type: 'String', // TODO: should be ENUM
defaultValue: 'horizontal',
documentation: function() {/* Set to 'horizontal' or 'vertical'. */},
postSet: function() {
this.layoutDirty = true; //this.calculateLayout();
}
},
{
model_: 'BooleanProperty',
name: 'fitContents',
defaultValue: false,
documentation: function() {/* If set to true, the layout will set
its own min and max constraints by the sum of the content. */},
postSet: function() { this.calculatePreferredSize(); }
},
{
model_: 'BooleanProperty',
name: 'stretchy',
defaultValue: true,
documentation: function() {/* If true, the layout will set its stretch
and shrink factors to the largest value of its children. If you don't
want a stretchy child to cause your layout to become stretchy, set to
false.
*/},
},
{
model_: 'BooleanProperty',
name: 'layoutDirty',
defaultValue: true,
hidden: true
}
],
listeners: [
{
name: 'performLayout',
//isMerged: 1000,
documentation: function() {/* Performs a full layout of child items. */},
code: function(evt) {
//console.log("performLayout", this.$UID);
// a child has triggered this change, do we probably have to lay out
this.layoutDirty = true;
// this change to our pref size may cause a parent to lay us out through a size change
this.calculatePreferredSize();
// If no parent caused us to change size and layout, force the layout operation
//if ( this.layoutDirty ) this.calculateLayout();
}
},
],
methods: {
calculateLayout: function() { /* lay out items along the primary axis */
// We are doing a layout operation, so we'll be clean now
this.layoutDirty = false;
// no children, nothing to do
if (this.children.length <= 0) return;
// size changes to ourself may impact percentage preferred size of some children,
// so calculate it too. This calculateLayout operation doesn't depend on
// anything that calculatePreferredSize() does.
//this.calculatePreferredSize();
// these helpers take care of orientation awareness
var constraintsF = Function("item", "return item."+ this.orientation+"Constraints");
var sizeF = Function("item", "return item."+
(this.orientation==='horizontal'? "width" : "height"));
var parentSizeF = Function("item", "return item."+
(this.orientation==='horizontal'? "width" : "height"));
var boundedF = function(val, constraints) {
return (constraints.min$Pix && val < constraints.min$Pix)? constraints.min$Pix :
(constraints.max$Pix && val > constraints.max$Pix)? constraints.max$Pix :
val;
}
var availableSpace = parentSizeF(this);
var sz = parentSizeF(this);
// initialize with all at preferred size
var itemSizes = [];
var i = 0;
this.children.forEach(function(child) {
if ( constraintsF(child) ) {
constraintsF(child).setTotalSize(sz); // for percentages
itemSizes[i] = boundedF(constraintsF(child).preferred$Pix, constraintsF(child));
availableSpace -= itemSizes[i];
} else {
itemSizes[i] = 0;
}
i++;
});
var resizeF = function(isShrink) {
var sizeOkF, factorF, sizeNotOkF, makeSizeOkF;
if (isShrink) {
sizeOkF = function(index, child) {
return itemSizes[index] > constraintsF(child).min$Pix;
}
factorF = function(child) {
return constraintsF(child).shrinkFactor;
}
sizeNotOkF = function(index, child) {
return itemSizes[index] < constraintsF(child).min$Pix;
}
makeSizeOkF = function(index, child) {
availableSpace += itemSizes[index] - constraintsF(child).min$Pix;
itemSizes[index] = constraintsF(child).min$Pix;
resizeF(true); // recurse with a smaller list now that item i is locked at minimum
// This will eventually catch the case where we can't shrink enough, since we
// will keep re-shrinking until the list of workingSet is empty.
return false;
}
} else { //grow
sizeOkF = function(index, child) {
return itemSizes[index] < constraintsF(child).max$Pix;
}
factorF = function(child) {
return constraintsF(child).stretchFactor;
}
sizeNotOkF = function(index, child) {
return itemSizes[index] > constraintsF(child).max$Pix;
}
makeSizeOkF = function(index, child) {
availableSpace += itemSizes[index] - constraintsF(child).max$Pix;
itemSizes[index] = constraintsF(child).max$Pix;
resizeF(false); // recurse with a smaller list now that item i is locked at minimum
// This will eventually catch the case where we can't shrink enough, since we
// will keep re-shrinking until the list of workingSet is empty.
return false;
}
}
// find all workingSet
var workingSet = []; // indexes into children[], since we reference itemSizes[] too
var modifyTotal = 0;
var i = 0;
this.children.forEach(function(child) {
if (constraintsF(child) && sizeOkF(i, child) // item is willing and able to shrink
&& factorF(child) > 0) {
workingSet.push(i);
modifyTotal += factorF(child);
}
i++;
});
if (workingSet.length === 0) { // if no willing items, try the ones with factor 0
i = 0;
this.children.forEach(function(child) {
if (constraintsF(child) && sizeOkF(i, child)) { // item is able to shrink, though not willing
workingSet.push(i);
modifyTotal += 1; // since constraintsF(child).shrinkFactor === 0
}
i++;
});
}
if (workingSet.length === 0) {
// absolutely nothing we can shrink. Abort!
// if (isShrink)
// console.warn("Layout failed to shrink due to minimum sizing: ", this, itemSizes, parentSizeF(this));
// else
// console.warn("Layout failed to stretch due to maximum sizing: ", this, itemSizes, parentSizeF(this));
applySizesF(); // size it anyway
return;
}
// float division, so we have to keep a running total later
// and round only when setting pos and size
var modifyEachBy = availableSpace / modifyTotal;
// apply the shrinkage
workingSet.every(function(i) {
var factor = factorF(this.children[i]);
if (factor < 1) factor = 1;
itemSizes[i] += modifyEachBy * factor;
availableSpace -= modifyEachBy * factor;
if (sizeNotOkF(i, this.children[i])) { // if we hit the limit for this item
return makeSizeOkF(i, this.children[i]);
}
return true;
}.bind(this));
// lock in changes, we're done
applySizesF();
}.bind(this);
var applySizesF = function() {
var applySizeF = Function("item", "val", "item."+
(this.orientation==='horizontal'? "width" : "height") + " = val;");
var applyPositionF = Function("item", "val", "item."+
(this.orientation==='horizontal'? "x" : "y")+ " = val;");
var applyOpposedPositionF = Function("item", "val", "item."+
(this.orientation==='vertical'? "x" : "y")+ " = val;");
// For the off-axis, try and apply our height to the items, but bound it by their max/min
var opposedConstraintsF = Function("item", "return item."+
((this.orientation === 'horizontal')? 'vertical':'horizontal')
+"Constraints");
var applyOpposedSizeF = Function("item", "val", "boundedF", "opposedConstraintsF",
"item."+ (this.orientation==='vertical'? "width" : "height") +
" = boundedF(val, opposedConstraintsF(item));");
var opposedParentSize = this.orientation==='horizontal'? this.height : this.width;
var i = 0;
var pos = 0;
this.children.forEach(function(child) {
if ( opposedConstraintsF(child) ) {
// we didn't care about the off-axis before, so ensure it's set
opposedConstraintsF(child).setTotalSize(opposedParentSize);
applySizeF(child, itemSizes[i]);
applyOpposedSizeF(child, opposedParentSize, boundedF, opposedConstraintsF);
applyPositionF(child, pos);
applyOpposedPositionF(child, 0);
pos += itemSizes[i];
}
i++;
});
}.bind(this);
if (availableSpace > 0) {
resizeF(false);
} else if (availableSpace < 0) {
resizeF(true);
} else {
// we're done!
applySizesF();
}
},
calculatePreferredSize: function() { /* Find the size of layout that accomodates all items
at their preferred sizes. */
var self = this;
var syncConstraints = this.fitContents ? ['min','max','preferred'] : ['preferred'];
var stretchConstraints = this.stretchy ? ['stretchFactor', 'shrinkFactor'] : [];
// no children, zero
if (self.children.length <= 0) {
// apply if valid for our layout item traits
if (self.horizontalConstraints) self.horizontalConstraints.preferred = 0;
if (self.verticalConstraints) self.verticalConstraints.preferred = 0;
return;
}
var constraintsF = Function("item", "return item."+ self.orientation+"Constraints");
var opposedConstraintsF = Function("item", "return item."+
((self.orientation === 'horizontal')? 'vertical':'horizontal')
+"Constraints");
var boundedF = function(val, constraints) {
return (constraints.min$Pix && val < constraints.min$Pix)? constraints.min$Pix :
(constraints.max$Pix && val > constraints.max$Pix)? constraints.max$Pix :
val;
}
var sz = self.orientation==='horizontal'? self.width : self.height;
var opposedSz = self.orientation==='horizontal'? self.height : self.width;
// sum up preferred sizes
var totalSizes = { min:0, max: sz, preferred: 0, stretchFactor: 0, shrinkFactor: 0 };
var opposedTotalSizes = { min:0, max: opposedSz, preferred: 0, stretchFactor: 0, shrinkFactor: 0 };
self.children.forEach(function(child) {
if ( constraintsF(child) && opposedConstraintsF(child) ) {
var childCnstr = constraintsF(child);
var opposedChildCnstr = opposedConstraintsF(child);
childCnstr.setTotalSize(sz); // for percentages
opposedChildCnstr.setTotalSize(opposedSz);
syncConstraints.forEach(function(cnst) {
totalSizes[cnst] += boundedF(childCnstr[cnst+'$Pix'], childCnstr);
// find smallest for min
if ((cnst==='max' && (opposedChildCnstr[cnst+'$Pix'] < opposedTotalSizes[cnst]))
|| (cnst!=='max' && (opposedChildCnstr[cnst+'$Pix'] > opposedTotalSizes[cnst]))) {
opposedTotalSizes[cnst] = opposedChildCnstr[cnst+'$Pix'];
}
});
// add up stretchFactor and shrinkFactor
stretchConstraints.forEach( function (factor) {
if ( childCnstr[factor] > totalSizes[factor] )
totalSizes[factor] = childCnstr[factor];
if ( opposedChildCnstr[factor] > opposedTotalSizes[factor] )
opposedTotalSizes[factor] = opposedChildCnstr[factor];
});
}
});
// apply if valid for our layout item traits
syncConstraints.concat(stretchConstraints).forEach(function(cnst) {
if (constraintsF(self)) constraintsF(self)[cnst] = totalSizes[cnst];
if (opposedConstraintsF(self)) opposedConstraintsF(self)[cnst] = opposedTotalSizes[cnst];
});
}
}
});