diff --git a/remoting/remoting_webapp_files.gypi b/remoting/remoting_webapp_files.gypi index 2e98b0422f04ee..77515ffbc36120 100644 --- a/remoting/remoting_webapp_files.gypi +++ b/remoting/remoting_webapp_files.gypi @@ -71,9 +71,9 @@ 'webapp/base/js/base_inherits_unittest.js', 'webapp/base/js/ipc_unittest.js', 'webapp/base/js/protocol_extension_manager_unittest.js', + 'webapp/base/js/viewport_unittest.js', 'webapp/crd/js/apps_v2_migration_unittest.js', 'webapp/crd/js/client_session_unittest.js', - 'webapp/crd/js/desktop_viewport_unittest.js', 'webapp/crd/js/client_session_factory_unittest.js', 'webapp/crd/js/dns_blackhole_checker_unittest.js', 'webapp/crd/js/error_unittest.js', @@ -241,6 +241,7 @@ ], # UI JavaScript files. 'remoting_webapp_js_ui_files': [ + 'webapp/base/js/viewport.js', 'webapp/base/js/window_shape.js', 'webapp/crd/js/bump_scroller.js', 'webapp/crd/js/butter_bar.js', diff --git a/remoting/webapp/app_remoting/js/app_connected_view.js b/remoting/webapp/app_remoting/js/app_connected_view.js index a6765515931c4f..73c308840c75ab 100644 --- a/remoting/webapp/app_remoting/js/app_connected_view.js +++ b/remoting/webapp/app_remoting/js/app_connected_view.js @@ -122,7 +122,7 @@ remoting.AppConnectedView.prototype.onDesktopSizeChanged_ = var hostSize = { width: hostDesktop.width, height: hostDesktop.height }; var hostDpi = { x: hostDesktop.xDpi, y: hostDesktop.yDpi }; var clientArea = { width: window.innerWidth, height: window.innerHeight }; - var newSize = remoting.DesktopViewport.choosePluginSize( + var newSize = remoting.Viewport.choosePluginSize( clientArea, window.devicePixelRatio, hostSize, hostDpi, this.host_.options.desktopScale, true /* fullscreen */ , true /* shrinkToFit */ ); diff --git a/remoting/webapp/base/js/viewport.js b/remoting/webapp/base/js/viewport.js new file mode 100644 index 00000000000000..8f35cffb1768a3 --- /dev/null +++ b/remoting/webapp/base/js/viewport.js @@ -0,0 +1,153 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview + * Provides shared view port management utilities. + */ + +/** @suppress {duplicate} */ +var remoting = remoting || {}; + +(function() { + +'use strict'; + +/** @type {Object} */ +remoting.Viewport = {}; + +/** + * Helper function accepting client and host dimensions, and returning a chosen + * size for the plugin element, in DIPs. + * + * @param {{width: number, height: number}} clientSizeDips Available client + * dimensions, in DIPs. + * @param {number} clientPixelRatio Number of physical pixels per client DIP. + * @param {{width: number, height: number}} desktopSize Size of the host desktop + * in physical pixels. + * @param {{x: number, y: number}} desktopDpi DPI of the host desktop in both + * dimensions. + * @param {number} desktopScale The scale factor configured for the host. + * @param {boolean} isFullscreen True if full-screen mode is active. + * @param {boolean} shrinkToFit True if shrink-to-fit should be applied. + * @return {{width: number, height: number}} Chosen plugin dimensions, in DIPs. + */ +remoting.Viewport.choosePluginSize = function( + clientSizeDips, clientPixelRatio, desktopSize, desktopDpi, desktopScale, + isFullscreen, shrinkToFit) { + base.debug.assert(clientSizeDips.width > 0); + base.debug.assert(clientSizeDips.height > 0); + base.debug.assert(clientPixelRatio >= 1.0); + base.debug.assert(desktopSize.width > 0); + base.debug.assert(desktopSize.height > 0); + base.debug.assert(desktopDpi.x > 0); + base.debug.assert(desktopDpi.y > 0); + base.debug.assert(desktopScale > 0); + + // We have the following goals in sizing the desktop display at the client: + // 1. Avoid losing detail by down-scaling beyond 1:1 host:device pixels. + // 2. Avoid up-scaling if that will cause the client to need scrollbars. + // 3. Avoid introducing blurriness with non-integer up-scaling factors. + // 4. Avoid having huge "letterboxes" around the desktop, if it's really + // small. + // 5. Compensate for mismatched DPIs, so that the behaviour of features like + // shrink-to-fit matches their "natural" rather than their pixel size. + // e.g. with shrink-to-fit active a 1024x768 low-DPI host on a 640x480 + // high-DPI client will be up-scaled to 1280x960, rather than displayed + // at 1:1 host:physical client pixels. + // + // To determine the ideal size we follow a four-stage process: + // 1. Determine the "natural" size at which to display the desktop. + // a. Initially assume 1:1 mapping of desktop to client device pixels. + // b. If host DPI is less than the client's then up-scale accordingly. + // c. If desktopScale is configured for the host then allow that to + // reduce the amount of up-scaling from (b). e.g. if the client:host + // DPIs are 2:1 then a desktopScale of 1.5 would reduce the up-scale + // to 4:3, while a desktopScale of 3.0 would result in no up-scaling. + // 2. If the natural size of the desktop is smaller than the client device + // then apply up-scaling by an integer scale factor to avoid excessive + // letterboxing. + // 3. If shrink-to-fit is configured then: + // a. If the natural size exceeds the client size then apply down-scaling + // by an arbitrary scale factor. + // b. If we're in full-screen mode and the client & host aspect-ratios + // are radically different (e.g. the host is actually multi-monitor) + // then shrink-to-fit to the shorter dimension, rather than leaving + // huge letterboxes; the user can then bump-scroll around the desktop. + // 4. If the overall scale factor is fractionally over an integer factor + // then reduce it to that integer factor, to avoid blurring. + + // All calculations are performed in device pixels. + var clientWidth = clientSizeDips.width * clientPixelRatio; + var clientHeight = clientSizeDips.height * clientPixelRatio; + + // 1. Determine a "natural" size at which to display the desktop. + var scale = 1.0; + + // Determine the effective host device pixel ratio. + // Note that we round up or down to the closest integer pixel ratio. + var hostPixelRatioX = Math.round(desktopDpi.x / 96); + var hostPixelRatioY = Math.round(desktopDpi.y / 96); + var hostPixelRatio = Math.min(hostPixelRatioX, hostPixelRatioY); + + // Allow up-scaling to account for DPI. + scale = Math.max(scale, clientPixelRatio / hostPixelRatio); + + // Allow some or all of the up-scaling to be cancelled by the desktopScale. + if (desktopScale > 1.0) { + scale = Math.max(1.0, scale / desktopScale); + } + + // 2. If the host is still much smaller than the client, then up-scale to + // avoid wasting space, but only by an integer factor, to avoid blurring. + if (desktopSize.width * scale <= clientWidth && + desktopSize.height * scale <= clientHeight) { + var scaleX = Math.floor(clientWidth / desktopSize.width); + var scaleY = Math.floor(clientHeight / desktopSize.height); + scale = Math.min(scaleX, scaleY); + base.debug.assert(scale >= 1.0); + } + + // 3. Apply shrink-to-fit, if configured. + if (shrinkToFit) { + var scaleFitWidth = Math.min(scale, clientWidth / desktopSize.width); + var scaleFitHeight = Math.min(scale, clientHeight / desktopSize.height); + scale = Math.min(scaleFitHeight, scaleFitWidth); + + // If we're running full-screen then try to handle common side-by-side + // multi-monitor combinations more intelligently. + if (isFullscreen) { + // If the host has two monitors each the same size as the client then + // scale-to-fit will have the desktop occupy only 50% of the client area, + // in which case it would be preferable to down-scale less and let the + // user bump-scroll around ("scale-and-pan"). + // Triggering scale-and-pan if less than 65% of the client area would be + // used adds enough fuzz to cope with e.g. 1280x800 client connecting to + // a (2x1280)x1024 host nicely. + // Note that we don't need to account for scrollbars while fullscreen. + if (scale <= scaleFitHeight * 0.65) { + scale = scaleFitHeight; + } + if (scale <= scaleFitWidth * 0.65) { + scale = scaleFitWidth; + } + } + } + + // 4. Avoid blurring for close-to-integer up-scaling factors. + if (scale > 1.0) { + var scaleBlurriness = scale / Math.floor(scale); + if (scaleBlurriness < 1.1) { + scale = Math.floor(scale); + } + } + + // Return the necessary plugin dimensions in DIPs. + scale = scale / clientPixelRatio; + var pluginWidth = Math.round(desktopSize.width * scale); + var pluginHeight = Math.round(desktopSize.height * scale); + return { width: pluginWidth, height: pluginHeight }; +}; + +}()); diff --git a/remoting/webapp/crd/js/desktop_viewport_unittest.js b/remoting/webapp/base/js/viewport_unittest.js similarity index 79% rename from remoting/webapp/crd/js/desktop_viewport_unittest.js rename to remoting/webapp/base/js/viewport_unittest.js index e9a3ddbc5e84db..71d9d88a9e056d 100644 --- a/remoting/webapp/crd/js/desktop_viewport_unittest.js +++ b/remoting/webapp/base/js/viewport_unittest.js @@ -24,37 +24,37 @@ function dpi(x, y) { return {x: x, y: y}; } -QUnit.module('DesktopViewport'); +QUnit.module('Viewport'); QUnit.test('choosePluginSize() handles low-DPI client & host', function(assert) { // 1. Client & host size the same. - var pluginSize = remoting.DesktopViewport.choosePluginSize( + var pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 1.0, size(640, 480), dpi(96, 96), 1.0, false, true); assert.deepEqual(pluginSize, size(640, 480)); // 2. Client logical dimensions smaller than host's. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 1.0, size(1024, 600), dpi(96, 96), 1.0, false, true); assert.deepEqual(pluginSize, size(640, (640 / 1024) * 600)); // 3. Client Y dimension larger than host's, X dimension smaller. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 640), 1.0, size(1024, 600), dpi(96, 96), 1.0, false, true); assert.deepEqual(pluginSize, size(640, (640 / 1024) * 600)); // 4. Client dimensions larger than host's by <2x. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1280, 900), 1.0, size(640, 480), dpi(96, 96), 1.0, false, true); assert.deepEqual(pluginSize, size(640, 480)); // 5. Client dimensions larger than host's by >2x. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1280, 1024), 1.0, size(640, 480), dpi(96, 96), 1.0, false, true); assert.deepEqual(pluginSize, size(2 * 640, 2 * 480)); // 6. Client X dimension larger than host's, Y dimension smaller. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1152, 600), 1.0, size(1024, 768), dpi(96, 96), 1.0, false, true); assert.deepEqual(pluginSize, size(1024 * (600 / 768), 600)); }); @@ -62,34 +62,34 @@ QUnit.test('choosePluginSize() handles low-DPI client & host', QUnit.test('choosePluginSize() handles high-DPI client, low-DPI host', function(assert) { // 1. Client & host size the same. - var pluginSize = remoting.DesktopViewport.choosePluginSize( + var pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 2.0, size(640, 480), dpi(96, 96), 1.0, false, true); assert.deepEqual(pluginSize, size(640, 480)); // 2. Client logical dimensions smaller than host's. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 2.0, size(1024, 600), dpi(96, 96), 1.0, false, true); assert.deepEqual(pluginSize, size(640, (640 / 1024) * 600)); // 3. Client Y dimension larger than host's, X dimension smaller. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 640), 2.0, size(1024, 600), dpi(96, 96), 1.0, false, true); assert.deepEqual(pluginSize, size(640, (640 / 1024) * 600)); // 4. Client logical dimensions larger than host's by <2x. // Host dimensions fit into the client's _device_ dimensions 3x, so the // size in client DIPs should be 1:3/2. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1280, 900), 2.0, size(640, 480), dpi(96, 96), 1.0, false, true); assert.deepEqual(pluginSize, size(640 * 3 / 2.0, 480 * 3 / 2.0)); // 5. Client dimensions larger than host's by >2x. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1280, 1024), 2.0, size(640, 480), dpi(96, 96), 1.0, false, true); assert.deepEqual(pluginSize, size(1280, (1280 / 640) * 480)); // 6. Client X dimension larger than host's, Y dimension smaller. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1152, 600), 2.0, size(1024, 768), dpi(96, 96), 1.0, false, true); assert.deepEqual(pluginSize, size(1024 * (600 / 768), 600)); }); @@ -97,32 +97,32 @@ QUnit.test('choosePluginSize() handles high-DPI client, low-DPI host', QUnit.test('choosePluginSize() handles low-DPI client, high-DPI host', function(assert) { // 1. Client & host size the same. - var pluginSize = remoting.DesktopViewport.choosePluginSize( + var pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 1.0, size(640, 480), dpi(192, 192), 1.0, false, true); assert.deepEqual(pluginSize, size(640, 480)); // 2. Client logical dimensions smaller than host's. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 1.0, size(1024, 600), dpi(192, 192), 1.0, false, true); assert.deepEqual(pluginSize, size(640, (640 / 1024) * 600)); // 3. Client Y dimension larger than host's, X dimension smaller. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 640), 1.0, size(1024, 600), dpi(192, 192), 1.0, false, true); assert.deepEqual(pluginSize, size(640, (640 / 1024) * 600)); // 4. Client dimensions larger than host's by <2x. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1280, 900), 1.0, size(640, 480), dpi(192, 192), 1.0, false, true); assert.deepEqual(pluginSize, size(640, 480)); // 5. Client dimensions larger than host's by >2x. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1280, 1024), 1.0, size(640, 480), dpi(192, 192), 1.0, false, true); assert.deepEqual(pluginSize, size(1280, (1280 / 640) * 480)); // 6. Client X dimension larger than host's, Y dimension smaller. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1152, 600), 1.0, size(1024, 768), dpi(192, 192), 1.0, false, true); assert.deepEqual(pluginSize, size(1024 * (600 / 768), 600)); }); @@ -130,34 +130,34 @@ QUnit.test('choosePluginSize() handles low-DPI client, high-DPI host', QUnit.test('choosePluginSize() handles high-DPI client and host', function(assert) { // 1. Client & host size the same. - var pluginSize = remoting.DesktopViewport.choosePluginSize( + var pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 2.0, size(640, 480), dpi(192, 192), 1.0, false, true); assert.deepEqual(pluginSize, size(640, 480)); // 2. Client logical dimensions smaller than host's. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 2.0, size(1024, 600), dpi(192, 192), 1.0, false, true); assert.deepEqual(pluginSize, size(1024 / 2.0, 600 / 2.0)); // 3. Client Y dimension larger than host's, X dimension smaller. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 640), 2.0, size(1024, 600), dpi(192, 192), 1.0, false, true); assert.deepEqual(pluginSize, size(1024 / 2.0, 600 / 2.0)); // 4. Client logical dimensions larger than host's by <2x. // Host dimensions fit into the client's _device_ dimensions 3x, so the // size in client DIPs should be 1:3/2. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1280, 900), 2.0, size(640, 480), dpi(192, 192), 1.0, false, true); assert.deepEqual(pluginSize, size(640 * 3 / 2.0, 480 * 3 / 2.0)); // 5. Client dimensions larger than host's by >2x. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1280, 1024), 2.0, size(640, 480), dpi(192, 192), 1.0, false, true); assert.deepEqual(pluginSize, size(1280, (1280 / 640) * 480)); // 6. Client X dimension larger than host's, Y dimension smaller. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1152, 600), 2.0, size(1024, 768), dpi(192, 192), 1.0, false, true); assert.deepEqual(pluginSize, size(1024 / 2.0, 768 / 2.0)); }); @@ -165,34 +165,34 @@ QUnit.test('choosePluginSize() handles high-DPI client and host', QUnit.test('choosePluginSize() handles high-DPI client, 150% DPI host', function(assert) { // 1. Client & host size the same. - var pluginSize = remoting.DesktopViewport.choosePluginSize( + var pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 2.0, size(640, 480), dpi(144, 144), 1.0, false, true); assert.deepEqual(pluginSize, size(640, 480)); // 2. Client dimensions smaller than host's. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 2.0, size(1024, 600), dpi(144, 144), 1.0, false, true); assert.deepEqual(pluginSize, size(1024 / 2.0, 600 / 2.0)); // 3. Client Y dimension larger than host's, X dimension smaller. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 640), 2.0, size(1024, 600), dpi(144, 144), 1.0, false, true); assert.deepEqual(pluginSize, size(1024 / 2.0, 600 / 2.0)); // 4. Client dimensions larger than host's by <2x. // Host dimensions fit into the client's _device_ dimensions 3x, so the // size in client DIPs should be 1:3/2. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1280, 900), 2.0, size(640, 480), dpi(144, 144), 1.0, false, true); assert.deepEqual(pluginSize, size(640 * 3 / 2.0, 480 * 3 / 2.0)); // 5. Client dimensions larger than host's by >2x. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1280, 1024), 2.0, size(640, 480), dpi(144, 144), 1.0, false, true); assert.deepEqual(pluginSize, size(1280, (1280 / 640) * 480)); // 6. Client X dimension larger than host's, Y dimension smaller. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1152, 600), 2.0, size(1024, 768), dpi(144, 144), 1.0, false, true); assert.deepEqual(pluginSize, size(1024 / 2.0, 768 / 2.0)); }); @@ -200,34 +200,34 @@ QUnit.test('choosePluginSize() handles high-DPI client, 150% DPI host', QUnit.test('choosePluginSize() handles high-DPI client, 125% DPI host', function(assert) { // 1. Client & host size the same. - var pluginSize = remoting.DesktopViewport.choosePluginSize( + var pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 2.0, size(640, 480), dpi(120, 120), 1.0, false, true); assert.deepEqual(pluginSize, size(640, 480)); // 2. Client dimensions smaller than host's. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 2.0, size(1024, 600), dpi(120, 120), 1.0, false, true); assert.deepEqual(pluginSize, size(640, 600 * (640 / 1024))); // 3. Client Y dimension larger than host's, X dimension smaller. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 640), 2.0, size(1024, 600), dpi(120, 120), 1.0, false, true); assert.deepEqual(pluginSize, size(640, 600 * (640 / 1024))); // 4. Client dimensions larger than host's by <2x. // Host dimensions fit into the client's _device_ dimensions 3x, so the // size in client DIPs should be 1:3/2. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1280, 900), 2.0, size(640, 480), dpi(120, 120), 1.0, false, true); assert.deepEqual(pluginSize, size(640 * 3 / 2.0, 480 * 3 / 2.0)); // 5. Client dimensions larger than host's by >2x. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1280, 1024), 2.0, size(640, 480), dpi(120, 120), 1.0, false, true); assert.deepEqual(pluginSize, size(1280, (1280 / 640) * 480)); // 6. Client X dimension larger than host's, Y dimension smaller. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1152, 600), 2.0, size(1024, 768), dpi(120, 120), 1.0, false, true); assert.deepEqual(pluginSize, size(1024 * (600 / 768), 600)); }); @@ -235,42 +235,42 @@ QUnit.test('choosePluginSize() handles high-DPI client, 125% DPI host', QUnit.test('choosePluginSize() with shrink-to-fit disabled', function(assert) { // 1. Client & host size the same. - var pluginSize = remoting.DesktopViewport.choosePluginSize( + var pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 1.0, size(640, 480), dpi(96, 96), 1.0, false, false); assert.deepEqual(pluginSize, size(640, 480)); // 2. Client logical dimensions smaller than host's. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 1.0, size(1024, 600), dpi(96, 96), 1.0, false, false); assert.deepEqual(pluginSize, size(1024, 600)); // 3. Client dimensions larger than host's by <2x. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1280, 900), 1.0, size(640, 480), dpi(96, 96), 1.0, false, false); assert.deepEqual(pluginSize, size(640, 480)); // 4. Client dimensions larger than host's by >2x. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1280, 1024), 1.0, size(640, 480), dpi(96, 96), 1.0, false, false); assert.deepEqual(pluginSize, size(1280, (1280 / 640) * 480)); // 5. Client smaller than host, client high-DPI, host low-DPI. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 2.0, size(1024, 600), dpi(96, 96), 1.0, false, false); assert.deepEqual(pluginSize, size(1024, 600)); // 6. Client smaller than host, client low-DPI, host high-DPI. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 1.0, size(1024, 600), dpi(192, 192), 1.0, false, false); assert.deepEqual(pluginSize, size(1024, 600)); // 7. Client smaller than host, both high-DPI. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 2.0, size(1024, 600), dpi(192, 192), 1.0, false, false); assert.deepEqual(pluginSize, size(512, (512 / 1024) * 600)); // 8. Client smaller than host, client high-DPI, host 150% DPI. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 2.0, size(1024, 600), dpi(144, 144), 1.0, false, false); assert.deepEqual(pluginSize, size(512, (512 / 1024) * 600)); }); @@ -280,47 +280,47 @@ QUnit.test('choosePluginSize() full-screen multi-monitor optimization', // Each test has a host sized to approximate two or more monitors. // 1. Client & host per-monitor dimensions match, two monitors side-by-side. - var pluginSize = remoting.DesktopViewport.choosePluginSize( + var pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 1.0, size(2 * 640, 480), dpi(96, 96), 1.0, true, true); assert.deepEqual(pluginSize, size(2 * 640, 480)); // 2. Client & host per-monitor dimensions match, two monitors stacked. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 1.0, size(640, 2 * 480), dpi(96, 96), 1.0, true, true); assert.deepEqual(pluginSize, size(640, 2 * 480)); // 3. Client larger, two monitors stacked. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1024, 768), 1.0, size(640, 2 * 480), dpi(96, 96), 1.0, true, true); assert.deepEqual(pluginSize, size(640 * (768 / (2 * 480)), 768)); // 4. Client smaller, two monitors stacked. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 1.0, size(1024, 2 * 768), dpi(96, 96), 1.0, true, true); assert.deepEqual(pluginSize, size(640, 2 * 768 * (640 / 1024))); // 5. Client wide-screen, host two standard monitors stacked. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(1920, 1080), 1.0, size(1024, 2 * 768), dpi(96, 96), 1.0, true, true); assert.deepEqual(pluginSize, size(1024 * (1080 / (2 * 768)), 1080)); // 6. Client & host per-monitor dimensions match, two monitors stacked, // high-DPI client. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 2.0, size(640, 2 * 480), dpi(96, 96), 1.0, true, true); assert.deepEqual(pluginSize, size(640, 2 * 480)); // 7. Client & host per-monitor dimensions match, two monitors stacked, // high-DPI host. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 1.0, size(640, 2 * 480), dpi(192, 192), 1.0, true, true); assert.deepEqual(pluginSize, size(640, 2 * 480)); // 8. Client & host per-monitor dimensions match, two monitors stacked, // high-DPI client & host. - pluginSize = remoting.DesktopViewport.choosePluginSize( + pluginSize = remoting.Viewport.choosePluginSize( size(640, 480), 2.0, size(640, 2 * 480), dpi(192, 192), 1.0, true, true); assert.deepEqual(pluginSize, size(640 / 2.0, (2 * 480) / 2.0)); diff --git a/remoting/webapp/crd/js/desktop_viewport.js b/remoting/webapp/crd/js/desktop_viewport.js index 5496ec46b4d2a8..0eae6052d84859 100644 --- a/remoting/webapp/crd/js/desktop_viewport.js +++ b/remoting/webapp/crd/js/desktop_viewport.js @@ -330,7 +330,7 @@ remoting.DesktopViewport.prototype.updateDimensions_ = function() { height: dimensions.height }; var desktopDpi = { x: dimensions.xDpi, y: dimensions.yDpi }; - var newSize = remoting.DesktopViewport.choosePluginSize( + var newSize = remoting.Viewport.choosePluginSize( this.getClientArea(), window.devicePixelRatio, desktopSize, desktopDpi, this.hostOptions_.desktopScale, remoting.fullscreen.isActive(), this.hostOptions_.shrinkToFit); @@ -341,139 +341,6 @@ remoting.DesktopViewport.prototype.updateDimensions_ = function() { this.pluginElement_.style.height = newSize.height + 'px'; }; -/** - * Helper function accepting client and host dimensions, and returning a chosen - * size for the plugin element, in DIPs. - * - * @param {{width: number, height: number}} clientSizeDips Available client - * dimensions, in DIPs. - * @param {number} clientPixelRatio Number of physical pixels per client DIP. - * @param {{width: number, height: number}} desktopSize Size of the host desktop - * in physical pixels. - * @param {{x: number, y: number}} desktopDpi DPI of the host desktop in both - * dimensions. - * @param {number} desktopScale The scale factor configured for the host. - * @param {boolean} isFullscreen True if full-screen mode is active. - * @param {boolean} shrinkToFit True if shrink-to-fit should be applied. - * @return {{width: number, height: number}} Chosen plugin dimensions, in DIPs. - */ -remoting.DesktopViewport.choosePluginSize = function( - clientSizeDips, clientPixelRatio, desktopSize, desktopDpi, desktopScale, - isFullscreen, shrinkToFit) { - base.debug.assert(clientSizeDips.width > 0); - base.debug.assert(clientSizeDips.height > 0); - base.debug.assert(clientPixelRatio >= 1.0); - base.debug.assert(desktopSize.width > 0); - base.debug.assert(desktopSize.height > 0); - base.debug.assert(desktopDpi.x > 0); - base.debug.assert(desktopDpi.y > 0); - base.debug.assert(desktopScale > 0); - - // We have the following goals in sizing the desktop display at the client: - // 1. Avoid losing detail by down-scaling beyond 1:1 host:device pixels. - // 2. Avoid up-scaling if that will cause the client to need scrollbars. - // 3. Avoid introducing blurriness with non-integer up-scaling factors. - // 4. Avoid having huge "letterboxes" around the desktop, if it's really - // small. - // 5. Compensate for mismatched DPIs, so that the behaviour of features like - // shrink-to-fit matches their "natural" rather than their pixel size. - // e.g. with shrink-to-fit active a 1024x768 low-DPI host on a 640x480 - // high-DPI client will be up-scaled to 1280x960, rather than displayed - // at 1:1 host:physical client pixels. - // - // To determine the ideal size we follow a four-stage process: - // 1. Determine the "natural" size at which to display the desktop. - // a. Initially assume 1:1 mapping of desktop to client device pixels. - // b. If host DPI is less than the client's then up-scale accordingly. - // c. If desktopScale is configured for the host then allow that to - // reduce the amount of up-scaling from (b). e.g. if the client:host - // DPIs are 2:1 then a desktopScale of 1.5 would reduce the up-scale - // to 4:3, while a desktopScale of 3.0 would result in no up-scaling. - // 2. If the natural size of the desktop is smaller than the client device - // then apply up-scaling by an integer scale factor to avoid excessive - // letterboxing. - // 3. If shrink-to-fit is configured then: - // a. If the natural size exceeds the client size then apply down-scaling - // by an arbitrary scale factor. - // b. If we're in full-screen mode and the client & host aspect-ratios - // are radically different (e.g. the host is actually multi-monitor) - // then shrink-to-fit to the shorter dimension, rather than leaving - // huge letterboxes; the user can then bump-scroll around the desktop. - // 4. If the overall scale factor is fractionally over an integer factor - // then reduce it to that integer factor, to avoid blurring. - - // All calculations are performed in device pixels. - var clientWidth = clientSizeDips.width * clientPixelRatio; - var clientHeight = clientSizeDips.height * clientPixelRatio; - - // 1. Determine a "natural" size at which to display the desktop. - var scale = 1.0; - - // Determine the effective host device pixel ratio. - // Note that we round up or down to the closest integer pixel ratio. - var hostPixelRatioX = Math.round(desktopDpi.x / 96); - var hostPixelRatioY = Math.round(desktopDpi.y / 96); - var hostPixelRatio = Math.min(hostPixelRatioX, hostPixelRatioY); - - // Allow up-scaling to account for DPI. - scale = Math.max(scale, clientPixelRatio / hostPixelRatio); - - // Allow some or all of the up-scaling to be cancelled by the desktopScale. - if (desktopScale > 1.0) { - scale = Math.max(1.0, scale / desktopScale); - } - - // 2. If the host is still much smaller than the client, then up-scale to - // avoid wasting space, but only by an integer factor, to avoid blurring. - if (desktopSize.width * scale <= clientWidth && - desktopSize.height * scale <= clientHeight) { - var scaleX = Math.floor(clientWidth / desktopSize.width); - var scaleY = Math.floor(clientHeight / desktopSize.height); - scale = Math.min(scaleX, scaleY); - base.debug.assert(scale >= 1.0); - } - - // 3. Apply shrink-to-fit, if configured. - if (shrinkToFit) { - var scaleFitWidth = Math.min(scale, clientWidth / desktopSize.width); - var scaleFitHeight = Math.min(scale, clientHeight / desktopSize.height); - scale = Math.min(scaleFitHeight, scaleFitWidth); - - // If we're running full-screen then try to handle common side-by-side - // multi-monitor combinations more intelligently. - if (isFullscreen) { - // If the host has two monitors each the same size as the client then - // scale-to-fit will have the desktop occupy only 50% of the client area, - // in which case it would be preferable to down-scale less and let the - // user bump-scroll around ("scale-and-pan"). - // Triggering scale-and-pan if less than 65% of the client area would be - // used adds enough fuzz to cope with e.g. 1280x800 client connecting to - // a (2x1280)x1024 host nicely. - // Note that we don't need to account for scrollbars while fullscreen. - if (scale <= scaleFitHeight * 0.65) { - scale = scaleFitHeight; - } - if (scale <= scaleFitWidth * 0.65) { - scale = scaleFitWidth; - } - } - } - - // 4. Avoid blurring for close-to-integer up-scaling factors. - if (scale > 1.0) { - var scaleBlurriness = scale / Math.floor(scale); - if (scaleBlurriness < 1.1) { - scale = Math.floor(scale); - } - } - - // Return the necessary plugin dimensions in DIPs. - scale = scale / clientPixelRatio; - var pluginWidth = Math.round(desktopSize.width * scale); - var pluginHeight = Math.round(desktopSize.height * scale); - return { width: pluginWidth, height: pluginHeight }; -}; - /** @private */ remoting.DesktopViewport.prototype.resetScroll_ = function() { this.pluginContainer_.style.marginTop = '0px'; diff --git a/remoting/webapp/files.gni b/remoting/webapp/files.gni index 1a006c040112ca..263bb5fd929b84 100644 --- a/remoting/webapp/files.gni +++ b/remoting/webapp/files.gni @@ -71,9 +71,9 @@ remoting_webapp_unittests_js_files = [ "base/js/base_inherits_unittest.js", "base/js/ipc_unittest.js", "base/js/protocol_extension_manager_unittest.js", + "base/js/viewport_unittest.js", "crd/js/apps_v2_migration_unittest.js", "crd/js/client_session_unittest.js", - "crd/js/desktop_viewport_unittest.js", "crd/js/dns_blackhole_checker_unittest.js", "crd/js/error_unittest.js", "crd/js/fallback_signal_strategy_unittest.js", @@ -245,6 +245,7 @@ remoting_webapp_js_signaling_files = [ # UI JavaScript files. remoting_webapp_js_ui_files = [ + "base/js/viewport.js", "base/js/window_shape.js", "crd/js/bump_scroller.js", "crd/js/butter_bar.js",