OLD | NEW |
---|---|
(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 }; | |
OLD | NEW |