| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| cr.exportPath('options'); |
| |
| /** |
| * Enumeration of display layout. These values must match the C++ values in |
| * ash::DisplayController. |
| * @enum {number} |
| */ |
| options.DisplayLayoutType = { |
| TOP: 0, |
| RIGHT: 1, |
| BOTTOM: 2, |
| LEFT: 3 |
| }; |
| |
| /** |
| * @typedef {{ |
| * left: number, |
| * top: number, |
| * width: number, |
| * height: number |
| * }} |
| */ |
| options.DisplayBounds; |
| |
| /** |
| * @typedef {{ |
| * x: number, |
| * y: number |
| * }} |
| */ |
| options.DisplayPosition; |
| |
| cr.define('options', function() { |
| 'use strict'; |
| |
| /** |
| * Snaps the region [point, width] to [basePoint, baseWidth] if |
| * the [point, width] is close enough to the base's edge. |
| * @param {number} point The starting point of the region. |
| * @param {number} width The width of the region. |
| * @param {number} basePoint The starting point of the base region. |
| * @param {number} baseWidth The width of the base region. |
| * @param {number=} opt_snapDistance Provide to override the snap distance. |
| * 0 means snap at any distance. |
| * @return {number} The moved point. Returns the point itself if it doesn't |
| * need to snap to the edge. |
| * @private |
| */ |
| function snapToEdge_(point, width, basePoint, baseWidth, opt_snapDistance) { |
| // If the edge of the region is smaller than this, it will snap to the |
| // base's edge. |
| /** @const */ var SNAP_DISTANCE_PX = 16; |
| var snapDist; |
| if (opt_snapDistance !== undefined) |
| snapDist = opt_snapDistance; |
| else |
| snapDist = SNAP_DISTANCE_PX; |
| |
| var startDiff = Math.abs(point - basePoint); |
| var endDiff = Math.abs(point + width - (basePoint + baseWidth)); |
| // Prefer the closer one if both edges are close enough. |
| if ((!snapDist || startDiff < snapDist) && startDiff < endDiff) |
| return basePoint; |
| else if (!snapDist || endDiff < snapDist) |
| return basePoint + baseWidth - width; |
| |
| return point; |
| } |
| |
| /** |
| * @constructor |
| * @param {string} id |
| * @param {string} name |
| * @param {!options.DisplayBounds} bounds |
| * @param {!options.DisplayLayoutType|undefined} layoutType |
| * @param {number|undefined} offset |
| * @param {string|undefined} parentId |
| * @return {!options.DisplayLayout} |
| */ |
| function DisplayLayout(id, name, bounds, layoutType, offset, parentId) { |
| this.bounds = bounds; |
| this.id = id; |
| if (layoutType != undefined) |
| this.layoutType = layoutType; |
| this.name = name; |
| if (offset != undefined) |
| this.offset = offset; |
| this.originalDivOffsets = {x: 0, y: 0}; |
| if (parentId != undefined) |
| this.parentId = parentId; |
| } |
| |
| // Class describing a display layout. |
| DisplayLayout.prototype = { |
| /** @type {?options.DisplayBounds} */ |
| bounds: null, |
| |
| /** @type {?HTMLElement} */ |
| div: null, |
| |
| /** @type {string} */ |
| id: '', |
| |
| /** @type {options.DisplayLayoutType} */ |
| layoutType: options.DisplayLayoutType.TOP, |
| |
| /** @type {string} */ |
| name: '', |
| |
| /** @type {number} */ |
| offset: 0, |
| |
| /** @type {?options.DisplayPosition} */ |
| originalDivOffsets: null, |
| |
| /** @type {string} */ |
| parentId: '', |
| |
| /** |
| * Calculates the div layout for displayLayout. |
| * @param {?options.DisplayPosition} offset |
| * @param {number} scale |
| * @param {?options.DisplayLayout} parentLayout |
| */ |
| layoutDivFromBounds: function(offset, scale, parentLayout) { |
| assert(offset); |
| var div = this.div; |
| var bounds = this.bounds; |
| |
| div.style.width = Math.floor(bounds.width * scale) + 'px'; |
| div.style.height = Math.floor(bounds.height * scale) + 'px'; |
| |
| if (!parentLayout) { |
| div.style.left = Math.trunc(bounds.left * scale) + offset.x + 'px'; |
| div.style.top = Math.trunc(bounds.top * scale) + offset.y + 'px'; |
| return; |
| } |
| |
| var parentDiv = parentLayout.div; |
| switch (this.layoutType) { |
| case options.DisplayLayoutType.TOP: |
| div.style.left = Math.trunc(bounds.left * scale) + offset.x + 'px'; |
| div.style.top = parentDiv.offsetTop - div.offsetHeight + 'px'; |
| break; |
| case options.DisplayLayoutType.RIGHT: |
| div.style.left = parentDiv.offsetLeft + parentDiv.offsetWidth + 'px'; |
| div.style.top = Math.trunc(bounds.top * scale) + offset.y + 'px'; |
| break; |
| case options.DisplayLayoutType.BOTTOM: |
| div.style.left = Math.trunc(bounds.left * scale) + offset.x + 'px'; |
| div.style.top = parentDiv.offsetTop + parentDiv.offsetHeight + 'px'; |
| break; |
| case options.DisplayLayoutType.LEFT: |
| div.style.left = parentDiv.offsetLeft - div.offsetWidth + 'px'; |
| div.style.top = Math.trunc(bounds.top * scale) + offset.y + 'px'; |
| break; |
| } |
| }, |
| |
| /** |
| * Calculates the offset relative to |parent|. |
| * @param {number} scale |
| * @param {?options.DisplayLayout} parent |
| */ |
| calculateOffset: function(scale, parent) { |
| // Offset is calculated from top or left edge. |
| if (!parent) { |
| this.offset = 0; |
| return; |
| } |
| assert(this.parentId == parent.id); |
| var offset; |
| if (this.layoutType == options.DisplayLayoutType.LEFT || |
| this.layoutType == options.DisplayLayoutType.RIGHT) { |
| offset = this.div.offsetTop - parent.div.offsetTop; |
| } else { |
| offset = this.div.offsetLeft - parent.div.offsetLeft; |
| } |
| this.offset = Math.trunc(offset / scale); |
| }, |
| |
| /** |
| * Calculates the bounds relative to |parentBounds|. |
| * @param {!options.DisplayBounds} parentBounds |
| * @return {!options.DisplayBounds} |
| */ |
| calculateBounds: function(parentBounds) { |
| var left = 0, top = 0; |
| switch (this.layoutType) { |
| case options.DisplayLayoutType.TOP: |
| left = parentBounds.left + this.offset; |
| top = parentBounds.top - this.bounds.height; |
| break; |
| case options.DisplayLayoutType.RIGHT: |
| left = parentBounds.left + parentBounds.width; |
| top = parentBounds.top + this.offset; |
| break; |
| case options.DisplayLayoutType.BOTTOM: |
| left = parentBounds.left + this.offset; |
| top = parentBounds.top + parentBounds.height; |
| break; |
| case options.DisplayLayoutType.LEFT: |
| left = parentBounds.left - this.bounds.width; |
| top = parentBounds.top + this.offset; |
| break; |
| } |
| return { |
| left: left, |
| top: top, |
| width: this.bounds.width, |
| height: this.bounds.height |
| }; |
| }, |
| |
| /** |
| * Return the position closest to |newPosition| along the edge of |
| * |parentDiv| specified by |layoutType|. |
| * @param {options.DisplayPosition} newPosition |
| * @param {?HTMLElement} parentDiv |
| * @param {!options.DisplayLayoutType} layoutType |
| * @return {!options.DisplayPosition} |
| */ |
| getSnapPosition: function(newPosition, parentDiv, layoutType) { |
| var x; |
| if (layoutType == options.DisplayLayoutType.LEFT) { |
| x = parentDiv.offsetLeft - this.div.offsetWidth; |
| } else if (layoutType == options.DisplayLayoutType.RIGHT) { |
| x = parentDiv.offsetLeft + parentDiv.offsetWidth; |
| } else { |
| x = this.snapToX(newPosition.x, parentDiv); |
| } |
| |
| var y; |
| if (layoutType == options.DisplayLayoutType.TOP) { |
| y = parentDiv.offsetTop - this.div.offsetHeight; |
| } else if (layoutType == options.DisplayLayoutType.BOTTOM) { |
| y = parentDiv.offsetTop + parentDiv.offsetHeight; |
| } else { |
| y = this.snapToY(newPosition.y, parentDiv); |
| } |
| |
| return {x: x, y: y}; |
| }, |
| |
| /** |
| * Update the div location to the position closest to |newPosition| along |
| * the edge of |parentDiv| specified by |layoutType|. |
| * @param {options.DisplayPosition} newPosition |
| * @param {?HTMLElement} parentDiv |
| * @param {!options.DisplayLayoutType} layoutType |
| */ |
| setDivPosition: function(newPosition, parentDiv, layoutType) { |
| var div = this.div; |
| switch (layoutType) { |
| case options.DisplayLayoutType.RIGHT: |
| div.style.left = parentDiv.offsetLeft + parentDiv.offsetWidth + 'px'; |
| div.style.top = newPosition.y + 'px'; |
| break; |
| case options.DisplayLayoutType.LEFT: |
| div.style.left = parentDiv.offsetLeft - div.offsetWidth + 'px'; |
| div.style.top = newPosition.y + 'px'; |
| break; |
| case options.DisplayLayoutType.TOP: |
| div.style.top = parentDiv.offsetTop - div.offsetHeight + 'px'; |
| div.style.left = newPosition.x + 'px'; |
| break; |
| case options.DisplayLayoutType.BOTTOM: |
| div.style.top = parentDiv.offsetTop + parentDiv.offsetHeight + 'px'; |
| div.style.left = newPosition.x + 'px'; |
| break; |
| } |
| }, |
| |
| /** |
| * Return the position of the corner of the div closest to |pos|. |
| * @param {?HTMLElement} parentDiv |
| * @param {options.DisplayPosition} pos |
| * @return {!options.DisplayPosition} |
| * @private |
| */ |
| getCornerPos: function(parentDiv, pos) { |
| var x; |
| if (pos.x > parentDiv.offsetLeft + parentDiv.offsetWidth / 2) |
| x = parentDiv.offsetLeft + parentDiv.offsetWidth; |
| else |
| x = parentDiv.offsetLeft - this.div.offsetWidth; |
| var y; |
| if (pos.y > parentDiv.offsetTop + parentDiv.offsetHeight / 2) |
| y = parentDiv.offsetTop + parentDiv.offsetHeight; |
| else |
| y = parentDiv.offsetTop - this.div.offsetHeight; |
| return {x: x, y: y}; |
| }, |
| |
| /** |
| * Ensures that there is a minimum overlap when displays meet at a corner. |
| * @param {?HTMLElement} parentDiv |
| * @param {options.DisplayLayoutType} layoutType |
| */ |
| adjustCorners: function(parentDiv, layoutType) { |
| // The number of pixels to share the edges between displays. |
| /** @const */ var MIN_OFFSET_OVERLAP = 5; |
| |
| var div = this.div; |
| if (layoutType == options.DisplayLayoutType.LEFT || |
| layoutType == options.DisplayLayoutType.RIGHT) { |
| var top = Math.max( |
| div.offsetTop, |
| parentDiv.offsetTop - div.offsetHeight + MIN_OFFSET_OVERLAP); |
| top = Math.min( |
| top, |
| parentDiv.offsetTop + parentDiv.offsetHeight - MIN_OFFSET_OVERLAP); |
| div.style.top = top + 'px'; |
| } else { |
| var left = Math.max( |
| div.offsetLeft, |
| parentDiv.offsetLeft - div.offsetWidth + MIN_OFFSET_OVERLAP); |
| left = Math.min( |
| left, |
| parentDiv.offsetLeft + parentDiv.offsetWidth - MIN_OFFSET_OVERLAP); |
| div.style.left = left + 'px'; |
| } |
| }, |
| |
| /** |
| * Calculates the layoutType for |position| relative to |parentDiv|. |
| * @param {?HTMLElement} parentDiv |
| * @param {!options.DisplayPosition} position |
| * @return {options.DisplayLayoutType} |
| */ |
| getLayoutTypeForPosition: function(parentDiv, position) { |
| var div = this.div; |
| |
| // Translate position from top-left to center. |
| var x = position.x + div.offsetWidth / 2; |
| var y = position.y + div.offsetHeight / 2; |
| |
| // Determine the distance from the new position to both of the near edges. |
| var left = parentDiv.offsetLeft; |
| var top = parentDiv.offsetTop; |
| var width = parentDiv.offsetWidth; |
| var height = parentDiv.offsetHeight; |
| |
| // Signed deltas to the center of the div. |
| var dx = x - (left + width / 2); |
| var dy = y - (top + height / 2); |
| |
| // Unsigned distance to each edge. |
| var distx = Math.abs(dx) - width / 2; |
| var disty = Math.abs(dy) - height / 2; |
| |
| if (distx > disty) { |
| if (dx < 0) |
| return options.DisplayLayoutType.LEFT; |
| else |
| return options.DisplayLayoutType.RIGHT; |
| } else { |
| if (dy < 0) |
| return options.DisplayLayoutType.TOP; |
| else |
| return options.DisplayLayoutType.BOTTOM; |
| } |
| }, |
| |
| /** |
| * Snaps a horizontal value, see SnapToEdge. |
| * @param {number} x |
| * @param {?HTMLElement} parentDiv |
| * @param {number=} opt_snapDistance Provide to override the snap distance. |
| * 0 means snap from any distance. |
| * @return {number} |
| */ |
| snapToX: function(x, parentDiv, opt_snapDistance) { |
| return snapToEdge_( |
| x, this.div.offsetWidth, parentDiv.offsetLeft, parentDiv.offsetWidth, |
| opt_snapDistance); |
| }, |
| |
| /** |
| * Snaps a vertical value, see SnapToEdge. |
| * @param {number} y |
| * @param {?HTMLElement} parentDiv |
| * @param {number=} opt_snapDistance Provide to override the snap distance. |
| * 0 means snap from any distance. |
| * @return {number} |
| */ |
| snapToY: function(y, parentDiv, opt_snapDistance) { |
| return snapToEdge_( |
| y, this.div.offsetHeight, parentDiv.offsetTop, parentDiv.offsetHeight, |
| opt_snapDistance); |
| }, |
| |
| /** |
| * Intersects this.div with |otherDiv|. If there is a collision, modifies |
| * |deltaPos| to limit movement to a single axis and avoid the collision |
| * and returns true. |
| * @param {?HTMLElement} otherDiv |
| * @param {!options.DisplayPosition} deltaPos |
| * @return {boolean} Whether there was a collision. |
| */ |
| collideWithDivAndModifyDelta: function(otherDiv, deltaPos) { |
| var div = this.div; |
| var newX = div.offsetLeft + deltaPos.x; |
| var newY = div.offsetTop + deltaPos.y; |
| |
| if ((newX + div.offsetWidth <= otherDiv.offsetLeft) || |
| (newX >= otherDiv.offsetLeft + otherDiv.offsetWidth) || |
| (newY + div.offsetHeight <= otherDiv.offsetTop) || |
| (newY >= otherDiv.offsetTop + otherDiv.offsetHeight)) { |
| return false; |
| } |
| |
| if (Math.abs(deltaPos.x) > Math.abs(deltaPos.y)) { |
| if (deltaPos.x > 0) { |
| var x = otherDiv.offsetLeft - div.offsetWidth; |
| if (x > div.offsetLeft) |
| deltaPos.x = x - div.offsetLeft; |
| else |
| deltaPos.x = 0; |
| } else { |
| var x = otherDiv.offsetLeft + otherDiv.offsetWidth; |
| if (x < div.offsetLeft) |
| deltaPos.x = x - div.offsetLeft; |
| else |
| deltaPos.x = 0; |
| } |
| deltaPos.y = 0; |
| } else { |
| deltaPos.x = 0; |
| if (deltaPos.y > 0) { |
| var y = otherDiv.offsetTop - div.offsetHeight; |
| if (y > div.offsetTop) |
| deltaPos.y = y - div.offsetTop; |
| else |
| deltaPos.y = 0; |
| } else if (deltaPos.y < 0) { |
| var y = otherDiv.offsetTop + otherDiv.offsetTop; |
| if (y < div.offsetTop) |
| deltaPos.y = y - div.offsetTop; |
| else |
| deltaPos.y = 0; |
| } |
| } |
| |
| return true; |
| } |
| }; |
| |
| // Export |
| return {DisplayLayout: DisplayLayout}; |
| }); |