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

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

Powered by Google App Engine
This is Rietveld 408576698