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