1 // Copyright 2014 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.
7 * @extends {WebInspector.VBox}
8 * @implements {WebInspector.OverridesSupport.PageResizer}
9 * @param {!WebInspector.InspectedPagePlaceholder} inspectedPagePlaceholder
11 WebInspector.ResponsiveDesignView = function(inspectedPagePlaceholder)
13 WebInspector.VBox.call(this);
14 this.setMinimumSize(150, 150);
15 this.registerRequiredCSS("responsiveDesignView.css");
16 this.element.classList.add("overflow-hidden");
18 this._responsiveDesignContainer = new WebInspector.VBox();
20 this._createToolbar();
22 this._mediaInspector = new WebInspector.MediaQueryInspector();
23 this._mediaInspectorContainer = this._responsiveDesignContainer.element.createChild("div");
24 this._updateMediaQueryInspector();
26 this._canvasContainer = new WebInspector.View();
27 this._canvasContainer.element.classList.add("responsive-design");
28 this._canvasContainer.show(this._responsiveDesignContainer.element);
30 this._canvas = this._canvasContainer.element.createChild("canvas", "fill");
32 this._rulerGlasspane = this._canvasContainer.element.createChild("div", "responsive-design-ruler-glasspane");
33 this._rulerGlasspane.appendChild(this._mediaInspector.rulerDecorationLayer());
35 this._warningMessage = this._canvasContainer.element.createChild("div", "responsive-design-warning hidden");
36 this._warningMessage.createChild("div", "warning-icon-small");
37 this._warningMessage.createChild("span");
38 var warningCloseButton = this._warningMessage.createChild("div", "close-button");
39 warningCloseButton.addEventListener("click", WebInspector.overridesSupport.clearWarningMessage.bind(WebInspector.overridesSupport), false);
40 WebInspector.overridesSupport.addEventListener(WebInspector.OverridesSupport.Events.OverridesWarningUpdated, this._overridesWarningUpdated, this);
42 this._slidersContainer = this._canvasContainer.element.createChild("div", "vbox responsive-design-sliders-container");
43 var hbox = this._slidersContainer.createChild("div", "hbox flex-auto");
44 this._heightSliderContainer = this._slidersContainer.createChild("div", "hbox responsive-design-slider-height");
45 this._pageContainer = hbox.createChild("div", "vbox flex-auto");
46 this._widthSliderContainer = hbox.createChild("div", "vbox responsive-design-slider-width");
48 this._widthSlider = this._widthSliderContainer.createChild("div", "responsive-design-slider-thumb");
49 this._widthSlider.createChild("div", "responsive-design-thumb-handle");
50 this._createResizer(this._widthSlider, false);
51 this._heightSlider = this._heightSliderContainer.createChild("div", "responsive-design-slider-thumb");
52 this._heightSlider.createChild("div", "responsive-design-thumb-handle");
53 this._createResizer(this._heightSlider, true);
55 this._inspectedPagePlaceholder = inspectedPagePlaceholder;
56 inspectedPagePlaceholder.show(this.element);
58 this._enabled = false;
60 WebInspector.zoomManager.addEventListener(WebInspector.ZoomManager.Events.ZoomChanged, this._onZoomChanged, this);
61 WebInspector.overridesSupport.addEventListener(WebInspector.OverridesSupport.Events.EmulationStateChanged, this._emulationEnabledChanged, this);
62 this._mediaInspector.addEventListener(WebInspector.MediaQueryInspector.Events.HeightUpdated, this.onResize, this);
63 this._emulationEnabledChanged();
64 this._overridesWarningUpdated();
68 WebInspector.ResponsiveDesignView.SliderWidth = 19;
69 WebInspector.ResponsiveDesignView.RulerWidth = 22;
71 WebInspector.ResponsiveDesignView.prototype = {
72 _invalidateCache: function()
74 delete this._cachedScale;
75 delete this._cachedCssCanvasWidth;
76 delete this._cachedCssCanvasHeight;
77 delete this._cachedCssHeight;
78 delete this._cachedCssWidth;
79 delete this._cachedZoomFactor;
80 delete this._availableSize;
83 _emulationEnabledChanged: function()
85 var enabled = WebInspector.overridesSupport.emulationEnabled();
86 this._mediaInspector.setEnabled(enabled);
87 if (enabled && !this._enabled) {
88 this._invalidateCache();
89 this._ignoreResize = true;
91 this._inspectedPagePlaceholder.clearMinimumSizeAndMargins();
92 this._inspectedPagePlaceholder.show(this._pageContainer);
93 this._responsiveDesignContainer.show(this.element);
94 delete this._ignoreResize;
96 } else if (!enabled && this._enabled) {
97 this._invalidateCache();
98 this._ignoreResize = true;
99 this._enabled = false;
101 this._inspectedPagePlaceholder.restoreMinimumSizeAndMargins();
102 this._responsiveDesignContainer.detach();
103 this._inspectedPagePlaceholder.show(this.element);
104 delete this._ignoreResize;
110 * WebInspector.OverridesSupport.PageResizer override.
111 * @param {number} dipWidth
112 * @param {number} dipHeight
113 * @param {number} scale
115 update: function(dipWidth, dipHeight, scale)
118 this._dipWidth = dipWidth;
119 this._dipHeight = dipHeight;
123 updatePageResizer: function()
125 WebInspector.overridesSupport.setPageResizer(this, this._availableDipSize());
131 _availableDipSize: function()
133 if (typeof this._availableSize === "undefined") {
134 var zoomFactor = WebInspector.zoomManager.zoomFactor();
135 var rect = this._canvasContainer.element.getBoundingClientRect();
136 this._availableSize = new Size(rect.width * zoomFactor - WebInspector.ResponsiveDesignView.RulerWidth,
137 rect.height * zoomFactor - WebInspector.ResponsiveDesignView.RulerWidth);
139 return this._availableSize;
143 * @param {!Element} element
144 * @param {boolean} vertical
145 * @return {!WebInspector.ResizerWidget}
147 _createResizer: function(element, vertical)
149 var resizer = new WebInspector.ResizerWidget();
150 resizer.addElement(element);
151 resizer.setVertical(vertical);
152 resizer.addEventListener(WebInspector.ResizerWidget.Events.ResizeStart, this._onResizeStart, this);
153 resizer.addEventListener(WebInspector.ResizerWidget.Events.ResizeUpdate, this._onResizeUpdate, this);
154 resizer.addEventListener(WebInspector.ResizerWidget.Events.ResizeEnd, this._onResizeEnd, this);
159 * @param {!WebInspector.Event} event
161 _onResizeStart: function(event)
163 var available = this._availableDipSize();
164 this._slowPositionStart = null;
165 this._resizeStartSize = event.target.isVertical() ? (this._dipHeight || available.height) : (this._dipWidth || available.width);
169 * @param {!WebInspector.Event} event
171 _onResizeUpdate: function(event)
173 if (event.data.shiftKey !== !!this._slowPositionStart)
174 this._slowPositionStart = event.data.shiftKey ? event.data.currentPosition : null;
175 var cssOffset = this._slowPositionStart ? (event.data.currentPosition - this._slowPositionStart) / 10 + this._slowPositionStart - event.data.startPosition : event.data.currentPosition - event.data.startPosition;
176 var dipOffset = Math.round(cssOffset * WebInspector.zoomManager.zoomFactor());
177 var newSize = Math.max(this._resizeStartSize + dipOffset, 1);
179 if (event.target.isVertical())
180 requested.height = newSize;
182 requested.width = newSize;
183 this.dispatchEventToListeners(WebInspector.OverridesSupport.PageResizer.Events.ResizeRequested, requested);
187 * @param {!WebInspector.Event} event
189 _onResizeEnd: function(event)
191 delete this._resizeStartSize;
195 * Draws canvas of the specified css size in DevTools page space.
196 * Canvas contains grid and rulers.
197 * @param {number} cssCanvasWidth
198 * @param {number} cssCanvasHeight
200 _drawCanvas: function(cssCanvasWidth, cssCanvasHeight)
205 var canvas = this._canvas;
206 var context = canvas.getContext("2d");
207 canvas.style.width = cssCanvasWidth + "px";
208 canvas.style.height = cssCanvasHeight + "px";
210 var zoomFactor = WebInspector.zoomManager.zoomFactor();
211 var dipCanvasWidth = cssCanvasWidth * zoomFactor;
212 var dipCanvasHeight = cssCanvasHeight * zoomFactor;
214 var deviceScaleFactor = window.devicePixelRatio;
215 canvas.width = deviceScaleFactor * cssCanvasWidth;
216 canvas.height = deviceScaleFactor * cssCanvasHeight;
217 context.scale(canvas.width / dipCanvasWidth, canvas.height / dipCanvasHeight);
218 context.font = "11px " + WebInspector.fontFamily();
220 const rulerStep = 100;
221 const rulerSubStep = 5;
223 const gridSubStep = 10;
224 const rulerBackgroundColor = "rgb(0, 0, 0)";
225 const backgroundColor = "rgb(102, 102, 102)";
226 const lightLineColor = "rgb(132, 132, 132)";
227 const darkLineColor = "rgb(114, 114, 114)";
228 const rulerColor = "rgb(125, 125, 125)";
229 const textColor = "rgb(186, 186, 186)";
231 var scale = this._scale || 1;
232 var rulerWidth = WebInspector.ResponsiveDesignView.RulerWidth;
233 var dipGridWidth = dipCanvasWidth / scale - rulerWidth;
234 var dipGridHeight = dipCanvasHeight / scale - rulerWidth;
236 context.scale(scale, scale);
237 context.translate(rulerWidth, rulerWidth);
239 context.fillStyle = rulerBackgroundColor;
240 context.fillRect(-rulerWidth, -rulerWidth, dipGridWidth + rulerWidth, rulerWidth);
241 context.fillRect(-rulerWidth, 0, rulerWidth, dipGridHeight);
243 context.fillStyle = backgroundColor;
244 context.fillRect(0, 0, dipGridWidth, dipGridHeight);
246 context.translate(0.5, 0.5);
247 context.strokeStyle = rulerColor;
248 context.fillStyle = textColor;
249 context.lineWidth = 1;
251 // Draw vertical ruler.
252 for (var x = 0; x < dipGridWidth; x += rulerSubStep) {
253 var y = -rulerWidth / 4;
254 if (!(x % (rulerStep / 4)))
256 if (!(x % (rulerStep / 2)))
259 if (!(x % rulerStep)) {
261 context.translate(x, 0);
262 context.fillText(x, 2, -rulerWidth / 2);
268 context.moveTo(x, y);
269 context.lineTo(x, 0);
273 // Draw horizontal ruler.
274 for (var y = 0; y < dipGridHeight; y += rulerSubStep) {
276 if (!(y % (rulerStep / 4)))
278 if (!(y % (rulerStep / 2)))
281 if (!(y % rulerStep)) {
283 context.translate(0, y);
284 context.rotate(-Math.PI / 2);
285 context.fillText(y, 2, -rulerWidth / 2);
291 context.moveTo(x, y);
292 context.lineTo(0, y);
297 drawGrid(darkLineColor, gridSubStep);
298 drawGrid(lightLineColor, gridStep);
301 * @param {string} color
302 * @param {number} step
304 function drawGrid(color, step)
306 context.strokeStyle = color;
307 for (var x = 0; x < dipGridWidth; x += step) {
309 context.moveTo(x, 0);
310 context.lineTo(x, dipGridHeight);
313 for (var y = 0; y < dipGridHeight; y += step) {
315 context.moveTo(0, y);
316 context.lineTo(dipGridWidth, y);
322 _updateUI: function()
324 if (!this._enabled || !this.isShowing())
327 var zoomFactor = WebInspector.zoomManager.zoomFactor();
328 var rect = this._canvas.parentElement.getBoundingClientRect();
329 var availableDip = this._availableDipSize();
330 var cssCanvasWidth = rect.width;
331 var cssCanvasHeight = rect.height;
333 this._widthSlider.classList.toggle("hidden", !!this._scale);
334 this._heightSlider.classList.toggle("hidden", !!this._scale);
335 this._widthSlider.classList.toggle("reversed", !this._dipWidth);
336 this._heightSlider.classList.toggle("reversed", !this._dipHeight);
338 if (this._cachedZoomFactor !== zoomFactor) {
339 var cssRulerWidth = WebInspector.ResponsiveDesignView.RulerWidth / zoomFactor + "px";
340 this._rulerGlasspane.style.height = cssRulerWidth;
341 this._rulerGlasspane.style.left = cssRulerWidth;
342 this._slidersContainer.style.left = cssRulerWidth;
343 this._mediaInspector.translateZero(WebInspector.ResponsiveDesignView.RulerWidth / zoomFactor);
344 this._slidersContainer.style.top = cssRulerWidth;
345 this._warningMessage.style.height = cssRulerWidth;
347 var cssSliderWidth = WebInspector.ResponsiveDesignView.SliderWidth / zoomFactor + "px";
348 this._heightSliderContainer.style.flexBasis = cssSliderWidth;
349 this._heightSliderContainer.style.marginBottom = "-" + cssSliderWidth;
350 this._widthSliderContainer.style.flexBasis = cssSliderWidth;
351 this._widthSliderContainer.style.marginRight = "-" + cssSliderWidth;
354 var cssWidth = this._dipWidth ? (this._dipWidth / zoomFactor + "px") : (availableDip.width / zoomFactor + "px");
355 var cssHeight = this._dipHeight ? (this._dipHeight / zoomFactor + "px") : (availableDip.height / zoomFactor + "px");
356 if (this._cachedCssWidth !== cssWidth || this._cachedCssHeight !== cssHeight) {
357 this._slidersContainer.style.width = cssWidth;
358 this._slidersContainer.style.height = cssHeight;
359 this._inspectedPagePlaceholder.onResize();
362 if (this._cachedScale !== this._scale || this._cachedCssCanvasWidth !== cssCanvasWidth || this._cachedCssCanvasHeight !== cssCanvasHeight || this._cachedZoomFactor !== zoomFactor)
363 this._drawCanvas(cssCanvasWidth, cssCanvasHeight);
365 this._cachedScale = this._scale;
366 this._cachedCssCanvasWidth = cssCanvasWidth;
367 this._cachedCssCanvasHeight = cssCanvasHeight;
368 this._cachedCssHeight = cssHeight;
369 this._cachedCssWidth = cssWidth;
370 this._cachedZoomFactor = zoomFactor;
375 if (!this._enabled || this._ignoreResize)
377 var oldSize = this._availableSize;
378 delete this._availableSize;
379 var newSize = this._availableDipSize();
380 if (!newSize.isEqual(oldSize))
381 this.dispatchEventToListeners(WebInspector.OverridesSupport.PageResizer.Events.AvailableSizeChanged, newSize);
383 this._inspectedPagePlaceholder.onResize();
386 _onZoomChanged: function()
391 _createToolbar: function()
393 this._toolbarElement = this._responsiveDesignContainer.element.createChild("div", "responsive-design-toolbar");
394 this._createButtonsSection();
395 this._toolbarElement.createChild("div", "responsive-design-separator");
396 this._createDeviceSection();
397 if (WebInspector.experimentsSettings.networkConditions.isEnabled()) {
398 this._toolbarElement.createChild("div", "responsive-design-separator");
399 this._createNetworkSection();
401 this._toolbarElement.createChild("div", "responsive-design-separator");
403 var moreButtonContainer = this._toolbarElement.createChild("div", "responsive-design-more-button-container");
404 var moreButton = moreButtonContainer.createChild("button", "responsive-design-more-button");
405 moreButton.title = WebInspector.UIString("More overrides");
406 moreButton.addEventListener("click", this._showEmulationInDrawer.bind(this), false);
407 moreButton.textContent = "\u2026";
410 _createButtonsSection: function()
412 var buttonsSection = this._toolbarElement.createChild("div", "responsive-design-section responsive-design-section-buttons");
414 var resetButton = new WebInspector.StatusBarButton(WebInspector.UIString("Reset all overrides."), "clear-status-bar-item");
415 buttonsSection.appendChild(resetButton.element);
416 resetButton.addEventListener("click", WebInspector.overridesSupport.reset, WebInspector.overridesSupport);
418 // Media Query Inspector.
419 this._toggleMediaInspectorButton = new WebInspector.StatusBarButton(WebInspector.UIString("Media queries."), "responsive-design-toggle-media-inspector");
420 this._toggleMediaInspectorButton.toggled = WebInspector.settings.showMediaQueryInspector.get();
421 this._toggleMediaInspectorButton.addEventListener("click", this._onToggleMediaInspectorButtonClick, this);
422 WebInspector.settings.showMediaQueryInspector.addChangeListener(this._updateMediaQueryInspector, this);
423 buttonsSection.appendChild(this._toggleMediaInspectorButton.element);
426 _createDeviceSection: function()
428 var deviceSection = this._toolbarElement.createChild("div", "responsive-design-section responsive-design-section-device");
431 var deviceElement = deviceSection.createChild("div", "responsive-design-suite responsive-design-suite-top").createChild("div");
432 var fieldsetElement = deviceElement.createChild("fieldset");
433 fieldsetElement.createChild("label").textContent = WebInspector.UIString("Device");
434 fieldsetElement.appendChild(WebInspector.overridesSupport.createDeviceSelect(document));
436 var separator = deviceSection.createChild("div", "responsive-design-section-separator");
438 var detailsElement = deviceSection.createChild("div", "responsive-design-suite");
441 var screenElement = detailsElement.createChild("div", "");
442 fieldsetElement = screenElement.createChild("fieldset");
443 var resolutionButton = new WebInspector.StatusBarButton(WebInspector.UIString("Screen resolution"), "responsive-design-icon responsive-design-icon-resolution");
444 resolutionButton.setEnabled(false);
445 fieldsetElement.appendChild(resolutionButton.element);
446 fieldsetElement.appendChild(WebInspector.SettingsUI.createSettingInputField("", WebInspector.overridesSupport.settings.deviceWidth, true, 4, "3em", WebInspector.OverridesSupport.deviceSizeValidator, true, true, WebInspector.UIString("\u2013")));
447 fieldsetElement.appendChild(document.createTextNode(" \u00D7 "));
448 fieldsetElement.appendChild(WebInspector.SettingsUI.createSettingInputField("", WebInspector.overridesSupport.settings.deviceHeight, true, 4, "3em", WebInspector.OverridesSupport.deviceSizeValidator, true, true, WebInspector.UIString("\u2013")));
450 var swapButton = new WebInspector.StatusBarButton(WebInspector.UIString("Swap dimensions"), "responsive-design-icon responsive-design-icon-swap");
451 swapButton.element.tabIndex = -1;
452 swapButton.addEventListener("click", WebInspector.overridesSupport.swapDimensions, WebInspector.overridesSupport);
453 fieldsetElement.appendChild(swapButton.element);
455 // Device pixel ratio.
456 detailsElement.createChild("div", "responsive-design-suite-separator");
457 var dprElement = detailsElement.createChild("div", "");
458 fieldsetElement = dprElement.createChild("fieldset");
459 var dprButton = new WebInspector.StatusBarButton(WebInspector.UIString("Device pixel ratio"), "responsive-design-icon responsive-design-icon-dpr");
460 dprButton.setEnabled(false);
461 fieldsetElement.appendChild(dprButton.element);
462 fieldsetElement.appendChild(WebInspector.SettingsUI.createSettingInputField("", WebInspector.overridesSupport.settings.deviceScaleFactor, true, 4, "2.5em", WebInspector.OverridesSupport.deviceScaleFactorValidator, true, true, WebInspector.UIString("\u2013")));
465 _createNetworkSection: function()
467 var networkSection = this._toolbarElement.createChild("div", "responsive-design-section responsive-design-section-network");
470 var bandwidthElement = networkSection.createChild("div", "responsive-design-suite responsive-design-suite-top").createChild("div");
471 var fieldsetElement = bandwidthElement.createChild("fieldset");
472 var networkCheckbox = fieldsetElement.createChild("label");
473 networkCheckbox.textContent = WebInspector.UIString("Network");
474 fieldsetElement.appendChild(WebInspector.overridesSupport.createNetworkThroughputSelect(document));
476 var separator = networkSection.createChild("div", "responsive-design-section-separator");
479 var userAgentElement = networkSection.createChild("div", "responsive-design-suite").createChild("div");
480 fieldsetElement = userAgentElement.createChild("fieldset");
481 fieldsetElement.appendChild(WebInspector.SettingsUI.createSettingInputField("UA", WebInspector.overridesSupport.settings.userAgent, false, 0, "", undefined, false, false, WebInspector.UIString("No override")));
484 _onToggleMediaInspectorButtonClick: function()
486 WebInspector.settings.showMediaQueryInspector.set(!this._toggleMediaInspectorButton.toggled);
489 _updateMediaQueryInspector: function()
491 this._toggleMediaInspectorButton.toggled = WebInspector.settings.showMediaQueryInspector.get();
492 if (this._mediaInspector.isShowing() === WebInspector.settings.showMediaQueryInspector.get())
494 if (this._mediaInspector.isShowing())
495 this._mediaInspector.detach();
497 this._mediaInspector.show(this._mediaInspectorContainer);
501 _overridesWarningUpdated: function()
503 var message = WebInspector.overridesSupport.warningMessage();
504 if (this._warningMessage.querySelector("span").textContent === message)
506 this._warningMessage.classList.toggle("hidden", !message);
507 this._warningMessage.querySelector("span").textContent = message;
508 this._invalidateCache();
512 _showEmulationInDrawer: function()
514 WebInspector.overridesSupport.reveal();
517 __proto__: WebInspector.VBox.prototype