Rewrite AndroidSyncSettings to be significantly simpler.
[chromium-blink-merge.git] / remoting / webapp / crd / js / desktop_viewport.js
blob02c9a5219e4bfbbe5aa9b64aff118b0af793954b
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.
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  */
13 /** @suppress {duplicate} */
14 var remoting = remoting || {};
16 (function() {
18 'use strict';
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;
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)));
63   if (this.hostOptions_.resizeToClient) {
64     this.resizeHostDesktop_();
65   } else {
66     this.onDesktopSizeChanged_();
67   }
70 remoting.DesktopViewport.prototype.dispose = function() {
71   base.dispose(this.eventHooks_);
72   this.eventHooks_ = null;
73   base.dispose(this.bumpScroller_);
74   this.bumpScroller_ = null;
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;
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;
91 /**
92  * @return {{top:number, left:number}} The top-left corner of the plugin.
93  */
94 remoting.DesktopViewport.prototype.getPluginPositionForTesting = function() {
95   /**
96    * @param {number|string} value
97    * @return {number}
98    */
99   function toFloat(value) {
100     var number = parseFloat(value);
101     return isNaN(number) ? 0 : number;
102   }
103   return {
104     top: toFloat(this.pluginContainer_.style.marginTop),
105     left: toFloat(this.pluginContainer_.style.marginLeft)
106   };
110  * @param {number} width
111  * @param {number} height
112  */
113 remoting.DesktopViewport.prototype.setPluginSizeForBumpScrollTesting =
114     function(width, height) {
115   this.pluginWidthForBumpScrollTesting_ = width;
116   this.pluginHeightForBumpScrollTesting_ = height;
120  * @return {remoting.BumpScroller}
121  */
122 remoting.DesktopViewport.prototype.getBumpScrollerForTesting = function() {
123   return this.bumpScroller_;
127  * Set the shrink-to-fit and resize-to-client flags and save them if this is
128  * a Me2Me connection.
130  * @param {boolean} shrinkToFit True if the remote desktop should be scaled
131  *     down if it is larger than the client window; false if scroll-bars
132  *     should be added in this case.
133  * @param {boolean} resizeToClient True if window resizes should cause the
134  *     host to attempt to resize its desktop to match the client window size;
135  *     false to disable this behaviour for subsequent window resizes--the
136  *     current host desktop size is not restored in this case.
137  * @return {void} Nothing.
138  */
139 remoting.DesktopViewport.prototype.setScreenMode =
140     function(shrinkToFit, resizeToClient) {
141   if (resizeToClient && !this.hostOptions_.resizeToClient) {
142     this.resizeHostDesktop_();
143   }
145   // If enabling shrink, reset bump-scroll offsets.
146   var needsScrollReset = shrinkToFit && !this.hostOptions_.shrinkToFit;
147   this.hostOptions_.shrinkToFit = shrinkToFit;
148   this.hostOptions_.resizeToClient = resizeToClient;
149   this.hostOptions_.save();
150   this.updateScrollbarVisibility_();
152   this.updateDimensions_();
153   if (needsScrollReset) {
154     this.resetScroll_();
155   }
159  * Scroll the client plugin by the specified amount, keeping it visible.
160  * Note that this is only used in content full-screen mode (not windowed or
161  * browser full-screen modes), where window.scrollBy and the scrollTop and
162  * scrollLeft properties don't work.
164  * @param {number} dx The amount by which to scroll horizontally. Positive to
165  *     scroll right; negative to scroll left.
166  * @param {number} dy The amount by which to scroll vertically. Positive to
167  *     scroll down; negative to scroll up.
168  * @return {boolean} False if the requested scroll had no effect because both
169  *     vertical and horizontal edges of the screen have been reached.
170  */
171 remoting.DesktopViewport.prototype.scroll = function(dx, dy) {
172   /**
173    * Helper function for x- and y-scrolling
174    * @param {number|string} curr The current margin, eg. "10px".
175    * @param {number} delta The requested scroll amount.
176    * @param {number} windowBound The size of the window, in pixels.
177    * @param {number} pluginBound The size of the plugin, in pixels.
178    * @param {{stop: boolean}} stop Reference parameter used to indicate when
179    *     the scroll has reached one of the edges and can be stopped in that
180    *     direction.
181    * @return {string} The new margin value.
182    */
183   var adjustMargin = function(curr, delta, windowBound, pluginBound, stop) {
184     var minMargin = Math.min(0, windowBound - pluginBound);
185     var result = (curr ? parseFloat(curr) : 0) - delta;
186     result = Math.min(0, Math.max(minMargin, result));
187     stop.stop = (result === 0 || result == minMargin);
188     return result + 'px';
189   };
191   var style = this.pluginContainer_.style;
193   var pluginWidth =
194       this.pluginWidthForBumpScrollTesting_ || this.pluginElement_.clientWidth;
195   var pluginHeight = this.pluginHeightForBumpScrollTesting_ ||
196                      this.pluginElement_.clientHeight;
198   var clientArea = this.getClientArea();
199   var stopX = { stop: false };
200   style.marginLeft =
201       adjustMargin(style.marginLeft, dx, clientArea.width, pluginWidth, stopX);
203   var stopY = { stop: false };
204   style.marginTop =
205       adjustMargin(style.marginTop, dy, clientArea.height, pluginHeight, stopY);
206   return !stopX.stop || !stopY.stop;
210  * Enable or disable bump-scrolling. When disabling bump scrolling, also reset
211  * the scroll offsets to (0, 0).
212  * @param {boolean} enable True to enable bump-scrolling, false to disable it.
213  */
214 remoting.DesktopViewport.prototype.enableBumpScroll = function(enable) {
215   if (enable) {
216     this.bumpScroller_ = new remoting.BumpScroller(this);
217   } else {
218     base.dispose(this.bumpScroller_);
219     this.bumpScroller_ = null;
220     this.resetScroll_();
221   }
225  * This is a callback that gets called when the window is resized.
227  * @return {void} Nothing.
228  */
229 remoting.DesktopViewport.prototype.onResize = function() {
230   this.updateDimensions_();
232   if (this.resizeTimer_) {
233     window.clearTimeout(this.resizeTimer_);
234     this.resizeTimer_ = null;
235   }
237   // Defer notifying the host of the change until the window stops resizing, to
238   // avoid overloading the control channel with notifications.
239   if (this.hostOptions_.resizeToClient) {
240     var kResizeRateLimitMs = 250;
241     var clientArea = this.getClientArea();
242     this.resizeTimer_ = window.setTimeout(this.resizeHostDesktop_.bind(this),
243                                           kResizeRateLimitMs);
244   }
246   // If bump-scrolling is enabled, adjust the plugin margins to fully utilize
247   // the new window area.
248   this.resetScroll_();
249   this.updateScrollbarVisibility_();
253  * @return {{width:number, height:number}} The height of the window's client
254  *     area. This differs between apps v1 and apps v2 due to the custom window
255  *     borders used by the latter.
256  */
257 remoting.DesktopViewport.prototype.getClientArea = function() {
258   return remoting.windowFrame ?
259       remoting.windowFrame.getClientArea() :
260       { 'width': window.innerWidth, 'height': window.innerHeight };
264  * Notifies the host of the client's current dimensions and DPI.
265  * Also takes into account per-host scaling factor, if configured.
266  * @private
267  */
268 remoting.DesktopViewport.prototype.resizeHostDesktop_ = function() {
269   var clientArea = this.getClientArea();
270   this.hostDesktop_.resize(clientArea.width * this.hostOptions_.desktopScale,
271                            clientArea.height * this.hostOptions_.desktopScale,
272                            window.devicePixelRatio);
276  * This is a callback that gets called when the plugin notifies us of a change
277  * in the size of the remote desktop.
279  * @return {void} Nothing.
280  * @private
281  */
282 remoting.DesktopViewport.prototype.onDesktopSizeChanged_ = function() {
283   var dimensions = this.hostDesktop_.getDimensions();
284   console.log('desktop size changed: ' +
285               dimensions.width + 'x' +
286               dimensions.height +' @ ' +
287               dimensions.xDpi + 'x' +
288               dimensions.yDpi + ' DPI');
289   this.updateDimensions_();
290   this.updateScrollbarVisibility_();
294  * Called when the window or desktop size or the scaling settings change,
295  * to set the scroll-bar visibility.
297  * TODO(jamiewalch): crbug.com/252796: Remove this once crbug.com/240772 is
298  * fixed.
299  */
300 remoting.DesktopViewport.prototype.updateScrollbarVisibility_ = function() {
301   // TODO(kelvinp): Remove the check once app-remoting no longer depends on
302   // this.
303   if (!this.rootElement_) {
304     return;
305   }
307   var needsScrollY = false;
308   var needsScrollX = false;
309   if (!this.hostOptions_.shrinkToFit) {
310     // Determine whether or not horizontal or vertical scrollbars are
311     // required, taking into account their width.
312     var clientArea = this.getClientArea();
313     var hostDesktop = this.hostDesktop_.getDimensions();
314     needsScrollY = clientArea.height < hostDesktop.height;
315     needsScrollX = clientArea.width < hostDesktop.width;
316     var kScrollBarWidth = 16;
317     if (needsScrollX && !needsScrollY) {
318       needsScrollY = clientArea.height - kScrollBarWidth < hostDesktop.height;
319     } else if (!needsScrollX && needsScrollY) {
320       needsScrollX = clientArea.width - kScrollBarWidth < hostDesktop.width;
321     }
322   }
324   this.rootElement_.classList.toggle('no-horizontal-scroll', !needsScrollX);
325   this.rootElement_.classList.toggle('no-vertical-scroll', !needsScrollY);
328 remoting.DesktopViewport.prototype.updateDimensions_ = function() {
329   var dimensions = this.hostDesktop_.getDimensions();
330   if (dimensions.width === 0 || dimensions.height === 0) {
331     return;
332   }
334   var desktopSize = { width: dimensions.width,
335                       height: dimensions.height };
336   var desktopDpi = { x: dimensions.xDpi,
337                      y: dimensions.yDpi };
338   var newSize = remoting.DesktopViewport.choosePluginSize(
339       this.getClientArea(), window.devicePixelRatio,
340       desktopSize, desktopDpi, this.hostOptions_.desktopScale,
341       remoting.fullscreen.isActive(), this.hostOptions_.shrinkToFit);
343   // Resize the plugin if necessary.
344   console.log('plugin dimensions:' + newSize.width + 'x' + newSize.height);
345   this.pluginElement_.style.width = newSize.width + 'px';
346   this.pluginElement_.style.height = newSize.height + 'px';
348   // When we receive the first plugin dimensions from the host, we know that
349   // remote host has started.
350   remoting.app.onVideoStreamingStarted();
354  * Helper function accepting client and host dimensions, and returning a chosen
355  * size for the plugin element, in DIPs.
357  * @param {{width: number, height: number}} clientSizeDips Available client
358  *     dimensions, in DIPs.
359  * @param {number} clientPixelRatio Number of physical pixels per client DIP.
360  * @param {{width: number, height: number}} desktopSize Size of the host desktop
361  *     in physical pixels.
362  * @param {{x: number, y: number}} desktopDpi DPI of the host desktop in both
363  *     dimensions.
364  * @param {number} desktopScale The scale factor configured for the host.
365  * @param {boolean} isFullscreen True if full-screen mode is active.
366  * @param {boolean} shrinkToFit True if shrink-to-fit should be applied.
367  * @return {{width: number, height: number}} Chosen plugin dimensions, in DIPs.
368  */
369 remoting.DesktopViewport.choosePluginSize = function(
370     clientSizeDips, clientPixelRatio, desktopSize, desktopDpi, desktopScale,
371     isFullscreen, shrinkToFit) {
372   base.debug.assert(clientSizeDips.width > 0);
373   base.debug.assert(clientSizeDips.height > 0);
374   base.debug.assert(clientPixelRatio >= 1.0);
375   base.debug.assert(desktopSize.width > 0);
376   base.debug.assert(desktopSize.height > 0);
377   base.debug.assert(desktopDpi.x > 0);
378   base.debug.assert(desktopDpi.y > 0);
379   base.debug.assert(desktopScale > 0);
381   // We have the following goals in sizing the desktop display at the client:
382   //  1. Avoid losing detail by down-scaling beyond 1:1 host:device pixels.
383   //  2. Avoid up-scaling if that will cause the client to need scrollbars.
384   //  3. Avoid introducing blurriness with non-integer up-scaling factors.
385   //  4. Avoid having huge "letterboxes" around the desktop, if it's really
386   //     small.
387   //  5. Compensate for mismatched DPIs, so that the behaviour of features like
388   //     shrink-to-fit matches their "natural" rather than their pixel size.
389   //     e.g. with shrink-to-fit active a 1024x768 low-DPI host on a 640x480
390   //     high-DPI client will be up-scaled to 1280x960, rather than displayed
391   //     at 1:1 host:physical client pixels.
392   //
393   // To determine the ideal size we follow a four-stage process:
394   //  1. Determine the "natural" size at which to display the desktop.
395   //    a. Initially assume 1:1 mapping of desktop to client device pixels.
396   //    b. If host DPI is less than the client's then up-scale accordingly.
397   //    c. If desktopScale is configured for the host then allow that to
398   //       reduce the amount of up-scaling from (b). e.g. if the client:host
399   //       DPIs are 2:1 then a desktopScale of 1.5 would reduce the up-scale
400   //       to 4:3, while a desktopScale of 3.0 would result in no up-scaling.
401   //  2. If the natural size of the desktop is smaller than the client device
402   //     then apply up-scaling by an integer scale factor to avoid excessive
403   //     letterboxing.
404   //  3. If shrink-to-fit is configured then:
405   //     a. If the natural size exceeds the client size then apply down-scaling
406   //        by an arbitrary scale factor.
407   //     b. If we're in full-screen mode and the client & host aspect-ratios
408   //        are radically different (e.g. the host is actually multi-monitor)
409   //        then shrink-to-fit to the shorter dimension, rather than leaving
410   //        huge letterboxes; the user can then bump-scroll around the desktop.
411   //  4. If the overall scale factor is fractionally over an integer factor
412   //     then reduce it to that integer factor, to avoid blurring.
414   // All calculations are performed in device pixels.
415   var clientWidth = clientSizeDips.width * clientPixelRatio;
416   var clientHeight = clientSizeDips.height * clientPixelRatio;
418   // 1. Determine a "natural" size at which to display the desktop.
419   var scale = 1.0;
421   // Determine the effective host device pixel ratio.
422   // Note that we round up or down to the closest integer pixel ratio.
423   var hostPixelRatioX = Math.round(desktopDpi.x / 96);
424   var hostPixelRatioY = Math.round(desktopDpi.y / 96);
425   var hostPixelRatio = Math.min(hostPixelRatioX, hostPixelRatioY);
427   // Allow up-scaling to account for DPI.
428   scale = Math.max(scale, clientPixelRatio / hostPixelRatio);
430   // Allow some or all of the up-scaling to be cancelled by the desktopScale.
431   if (desktopScale > 1.0) {
432     scale = Math.max(1.0, scale / desktopScale);
433   }
435   // 2. If the host is still much smaller than the client, then up-scale to
436   //    avoid wasting space, but only by an integer factor, to avoid blurring.
437   if (desktopSize.width * scale <= clientWidth &&
438       desktopSize.height * scale <= clientHeight) {
439     var scaleX = Math.floor(clientWidth / desktopSize.width);
440     var scaleY = Math.floor(clientHeight / desktopSize.height);
441     scale = Math.min(scaleX, scaleY);
442     base.debug.assert(scale >= 1.0);
443   }
445   // 3. Apply shrink-to-fit, if configured.
446   if (shrinkToFit) {
447     var scaleFitWidth = Math.min(scale, clientWidth / desktopSize.width);
448     var scaleFitHeight = Math.min(scale, clientHeight / desktopSize.height);
449     scale = Math.min(scaleFitHeight, scaleFitWidth);
451     // If we're running full-screen then try to handle common side-by-side
452     // multi-monitor combinations more intelligently.
453     if (isFullscreen) {
454       // If the host has two monitors each the same size as the client then
455       // scale-to-fit will have the desktop occupy only 50% of the client area,
456       // in which case it would be preferable to down-scale less and let the
457       // user bump-scroll around ("scale-and-pan").
458       // Triggering scale-and-pan if less than 65% of the client area would be
459       // used adds enough fuzz to cope with e.g. 1280x800 client connecting to
460       // a (2x1280)x1024 host nicely.
461       // Note that we don't need to account for scrollbars while fullscreen.
462       if (scale <= scaleFitHeight * 0.65) {
463         scale = scaleFitHeight;
464       }
465       if (scale <= scaleFitWidth * 0.65) {
466         scale = scaleFitWidth;
467       }
468     }
469   }
471   // 4. Avoid blurring for close-to-integer up-scaling factors.
472   if (scale > 1.0) {
473     var scaleBlurriness = scale / Math.floor(scale);
474     if (scaleBlurriness < 1.1) {
475       scale = Math.floor(scale);
476     }
477   }
479   // Return the necessary plugin dimensions in DIPs.
480   scale = scale / clientPixelRatio;
481   var pluginWidth = Math.round(desktopSize.width * scale);
482   var pluginHeight = Math.round(desktopSize.height * scale);
483   return { width: pluginWidth, height: pluginHeight };
486 /** @private */
487 remoting.DesktopViewport.prototype.resetScroll_ = function() {
488   this.pluginContainer_.style.marginTop = '0px';
489   this.pluginContainer_.style.marginLeft = '0px';
492 }());