| 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..ccc2c32865f307aa268ada9ab91ba7a3a38fed0a
|
| --- /dev/null
|
| +++ b/chrome/browser/resources/settings/device_page/layout_behavior.js
|
| @@ -0,0 +1,538 @@
|
| +// 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 closest edge.
|
| + 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 after snapping. This should not collide with the
|
| + // closest parent.
|
| + this.collideAndModifyDelta_(id, oldBounds, deltaPos);
|
| +
|
| + // If the edge changed, update and highlight it.
|
| + if (layoutPosition != this.dragLayoutPosition_ ||
|
| + 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_ ||
|
| + !this.dragLayoutPosition_) {
|
| + 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 + bounds.width / 2;
|
| + var y = bounds.top + bounds.height / 2;
|
| + var closestId = '';
|
| + var closestDelta2 = 0;
|
| + for (let otherId of this.calculatedBoundsMap_.keys()) {
|
| + if (otherId == displayId)
|
| + continue;
|
| + if (opt_ignoreIds && opt_ignoreIds.includes(otherId))
|
| + 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) {
|
| + // 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
|
| + * @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|).
|
| + * Note: this assumes that deltaPos is already 'snapped' to the parent edge,
|
| + * and therefore will not collide with the parent, i.e. this is to prevent
|
| + * overlapping with displays other than the parent.
|
| + * @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. See note for |collideAndModifyDelta_|.
|
| + * @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;
|
| + }
|
| +
|
| + // |deltaPos| should already be restricted to X or Y. This shortens the
|
| + // delta to stay outside the bounds, however it does not change the sign of
|
| + // the delta, i.e. it does not "push" the point outside the bounds if
|
| + // the point is already inside.
|
| + if (Math.abs(deltaPos.x) > Math.abs(deltaPos.y)) {
|
| + deltaPos.y = 0;
|
| + let snapDeltaX;
|
| + if (deltaPos.x > 0) {
|
| + let x = otherBounds.left - bounds.width;
|
| + snapDeltaX = Math.max(0, x - bounds.left);
|
| + } else {
|
| + let x = otherBounds.left + otherBounds.width;
|
| + snapDeltaX = Math.min(x - bounds.left, 0);
|
| + }
|
| + deltaPos.x = snapDeltaX;
|
| + } else {
|
| + deltaPos.x = 0;
|
| + let snapDeltaY;
|
| + if (deltaPos.y > 0) {
|
| + let y = otherBounds.top - bounds.height;
|
| + snapDeltaY = Math.min(0, y - bounds.top);
|
| + } else if (deltaPos.y < 0) {
|
| + let y = otherBounds.top + otherBounds.height;
|
| + snapDeltaY = Math.max(y - bounds.top, 0);
|
| + } else {
|
| + snapDeltaY = 0;
|
| + }
|
| + deltaPos.y = snapDeltaY;
|
| + }
|
| +
|
| + 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);
|
| + }
|
| + },
|
| +};
|
|
|