OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 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 |
| 7 * Provides view port management utilities below for a desktop remoting session. |
| 8 * - Enabling bump scrolling |
| 9 * - Resizing the viewport to fit the host desktop |
| 10 * - Resizing the host desktop to fit the client viewport. |
| 11 */ |
| 12 |
| 13 /** @suppress {duplicate} */ |
| 14 var remoting = remoting || {}; |
| 15 |
| 16 (function() { |
| 17 |
| 18 'use strict'; |
| 19 |
| 20 /** |
| 21 * @param {HTMLElement} rootElement The outer element with id=scroller that we |
| 22 * are showing scrollbars on. |
| 23 * @param {remoting.HostDesktop} hostDesktop |
| 24 * @param {remoting.Host.Options} hostOptions |
| 25 * |
| 26 * @constructor |
| 27 * @implements {base.Disposable} |
| 28 */ |
| 29 remoting.DesktopViewport = function(rootElement, hostDesktop, hostOptions) { |
| 30 /** @private */ |
| 31 this.rootElement_ = rootElement; |
| 32 /** @private */ |
| 33 // TODO(kelvinp): Query the container by class name instead of id. |
| 34 this.pluginContainer_ = rootElement.querySelector('#client-container'); |
| 35 /** @private */ |
| 36 this.pluginElement_ = rootElement.querySelector('embed'); |
| 37 /** @private */ |
| 38 this.hostDesktop_ = hostDesktop; |
| 39 /** @private */ |
| 40 this.hostOptions_ = hostOptions; |
| 41 /** @private {number?} */ |
| 42 this.resizeTimer_ = null; |
| 43 /** @private {remoting.BumpScroller} */ |
| 44 this.bumpScroller_ = null; |
| 45 // Bump-scroll test variables. Override to use a fake value for the width |
| 46 // and height of the client plugin so that bump-scrolling can be tested |
| 47 // without relying on the actual size of the host desktop. |
| 48 /** @private {number} */ |
| 49 this.pluginWidthForBumpScrollTesting_ = 0; |
| 50 /** @private {number} */ |
| 51 this.pluginHeightForBumpScrollTesting_ = 0; |
| 52 |
| 53 this.eventHooks_ = new base.Disposables( |
| 54 new base.EventHook( |
| 55 this.hostDesktop_, remoting.HostDesktop.Events.sizeChanged, |
| 56 this.onDesktopSizeChanged_.bind(this)), |
| 57 // TODO(kelvinp): Move window shape related logic into |
| 58 // remoting.AppConnectedView. |
| 59 new base.EventHook( |
| 60 this.hostDesktop_, remoting.HostDesktop.Events.shapeChanged, |
| 61 remoting.windowShape.setDesktopRects.bind(remoting.windowShape))); |
| 62 |
| 63 if (this.hostOptions_.resizeToClient) { |
| 64 this.resizeHostDesktop_(); |
| 65 } else { |
| 66 this.onDesktopSizeChanged_(); |
| 67 } |
| 68 }; |
| 69 |
| 70 remoting.DesktopViewport.prototype.dispose = function() { |
| 71 base.dispose(this.eventHooks_); |
| 72 this.eventHooks_ = null; |
| 73 base.dispose(this.bumpScroller_); |
| 74 this.bumpScroller_ = null; |
| 75 }; |
| 76 |
| 77 /** |
| 78 * @return {boolean} True if shrink-to-fit is enabled; false otherwise. |
| 79 */ |
| 80 remoting.DesktopViewport.prototype.getShrinkToFit = function() { |
| 81 return this.hostOptions_.shrinkToFit; |
| 82 }; |
| 83 |
| 84 /** |
| 85 * @return {boolean} True if resize-to-client is enabled; false otherwise. |
| 86 */ |
| 87 remoting.DesktopViewport.prototype.getResizeToClient = function() { |
| 88 return this.hostOptions_.resizeToClient; |
| 89 }; |
| 90 |
| 91 /** |
| 92 * @return {{top:number, left:number}} The top-left corner of the plugin. |
| 93 */ |
| 94 remoting.DesktopViewport.prototype.getPluginPositionForTesting = function() { |
| 95 var style = this.pluginContainer_.style; |
| 96 return { |
| 97 top: parseFloat(style.marginTop), |
| 98 left: parseFloat(style.marginLeft) |
| 99 }; |
| 100 }; |
| 101 |
| 102 /** |
| 103 * @param {number} width |
| 104 * @param {number} height |
| 105 */ |
| 106 remoting.DesktopViewport.prototype.setPluginSizeForBumpScrollTesting = |
| 107 function(width, height) { |
| 108 this.pluginWidthForBumpScrollTesting_ = width; |
| 109 this.pluginHeightForBumpScrollTesting_ = height; |
| 110 }; |
| 111 |
| 112 /** |
| 113 * @return {remoting.BumpScroller} |
| 114 */ |
| 115 remoting.DesktopViewport.prototype.getBumpScrollerForTesting = function() { |
| 116 return this.bumpScroller_; |
| 117 }; |
| 118 |
| 119 /** |
| 120 * Set the shrink-to-fit and resize-to-client flags and save them if this is |
| 121 * a Me2Me connection. |
| 122 * |
| 123 * @param {boolean} shrinkToFit True if the remote desktop should be scaled |
| 124 * down if it is larger than the client window; false if scroll-bars |
| 125 * should be added in this case. |
| 126 * @param {boolean} resizeToClient True if window resizes should cause the |
| 127 * host to attempt to resize its desktop to match the client window size; |
| 128 * false to disable this behaviour for subsequent window resizes--the |
| 129 * current host desktop size is not restored in this case. |
| 130 * @return {void} Nothing. |
| 131 */ |
| 132 remoting.DesktopViewport.prototype.setScreenMode = |
| 133 function(shrinkToFit, resizeToClient) { |
| 134 if (resizeToClient && !this.hostOptions_.resizeToClient) { |
| 135 this.resizeHostDesktop_(); |
| 136 } |
| 137 |
| 138 // If enabling shrink, reset bump-scroll offsets. |
| 139 var needsScrollReset = shrinkToFit && !this.hostOptions_.shrinkToFit; |
| 140 this.hostOptions_.shrinkToFit = shrinkToFit; |
| 141 this.hostOptions_.resizeToClient = resizeToClient; |
| 142 this.hostOptions_.save(); |
| 143 this.updateScrollbarVisibility_(); |
| 144 |
| 145 this.updateDimensions_(); |
| 146 if (needsScrollReset) { |
| 147 this.resetScroll_(); |
| 148 } |
| 149 }; |
| 150 |
| 151 /** |
| 152 * Scroll the client plugin by the specified amount, keeping it visible. |
| 153 * Note that this is only used in content full-screen mode (not windowed or |
| 154 * browser full-screen modes), where window.scrollBy and the scrollTop and |
| 155 * scrollLeft properties don't work. |
| 156 * |
| 157 * @param {number} dx The amount by which to scroll horizontally. Positive to |
| 158 * scroll right; negative to scroll left. |
| 159 * @param {number} dy The amount by which to scroll vertically. Positive to |
| 160 * scroll down; negative to scroll up. |
| 161 * @return {boolean} False if the requested scroll had no effect because both |
| 162 * vertical and horizontal edges of the screen have been reached. |
| 163 */ |
| 164 remoting.DesktopViewport.prototype.scroll = function(dx, dy) { |
| 165 /** |
| 166 * Helper function for x- and y-scrolling |
| 167 * @param {number|string} curr The current margin, eg. "10px". |
| 168 * @param {number} delta The requested scroll amount. |
| 169 * @param {number} windowBound The size of the window, in pixels. |
| 170 * @param {number} pluginBound The size of the plugin, in pixels. |
| 171 * @param {{stop: boolean}} stop Reference parameter used to indicate when |
| 172 * the scroll has reached one of the edges and can be stopped in that |
| 173 * direction. |
| 174 * @return {string} The new margin value. |
| 175 */ |
| 176 var adjustMargin = function(curr, delta, windowBound, pluginBound, stop) { |
| 177 var minMargin = Math.min(0, windowBound - pluginBound); |
| 178 var result = (curr ? parseFloat(curr) : 0) - delta; |
| 179 result = Math.min(0, Math.max(minMargin, result)); |
| 180 stop.stop = (result === 0 || result == minMargin); |
| 181 return result + 'px'; |
| 182 }; |
| 183 |
| 184 var style = this.pluginContainer_.style; |
| 185 |
| 186 var pluginWidth = |
| 187 this.pluginWidthForBumpScrollTesting_ || this.pluginElement_.clientWidth; |
| 188 var pluginHeight = this.pluginHeightForBumpScrollTesting_ || |
| 189 this.pluginElement_.clientHeight; |
| 190 |
| 191 var clientArea = this.getClientArea(); |
| 192 var stopX = { stop: false }; |
| 193 style.marginLeft = |
| 194 adjustMargin(style.marginLeft, dx, clientArea.width, pluginWidth, stopX); |
| 195 |
| 196 var stopY = { stop: false }; |
| 197 style.marginTop = |
| 198 adjustMargin(style.marginTop, dy, clientArea.height, pluginHeight, stopY); |
| 199 return !stopX.stop || !stopY.stop; |
| 200 }; |
| 201 |
| 202 /** |
| 203 * Enable or disable bump-scrolling. When disabling bump scrolling, also reset |
| 204 * the scroll offsets to (0, 0). |
| 205 * @param {boolean} enable True to enable bump-scrolling, false to disable it. |
| 206 */ |
| 207 remoting.DesktopViewport.prototype.enableBumpScroll = function(enable) { |
| 208 if (enable) { |
| 209 this.bumpScroller_ = new remoting.BumpScroller(this); |
| 210 } else { |
| 211 base.dispose(this.bumpScroller_); |
| 212 this.bumpScroller_ = null; |
| 213 this.resetScroll_(); |
| 214 } |
| 215 }; |
| 216 |
| 217 /** |
| 218 * This is a callback that gets called when the window is resized. |
| 219 * |
| 220 * @return {void} Nothing. |
| 221 */ |
| 222 remoting.DesktopViewport.prototype.onResize = function() { |
| 223 this.updateDimensions_(); |
| 224 |
| 225 if (this.resizeTimer_) { |
| 226 window.clearTimeout(this.resizeTimer_); |
| 227 this.resizeTimer_ = null; |
| 228 } |
| 229 |
| 230 // Defer notifying the host of the change until the window stops resizing, to |
| 231 // avoid overloading the control channel with notifications. |
| 232 if (this.hostOptions_.resizeToClient) { |
| 233 var kResizeRateLimitMs = 250; |
| 234 var clientArea = this.getClientArea(); |
| 235 this.resizeTimer_ = window.setTimeout(this.resizeHostDesktop_.bind(this), |
| 236 kResizeRateLimitMs); |
| 237 } |
| 238 |
| 239 // If bump-scrolling is enabled, adjust the plugin margins to fully utilize |
| 240 // the new window area. |
| 241 this.resetScroll_(); |
| 242 this.updateScrollbarVisibility_(); |
| 243 }; |
| 244 |
| 245 /** |
| 246 * @return {{width:number, height:number}} The height of the window's client |
| 247 * area. This differs between apps v1 and apps v2 due to the custom window |
| 248 * borders used by the latter. |
| 249 */ |
| 250 remoting.DesktopViewport.prototype.getClientArea = function() { |
| 251 return remoting.windowFrame ? |
| 252 remoting.windowFrame.getClientArea() : |
| 253 { 'width': window.innerWidth, 'height': window.innerHeight }; |
| 254 }; |
| 255 |
| 256 /** |
| 257 * Notifies the host of the client's current dimensions and DPI. |
| 258 * Also takes into account per-host scaling factor, if configured. |
| 259 * @private |
| 260 */ |
| 261 remoting.DesktopViewport.prototype.resizeHostDesktop_ = function() { |
| 262 var clientArea = this.getClientArea(); |
| 263 this.hostDesktop_.resize(clientArea.width * this.hostOptions_.desktopScale, |
| 264 clientArea.height * this.hostOptions_.desktopScale, |
| 265 window.devicePixelRatio); |
| 266 }; |
| 267 |
| 268 /** |
| 269 * This is a callback that gets called when the plugin notifies us of a change |
| 270 * in the size of the remote desktop. |
| 271 * |
| 272 * @return {void} Nothing. |
| 273 * @private |
| 274 */ |
| 275 remoting.DesktopViewport.prototype.onDesktopSizeChanged_ = function() { |
| 276 var dimensions = this.hostDesktop_.getDimensions(); |
| 277 console.log('desktop size changed: ' + |
| 278 dimensions.width + 'x' + |
| 279 dimensions.height +' @ ' + |
| 280 dimensions.xDpi + 'x' + |
| 281 dimensions.yDpi + ' DPI'); |
| 282 this.updateDimensions_(); |
| 283 this.updateScrollbarVisibility_(); |
| 284 }; |
| 285 |
| 286 /** |
| 287 * Called when the window or desktop size or the scaling settings change, |
| 288 * to set the scroll-bar visibility. |
| 289 * |
| 290 * TODO(jamiewalch): crbug.com/252796: Remove this once crbug.com/240772 is |
| 291 * fixed. |
| 292 */ |
| 293 remoting.DesktopViewport.prototype.updateScrollbarVisibility_ = function() { |
| 294 // TODO(kelvinp): Remove the check once app-remoting no longer depends on |
| 295 // this. |
| 296 if (!this.rootElement_) { |
| 297 return; |
| 298 } |
| 299 |
| 300 var needsScrollY = false; |
| 301 var needsScrollX = false; |
| 302 if (!this.hostOptions_.shrinkToFit) { |
| 303 // Determine whether or not horizontal or vertical scrollbars are |
| 304 // required, taking into account their width. |
| 305 var clientArea = this.getClientArea(); |
| 306 var hostDesktop = this.hostDesktop_.getDimensions(); |
| 307 needsScrollY = clientArea.height < hostDesktop.height; |
| 308 needsScrollX = clientArea.width < hostDesktop.width; |
| 309 var kScrollBarWidth = 16; |
| 310 if (needsScrollX && !needsScrollY) { |
| 311 needsScrollY = clientArea.height - kScrollBarWidth < hostDesktop.height; |
| 312 } else if (!needsScrollX && needsScrollY) { |
| 313 needsScrollX = clientArea.width - kScrollBarWidth < hostDesktop.width; |
| 314 } |
| 315 } |
| 316 |
| 317 this.rootElement_.classList.toggle('no-horizontal-scroll', !needsScrollX); |
| 318 this.rootElement_.classList.toggle('no-vertical-scroll', !needsScrollY); |
| 319 }; |
| 320 |
| 321 remoting.DesktopViewport.prototype.updateDimensions_ = function() { |
| 322 var dimensions = this.hostDesktop_.getDimensions(); |
| 323 if (dimensions.width === 0 || dimensions.height === 0) { |
| 324 return; |
| 325 } |
| 326 |
| 327 var desktopSize = { width: dimensions.width, |
| 328 height: dimensions.height }; |
| 329 var desktopDpi = { x: dimensions.xDpi, |
| 330 y: dimensions.yDpi }; |
| 331 var newSize = remoting.DesktopViewport.choosePluginSize( |
| 332 this.getClientArea(), window.devicePixelRatio, |
| 333 desktopSize, desktopDpi, this.hostOptions_.desktopScale, |
| 334 remoting.fullscreen.isActive(), this.hostOptions_.shrinkToFit); |
| 335 |
| 336 // Resize the plugin if necessary. |
| 337 console.log('plugin dimensions:' + newSize.width + 'x' + newSize.height); |
| 338 this.pluginElement_.style.width = newSize.width + 'px'; |
| 339 this.pluginElement_.style.height = newSize.height + 'px'; |
| 340 |
| 341 // When we receive the first plugin dimensions from the host, we know that |
| 342 // remote host has started. |
| 343 remoting.app.onVideoStreamingStarted(); |
| 344 }; |
| 345 |
| 346 /** |
| 347 * Helper function accepting client and host dimensions, and returning a chosen |
| 348 * size for the plugin element, in DIPs. |
| 349 * |
| 350 * @param {{width: number, height: number}} clientSizeDips Available client |
| 351 * dimensions, in DIPs. |
| 352 * @param {number} clientPixelRatio Number of physical pixels per client DIP. |
| 353 * @param {{width: number, height: number}} desktopSize Size of the host desktop |
| 354 * in physical pixels. |
| 355 * @param {{x: number, y: number}} desktopDpi DPI of the host desktop in both |
| 356 * dimensions. |
| 357 * @param {number} desktopScale The scale factor configured for the host. |
| 358 * @param {boolean} isFullscreen True if full-screen mode is active. |
| 359 * @param {boolean} shrinkToFit True if shrink-to-fit should be applied. |
| 360 * @return {{width: number, height: number}} Chosen plugin dimensions, in DIPs. |
| 361 */ |
| 362 remoting.DesktopViewport.choosePluginSize = function( |
| 363 clientSizeDips, clientPixelRatio, desktopSize, desktopDpi, desktopScale, |
| 364 isFullscreen, shrinkToFit) { |
| 365 base.debug.assert(clientSizeDips.width > 0); |
| 366 base.debug.assert(clientSizeDips.height > 0); |
| 367 base.debug.assert(clientPixelRatio >= 1.0); |
| 368 base.debug.assert(desktopSize.width > 0); |
| 369 base.debug.assert(desktopSize.height > 0); |
| 370 base.debug.assert(desktopDpi.x > 0); |
| 371 base.debug.assert(desktopDpi.y > 0); |
| 372 base.debug.assert(desktopScale > 0); |
| 373 |
| 374 // We have the following goals in sizing the desktop display at the client: |
| 375 // 1. Avoid losing detail by down-scaling beyond 1:1 host:device pixels. |
| 376 // 2. Avoid up-scaling if that will cause the client to need scrollbars. |
| 377 // 3. Avoid introducing blurriness with non-integer up-scaling factors. |
| 378 // 4. Avoid having huge "letterboxes" around the desktop, if it's really |
| 379 // small. |
| 380 // 5. Compensate for mismatched DPIs, so that the behaviour of features like |
| 381 // shrink-to-fit matches their "natural" rather than their pixel size. |
| 382 // e.g. with shrink-to-fit active a 1024x768 low-DPI host on a 640x480 |
| 383 // high-DPI client will be up-scaled to 1280x960, rather than displayed |
| 384 // at 1:1 host:physical client pixels. |
| 385 // |
| 386 // To determine the ideal size we follow a four-stage process: |
| 387 // 1. Determine the "natural" size at which to display the desktop. |
| 388 // a. Initially assume 1:1 mapping of desktop to client device pixels. |
| 389 // b. If host DPI is less than the client's then up-scale accordingly. |
| 390 // c. If desktopScale is configured for the host then allow that to |
| 391 // reduce the amount of up-scaling from (b). e.g. if the client:host |
| 392 // DPIs are 2:1 then a desktopScale of 1.5 would reduce the up-scale |
| 393 // to 4:3, while a desktopScale of 3.0 would result in no up-scaling. |
| 394 // 2. If the natural size of the desktop is smaller than the client device |
| 395 // then apply up-scaling by an integer scale factor to avoid excessive |
| 396 // letterboxing. |
| 397 // 3. If shrink-to-fit is configured then: |
| 398 // a. If the natural size exceeds the client size then apply down-scaling |
| 399 // by an arbitrary scale factor. |
| 400 // b. If we're in full-screen mode and the client & host aspect-ratios |
| 401 // are radically different (e.g. the host is actually multi-monitor) |
| 402 // then shrink-to-fit to the shorter dimension, rather than leaving |
| 403 // huge letterboxes; the user can then bump-scroll around the desktop. |
| 404 // 4. If the overall scale factor is fractionally over an integer factor |
| 405 // then reduce it to that integer factor, to avoid blurring. |
| 406 |
| 407 // All calculations are performed in device pixels. |
| 408 var clientWidth = clientSizeDips.width * clientPixelRatio; |
| 409 var clientHeight = clientSizeDips.height * clientPixelRatio; |
| 410 |
| 411 // 1. Determine a "natural" size at which to display the desktop. |
| 412 var scale = 1.0; |
| 413 |
| 414 // Determine the effective host device pixel ratio. |
| 415 // Note that we round up or down to the closest integer pixel ratio. |
| 416 var hostPixelRatioX = Math.round(desktopDpi.x / 96); |
| 417 var hostPixelRatioY = Math.round(desktopDpi.y / 96); |
| 418 var hostPixelRatio = Math.min(hostPixelRatioX, hostPixelRatioY); |
| 419 |
| 420 // Allow up-scaling to account for DPI. |
| 421 scale = Math.max(scale, clientPixelRatio / hostPixelRatio); |
| 422 |
| 423 // Allow some or all of the up-scaling to be cancelled by the desktopScale. |
| 424 if (desktopScale > 1.0) { |
| 425 scale = Math.max(1.0, scale / desktopScale); |
| 426 } |
| 427 |
| 428 // 2. If the host is still much smaller than the client, then up-scale to |
| 429 // avoid wasting space, but only by an integer factor, to avoid blurring. |
| 430 if (desktopSize.width * scale <= clientWidth && |
| 431 desktopSize.height * scale <= clientHeight) { |
| 432 var scaleX = Math.floor(clientWidth / desktopSize.width); |
| 433 var scaleY = Math.floor(clientHeight / desktopSize.height); |
| 434 scale = Math.min(scaleX, scaleY); |
| 435 base.debug.assert(scale >= 1.0); |
| 436 } |
| 437 |
| 438 // 3. Apply shrink-to-fit, if configured. |
| 439 if (shrinkToFit) { |
| 440 var scaleFitWidth = Math.min(scale, clientWidth / desktopSize.width); |
| 441 var scaleFitHeight = Math.min(scale, clientHeight / desktopSize.height); |
| 442 scale = Math.min(scaleFitHeight, scaleFitWidth); |
| 443 |
| 444 // If we're running full-screen then try to handle common side-by-side |
| 445 // multi-monitor combinations more intelligently. |
| 446 if (isFullscreen) { |
| 447 // If the host has two monitors each the same size as the client then |
| 448 // scale-to-fit will have the desktop occupy only 50% of the client area, |
| 449 // in which case it would be preferable to down-scale less and let the |
| 450 // user bump-scroll around ("scale-and-pan"). |
| 451 // Triggering scale-and-pan if less than 65% of the client area would be |
| 452 // used adds enough fuzz to cope with e.g. 1280x800 client connecting to |
| 453 // a (2x1280)x1024 host nicely. |
| 454 // Note that we don't need to account for scrollbars while fullscreen. |
| 455 if (scale <= scaleFitHeight * 0.65) { |
| 456 scale = scaleFitHeight; |
| 457 } |
| 458 if (scale <= scaleFitWidth * 0.65) { |
| 459 scale = scaleFitWidth; |
| 460 } |
| 461 } |
| 462 } |
| 463 |
| 464 // 4. Avoid blurring for close-to-integer up-scaling factors. |
| 465 if (scale > 1.0) { |
| 466 var scaleBlurriness = scale / Math.floor(scale); |
| 467 if (scaleBlurriness < 1.1) { |
| 468 scale = Math.floor(scale); |
| 469 } |
| 470 } |
| 471 |
| 472 // Return the necessary plugin dimensions in DIPs. |
| 473 scale = scale / clientPixelRatio; |
| 474 var pluginWidth = Math.round(desktopSize.width * scale); |
| 475 var pluginHeight = Math.round(desktopSize.height * scale); |
| 476 return { width: pluginWidth, height: pluginHeight }; |
| 477 }; |
| 478 |
| 479 /** @private */ |
| 480 remoting.DesktopViewport.prototype.resetScroll_ = function() { |
| 481 this.pluginContainer_.style.marginTop = '0px'; |
| 482 this.pluginContainer_.style.marginLeft = '0px'; |
| 483 }; |
| 484 |
| 485 }()); |
OLD | NEW |