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

Side by Side 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, 5 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 unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 /**
6 * @fileoverview Behavior for handling display layout, specifically
7 * edge snapping and collisions.
8 */
9
10 /** @polymerBehavior */
11 var LayoutBehavior = {
12 properties: {
13 /**
14 * Array of display layouts.
15 * @type {!Array<!chrome.system.display.DisplayLayout>}
16 */
17 layouts: Array,
18 },
19
20 /** @private {!Map<string, chrome.system.display.Bounds>} */
21 displayBoundsMap_: new Map(),
22
23 /** @private {!Map<string, chrome.system.display.DisplayLayout>} */
24 displayLayoutMap_: new Map(),
25
26 /**
27 * The calculated bounds used for generating the div bounds.
28 * @private {!Map<string, chrome.system.display.Bounds>}
29 */
30 calculatedBoundsMap_: new Map(),
31
32 /** @private {string} */
33 dragLayoutId: '',
34
35 /** @private {string} */
36 dragParentId_: '',
37
38 /** @private {!chrome.system.display.Bounds|undefined} */
39 dragBounds_: undefined,
40
41 /** @private {!chrome.system.display.LayoutPosition|undefined} */
42 dragLayoutPosition_: undefined,
43
44 /**
45 * @param {!Array<!chrome.system.display.DisplayUnitInfo>} displays
46 * @param {!Array<!chrome.system.display.DisplayLayout>} layouts
47 */
48 initializeDisplayLayout: function(displays, layouts) {
49 this.dragLayoutId = '';
50 this.dragParentId_ = '';
51
52 this.displayBoundsMap_.clear();
53 for (let display of displays)
54 this.displayBoundsMap_.set(display.id, display.bounds);
55
56 this.displayLayoutMap_.clear();
57 for (let layout of layouts)
58 this.displayLayoutMap_.set(layout.id, layout);
59
60 this.calculatedBoundsMap_.clear();
61 for (let display of displays) {
62 if (!this.calculatedBoundsMap_.has(display.id)) {
63 let bounds = display.bounds;
64 this.calculateBounds_(display.id, bounds.width, bounds.height);
65 }
66 }
67 },
68
69 /**
70 * Called when a drag event occurs. Checks collisions and updates the layout.
71 * @param {string} id
72 * @param {!chrome.system.display.Bounds} newBounds The new calculated
73 * bounds for the display.
74 * @return {!chrome.system.display.Bounds}
75 */
76 updateDisplayBounds(id, newBounds) {
77 this.dragLayoutId = id;
78
79 // Find the closest parent.
80 var closestId = this.findClosest_(id, newBounds);
81
82 // Find the closest edge.
83 var layoutPosition = this.getLayoutPositionForBounds_(newBounds, closestId);
84
85 // Snap to the closest edge.
86 this.snapBounds_(closestId, layoutPosition, newBounds);
87
88 // Calculate the new bounds and delta.
89 var oldBounds = this.dragBounds_ || this.getCalculatedDisplayBounds(id);
90 var deltaPos = {
91 x: newBounds.left - oldBounds.left,
92 y: newBounds.top - oldBounds.top
93 };
94
95 // Check for collisions after snapping. This should not collide with the
96 // closest parent.
97 this.collideAndModifyDelta_(id, oldBounds, deltaPos);
98
99 // If the edge changed, update and highlight it.
100 if (layoutPosition != this.dragLayoutPosition_ ||
101 closestId != this.dragParentId_) {
102 this.dragLayoutPosition_ = layoutPosition;
103 this.dragParentId_ = closestId;
104 this.highlightEdge_(closestId, layoutPosition);
105 }
106
107 newBounds.left = oldBounds.left + deltaPos.x;
108 newBounds.top = oldBounds.top + deltaPos.y;
109
110 this.dragBounds_ = newBounds;
111
112 return newBounds;
113 },
114
115 /**
116 * Called when dragging ends. Sends the updated layout to chrome.
117 * @param {string} id
118 */
119 finishUpdateDisplayBounds(id) {
120 this.highlightEdge_('', undefined); // Remove any highlights.
121 if (id != this.dragLayoutId || !this.dragBounds_ ||
122 !this.dragLayoutPosition_) {
123 return;
124 }
125 var layout = this.displayLayoutMap_.get(id);
126 if (!layout)
127 return;
128 // Note: This updates layout in this.displayLayoutMap_ which is also the
129 // entry in this.layouts.
130 this.updateOffsetAndPosition_(
131 this.dragBounds_, this.dragLayoutPosition_, layout);
132
133 // Send the updated layouts.
134 chrome.system.display.setDisplayLayout(this.layouts, function() {
135 if (chrome.runtime.lastError) {
136 console.error(
137 'setDisplayLayout Error: ' + chrome.runtime.lastError.message);
138 }
139 });
140 },
141
142 /**
143 * @param {string} displayId
144 * @return {!chrome.system.display.Bounds} bounds
145 */
146 getCalculatedDisplayBounds: function(displayId) {
147 var bounds = this.calculatedBoundsMap_.get(displayId);
148 assert(bounds);
149 return bounds;
150 },
151
152 /**
153 * @param {string} displayId
154 * @param {!chrome.system.display.Bounds|undefined} bounds
155 * @private
156 */
157 setCalculatedDisplayBounds_: function(displayId, bounds) {
158 assert(bounds);
159 this.calculatedBoundsMap_.set(
160 displayId,
161 /** @type {!chrome.system.display.Bounds} */ (
162 Object.assign({}, bounds)));
163 },
164
165 /**
166 * Recursively calculate the absolute bounds of a display.
167 * Caches the display bounds so that parent bounds are only calculated once.
168 * @param {string} id
169 * @param {number} width
170 * @param {number} height
171 * @private
172 */
173 calculateBounds_: function(id, width, height) {
174 var left, top;
175 var layout = this.displayLayoutMap_.get(id);
176 if (!layout || !layout.parentId) {
177 left = -width / 2;
178 top = -height / 2;
179 } else {
180 if (!this.calculatedBoundsMap_.has(layout.parentId)) {
181 var pbounds = this.displayBoundsMap_.get(layout.parentId);
182 this.calculateBounds_(layout.parentId, pbounds.width, pbounds.height);
183 }
184 var parentBounds = this.getCalculatedDisplayBounds(layout.parentId);
185 left = parentBounds.left;
186 top = parentBounds.top;
187 switch (layout.position) {
188 case chrome.system.display.LayoutPosition.TOP:
189 left += layout.offset;
190 top -= height;
191 break;
192 case chrome.system.display.LayoutPosition.RIGHT:
193 left += parentBounds.width;
194 top += layout.offset;
195 break;
196 case chrome.system.display.LayoutPosition.BOTTOM:
197 left += layout.offset;
198 top += parentBounds.height;
199 break;
200 case chrome.system.display.LayoutPosition.LEFT:
201 left -= width;
202 top += layout.offset;
203 break;
204 }
205 }
206 var result = {
207 left: left,
208 top: top,
209 width: width,
210 height: height,
211 };
212 this.setCalculatedDisplayBounds_(id, result);
213 },
214
215 /**
216 * Finds the display closest to |bounds| ignoring |opt_ignoreIds|.
217 * @param {string} displayId
218 * @param {!chrome.system.display.Bounds} bounds
219 * @param {Array<string>=} opt_ignoreIds Ids to ignore.
220 * @return {string}
221 * @private
222 */
223 findClosest_: function(displayId, bounds, opt_ignoreIds) {
224 var x = bounds.left + bounds.width / 2;
225 var y = bounds.top + bounds.height / 2;
226 var closestId = '';
227 var closestDelta2 = 0;
228 for (let otherId of this.calculatedBoundsMap_.keys()) {
229 if (otherId == displayId)
230 continue;
231 if (opt_ignoreIds && opt_ignoreIds.includes(otherId))
232 continue;
233 var otherBounds = this.getCalculatedDisplayBounds(otherId);
234 var left = otherBounds.left;
235 var top = otherBounds.top;
236 var width = otherBounds.width;
237 var height = otherBounds.height;
238 if (x >= left && x < left + width && y >= top && y < top + height)
239 return otherId; // point is inside rect
240 var dx, dy;
241 if (x < left)
242 dx = left - x;
243 else if (x > left + width)
244 dx = x - (left + width);
245 else
246 dx = 0;
247 if (y < top)
248 dy = top - y;
249 else if (y > top + height)
250 dy = y - (top + height);
251 else
252 dy = 0;
253 var delta2 = dx * dx + dy * dy;
254 if (closestId == '' || delta2 < closestDelta2) {
255 closestId = otherId;
256 closestDelta2 = delta2;
257 }
258 }
259 return closestId;
260 },
261
262 /**
263 * Calculates the LayoutPosition for |bounds| relative to |parentId|.
264 * @param {!chrome.system.display.Bounds} bounds
265 * @param {string} parentId
266 * @return {!chrome.system.display.LayoutPosition}
267 */
268 getLayoutPositionForBounds_: function(bounds, parentId) {
269 // Translate bounds from top-left to center.
270 var x = bounds.left + bounds.width / 2;
271 var y = bounds.top + bounds.height / 2;
272
273 // Determine the distance from the new bounds to both of the near edges.
274 var parentBounds = this.getCalculatedDisplayBounds(parentId);
275 var left = parentBounds.left;
276 var top = parentBounds.top;
277 var width = parentBounds.width;
278 var height = parentBounds.height;
279
280 // Signed deltas to the center of the div.
281 var dx = x - (left + width / 2);
282 var dy = y - (top + height / 2);
283
284 // Unsigned distance to each edge.
285 var distx = Math.abs(dx) - width / 2;
286 var disty = Math.abs(dy) - height / 2;
287
288 if (distx > disty) {
289 if (dx < 0)
290 return chrome.system.display.LayoutPosition.LEFT;
291 else
292 return chrome.system.display.LayoutPosition.RIGHT;
293 } else {
294 if (dy < 0)
295 return chrome.system.display.LayoutPosition.TOP;
296 else
297 return chrome.system.display.LayoutPosition.BOTTOM;
298 }
299 },
300
301 /**
302 * Modifes |bounds| to the position closest to it along the edge of |parentId|
303 * specified by |layoutPosition|.
304 * @param {string} parentId
305 * @param {!chrome.system.display.LayoutPosition} layoutPosition
306 * @param {!chrome.system.display.Bounds} bounds
307 */
308 snapBounds_: function(parentId, layoutPosition, bounds) {
309 var parentBounds = this.getCalculatedDisplayBounds(parentId);
310
311 var x;
312 if (layoutPosition == chrome.system.display.LayoutPosition.LEFT) {
313 x = parentBounds.left - bounds.width;
314 } else if (layoutPosition == chrome.system.display.LayoutPosition.RIGHT) {
315 x = parentBounds.left + parentBounds.width;
316 } else {
317 x = this.snapToX_(bounds, parentBounds);
318 }
319
320 var y;
321 if (layoutPosition == chrome.system.display.LayoutPosition.TOP) {
322 y = parentBounds.top - bounds.height;
323 } else if (layoutPosition == chrome.system.display.LayoutPosition.BOTTOM) {
324 y = parentBounds.top + parentBounds.height;
325 } else {
326 y = this.snapToY_(bounds, parentBounds);
327 }
328
329 bounds.left = x;
330 bounds.top = y;
331 },
332
333 /**
334 * Snaps a horizontal value, see snapToEdge.
335 * @param {!chrome.system.display.Bounds} newBounds
336 * @param {!chrome.system.display.Bounds} parentBounds
337 * @param {number=} opt_snapDistance Provide to override the snap distance.
338 * 0 means snap from any distance.
339 * @return {number}
340 */
341 snapToX_: function(newBounds, parentBounds, opt_snapDistance) {
342 return this.snapToEdge_(
343 newBounds.left, newBounds.width, parentBounds.left, parentBounds.width,
344 opt_snapDistance);
345 },
346
347 /**
348 * Snaps a vertical value, see snapToEdge.
349 * @param {!chrome.system.display.Bounds} newBounds
350 * @param {!chrome.system.display.Bounds} parentBounds
351 * @param {number=} opt_snapDistance Provide to override the snap distance.
352 * 0 means snap from any distance.
353 * @return {number}
354 */
355 snapToY_: function(newBounds, parentBounds, opt_snapDistance) {
356 return this.snapToEdge_(
357 newBounds.top, newBounds.height, parentBounds.top, parentBounds.height,
358 opt_snapDistance);
359 },
360
361 /**
362 * Snaps the region [point, width] to [basePoint, baseWidth] if
363 * the [point, width] is close enough to the base's edge.
364 * @param {number} point The starting point of the region.
365 * @param {number} width The width of the region.
366 * @param {number} basePoint The starting point of the base region.
367 * @param {number} baseWidth The width of the base region.
368 * @param {number=} opt_snapDistance Provide to override the snap distance.
369 * 0 means snap at any distance.
370 * @return {number} The moved point. Returns the point itself if it doesn't
371 * need to snap to the edge.
372 * @private
373 */
374 snapToEdge_: function(point, width, basePoint, baseWidth, opt_snapDistance) {
375 // If the edge of the region is smaller than this, it will snap to the
376 // base's edge.
377 /** @const */ var SNAP_DISTANCE_PX = 16;
378 var snapDist =
379 (opt_snapDistance !== undefined) ? opt_snapDistance : SNAP_DISTANCE_PX;
380
381 var startDiff = Math.abs(point - basePoint);
382 var endDiff = Math.abs(point + width - (basePoint + baseWidth));
383 // Prefer the closer one if both edges are close enough.
384 if ((!snapDist || startDiff < snapDist) && startDiff < endDiff)
385 return basePoint;
386 else if (!snapDist || endDiff < snapDist)
387 return basePoint + baseWidth - width;
388
389 return point;
390 },
391
392 /**
393 * Intersects |layout| with each other layout and reduces |deltaPos| to
394 * avoid any collisions (or sets it to [0,0] if the display can not be moved
395 * in the direction of |deltaPos|).
396 * Note: this assumes that deltaPos is already 'snapped' to the parent edge,
397 * and therefore will not collide with the parent, i.e. this is to prevent
398 * overlapping with displays other than the parent.
399 * @param {string} id
400 * @param {!chrome.system.display.Bounds} bounds
401 * @param {!{x: number, y: number}} deltaPos
402 */
403 collideAndModifyDelta_: function(id, bounds, deltaPos) {
404 var keys = this.calculatedBoundsMap_.keys();
405 var others = new Set(keys);
406 others.delete(id);
407 var checkCollisions = true;
408 while (checkCollisions) {
409 checkCollisions = false;
410 for (let otherId of others) {
411 var otherBounds = this.getCalculatedDisplayBounds(otherId);
412 if (this.collideWithBoundsAndModifyDelta_(
413 bounds, otherBounds, deltaPos)) {
414 if (deltaPos.x == 0 && deltaPos.y == 0)
415 return;
416 others.delete(otherId);
417 checkCollisions = true;
418 break;
419 }
420 }
421 }
422 },
423
424 /**
425 * Intersects |bounds| with |otherBounds|. If there is a collision, modifies
426 * |deltaPos| to limit movement to a single axis and avoid the collision
427 * and returns true. See note for |collideAndModifyDelta_|.
428 * @param {!chrome.system.display.Bounds} bounds
429 * @param {!chrome.system.display.Bounds} otherBounds
430 * @param {!{x: number, y: number}} deltaPos
431 * @return {boolean} Whether there was a collision.
432 */
433 collideWithBoundsAndModifyDelta_: function(bounds, otherBounds, deltaPos) {
434 var newX = bounds.left + deltaPos.x;
435 var newY = bounds.top + deltaPos.y;
436
437 if ((newX + bounds.width <= otherBounds.left) ||
438 (newX >= otherBounds.left + otherBounds.width) ||
439 (newY + bounds.height <= otherBounds.top) ||
440 (newY >= otherBounds.top + otherBounds.height)) {
441 return false;
442 }
443
444 // |deltaPos| should already be restricted to X or Y. This shortens the
445 // delta to stay outside the bounds, however it does not change the sign of
446 // the delta, i.e. it does not "push" the point outside the bounds if
447 // the point is already inside.
448 if (Math.abs(deltaPos.x) > Math.abs(deltaPos.y)) {
449 deltaPos.y = 0;
450 let snapDeltaX;
451 if (deltaPos.x > 0) {
452 let x = otherBounds.left - bounds.width;
453 snapDeltaX = Math.max(0, x - bounds.left);
454 } else {
455 let x = otherBounds.left + otherBounds.width;
456 snapDeltaX = Math.min(x - bounds.left, 0);
457 }
458 deltaPos.x = snapDeltaX;
459 } else {
460 deltaPos.x = 0;
461 let snapDeltaY;
462 if (deltaPos.y > 0) {
463 let y = otherBounds.top - bounds.height;
464 snapDeltaY = Math.min(0, y - bounds.top);
465 } else if (deltaPos.y < 0) {
466 let y = otherBounds.top + otherBounds.height;
467 snapDeltaY = Math.max(y - bounds.top, 0);
468 } else {
469 snapDeltaY = 0;
470 }
471 deltaPos.y = snapDeltaY;
472 }
473
474 return true;
475 },
476
477 /**
478 * Updates the offset for |layout| from |bounds|.
479 * @param {!chrome.system.display.Bounds} bounds
480 * @param {!chrome.system.display.LayoutPosition} position
481 * @param {!chrome.system.display.DisplayLayout} layout
482 */
483 updateOffsetAndPosition_: function(bounds, position, layout) {
484 layout.position = position;
485 if (!layout.parentId) {
486 layout.offset = 0;
487 return;
488 }
489
490 // Offset is calculated from top or left edge.
491 var parentBounds = this.getCalculatedDisplayBounds(layout.parentId);
492 var offset, minOffset, maxOffset;
493 if (position == chrome.system.display.LayoutPosition.LEFT ||
494 position == chrome.system.display.LayoutPosition.RIGHT) {
495 offset = bounds.top - parentBounds.top;
496 minOffset = -bounds.height;
497 maxOffset = parentBounds.height;
498 } else {
499 offset = bounds.left - parentBounds.left;
500 minOffset = -bounds.width;
501 maxOffset = parentBounds.width;
502 }
503 /** @const */ var MIN_OFFSET_OVERLAP = 50;
504 minOffset += MIN_OFFSET_OVERLAP;
505 maxOffset -= MIN_OFFSET_OVERLAP;
506 layout.offset = Math.max(minOffset, Math.min(offset, maxOffset));
507
508 // Update the calculated bounds to match the new offset.
509 this.calculateBounds_(layout.id, bounds.width, bounds.height);
510 },
511
512 /**
513 * Highlights the edge of the div associated with |id| based on
514 * |layoutPosition| and removes any other highlights. If |layoutPosition| is
515 * undefined, removes all highlights.
516 * @param {string} id
517 * @param {chrome.system.display.LayoutPosition|undefined} layoutPosition
518 * @private
519 */
520 highlightEdge_: function(id, layoutPosition) {
521 for (let layout of this.layouts) {
522 var highlight = (layout.id == id) ? layoutPosition : undefined;
523 var div = this.$$('#_' + layout.id);
524 div.classList.toggle(
525 'highlight-right',
526 highlight == chrome.system.display.LayoutPosition.RIGHT);
527 div.classList.toggle(
528 'highlight-left',
529 highlight == chrome.system.display.LayoutPosition.LEFT);
530 div.classList.toggle(
531 'highlight-top',
532 highlight == chrome.system.display.LayoutPosition.TOP);
533 div.classList.toggle(
534 'highlight-bottom',
535 highlight == chrome.system.display.LayoutPosition.BOTTOM);
536 }
537 },
538 };
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698