Bug 1276971 - Adding UI to display and change the current DPI / DPR setting; r=gl draft
authorMatteo Ferretti <mferretti@mozilla.com>
Sat, 05 Nov 2016 13:45:10 +0100
changeset 434804 5bc47cc4641bc6fd90d0c3c643364f7d42d549bc
parent 434388 1cb8dac3eb40e519ee45a3fc5a9490b889ee5fc7
child 536121 9c5b784010bab2272636871878ae3d4546160c95
push id34830
push userbmo:zer0@mozilla.com
push dateMon, 07 Nov 2016 11:58:22 +0000
reviewersgl
bugs1276971
milestone52.0a1
Bug 1276971 - Adding UI to display and change the current DPI / DPR setting; r=gl MozReview-Commit-ID: Dnb6OLhOvOK
devtools/client/locales/en-US/responsive.properties
devtools/client/responsive.html/actions/display-pixel-ratio.js
devtools/client/responsive.html/actions/index.js
devtools/client/responsive.html/actions/moz.build
devtools/client/responsive.html/actions/viewports.js
devtools/client/responsive.html/app.js
devtools/client/responsive.html/components/dpr-selector.js
devtools/client/responsive.html/components/global-toolbar.js
devtools/client/responsive.html/components/moz.build
devtools/client/responsive.html/index.css
devtools/client/responsive.html/index.js
devtools/client/responsive.html/manager.js
devtools/client/responsive.html/reducers.js
devtools/client/responsive.html/reducers/display-pixel-ratio.js
devtools/client/responsive.html/reducers/moz.build
devtools/client/responsive.html/reducers/viewports.js
devtools/client/responsive.html/test/browser/browser.ini
devtools/client/responsive.html/test/browser/browser_device_change.js
devtools/client/responsive.html/test/browser/browser_dpr_change.js
devtools/client/responsive.html/test/browser/browser_network_throttling.js
devtools/client/responsive.html/test/browser/head.js
devtools/client/responsive.html/test/unit/test_change_display_pixel_ratio.js
devtools/client/responsive.html/test/unit/test_change_viewport_pixel_ratio.js
devtools/client/responsive.html/test/unit/xpcshell.ini
--- a/devtools/client/locales/en-US/responsive.properties
+++ b/devtools/client/locales/en-US/responsive.properties
@@ -64,8 +64,18 @@ responsive.remoteOnly=Responsive Design 
 # container tab.
 responsive.noContainerTabs=Responsive Design Mode is currently unavailable in container tabs.
 
 # LOCALIZATION NOTE (responsive.noThrottling): UI option in a menu to configure
 # network throttling.  This option is the default and disables throttling so you
 # just have normal network conditions.  There is not very much room in the UI
 # so a short string would be best if possible.
 responsive.noThrottling=No throttling
+
+# LOCALIZATION NOTE (responsive.devicePixelRatio): tooltip for the
+# DevicePixelRatio (DPR) dropdown when is enabled.
+responsive.devicePixelRatio=Device Pixel Ratio
+
+# LOCALIZATION NOTE (responsive.autoDPR): tooltip for the DevicePixelRatio
+# (DPR) dropdown when is disabled because a device is selected.
+# The argument (%1$S) is the selected device (e.g. iPhone 6) that set
+# automatically the DPR value.
+responsive.autoDPR=DPR automatically set by %1$S
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/actions/display-pixel-ratio.js
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { CHANGE_DISPLAY_PIXEL_RATIO } = require("./index");
+
+module.exports = {
+
+  /**
+   * The pixel ratio of the display has changed. This may be triggered by the user
+   * when changing the monitor resolution, or when the window is dragged to a different
+   * display with a different pixel ratio.
+   */
+  changeDisplayPixelRatio(displayPixelRatio) {
+    return {
+      type: CHANGE_DISPLAY_PIXEL_RATIO,
+      displayPixelRatio,
+    };
+  },
+
+};
--- a/devtools/client/responsive.html/actions/index.js
+++ b/devtools/client/responsive.html/actions/index.js
@@ -23,19 +23,29 @@ createEnum([
 
   // Change the device displayed in the viewport.
   "CHANGE_DEVICE",
 
   // Change the location of the page.  This may be triggered by the user
   // directly entering a new URL, navigating with links, etc.
   "CHANGE_LOCATION",
 
+  // The pixel ratio of the display has changed. This may be triggered by the user
+  // when changing the monitor resolution, or when the window is dragged to a different
+  // display with a different pixel ratio.
+  "CHANGE_DISPLAY_PIXEL_RATIO",
+
   // Change the network throttling profile.
   "CHANGE_NETWORK_THROTTLING",
 
+  // The pixel ratio of the viewport has changed. This may be triggered by the user
+  // when changing the device displayed in the viewport, or when a pixel ratio is
+  // selected from the DPR dropdown.
+  "CHANGE_VIEWPORT_PIXEL_RATIO",
+
   // Indicates that the device list is being loaded
   "LOAD_DEVICE_LIST_START",
 
   // Indicates that the device list loading action threw an error
   "LOAD_DEVICE_LIST_ERROR",
 
   // Indicates that the device list has been loaded successfully
   "LOAD_DEVICE_LIST_END",
--- a/devtools/client/responsive.html/actions/moz.build
+++ b/devtools/client/responsive.html/actions/moz.build
@@ -1,15 +1,16 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'devices.js',
+    'display-pixel-ratio.js',
     'index.js',
     'location.js',
     'network-throttling.js',
     'screenshot.js',
     'touch-simulation.js',
     'viewports.js',
 )
--- a/devtools/client/responsive.html/actions/viewports.js
+++ b/devtools/client/responsive.html/actions/viewports.js
@@ -2,16 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {
   ADD_VIEWPORT,
   CHANGE_DEVICE,
+  CHANGE_VIEWPORT_PIXEL_RATIO,
   RESIZE_VIEWPORT,
   ROTATE_VIEWPORT
 } = require("./index");
 
 module.exports = {
 
   /**
    * Add an additional viewport to display the document.
@@ -29,16 +30,27 @@ module.exports = {
     return {
       type: CHANGE_DEVICE,
       id,
       device,
     };
   },
 
   /**
+   * Change the viewport pixel ratio.
+   */
+  changeViewportPixelRatio(id, pixelRatio = 0) {
+    return {
+      type: CHANGE_VIEWPORT_PIXEL_RATIO,
+      id,
+      pixelRatio,
+    };
+  },
+
+  /**
    * Resize the viewport.
    */
   resizeViewport(id, width, height) {
     return {
       type: RESIZE_VIEWPORT,
       id,
       width,
       height,
--- a/devtools/client/responsive.html/app.js
+++ b/devtools/client/responsive.html/app.js
@@ -15,29 +15,31 @@ const {
   updateDeviceModalOpen,
   updatePreferredDevices,
 } = require("./actions/devices");
 const { changeNetworkThrottling } = require("./actions/network-throttling");
 const { takeScreenshot } = require("./actions/screenshot");
 const { updateTouchSimulationEnabled } = require("./actions/touch-simulation");
 const {
   changeDevice,
+  changeViewportPixelRatio,
   resizeViewport,
   rotateViewport
 } = require("./actions/viewports");
 const DeviceModal = createFactory(require("./components/device-modal"));
 const GlobalToolbar = createFactory(require("./components/global-toolbar"));
 const Viewports = createFactory(require("./components/viewports"));
 const Types = require("./types");
 
 let App = createClass({
   displayName: "App",
 
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
+    displayPixelRatio: PropTypes.number.isRequired,
     location: Types.location.isRequired,
     networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
     screenshot: PropTypes.shape(Types.screenshot).isRequired,
     touchSimulation: PropTypes.shape(Types.touchSimulation).isRequired,
     viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
   },
 
   onBrowserMounted() {
@@ -55,16 +57,26 @@ let App = createClass({
 
   onChangeViewportDevice(id, device) {
     window.postMessage({
       type: "change-viewport-device",
       device,
     }, "*");
     this.props.dispatch(changeDevice(id, device.name));
     this.props.dispatch(updateTouchSimulationEnabled(device.touch));
+    this.props.dispatch(changeViewportPixelRatio(id, device.pixelRatio));
+  },
+
+  onChangeViewportPixelRatio(pixelRatio) {
+    window.postMessage({
+      type: "change-viewport-pixel-ratio",
+      pixelRatio,
+    }, "*");
+
+    this.props.dispatch(changeViewportPixelRatio(0, pixelRatio));
   },
 
   onContentResize({ width, height }) {
     window.postMessage({
       type: "content-resize",
       width,
       height,
     }, "*");
@@ -105,47 +117,62 @@ let App = createClass({
     }, "*");
 
     this.props.dispatch(updateTouchSimulationEnabled(isEnabled));
   },
 
   render() {
     let {
       devices,
+      displayPixelRatio,
       location,
       networkThrottling,
       screenshot,
       touchSimulation,
       viewports,
     } = this.props;
 
     let {
       onBrowserMounted,
       onChangeNetworkThrottling,
       onChangeViewportDevice,
+      onChangeViewportPixelRatio,
       onContentResize,
       onDeviceListUpdate,
       onExit,
       onResizeViewport,
       onRotateViewport,
       onScreenshot,
       onUpdateDeviceDisplayed,
       onUpdateDeviceModalOpen,
       onUpdateTouchSimulation,
     } = this;
 
+    let selectedDevice = "";
+    let selectedPixelRatio = 0;
+
+    if (viewports.length) {
+      selectedDevice = viewports[0].device;
+      selectedPixelRatio = viewports[0].pixelRatio;
+    }
+
     return dom.div(
       {
         id: "app",
       },
       GlobalToolbar({
+        devices,
+        displayPixelRatio,
         networkThrottling,
         screenshot,
+        selectedDevice,
+        selectedPixelRatio,
         touchSimulation,
         onChangeNetworkThrottling,
+        onChangeViewportPixelRatio,
         onExit,
         onScreenshot,
         onUpdateTouchSimulation,
       }),
       Viewports({
         devices,
         location,
         screenshot,
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/components/dpr-selector.js
@@ -0,0 +1,131 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { DOM: dom, createClass, PropTypes, addons } =
+  require("devtools/client/shared/vendor/react");
+
+const Types = require("../types");
+const { getStr, getFormatStr } = require("../utils/l10n");
+
+const PIXEL_RATIO_PRESET = [1, 2, 3];
+
+const createVisibleOption = value =>
+  dom.option({
+    value,
+    title: value,
+    key: value,
+  }, value);
+
+const createHiddenOption = value =>
+  dom.option({
+    value,
+    title: value,
+    hidden: true,
+    disabled: true,
+  }, value);
+
+module.exports = createClass({
+  displayName: "DPRSelector",
+
+  propTypes: {
+    devices: PropTypes.shape(Types.devices).isRequired,
+    displayPixelRatio: PropTypes.number.isRequired,
+    selectedDevice: PropTypes.string.isRequired,
+    selectedPixelRatio: PropTypes.number.isRequired,
+    onChangeViewportPixelRatio: PropTypes.func.isRequired,
+  },
+
+  mixins: [ addons.PureRenderMixin ],
+
+  getInitialState() {
+    return {
+      isFocused: false
+    };
+  },
+
+  onFocusChange({type}) {
+    this.setState({
+      isFocused: type === "focus"
+    });
+  },
+
+  onSelectChange({ target }) {
+    this.props.onChangeViewportPixelRatio(+target.value);
+  },
+
+  render() {
+    let {
+      devices,
+      displayPixelRatio,
+      selectedDevice,
+      selectedPixelRatio,
+    } = this.props;
+
+    let hiddenOptions = [];
+
+    for (let type of devices.types) {
+      for (let device of devices[type]) {
+        if (device.displayed &&
+            !hiddenOptions.includes(device.pixelRatio) &&
+            !PIXEL_RATIO_PRESET.includes(device.pixelRatio)) {
+          hiddenOptions.push(device.pixelRatio);
+        }
+      }
+    }
+
+    if (!PIXEL_RATIO_PRESET.includes(displayPixelRatio)) {
+      hiddenOptions.push(displayPixelRatio);
+    }
+
+    let state = devices.listState;
+    let isDisabled = (state !== Types.deviceListState.LOADED) || (selectedDevice !== "");
+    let selectorClass = "";
+    let title;
+
+    if (isDisabled) {
+      selectorClass += " disabled";
+      title = getFormatStr("responsive.autoDPR", selectedDevice);
+    } else {
+      title = getStr("responsive.devicePixelRatio");
+
+      if (selectedPixelRatio) {
+        selectorClass += " selected";
+      }
+    }
+
+    if (this.state.isFocused) {
+      selectorClass += " focused";
+    }
+
+    let listContent = PIXEL_RATIO_PRESET.map(createVisibleOption);
+
+    if (state == Types.deviceListState.LOADED) {
+      listContent = listContent.concat(hiddenOptions.map(createHiddenOption));
+    }
+
+    return dom.label(
+      {
+        id: "global-dpr-selector",
+        className: selectorClass,
+        title,
+      },
+      "DPR",
+      dom.select(
+        {
+          value: selectedPixelRatio || displayPixelRatio,
+          disabled: isDisabled,
+          onChange: this.onSelectChange,
+          onFocus: this.onFocusChange,
+          onBlur: this.onFocusChange,
+        },
+        ...listContent
+      )
+    );
+  },
+
+});
--- a/devtools/client/responsive.html/components/global-toolbar.js
+++ b/devtools/client/responsive.html/components/global-toolbar.js
@@ -4,39 +4,50 @@
 
 "use strict";
 
 const { DOM: dom, createClass, createFactory, PropTypes, addons } =
   require("devtools/client/shared/vendor/react");
 
 const { getStr } = require("../utils/l10n");
 const Types = require("../types");
+const DPRSelector = createFactory(require("./dpr-selector"));
 const NetworkThrottlingSelector = createFactory(require("./network-throttling-selector"));
 
 module.exports = createClass({
   displayName: "GlobalToolbar",
 
   propTypes: {
+    devices: PropTypes.shape(Types.devices).isRequired,
+    displayPixelRatio: PropTypes.number.isRequired,
     networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired,
     screenshot: PropTypes.shape(Types.screenshot).isRequired,
+    selectedDevice: PropTypes.string.isRequired,
+    selectedPixelRatio: PropTypes.number.isRequired,
     touchSimulation: PropTypes.shape(Types.touchSimulation).isRequired,
     onChangeNetworkThrottling: PropTypes.func.isRequired,
+    onChangeViewportPixelRatio: PropTypes.func.isRequired,
     onExit: PropTypes.func.isRequired,
     onScreenshot: PropTypes.func.isRequired,
     onUpdateTouchSimulation: PropTypes.func.isRequired,
   },
 
   mixins: [ addons.PureRenderMixin ],
 
   render() {
     let {
+      devices,
+      displayPixelRatio,
       networkThrottling,
       screenshot,
+      selectedDevice,
+      selectedPixelRatio,
       touchSimulation,
       onChangeNetworkThrottling,
+      onChangeViewportPixelRatio,
       onExit,
       onScreenshot,
       onUpdateTouchSimulation
     } = this.props;
 
     let touchButtonClass = "toolbar-button devtools-button";
     if (touchSimulation.enabled) {
       touchButtonClass += " active";
@@ -52,16 +63,23 @@ module.exports = createClass({
           className: "title",
         },
         getStr("responsive.title")
       ),
       NetworkThrottlingSelector({
         networkThrottling,
         onChangeNetworkThrottling,
       }),
+      DPRSelector({
+        devices,
+        displayPixelRatio,
+        selectedDevice,
+        selectedPixelRatio,
+        onChangeViewportPixelRatio,
+      }),
       dom.button({
         id: "global-touch-simulation-button",
         className: touchButtonClass,
         title: (touchSimulation.enabled ?
           getStr("responsive.disableTouch") : getStr("responsive.enableTouch")),
         onClick: () => onUpdateTouchSimulation(!touchSimulation.enabled),
       }),
       dom.button({
--- a/devtools/client/responsive.html/components/moz.build
+++ b/devtools/client/responsive.html/components/moz.build
@@ -3,16 +3,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'browser.js',
     'device-modal.js',
     'device-selector.js',
+    'dpr-selector.js',
     'global-toolbar.js',
     'network-throttling-selector.js',
     'resizable-viewport.js',
     'viewport-dimension.js',
     'viewport-toolbar.js',
     'viewport.js',
     'viewports.js',
 )
--- a/devtools/client/responsive.html/index.css
+++ b/devtools/client/responsive.html/index.css
@@ -81,28 +81,29 @@ select {
   -moz-appearance: none;
   background-color: var(--theme-toolbar-background);
   background-image: var(--viewport-selection-arrow);
   background-position: 100% 50%;
   background-repeat: no-repeat;
   background-size: 7px;
   border: none;
   color: var(--viewport-color);
+  height: 100%;
   padding: 0 8px;
   text-align: center;
   text-overflow: ellipsis;
   font-size: 11px;
 }
 
 select.selected {
   background-image: var(--viewport-selection-arrow-selected);
   color: var(--viewport-active-color);
 }
 
-select:hover {
+select:not(:disabled):hover {
   background-image: var(--viewport-selection-arrow-hovered);
   color: var(--viewport-hover-color);
 }
 
 /* This is (believed to be?) separate from the identical select.selected rule
    set so that it overrides select:hover because of file ordering once the
    select is focused.  It's unclear whether the visual effect that results here
    is intentional and desired. */
@@ -183,16 +184,53 @@ select > option.divider {
 }
 
 #global-network-throttling-selector {
   height: 15px;
   padding-left: 0;
   width: 103px;
 }
 
+#global-dpr-selector > select {
+  padding: 0 8px 0 0;
+  margin-left: 2px;
+}
+
+#global-dpr-selector {
+  margin: 0 8px;
+  -moz-user-select: none;
+  color: var(--viewport-color);
+  font-size: 11px;
+  height: 15px;
+}
+
+#global-dpr-selector.focused,
+#global-dpr-selector:not(.disabled):hover {
+  color: var(--viewport-hover-color);
+}
+
+#global-dpr-selector:not(.disabled):hover > select {
+  background-image: var(--viewport-selection-arrow-hovered);
+  color: var(--viewport-hover-color);
+}
+
+#global-dpr-selector:focus > select {
+  background-image: var(--viewport-selection-arrow-selected);
+  color: var(--viewport-active-color);
+}
+
+#global-dpr-selector.selected,
+#global-dpr-selector.selected > select {
+  color: var(--viewport-active-color);
+}
+
+#global-dpr-selector > select > option {
+  padding: 5px;
+}
+
 #viewports {
   /* Make sure left-most viewport is visible when there's horizontal overflow.
      That is, when the horizontal space become smaller than the viewports and a
      scrollbar appears, then the first viewport will still be visible */
   position: sticky;
   left: 0;
   /* Individual viewports are inline elements, make sure they stay on a single
      line */
--- a/devtools/client/responsive.html/index.js
+++ b/devtools/client/responsive.html/index.js
@@ -21,16 +21,17 @@ const { createFactory, createElement } =
   require("devtools/client/shared/vendor/react");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
 
 const message = require("./utils/message");
 const App = createFactory(require("./app"));
 const Store = require("./store");
 const { changeLocation } = require("./actions/location");
+const { changeDisplayPixelRatio } = require("./actions/display-pixel-ratio");
 const { addViewport, resizeViewport } = require("./actions/viewports");
 const { loadDevices } = require("./actions/devices");
 
 let bootstrap = {
 
   telemetry: new Telemetry(),
 
   store: null,
@@ -84,22 +85,40 @@ window.addEventListener("unload", functi
 window.dispatch = action => bootstrap.dispatch(action);
 
 // Expose the store on window for testing
 Object.defineProperty(window, "store", {
   get: () => bootstrap.store,
   enumerable: true,
 });
 
+// Dispatch a `changeDisplayPixelRatio` action when the browser's pixel ratio is changing.
+// This is usually triggered when the user changes the monitor resolution, or when the
+// browser's window is dragged to a different display with a different pixel ratio.
+function onDPRChange() {
+  let dpr = window.devicePixelRatio;
+  let mql = window.matchMedia(`(resolution: ${dpr}dppx)`);
+
+  function listener() {
+    bootstrap.dispatch(changeDisplayPixelRatio(window.devicePixelRatio));
+    mql.removeListener(listener);
+    onDPRChange();
+  }
+
+  mql.addListener(listener);
+}
+
 /**
  * Called by manager.js to add the initial viewport based on the original page.
  */
 window.addInitialViewport = contentURI => {
   try {
+    onDPRChange();
     bootstrap.dispatch(changeLocation(contentURI));
+    bootstrap.dispatch(changeDisplayPixelRatio(window.devicePixelRatio));
     bootstrap.dispatch(addViewport());
   } catch (e) {
     console.error(e);
   }
 };
 
 /**
  * Called by manager.js when tests want to check the viewport size.
--- a/devtools/client/responsive.html/manager.js
+++ b/devtools/client/responsive.html/manager.js
@@ -447,16 +447,19 @@ ResponsiveUI.prototype = {
 
     switch (event.data.type) {
       case "change-network-throtting":
         this.onChangeNetworkThrottling(event);
         break;
       case "change-viewport-device":
         this.onChangeViewportDevice(event);
         break;
+      case "change-viewport-pixel-ratio":
+        this.updateDPPX(event.data.pixelRatio);
+        break;
       case "content-resize":
         this.onContentResize(event);
         break;
       case "exit":
         this.onExit();
         break;
       case "update-touch-simulation":
         this.onUpdateTouchSimulation(event);
--- a/devtools/client/responsive.html/reducers.js
+++ b/devtools/client/responsive.html/reducers.js
@@ -1,12 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 exports.devices = require("./reducers/devices");
+exports.displayPixelRatio = require("./reducers/display-pixel-ratio");
 exports.location = require("./reducers/location");
 exports.networkThrottling = require("./reducers/network-throttling");
 exports.screenshot = require("./reducers/screenshot");
 exports.touchSimulation = require("./reducers/touch-simulation");
 exports.viewports = require("./reducers/viewports");
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/reducers/display-pixel-ratio.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env browser */
+
+"use strict";
+
+const { CHANGE_DISPLAY_PIXEL_RATIO } = require("../actions/index");
+const INITIAL_DISPLAY_PIXEL_RATIO = 0;
+
+let reducers = {
+
+  [CHANGE_DISPLAY_PIXEL_RATIO](_, action) {
+    return action.displayPixelRatio;
+  },
+
+};
+
+module.exports = function (displayPixelRatio = INITIAL_DISPLAY_PIXEL_RATIO, action) {
+  let reducer = reducers[action.type];
+  if (!reducer) {
+    return displayPixelRatio;
+  }
+  return reducer(displayPixelRatio, action);
+};
--- a/devtools/client/responsive.html/reducers/moz.build
+++ b/devtools/client/responsive.html/reducers/moz.build
@@ -1,14 +1,15 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'devices.js',
+    'display-pixel-ratio.js',
     'location.js',
     'network-throttling.js',
     'screenshot.js',
     'touch-simulation.js',
     'viewports.js',
 )
--- a/devtools/client/responsive.html/reducers/viewports.js
+++ b/devtools/client/responsive.html/reducers/viewports.js
@@ -2,28 +2,30 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {
   ADD_VIEWPORT,
   CHANGE_DEVICE,
+  CHANGE_VIEWPORT_PIXEL_RATIO,
   RESIZE_VIEWPORT,
   ROTATE_VIEWPORT,
 } = require("../actions/index");
 
 let nextViewportId = 0;
 
 const INITIAL_VIEWPORTS = [];
 const INITIAL_VIEWPORT = {
   id: nextViewportId++,
   device: "",
   width: 320,
   height: 480,
+  pixelRatio: 0,
 };
 
 let reducers = {
 
   [ADD_VIEWPORT](viewports) {
     // For the moment, there can be at most one viewport.
     if (viewports.length === 1) {
       return viewports;
@@ -38,16 +40,28 @@ let reducers = {
       }
 
       return Object.assign({}, viewport, {
         device,
       });
     });
   },
 
+  [CHANGE_VIEWPORT_PIXEL_RATIO](viewports, {id, pixelRatio }) {
+    return viewports.map(viewport => {
+      if (viewport.id !== id) {
+        return viewport;
+      }
+
+      return Object.assign({}, viewport, {
+        pixelRatio,
+      });
+    });
+  },
+
   [RESIZE_VIEWPORT](viewports, { id, width, height }) {
     return viewports.map(viewport => {
       if (viewport.id !== id) {
         return viewport;
       }
 
       if (!width) {
         width = viewport.width;
--- a/devtools/client/responsive.html/test/browser/browser.ini
+++ b/devtools/client/responsive.html/test/browser/browser.ini
@@ -16,16 +16,17 @@ support-files =
   !/devtools/client/shared/test/test-actor.js
   !/devtools/client/shared/test/test-actor-registry.js
 
 [browser_device_change.js]
 [browser_device_modal_error.js]
 [browser_device_modal_exit.js]
 [browser_device_modal_submit.js]
 [browser_device_width.js]
+[browser_dpr_change.js]
 [browser_exit_button.js]
 [browser_frame_script_active.js]
 [browser_menu_item_01.js]
 [browser_menu_item_02.js]
 [browser_mouse_resize.js]
 [browser_navigation.js]
 [browser_network_throttling.js]
 [browser_page_state.js]
--- a/devtools/client/responsive.html/test/browser/browser_device_change.js
+++ b/devtools/client/responsive.html/test/browser/browser_device_change.js
@@ -38,34 +38,34 @@ addRDMTask(TEST_URL, function* ({ ui, ma
   // Test defaults
   testViewportDimensions(ui, 320, 480);
   yield testUserAgent(ui, DEFAULT_UA);
   yield testDevicePixelRatio(ui, DEFAULT_DPPX);
   yield testTouchEventsOverride(ui, false);
   testViewportSelectLabel(ui, "no device selected");
 
   // Test device with custom properties
-  yield switchDevice(ui, "Fake Phone RDM Test");
+  yield selectDevice(ui, "Fake Phone RDM Test");
   yield waitForViewportResizeTo(ui, testDevice.width, testDevice.height);
   yield testUserAgent(ui, testDevice.userAgent);
   yield testDevicePixelRatio(ui, testDevice.pixelRatio);
   yield testTouchEventsOverride(ui, true);
 
   // Test resetting device when resizing viewport
   let deviceChanged = once(ui, "viewport-device-changed");
   yield testViewportResize(ui, ".viewport-vertical-resize-handle",
     [-10, -10], [testDevice.width, testDevice.height - 10], [0, -10], ui);
   yield deviceChanged;
   yield testUserAgent(ui, DEFAULT_UA);
   yield testDevicePixelRatio(ui, DEFAULT_DPPX);
   yield testTouchEventsOverride(ui, false);
   testViewportSelectLabel(ui, "no device selected");
 
   // Test device with generic properties
-  yield switchDevice(ui, "Laptop (1366 x 768)");
+  yield selectDevice(ui, "Laptop (1366 x 768)");
   yield waitForViewportResizeTo(ui, 1366, 768);
   yield testUserAgent(ui, DEFAULT_UA);
   yield testDevicePixelRatio(ui, 1);
   yield testTouchEventsOverride(ui, false);
 });
 
 function testViewportDimensions(ui, w, h) {
   let viewport = ui.toolWindow.document.querySelector(".viewport-content");
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_dpr_change.js
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests changing viewport device
+const TEST_URL = "data:text/html;charset=utf-8,DPR list test";
+const DEFAULT_DPPX = window.devicePixelRatio;
+const VIEWPORT_DPPX = DEFAULT_DPPX + 2;
+const Types = require("devtools/client/responsive.html/types");
+
+const testDevice = {
+  "name": "Fake Phone RDM Test",
+  "width": 320,
+  "height": 470,
+  "pixelRatio": 5.5,
+  "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+  "touch": true,
+  "firefoxOS": true,
+  "os": "custom",
+  "featured": true,
+};
+
+// Add the new device to the list
+addDeviceForTest(testDevice);
+
+addRDMTask(TEST_URL, function* ({ ui, manager }) {
+  yield waitStartup(ui);
+
+  yield testDefaults(ui);
+  yield testChangingDevice(ui);
+  yield testResetWhenResizingViewport(ui);
+  yield testChangingDPR(ui);
+});
+
+function* waitStartup(ui) {
+  let { store } = ui.toolWindow;
+
+  // Wait until the viewport has been added and the device list has been loaded
+  yield waitUntilState(store, state => state.viewports.length == 1
+    && state.devices.listState == Types.deviceListState.LOADED);
+}
+
+function* testDefaults(ui) {
+  info("Test Defaults");
+
+  yield testDevicePixelRatio(ui, window.devicePixelRatio);
+  testViewportDPRSelect(ui, {value: window.devicePixelRatio, disabled: false});
+  testViewportDeviceSelectLabel(ui, "no device selected");
+}
+
+function* testChangingDevice(ui) {
+  info("Test Changing Device");
+
+  let waitPixelRatioChange = onceDevicePixelRatioChange(ui);
+
+  yield selectDevice(ui, testDevice.name);
+  yield waitForViewportResizeTo(ui, testDevice.width, testDevice.height);
+  yield waitPixelRatioChange;
+  yield testDevicePixelRatio(ui, testDevice.pixelRatio);
+  testViewportDPRSelect(ui, {value: testDevice.pixelRatio, disabled: true});
+  testViewportDeviceSelectLabel(ui, testDevice.name);
+}
+
+function* testResetWhenResizingViewport(ui) {
+  info("Test reset when resizing the viewport");
+
+  let waitPixelRatioChange = onceDevicePixelRatioChange(ui);
+
+  yield testViewportResize(ui, ".viewport-vertical-resize-handle",
+    [-10, -10], [testDevice.width, testDevice.height - 10], [0, -10], ui);
+
+  yield waitPixelRatioChange;
+  yield testDevicePixelRatio(ui, window.devicePixelRatio);
+
+  testViewportDPRSelect(ui, {value: window.devicePixelRatio, disabled: false});
+  testViewportDeviceSelectLabel(ui, "no device selected");
+}
+
+function* testChangingDPR(ui) {
+  info("Test changing device pixel ratio");
+
+  let waitPixelRatioChange = onceDevicePixelRatioChange(ui);
+
+  yield selectDPR(ui, VIEWPORT_DPPX);
+  yield waitPixelRatioChange;
+  yield testDevicePixelRatio(ui, VIEWPORT_DPPX);
+  testViewportDPRSelect(ui, {value: VIEWPORT_DPPX, disabled: false});
+  testViewportDeviceSelectLabel(ui, "no device selected");
+}
+
+function testViewportDPRSelect(ui, expected) {
+  info("Test viewport's DPR Select");
+
+  let select = ui.toolWindow.document.querySelector("#global-dpr-selector > select");
+  is(select.value, expected.value,
+     `DPR Select value should be: ${expected.value}`);
+  is(select.disabled, expected.disabled,
+    `DPR Select should be ${expected.disabled ? "disabled" : "enabled"}.`);
+}
+
+function testViewportDeviceSelectLabel(ui, expected) {
+  info("Test viewport's device select label");
+
+  let select = ui.toolWindow.document.querySelector(".viewport-device-selector");
+  is(select.selectedOptions[0].textContent, expected,
+     `Device Select value should be: ${expected}`);
+}
+
+function* testDevicePixelRatio(ui, expected) {
+  info("Test device pixel ratio");
+
+  let dppx = yield getViewportDevicePixelRatio(ui);
+  is(dppx, expected, `devicePixelRatio should be: ${expected}`);
+}
+
+function* getViewportDevicePixelRatio(ui) {
+  return yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+    return content.devicePixelRatio;
+  });
+}
+
+function onceDevicePixelRatioChange(ui) {
+  return ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
+    info(`Listening for a pixel ratio change (current: ${content.devicePixelRatio}dppx)`);
+
+    let pixelRatio = content.devicePixelRatio;
+    let mql = content.matchMedia(`(resolution: ${pixelRatio}dppx)`);
+
+    return new Promise(resolve => {
+      const onWindowCreated = () => {
+        if (pixelRatio !== content.devicePixelRatio) {
+          resolve();
+        }
+      };
+
+      addEventListener("DOMWindowCreated", onWindowCreated, {once: true});
+
+      mql.addListener(function listener() {
+        mql.removeListener(listener);
+        removeEventListener("DOMWindowCreated", onWindowCreated, {once: true});
+        resolve();
+      });
+    });
+  });
+}
--- a/devtools/client/responsive.html/test/browser/browser_network_throttling.js
+++ b/devtools/client/responsive.html/test/browser/browser_network_throttling.js
@@ -20,17 +20,17 @@ addRDMTask(TEST_URL, function* ({ ui, ma
 
   // Test a fast profile
   yield testThrottlingProfile(ui, "Wi-Fi");
 
   // Test a slower profile
   yield testThrottlingProfile(ui, "Regular 3G");
 
   // Test switching back to no throttling
-  yield switchNetworkThrottling(ui, "No throttling");
+  yield selectNetworkThrottling(ui, "No throttling");
   testNetworkThrottlingSelectorLabel(ui, "No throttling");
   yield testNetworkThrottlingState(ui, null);
 });
 
 function testNetworkThrottlingSelectorLabel(ui, expected) {
   let selector = "#global-network-throttling-selector";
   let select = ui.toolWindow.document.querySelector(selector);
   is(select.selectedOptions[0].textContent, expected,
@@ -39,17 +39,17 @@ function testNetworkThrottlingSelectorLa
 
 var testNetworkThrottlingState = Task.async(function* (ui, expected) {
   let state = yield ui.emulationFront.getNetworkThrottling();
   Assert.deepEqual(state, expected, "Network throttling state should be " +
                                     JSON.stringify(expected, null, 2));
 });
 
 var testThrottlingProfile = Task.async(function* (ui, profile) {
-  yield switchNetworkThrottling(ui, profile);
+  yield selectNetworkThrottling(ui, profile);
   testNetworkThrottlingSelectorLabel(ui, profile);
   let data = throttlingProfiles.find(({ id }) => id == profile);
   let { download, upload, latency } = data;
   yield testNetworkThrottlingState(ui, {
     downloadThroughput: download,
     uploadThroughput: upload,
     latency,
   });
--- a/devtools/client/responsive.html/test/browser/head.js
+++ b/devtools/client/responsive.html/test/browser/head.js
@@ -206,39 +206,43 @@ function* testViewportResize(ui, selecto
 
   let endRect = getElRect(selector, win);
   is(endRect.left - startRect.left, expectedHandleMove[0],
     `The x move of ${selector} is as expected`);
   is(endRect.top - startRect.top, expectedHandleMove[1],
     `The y move of ${selector} is as expected`);
 }
 
-function openDeviceModal(ui) {
-  let { document } = ui.toolWindow;
+function openDeviceModal({toolWindow}) {
+  let { document } = toolWindow;
   let select = document.querySelector(".viewport-device-selector");
   let modal = document.querySelector("#device-modal-wrapper");
-  let editDeviceOption = [...select.options].filter(o => {
-    return o.value === OPEN_DEVICE_MODAL_VALUE;
-  })[0];
 
   info("Checking initial device modal state");
   ok(modal.classList.contains("closed") && !modal.classList.contains("opened"),
     "The device modal is closed by default.");
 
   info("Opening device modal through device selector.");
-  EventUtils.synthesizeMouseAtCenter(select, {type: "mousedown"},
-    ui.toolWindow);
-  EventUtils.synthesizeMouseAtCenter(editDeviceOption, {type: "mouseup"},
-    ui.toolWindow);
+
+  let event = new toolWindow.UIEvent("change", {
+    view: toolWindow,
+    bubbles: true,
+    cancelable: true
+  });
+
+  select.value = OPEN_DEVICE_MODAL_VALUE;
+  select.dispatchEvent(event);
 
   ok(modal.classList.contains("opened") && !modal.classList.contains("closed"),
     "The device modal is displayed.");
 }
 
-function switchSelector({ toolWindow }, selector, value) {
+function changeSelectValue({ toolWindow }, selector, value) {
+  info(`Selecting ${value} in ${selector}.`);
+
   return new Promise(resolve => {
     let select = toolWindow.document.querySelector(selector);
     isnot(select, null, `selector "${selector}" should match an existing element.`);
 
     let option = [...select.options].find(o => o.value === String(value));
     isnot(option, undefined, `value "${value}" should match an existing option.`);
 
     let event = new toolWindow.UIEvent("change", {
@@ -253,27 +257,28 @@ function switchSelector({ toolWindow }, 
       resolve();
     }, { once: true });
 
     select.value = value;
     select.dispatchEvent(event);
   });
 }
 
-let switchDevice = Task.async(function* (ui, value) {
-  let changed = once(ui, "viewport-device-changed");
-  yield switchSelector(ui, ".viewport-device-selector", value);
-  yield changed;
-});
+const selectDevice = (ui, value) => Promise.all([
+  once(ui, "viewport-device-changed"),
+  changeSelectValue(ui, ".viewport-device-selector", value)
+]);
 
-let switchNetworkThrottling = Task.async(function* (ui, value) {
-  let changed = once(ui, "network-throttling-changed");
-  yield switchSelector(ui, "#global-network-throttling-selector", value);
-  yield changed;
-});
+const selectDPR = (ui, value) =>
+  changeSelectValue(ui, "#global-dpr-selector > select", value);
+
+const selectNetworkThrottling = (ui, value) => Promise.all([
+  once(ui, "network-throttling-changed"),
+  changeSelectValue(ui, "#global-network-throttling-selector", value)
+]);
 
 function getSessionHistory(browser) {
   return ContentTask.spawn(browser, {}, function* () {
     /* eslint-disable no-undef */
     let { interfaces: Ci } = Components;
     let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
     let sessionHistory = webNav.sessionHistory;
     let result = {
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_change_display_pixel_ratio.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test changing the display pixel ratio.
+
+const { changeDisplayPixelRatio } =
+  require("devtools/client/responsive.html/actions/display-pixel-ratio");
+const NEW_PIXEL_RATIO = 5.5;
+
+add_task(function* () {
+  let store = Store();
+  const { getState, dispatch } = store;
+
+  equal(getState().displayPixelRatio, 0,
+        "Defaults to 0 at startup");
+
+  dispatch(changeDisplayPixelRatio(NEW_PIXEL_RATIO));
+  equal(getState().displayPixelRatio, NEW_PIXEL_RATIO,
+    `Display Pixel Ratio changed to ${NEW_PIXEL_RATIO}`);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_change_viewport_pixel_ratio.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test changing the viewport pixel ratio.
+
+const { addViewport, changeViewportPixelRatio } =
+  require("devtools/client/responsive.html/actions/viewports");
+const NEW_PIXEL_RATIO = 5.5;
+
+add_task(function* () {
+  let store = Store();
+  const { getState, dispatch } = store;
+
+  dispatch(addViewport());
+  dispatch(changeViewportPixelRatio(0, NEW_PIXEL_RATIO));
+
+  let viewport = getState().viewports[0];
+  equal(viewport.pixelRatio, NEW_PIXEL_RATIO,
+    `Viewport's pixel ratio changed to ${NEW_PIXEL_RATIO}`);
+});
--- a/devtools/client/responsive.html/test/unit/xpcshell.ini
+++ b/devtools/client/responsive.html/test/unit/xpcshell.ini
@@ -2,15 +2,17 @@
 tags = devtools
 head = head.js ../../../framework/test/shared-redux-head.js
 tail =
 firefox-appdir = browser
 
 [test_add_device.js]
 [test_add_device_type.js]
 [test_add_viewport.js]
+[test_change_display_pixel_ratio.js]
 [test_change_location.js]
 [test_change_network_throttling.js]
 [test_change_viewport_device.js]
+[test_change_viewport_pixel_ratio.js]
 [test_resize_viewport.js]
 [test_rotate_viewport.js]
 [test_update_device_displayed.js]
 [test_update_touch_simulation_enabled.js]