@qooxdoo/framework
Version:
The JS Framework for Coders
558 lines (476 loc) • 15.8 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2008 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Sebastian Werner (wpbasti)
* Fabian Jakobs (fjakobs)
************************************************************************ */
/**
* A horizontal box layout.
*
* The horizontal box layout lays out widgets in a horizontal row, from left
* to right.
*
* *Features*
*
* * Minimum and maximum dimensions
* * Prioritized growing/shrinking (flex)
* * Margins (with horizontal collapsing)
* * Auto sizing (ignoring percent values)
* * Percent widths (not relevant for size hint)
* * Alignment (child property {@link qx.ui.core.LayoutItem#alignX} is ignored)
* * Horizontal spacing (collapsed with margins)
* * Reversed children layout (from last to first)
* * Vertical children stretching (respecting size hints)
*
* *Item Properties*
*
* <ul>
* <li><strong>flex</strong> <em>(Integer)</em>: The flexibility of a layout item determines how the container
* distributes remaining empty space among its children. If items are made
* flexible, they can grow or shrink accordingly. Their relative flex values
* determine how the items are being resized, i.e. the larger the flex ratio
* of two items, the larger the resizing of the first item compared to the
* second.
*
* If there is only one flex item in a layout container, its actual flex
* value is not relevant. To disallow items to become flexible, set the
* flex value to zero.
* </li>
* <li><strong>flexShrink</strong> <em>(Boolean)</em>: Only valid if `flex` is
* set to a non-zero value, `flexShrink` tells the layout to force the child
* widget to shink if there is not enough space available for all of the children.
* This is used in scenarios such as when the child insists that it has a `minWidth`
* but there simply is not enough space to support that minimum width, so the
* overflow has to be cut off. This setting allows the container to pick
* which children are able to have their `minWidth` sacrificed. Without this
* setting, one oversized child can force later children out of view, regardless
* of `flex` settings
* </li>
* <li><strong>width</strong> <em>(String)</em>: Allows to define a percent
* width for the item. The width in percent, if specified, is used instead
* of the width defined by the size hint. The minimum and maximum width still
* takes care of the element's limits. It has no influence on the layout's
* size hint. Percent values are mostly useful for widgets which are sized by
* the outer hierarchy.
* </li>
* </ul>
*
* *Example*
*
* Here is a little example of how to use the HBox layout.
*
* <pre class="javascript">
* var layout = new qx.ui.layout.HBox();
* layout.setSpacing(4); // apply spacing
*
* var container = new qx.ui.container.Composite(layout);
*
* container.add(new qx.ui.core.Widget());
* container.add(new qx.ui.core.Widget());
* container.add(new qx.ui.core.Widget());
* </pre>
*
* *External Documentation*
*
* See <a href='https://qooxdoo.org/documentation/#/desktop/layout/box.md'>extended documentation</a>
* and links to demos for this layout.
*
*/
qx.Class.define("qx.ui.layout.HBox", {
extend: qx.ui.layout.Abstract,
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
/**
* @param spacing {Integer?0} The spacing between child widgets {@link #spacing}.
* @param alignX {String?"left"} Horizontal alignment of the whole children
* block {@link #alignX}.
* @param separator {String|qx.ui.decoration.IDecorator?} A separator to render between the items
*/
construct(spacing, alignX, separator) {
super();
if (spacing) {
this.setSpacing(spacing);
}
if (alignX) {
this.setAlignX(alignX);
}
if (separator) {
this.setSeparator(separator);
}
},
/*
*****************************************************************************
PROPERTIES
*****************************************************************************
*/
properties: {
/**
* Horizontal alignment of the whole children block. The horizontal
* alignment of the child is completely ignored in HBoxes (
* {@link qx.ui.core.LayoutItem#alignX}).
*/
alignX: {
check: ["left", "center", "right"],
init: "left",
apply: "_applyLayoutChange"
},
/**
* Vertical alignment of each child. Can be overridden through
* {@link qx.ui.core.LayoutItem#alignY}.
*/
alignY: {
check: ["top", "middle", "bottom"],
init: "top",
apply: "_applyLayoutChange"
},
/** Horizontal spacing between two children */
spacing: {
check: "Integer",
init: 0,
apply: "_applyLayoutChange"
},
/** Separator lines to use between the objects */
separator: {
check: "Decorator",
nullable: true,
apply: "_applyLayoutChange"
},
/** Whether the actual children list should be laid out in reversed order. */
reversed: {
check: "Boolean",
init: false,
apply: "_applyReversed"
}
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members: {
__widths: null,
__flexs: null,
__enableFlex: null,
__children: null,
/*
---------------------------------------------------------------------------
HELPER METHODS
---------------------------------------------------------------------------
*/
// property apply
_applyReversed() {
// easiest way is to invalidate the cache
this._invalidChildrenCache = true;
// call normal layout change
this._applyLayoutChange();
},
/**
* Rebuilds caches for flex and percent layout properties
*/
__rebuildCache() {
var children = this._getLayoutChildren();
var length = children.length;
var enableFlex = false;
var reuse =
this.__widths &&
this.__widths.length != length &&
this.__flexs &&
this.__widths;
var props;
// Sparse array (keep old one if lengths has not been modified)
var widths = reuse ? this.__widths : new Array(length);
var flexs = reuse ? this.__flexs : new Array(length);
// Reverse support
if (this.getReversed()) {
children = children.concat().reverse();
}
// Loop through children to preparse values
for (var i = 0; i < length; i++) {
props = children[i].getLayoutProperties();
if (props.width != null) {
widths[i] = parseFloat(props.width) / 100;
}
if (props.flex != null) {
flexs[i] = props.flex;
enableFlex = true;
} else {
// reset (in case the index of the children changed: BUG #3131)
flexs[i] = 0;
}
}
// Store data
if (!reuse) {
this.__widths = widths;
this.__flexs = flexs;
}
this.__enableFlex = enableFlex;
this.__children = children;
// Clear invalidation marker
delete this._invalidChildrenCache;
},
/*
---------------------------------------------------------------------------
LAYOUT INTERFACE
---------------------------------------------------------------------------
*/
// overridden
verifyLayoutProperty: qx.core.Environment.select("qx.debug", {
true(item, name, value) {
if (name === "width") {
this.assertMatch(value, qx.ui.layout.Util.PERCENT_VALUE);
} else if (name === "flex") {
this.assertNumber(value);
this.assert(value >= 0);
} else if (name === "flexShrink") {
this.assertBoolean(value);
} else {
this.assert(
false,
"The property '" + name + "' is not supported by the HBox layout!"
);
}
},
false: null
}),
// overridden
renderLayout(availWidth, availHeight, padding) {
// Rebuild flex/width caches
if (this._invalidChildrenCache) {
this.__rebuildCache();
}
// Cache children
var children = this.__children;
var length = children.length;
var util = qx.ui.layout.Util;
// Compute gaps
var spacing = this.getSpacing();
var separator = this.getSeparator();
var gaps;
if (separator) {
gaps = util.computeHorizontalSeparatorGaps(
children,
spacing,
separator
);
} else {
gaps = util.computeHorizontalGaps(children, spacing, true);
}
// First run to cache children data and compute allocated width
var i, child, width, percent;
var widths = [],
hint;
var allocatedWidth = gaps;
for (i = 0; i < length; i += 1) {
percent = this.__widths[i];
hint = children[i].getSizeHint();
width =
percent != null
? Math.floor((availWidth - gaps) * percent)
: hint.width;
// Limit computed value
if (width < hint.minWidth) {
width = hint.minWidth;
} else if (width > hint.maxWidth) {
width = hint.maxWidth;
}
widths.push(width);
allocatedWidth += width;
}
// Flex support (growing/shrinking)
if (this.__enableFlex && allocatedWidth != availWidth) {
var flexibles = {};
var flex, offset;
var notEnoughSpace = allocatedWidth > availWidth;
for (i = 0; i < length; i += 1) {
flex = this.__flexs[i];
if (flex > 0) {
hint = children[i].getSizeHint();
flexibles[i] = {
min: hint.minWidth,
value: widths[i],
max: hint.maxWidth,
flex: flex
};
if (notEnoughSpace) {
var props = children[i].getLayoutProperties();
if (props && props.flexShrink) {
flexibles[i].min = 0;
}
}
}
}
var result = util.computeFlexOffsets(
flexibles,
availWidth,
allocatedWidth
);
for (i in result) {
offset = result[i].offset;
widths[i] += offset;
allocatedWidth += offset;
}
}
// Start with left coordinate
var left = children[0].getMarginLeft();
// Alignment support
if (allocatedWidth < availWidth && this.getAlignX() != "left") {
left = availWidth - allocatedWidth;
if (this.getAlignX() === "center") {
left = Math.round(left / 2);
}
}
// Layouting children
var hint, top, height, width, marginRight, marginTop, marginBottom;
var spacing = this.getSpacing();
// Pre configure separators
this._clearSeparators();
// Compute separator width
if (separator) {
var separatorInsets = qx.theme.manager.Decoration.getInstance()
.resolve(separator)
.getInsets();
var separatorWidth = separatorInsets.left + separatorInsets.right;
}
// Render children and separators
for (i = 0; i < length; i += 1) {
child = children[i];
width = widths[i];
hint = child.getSizeHint();
marginTop = child.getMarginTop();
marginBottom = child.getMarginBottom();
// Find usable height
height = Math.max(
hint.minHeight,
Math.min(availHeight - marginTop - marginBottom, hint.maxHeight)
);
// Respect vertical alignment
top = util.computeVerticalAlignOffset(
child.getAlignY() || this.getAlignY(),
height,
availHeight,
marginTop,
marginBottom
);
// Add collapsed margin
if (i > 0) {
// Whether a separator has been configured
if (separator) {
// add margin of last child and spacing
left += marginRight + spacing;
// then render the separator at this position
this._renderSeparator(separator, {
left: left + padding.left,
top: padding.top,
width: separatorWidth,
height: availHeight
});
// and finally add the size of the separator, the spacing (again) and the left margin
left += separatorWidth + spacing + child.getMarginLeft();
} else {
// Support margin collapsing when no separator is defined
left += util.collapseMargins(
spacing,
marginRight,
child.getMarginLeft()
);
}
}
// Layout child
child.renderLayout(
left + padding.left,
top + padding.top,
width,
height
);
// Add width
left += width;
// Remember right margin (for collapsing)
marginRight = child.getMarginRight();
}
},
// overridden
_computeSizeHint() {
// Rebuild flex/width caches
if (this._invalidChildrenCache) {
this.__rebuildCache();
}
var util = qx.ui.layout.Util;
var children = this.__children;
// Initialize
var minWidth = 0,
width = 0,
percentMinWidth = 0;
var minHeight = 0,
height = 0;
var child, hint, margin;
// Iterate over children
for (var i = 0, l = children.length; i < l; i += 1) {
child = children[i];
hint = child.getSizeHint();
// Sum up widths
width += hint.width;
// Detect if child is shrinkable or has percent width and update minWidth
var flex = this.__flexs[i];
var percent = this.__widths[i];
if (flex) {
minWidth += hint.minWidth;
} else if (percent) {
percentMinWidth = Math.max(
percentMinWidth,
Math.round(hint.minWidth / percent)
);
} else {
minWidth += hint.width;
}
// Build vertical margin sum
margin = child.getMarginTop() + child.getMarginBottom();
// Find biggest height
if (hint.height + margin > height) {
height = hint.height + margin;
}
// Find biggest minHeight
if (hint.minHeight + margin > minHeight) {
minHeight = hint.minHeight + margin;
}
}
minWidth += percentMinWidth;
// Respect gaps
var spacing = this.getSpacing();
var separator = this.getSeparator();
var gaps;
if (separator) {
gaps = util.computeHorizontalSeparatorGaps(
children,
spacing,
separator
);
} else {
gaps = util.computeHorizontalGaps(children, spacing, true);
}
// Return hint
return {
minWidth: minWidth + gaps,
width: width + gaps,
minHeight: minHeight,
height: height
};
}
},
/*
*****************************************************************************
DESTRUCTOR
*****************************************************************************
*/
destruct() {
this.__widths = this.__flexs = this.__children = null;
}
});