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