UNPKG

autolayout

Version:

Apple's Auto Layout and Visual Format Language for javascript (using cassowary constraints)

436 lines (416 loc) 15.5 kB
import c from 'cassowary/bin/c'; //import kiwi from 'kiwi.js'; import Attribute from './Attribute'; import Relation from './Relation'; import SubView from './SubView'; const defaultPriorityStrength = process.env.CASSOWARYJS ? new c.Strength('defaultPriority', 0, 1000, 1000) : kiwi.Strength.create(0, 1000, 1000); function _getConst(name, value) { if (process.env.CASSOWARYJS) { const vr = new c.Variable({value: value}); this._solver.addConstraint(new c.StayConstraint(vr, c.Strength.required, 0)); return vr; } else { const vr = new kiwi.Variable(); this._solver.addConstraint(new kiwi.Constraint(vr, kiwi.Operator.Eq, value)); return vr; } } function _getSubView(viewName) { if (!viewName) { return this._parentSubView; } else if (viewName.name) { this._subViews[viewName.name] = this._subViews[viewName.name] || new SubView({ name: viewName.name, solver: this._solver }); this._subViews[viewName.name]._type = this._subViews[viewName.name]._type || viewName.type; return this._subViews[viewName.name]; } else { this._subViews[viewName] = this._subViews[viewName] || new SubView({ name: viewName, solver: this._solver }); return this._subViews[viewName]; } } function _getSpacing(constraint) { let index = 4; if (!constraint.view1 && (constraint.attr1 === 'left')) { index = 3; } else if (!constraint.view1 && (constraint.attr1 === 'top')) { index = 0; } else if (!constraint.view2 && (constraint.attr2 === 'right')) { index = 1; } else if (!constraint.view2 && (constraint.attr2 === 'bottom')) { index = 2; } else { switch (constraint.attr1) { case 'left': case 'right': case 'centerX': case 'leading': case 'trailing': index = 4; break; case 'zIndex': index = 6; break; default: index = 5; } } this._spacingVars = this._spacingVars || new Array(7); this._spacingExpr = this._spacingExpr || new Array(7); if (!this._spacingVars[index]) { if (process.env.CASSOWARYJS) { this._spacingVars[index] = new c.Variable(); this._solver.addEditVar(this._spacingVars[index]); this._spacingExpr[index] = c.minus(0, this._spacingVars[index]); } else { this._spacingVars[index] = new kiwi.Variable(); this._solver.addEditVariable(this._spacingVars[index], kiwi.Strength.create(999, 1000, 1000)); this._spacingExpr[index] = this._spacingVars[index].multiply(-1); } this._solver.suggestValue(this._spacingVars[index], this._spacing[index]); } return this._spacingExpr[index]; } function _addConstraint(constraint) { //this.constraints.push(constraint); let relation; const multiplier = (constraint.multiplier !== undefined) ? constraint.multiplier : 1; let constant = (constraint.constant !== undefined) ? constraint.constant : 0; if (constant === 'default') { constant = _getSpacing.call(this, constraint); } const attr1 = _getSubView.call(this, constraint.view1)._getAttr(constraint.attr1); let attr2; if (process.env.CASSOWARYJS) { if (constraint.attr2 === Attribute.CONST) { attr2 = _getConst.call(this, undefined, constraint.constant); } else { attr2 = _getSubView.call(this, constraint.view2)._getAttr(constraint.attr2); if ((multiplier !== 1) && constant) { attr2 = c.plus(c.times(attr2, multiplier), constant); } else if (constant) { attr2 = c.plus(attr2, constant); } else if (multiplier !== 1) { attr2 = c.times(attr2, multiplier); } } const strength = ((constraint.priority !== undefined) && (constraint.priority < 1000)) ? new c.Strength('priority', 0, constraint.priority, 1000) : defaultPriorityStrength; switch (constraint.relation) { case Relation.EQU: relation = new c.Equation(attr1, attr2, strength); break; case Relation.GEQ: relation = new c.Inequality(attr1, c.GEQ, attr2, strength); break; case Relation.LEQ: relation = new c.Inequality(attr1, c.LEQ, attr2, strength); break; default: throw 'Invalid relation specified: ' + constraint.relation; } } else { if (constraint.attr2 === Attribute.CONST) { attr2 = _getConst.call(this, undefined, constraint.constant); } else { attr2 = _getSubView.call(this, constraint.view2)._getAttr(constraint.attr2); if ((multiplier !== 1) && constant) { attr2 = attr2.multiply(multiplier).plus(constant); } else if (constant) { attr2 = attr2.plus(constant); } else if (multiplier !== 1) { attr2 = attr2.multiply(multiplier); } } const strength = ((constraint.priority !== undefined) && (constraint.priority < 1000)) ? kiwi.Strength.create(0, constraint.priority, 1000) : defaultPriorityStrength; switch (constraint.relation) { case Relation.EQU: relation = new kiwi.Constraint(attr1, kiwi.Operator.Eq, attr2, strength); break; case Relation.GEQ: relation = new kiwi.Constraint(attr1, kiwi.Operator.Ge, attr2, strength); break; case Relation.LEQ: relation = new kiwi.Constraint(attr1, kiwi.Operator.Le, attr2, strength); break; default: throw 'Invalid relation specified: ' + constraint.relation; } } this._solver.addConstraint(relation); } function _compareSpacing(old, newz) { if (old === newz) { return true; } if (!old || !newz) { return false; } for (var i = 0; i < 7; i++) { if (old[i] !== newz[i]) { return false; } } return true; } /** * AutoLayoutJS API reference. * * ### Index * * |Entity|Type|Description| * |---|---|---| * |[AutoLayout](#autolayout)|`namespace`|Top level AutoLayout object.| * |[VisualFormat](#autolayoutvisualformat--object)|`namespace`|Parses VFL into constraints.| * |[View](#autolayoutview)|`class`|Main entity for adding & evaluating constraints.| * |[SubView](#autolayoutsubview--object)|`class`|SubView's are automatically created when constraints are added to views. They give access to the evaluated results.| * |[Attribute](#autolayoutattribute--enum)|`enum`|Attribute types that are supported when adding constraints.| * |[Relation](#autolayoutrelation--enum)|`enum`|Relationship types that are supported when adding constraints.| * |[Priority](#autolayoutpriority--enum)|`enum`|Default priority values for when adding constraints.| * * ### AutoLayout * * @module AutoLayout */ class View { /** * @class View * @param {Object} [options] Configuration options. * @param {Number} [options.width] Initial width of the view. * @param {Number} [options.height] Initial height of the view. * @param {Number|Object} [options.spacing] Spacing for the view (default: 8) (see `setSpacing`). * @param {Array} [options.constraints] One or more constraint definitions (see `addConstraints`). */ constructor(options) { this._solver = process.env.CASSOWARYJS ? new c.SimplexSolver() : new kiwi.Solver(); this._subViews = {}; //this._spacing = undefined; this._parentSubView = new SubView({ solver: this._solver }); this.setSpacing((options && (options.spacing !== undefined)) ? options.spacing : 8); //this.constraints = []; if (options) { if ((options.width !== undefined) || (options.height !== undefined)) { this.setSize(options.width, options.height); } if (options.constraints) { this.addConstraints(options.constraints); } } } /** * Sets the width and height of the view. * * @param {Number} width Width of the view. * @param {Number} height Height of the view. * @return {View} this */ setSize(width, height /*, depth*/) { this._parentSubView.intrinsicWidth = width; this._parentSubView.intrinsicHeight = height; return this; } /** * Width that was set using `setSize`. * @readonly * @type {Number} */ get width() { return this._parentSubView.intrinsicWidth; } /** * Height that was set using `setSize`. * @readonly * @type {Number} */ get height() { return this._parentSubView.intrinsicHeight; } /** * Width that is calculated from the constraints and the `.intrinsicWidth` of * the sub-views. * * When the width has been explicitely set using `setSize`, the fittingWidth * will **always** be the same as the explicitely set width. To calculate the size * based on the content, use: * ```javascript * var view = new AutoLayout.View({ * constraints: VisualFormat.parse('|-[view1]-[view2]-'), * spacing: 20 * }); * view.subViews.view1.intrinsicWidth = 100; * view.subViews.view2.intrinsicWidth = 100; * console.log('fittingWidth: ' + view.fittingWidth); // 260 * ``` * * @readonly * @type {Number} */ get fittingWidth() { return this._parentSubView.width; } /** * Height that is calculated from the constraints and the `.intrinsicHeight` of * the sub-views. * * See `.fittingWidth`. * * @readonly * @type {Number} */ get fittingHeight() { return this._parentSubView.height; } /** * Sets the spacing for the view. * * The spacing can be set for 7 different variables: * `top`, `right`, `bottom`, `left`, `width`, `height` and `zIndex`. The `left`-spacing is * used when a spacer is used between the parent-view and a sub-view (e.g. `|-[subView]`). * The same is true for the `right`, `top` and `bottom` spacers. The `width` and `height` are * used for spacers in between sub-views (e.g. `[view1]-[view2]`). * * Instead of using the full spacing syntax, it is also possible to use shorthand notations: * * |Syntax|Type|Description| * |---|---|---| * |`[top, right, bottom, left, width, height, zIndex]`|Array(7)|Full syntax including z-index **(clockwise order)**.| * |`[top, right, bottom, left, width, height]`|Array(6)|Full horizontal & vertical spacing syntax (no z-index) **(clockwise order)**.| * |`[horizontal, vertical, zIndex]`|Array(3)|Horizontal = left, right, width, vertical = top, bottom, height.| * |`[horizontal, vertical]`|Array(2)|Horizontal = left, right, width, vertical = top, bottom, height, z-index = 1.| * |`spacing`|Number|Horizontal & vertical spacing are all the same, z-index = 1.| * * Examples: * ```javascript * view.setSpacing(10); // horizontal & vertical spacing 10 * view.setSpacing([10, 15, 2]); // horizontal spacing 10, vertical spacing 15, z-axis spacing 2 * view.setSpacing([10, 20, 10, 20, 5, 5]); // top, right, bottom, left, horizontal, vertical * view.setSpacing([10, 20, 10, 20, 5, 5, 1]); // top, right, bottom, left, horizontal, vertical, z * ``` * * @param {Number|Array} spacing * @return {View} this */ setSpacing(spacing) { // convert spacing into array: [top, right, bottom, left, horz, vert, z-index] switch (Array.isArray(spacing) ? spacing.length : -1) { case -1: spacing = [spacing, spacing, spacing, spacing, spacing, spacing, 1]; break; case 1: spacing = [spacing[0], spacing[0], spacing[0], spacing[0], spacing[0], spacing[0], 1]; break; case 2: spacing = [spacing[1], spacing[0], spacing[1], spacing[0], spacing[0], spacing[1], 1]; break; case 3: spacing = [spacing[1], spacing[0], spacing[1], spacing[0], spacing[0], spacing[1], spacing[2]]; break; case 6: spacing = [spacing[0], spacing[1], spacing[2], spacing[3], spacing[4], spacing[5], 1]; break; case 7: break; default: throw 'Invalid spacing syntax'; } if (!_compareSpacing(this._spacing, spacing)) { this._spacing = spacing; // update spacing variables if (this._spacingVars) { for (var i = 0; i < this._spacingVars.length; i++) { if (this._spacingVars[i]) { this._solver.suggestValue(this._spacingVars[i], this._spacing[i]); } } if (process.env.CASSOWARYJS) { this._solver.resolve(); } else { this._solver.updateVariables(); } } } return this; } /** * Adds a constraint definition. * * A constraint definition has the following format: * * ```javascript * constraint: { * view1: {String}, * attr1: {AutoLayout.Attribute}, * relation: {AutoLayout.Relation}, * view2: {String}, * attr2: {AutoLayout.Attribute}, * multiplier: {Number}, * constant: {Number}, * priority: {Number}(0..1000) * } * ``` * @param {Object} constraint Constraint definition. * @return {View} this */ addConstraint(constraint) { _addConstraint.call(this, constraint); if (!process.env.CASSOWARYJS) { this._solver.updateVariables(); } return this; } /** * Adds one or more constraint definitions. * * A constraint definition has the following format: * * ```javascript * constraint: { * view1: {String}, * attr1: {AutoLayout.Attribute}, * relation: {AutoLayout.Relation}, * view2: {String}, * attr2: {AutoLayout.Attribute}, * multiplier: {Number}, * constant: {Number}, * priority: {Number}(0..1000) * } * ``` * @param {Array} constraints One or more constraint definitions. * @return {View} this */ addConstraints(constraints) { for (var j = 0; j < constraints.length; j++) { _addConstraint.call(this, constraints[j]); } if (!process.env.CASSOWARYJS) { this._solver.updateVariables(); } return this; } /** * Dictionary of `SubView` objects that have been created when adding constraints. * @readonly * @type {Object.SubView} */ get subViews() { return this._subViews; } /** * Checks whether the constraints incompletely specify the location * of the subViews. * @private */ //get hasAmbiguousLayout() { // Todo //} } export default View;