Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(2727)

Unified Diff: chrome/browser/resources/settings/device_page/layout_behavior.js

Issue 2097763004: MD Settings: Display: Add snapping and collisions (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@issue_547080_display_settings8a_drag
Patch Set: Rebase + More Feedback Created 4 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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);
+ }
+ },
+};

Powered by Google App Engine
This is Rietveld 408576698