Chromium Code Reviews| Index: chrome/browser/resources/settings/device_page/layout_behavior.js |
| diff --git a/chrome/browser/resources/settings/device_page/layout_behavior.js b/chrome/browser/resources/settings/device_page/layout_behavior.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..5db42f1e7e6ea249c2cd453906f75a243df65b07 |
| --- /dev/null |
| +++ b/chrome/browser/resources/settings/device_page/layout_behavior.js |
| @@ -0,0 +1,534 @@ |
| +// 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. |
| + |
| +/** |
| + * @fileoverview Behavior for handling display layout, specifically |
| + * edge snapping and collisions. |
| + */ |
| + |
| +/** @polymerBehavior */ |
| +var LayoutBehavior = { |
| + properties: { |
| + /** |
| + * Array of display layouts. |
| + * @type {!Array<!chrome.system.display.DisplayLayout>} |
| + */ |
| + layouts: Array, |
| + }, |
| + |
| + /** @private {!Map<string, chrome.system.display.Bounds>} */ |
| + displayBoundsMap_: new Map(), |
| + |
| + /** @private {!Map<string, chrome.system.display.DisplayLayout>} */ |
| + displayLayoutMap_: new Map(), |
| + |
| + /** |
| + * The calculated bounds used for generating the div bounds. |
| + * @private {!Map<string, chrome.system.display.Bounds>} |
| + */ |
| + calculatedBoundsMap_: new Map(), |
| + |
| + /** @private {string} */ |
| + dragLayoutId: '', |
| + |
| + /** @private {string} */ |
| + dragParentId_: '', |
| + |
| + /** @private {!chrome.system.display.Bounds|undefined} */ |
| + dragBounds_: undefined, |
| + |
| + /** @private {!chrome.system.display.LayoutPosition|undefined} */ |
| + dragLayoutPosition_: undefined, |
| + |
| + /** |
| + * @param {!Array<!chrome.system.display.DisplayUnitInfo>} displays |
| + * @param {!Array<!chrome.system.display.DisplayLayout>} layouts |
| + */ |
| + initializeDisplayLayout: function(displays, layouts) { |
| + this.dragLayoutId = ''; |
| + this.dragParentId_ = ''; |
| + |
| + this.displayBoundsMap_.clear(); |
| + for (let display of displays) |
| + this.displayBoundsMap_.set(display.id, display.bounds); |
| + |
| + this.displayLayoutMap_.clear(); |
| + for (let layout of layouts) |
| + this.displayLayoutMap_.set(layout.id, layout); |
| + |
| + this.calculatedBoundsMap_.clear(); |
| + for (let display of displays) { |
| + if (!this.calculatedBoundsMap_.has(display.id)) { |
| + let bounds = display.bounds; |
| + this.calculateBounds_(display.id, bounds.width, bounds.height); |
| + } |
| + } |
| + }, |
| + |
| + /** |
| + * Called when a drag event occurs. Checks collisions and updates the layout. |
| + * @param {string} id |
| + * @param {!chrome.system.display.Bounds} newBounds The new calculated |
| + * bounds for the display. |
| + * @return {!chrome.system.display.Bounds} |
| + */ |
| + updateDisplayBounds(id, newBounds) { |
| + this.dragLayoutId = id; |
| + |
| + // Find the closest parent. |
| + var closestId = this.findClosest_(id, newBounds); |
| + |
| + // Find the closest edge. |
| + var layoutPosition = this.getlayoutPositionForBounds_(newBounds, closestId); |
| + |
| + // Snap to the closet edge |
|
michaelpg
2016/06/27 22:09:32
"closest edge."
stevenjb
2016/06/27 23:25:47
Out of the closet, edge...
Done
michaelpg
2016/06/29 16:42:07
(also the period.)
stevenjb
2016/06/29 22:34:41
Done.
|
| + this.snapBounds_(closestId, layoutPosition, newBounds); |
| + |
| + // Calculate the new bounds and delta. |
| + var oldBounds = this.dragBounds_ || this.getCalculatedDisplayBounds(id); |
| + var deltaPos = { |
| + x: newBounds.left - oldBounds.left, |
| + y: newBounds.top - oldBounds.top |
| + }; |
| + |
| + // Check for collisions. |
| + this.collideAndModifyDelta_(id, oldBounds, deltaPos); |
| + |
| + // If the edge changed, update and highlight it. |
| + if (layoutPosition != this.draglayoutPosition_ || |
|
michaelpg
2016/06/29 19:58:46
dragLayoutPosition_ (as in properties) here & else
stevenjb
2016/06/29 22:34:41
Doh. I think I fixed this before but lost it in th
|
| + closestId != this.dragParentId_) { |
| + this.draglayoutPosition_ = layoutPosition; |
| + this.dragParentId_ = closestId; |
| + this.highlightEdge_(closestId, layoutPosition); |
| + } |
| + |
| + newBounds.left = oldBounds.left + deltaPos.x; |
| + newBounds.top = oldBounds.top + deltaPos.y; |
| + |
| + this.dragBounds_ = newBounds; |
| + |
| + return newBounds; |
| + }, |
| + |
| + /** |
| + * Called when dragging ends. Sends the updated layout to chrome. |
| + * @param {string} id |
| + */ |
| + finishUpdateDisplayBounds(id) { |
| + this.highlightEdge_('', undefined); // Remove any highlights. |
| + if (id != this.dragLayoutId || !this.dragBounds_) |
| + return; |
| + var layout = this.displayLayoutMap_.get(id); |
| + if (!layout) |
| + return; |
| + // Note: This updates layout in this.displayLayoutMap_ which is also the |
| + // entry in this.layouts. |
| + this.updateOffsetAndPosition_( |
| + this.dragBounds_, this.draglayoutPosition_, layout); |
| + |
| + // Send the updated layouts. |
| + chrome.system.display.setDisplayLayout(this.layouts, function() { |
| + if (chrome.runtime.lastError) { |
| + console.error( |
| + 'setDisplayLayout Error: ' + chrome.runtime.lastError.message); |
| + } |
| + }); |
| + }, |
| + |
| + /** |
| + * @param {string} displayId |
| + * @return {!chrome.system.display.Bounds} bounds |
| + */ |
| + getCalculatedDisplayBounds: function(displayId) { |
| + var bounds = this.calculatedBoundsMap_.get(displayId); |
| + assert(bounds); |
| + return bounds; |
| + }, |
| + |
| + /** |
| + * @param {string} displayId |
| + * @param {!chrome.system.display.Bounds|undefined} bounds |
| + * @private |
| + */ |
| + setCalculatedDisplayBounds_: function(displayId, bounds) { |
| + assert(bounds); |
| + this.calculatedBoundsMap_.set( |
| + displayId, |
| + /** @type {!chrome.system.display.Bounds} */ ( |
| + Object.assign({}, bounds))); |
| + }, |
| + |
| + /** |
| + * Recursively calculate the absolute bounds of a display. |
| + * Caches the display bounds so that parent bounds are only calculated once. |
| + * @param {string} id |
| + * @param {number} width |
| + * @param {number} height |
| + * @private |
| + */ |
| + calculateBounds_: function(id, width, height) { |
| + var left, top; |
| + var layout = this.displayLayoutMap_.get(id); |
| + if (!layout || !layout.parentId) { |
| + left = -width / 2; |
| + top = -height / 2; |
| + } else { |
| + if (!this.calculatedBoundsMap_.has(layout.parentId)) { |
| + var pbounds = this.displayBoundsMap_.get(layout.parentId); |
| + this.calculateBounds_(layout.parentId, pbounds.width, pbounds.height); |
| + } |
| + var parentBounds = this.getCalculatedDisplayBounds(layout.parentId); |
| + left = parentBounds.left; |
| + top = parentBounds.top; |
| + switch (layout.position) { |
| + case chrome.system.display.LayoutPosition.TOP: |
| + left += layout.offset; |
| + top -= height; |
| + break; |
| + case chrome.system.display.LayoutPosition.RIGHT: |
| + left += parentBounds.width; |
| + top += layout.offset; |
| + break; |
| + case chrome.system.display.LayoutPosition.BOTTOM: |
| + left += layout.offset; |
| + top += parentBounds.height; |
| + break; |
| + case chrome.system.display.LayoutPosition.LEFT: |
| + left -= width; |
| + top += layout.offset; |
| + break; |
| + } |
| + } |
| + var result = { |
| + left: left, |
| + top: top, |
| + width: width, |
| + height: height, |
| + }; |
| + this.setCalculatedDisplayBounds_(id, result); |
| + }, |
| + |
| + /** |
| + * Finds the display closest to |bounds| ignoring |opt_ignoreIds|. |
| + * @param {string} displayId |
| + * @param {!chrome.system.display.Bounds} bounds |
| + * @param {Array<string>=} opt_ignoreIds Ids to ignore. |
| + * @return {string} |
| + * @private |
| + */ |
| + findClosest_: function(displayId, bounds, opt_ignoreIds) { |
| + var x = bounds.left; |
| + var y = bounds.top; |
| + var closestId = ''; |
| + var closestDelta2 = 0; |
| + for (let otherId of this.calculatedBoundsMap_.keys()) { |
| + if (otherId == displayId) |
| + continue; |
| + if (opt_ignoreIds && opt_ignoreIds.indexOf(otherId) != -1) |
|
michaelpg
2016/06/27 22:09:32
replace "indexOf(otherId) != -1" with "includes(ot
stevenjb
2016/06/27 23:25:46
Nice. Done.
|
| + continue; |
| + var otherBounds = this.getCalculatedDisplayBounds(otherId); |
| + var left = otherBounds.left; |
| + var top = otherBounds.top; |
| + var width = otherBounds.width; |
| + var height = otherBounds.height; |
| + if (x >= left && x < left + width && y >= top && y < top + height) |
| + return otherId; // point is inside rect |
| + var dx, dy; |
| + if (x < left) |
| + dx = left - x; |
| + else if (x > left + width) |
| + dx = x - (left + width); |
| + else |
| + dx = 0; |
| + if (y < top) |
| + dy = top - y; |
| + else if (y > top + height) |
| + dy = y - (top + height); |
| + else |
| + dy = 0; |
| + var delta2 = dx * dx + dy * dy; |
| + if (closestId == '' || delta2 < closestDelta2) { |
| + closestId = otherId; |
| + closestDelta2 = delta2; |
| + } |
| + } |
| + return closestId; |
| + }, |
| + |
| + /** |
| + * Calculates the LayoutPosition for |bounds| relative to |parentId|. |
| + * @param {!chrome.system.display.Bounds} bounds |
| + * @param {string} parentId |
| + * @return {!chrome.system.display.LayoutPosition} |
| + */ |
| + getlayoutPositionForBounds_: function(bounds, parentId) { |
|
michaelpg
2016/06/27 22:09:32
capital L in getLayout
stevenjb
2016/06/27 23:25:46
Done.
|
| + // Translate bounds from top-left to center. |
| + var x = bounds.left + bounds.width / 2; |
| + var y = bounds.top + bounds.height / 2; |
| + |
| + // Determine the distance from the new bounds to both of the near edges. |
| + var parentBounds = this.getCalculatedDisplayBounds(parentId); |
| + var left = parentBounds.left; |
| + var top = parentBounds.top; |
| + var width = parentBounds.width; |
| + var height = parentBounds.height; |
| + |
| + // 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 chrome.system.display.LayoutPosition.LEFT; |
| + else |
| + return chrome.system.display.LayoutPosition.RIGHT; |
| + } else { |
| + if (dy < 0) |
| + return chrome.system.display.LayoutPosition.TOP; |
| + else |
| + return chrome.system.display.LayoutPosition.BOTTOM; |
| + } |
| + }, |
| + |
| + /** |
| + * Modifes |bounds| to the position closest to it along the edge of |parentId| |
| + * specified by |layoutPosition|. |
| + * @param {string} parentId |
| + * @param {!chrome.system.display.LayoutPosition} layoutPosition |
| + * @param {!chrome.system.display.Bounds} bounds |
| + */ |
| + snapBounds_: function(parentId, layoutPosition, bounds) { |
| + var parentBounds = this.getCalculatedDisplayBounds(parentId); |
| + |
| + var x; |
| + if (layoutPosition == chrome.system.display.LayoutPosition.LEFT) { |
| + x = parentBounds.left - bounds.width; |
| + } else if (layoutPosition == chrome.system.display.LayoutPosition.RIGHT) { |
| + x = parentBounds.left + parentBounds.width; |
| + } else { |
| + x = this.snapToX_(bounds, parentBounds); |
| + } |
| + |
| + var y; |
| + if (layoutPosition == chrome.system.display.LayoutPosition.TOP) { |
| + y = parentBounds.top - bounds.height; |
| + } else if (layoutPosition == chrome.system.display.LayoutPosition.BOTTOM) { |
| + y = parentBounds.top + parentBounds.height; |
| + } else { |
| + y = this.snapToY_(bounds, parentBounds); |
| + } |
| + |
| + bounds.left = x; |
| + bounds.top = y; |
| + }, |
| + |
| + /** |
| + * Snaps a horizontal value, see SnapToEdge. |
| + * @param {!chrome.system.display.Bounds} newBounds |
|
michaelpg
2016/06/27 22:09:32
lowercase snapToEdge
stevenjb
2016/06/27 23:25:47
Done.
|
| + * @param {!chrome.system.display.Bounds} parentBounds |
| + * @param {number=} opt_snapDistance Provide to override the snap distance. |
| + * 0 means snap from any distance. |
| + * @return {number} |
| + */ |
| + snapToX_: function(newBounds, parentBounds, opt_snapDistance) { |
| + return this.snapToEdge_( |
| + newBounds.left, newBounds.width, parentBounds.left, parentBounds.width, |
| + opt_snapDistance); |
| + }, |
| + |
| + /** |
| + * Snaps a vertical value, see SnapToEdge. |
| + * @param {!chrome.system.display.Bounds} newBounds |
| + * @param {!chrome.system.display.Bounds} parentBounds |
| + * @param {number=} opt_snapDistance Provide to override the snap distance. |
| + * 0 means snap from any distance. |
| + * @return {number} |
| + */ |
| + snapToY_: function(newBounds, parentBounds, opt_snapDistance) { |
| + return this.snapToEdge_( |
| + newBounds.top, newBounds.height, parentBounds.top, parentBounds.height, |
| + opt_snapDistance); |
| + }, |
| + |
| + /** |
| + * 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 |
| + */ |
| + snapToEdge_: function(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 = |
| + (opt_snapDistance !== undefined) ? opt_snapDistance : 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; |
| + }, |
| + |
| + /** |
| + * Intersects |layout| with each other layout and reduces |deltaPos| to |
| + * avoid any collisions (or sets it to [0,0] if the display can not be moved |
| + * in the direction of |deltaPos|). |
| + * @param {string} id |
| + * @param {!chrome.system.display.Bounds} bounds |
| + * @param {!{x: number, y: number}} deltaPos |
| + */ |
| + collideAndModifyDelta_: function(id, bounds, deltaPos) { |
| + var keys = this.calculatedBoundsMap_.keys(); |
| + var others = new Set(keys); |
| + others.delete(id); |
| + var checkCollisions = true; |
| + while (checkCollisions) { |
| + checkCollisions = false; |
| + for (let otherId of others) { |
| + var otherBounds = this.getCalculatedDisplayBounds(otherId); |
| + if (this.collideWithBoundsAndModifyDelta_( |
| + bounds, otherBounds, deltaPos)) { |
| + if (deltaPos.x == 0 && deltaPos.y == 0) |
| + return; |
| + others.delete(otherId); |
| + checkCollisions = true; |
| + break; |
| + } |
| + } |
| + } |
| + }, |
| + |
| + /** |
| + * Intersects |bounds| with |otherBounds|. If there is a collision, modifies |
| + * |deltaPos| to limit movement to a single axis and avoid the collision |
| + * and returns true. |
| + * @param {!chrome.system.display.Bounds} bounds |
| + * @param {!chrome.system.display.Bounds} otherBounds |
| + * @param {!{x: number, y: number}} deltaPos |
| + * @return {boolean} Whether there was a collision. |
| + */ |
| + collideWithBoundsAndModifyDelta_: function(bounds, otherBounds, deltaPos) { |
| + var newX = bounds.left + deltaPos.x; |
| + var newY = bounds.top + deltaPos.y; |
| + |
| + if ((newX + bounds.width <= otherBounds.left) || |
| + (newX >= otherBounds.left + otherBounds.width) || |
| + (newY + bounds.height <= otherBounds.top) || |
| + (newY >= otherBounds.top + otherBounds.height)) { |
| + return false; |
| + } |
| + |
| + if (Math.abs(deltaPos.x) > Math.abs(deltaPos.y)) { |
| + if (deltaPos.x > 0) { |
| + var x = otherBounds.left - bounds.width; |
| + if (x > bounds.left) |
| + deltaPos.x = x - bounds.left; |
| + else |
| + deltaPos.x = 0; |
| + } else { |
| + var x = otherBounds.left + otherBounds.width; |
| + if (x < bounds.left) |
| + deltaPos.x = x - bounds.left; |
| + else |
| + deltaPos.x = 0; |
| + } |
| + deltaPos.y = 0; |
|
michaelpg
2016/06/27 22:09:32
opt nit: put this at the top of the block, like li
michaelpg
2016/06/27 22:09:32
Is this logic copied from Options? It's odd that t
stevenjb
2016/06/27 23:25:46
It was thinking that x then y was more clear, but
stevenjb
2016/06/27 23:25:46
It was copied, but that doesn't mean it's entirely
|
| + } else { |
| + deltaPos.x = 0; |
| + if (deltaPos.y > 0) { |
| + var y = otherBounds.top - bounds.height; |
| + if (y > bounds.top) |
| + deltaPos.y = y - bounds.top; |
| + else |
| + deltaPos.y = 0; |
| + } else if (deltaPos.y < 0) { |
| + var y = otherBounds.top + otherBounds.top; |
| + if (y < bounds.top) |
| + deltaPos.y = y - bounds.top; |
| + else |
| + deltaPos.y = 0; |
| + } |
| + } |
| + |
| + return true; |
| + }, |
| + |
| + /** |
| + * Updates the offset for |layout| from |bounds|. |
| + * @param {!chrome.system.display.Bounds} bounds |
| + * @param {!chrome.system.display.LayoutPosition} position |
| + * @param {!chrome.system.display.DisplayLayout} layout |
| + */ |
| + updateOffsetAndPosition_: function(bounds, position, layout) { |
| + layout.position = position; |
| + if (!layout.parentId) { |
| + layout.offset = 0; |
| + return; |
| + } |
| + |
| + // Offset is calculated from top or left edge. |
| + var parentBounds = this.getCalculatedDisplayBounds(layout.parentId); |
| + var offset, minOffset, maxOffset; |
| + if (position == chrome.system.display.LayoutPosition.LEFT || |
| + position == chrome.system.display.LayoutPosition.RIGHT) { |
| + offset = bounds.top - parentBounds.top; |
| + minOffset = -bounds.height; |
| + maxOffset = parentBounds.height; |
| + } else { |
| + offset = bounds.left - parentBounds.left; |
| + minOffset = -bounds.width; |
| + maxOffset = parentBounds.width; |
| + } |
| + /** @const */ var MIN_OFFSET_OVERLAP = 50; |
| + minOffset += MIN_OFFSET_OVERLAP; |
| + maxOffset -= MIN_OFFSET_OVERLAP; |
| + layout.offset = Math.max(minOffset, Math.min(offset, maxOffset)); |
| + |
| + // Update the calculated bounds to match the new offset. |
| + this.calculateBounds_(layout.id, bounds.width, bounds.height); |
| + }, |
| + |
| + /** |
| + * Highlights the edge of the div associated with |id| based on |
| + * |layoutPosition| and removes any other highlights. If |layoutPosition| is |
| + * undefined, removes all highlights. |
| + * @param {string} id |
| + * @param {chrome.system.display.LayoutPosition|undefined} layoutPosition |
| + * @private |
| + */ |
| + highlightEdge_: function(id, layoutPosition) { |
| + for (let layout of this.layouts) { |
| + var highlight = (layout.id == id) ? layoutPosition : undefined; |
| + var div = this.$$('#_' + layout.id); |
| + div.classList.toggle( |
| + 'highlight-right', |
| + highlight == chrome.system.display.LayoutPosition.RIGHT); |
| + div.classList.toggle( |
| + 'highlight-left', |
| + highlight == chrome.system.display.LayoutPosition.LEFT); |
| + div.classList.toggle( |
| + 'highlight-top', |
| + highlight == chrome.system.display.LayoutPosition.TOP); |
| + div.classList.toggle( |
| + 'highlight-bottom', |
| + highlight == chrome.system.display.LayoutPosition.BOTTOM); |
| + } |
| + }, |
| +}; |