• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2013 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 *     * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *     * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 *     * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31/**
32 * @constructor
33 * @extends {WebInspector.View}
34 * @param {!WebInspector.LayerTreeModel} model
35 */
36WebInspector.Layers3DView = function(model)
37{
38    WebInspector.View.call(this);
39    this.element.classList.add("fill");
40    this.element.classList.add("layers-3d-view");
41    this._emptyView = new WebInspector.EmptyView(WebInspector.UIString("Not in the composited mode.\nConsider forcing composited mode in Settings."));
42    this._model = model;
43    this._model.addEventListener(WebInspector.LayerTreeModel.Events.LayerTreeChanged, this._update, this);
44    this._model.addEventListener(WebInspector.LayerTreeModel.Events.LayerPainted, this._onLayerPainted, this);
45    this._rotatingContainerElement = this.element.createChild("div", "fill rotating-container");
46    this.element.addEventListener("mousemove", this._onMouseMove.bind(this), false);
47    this.element.addEventListener("mouseout", this._onMouseMove.bind(this), false);
48    this.element.addEventListener("mousedown", this._onMouseDown.bind(this), false);
49    this.element.addEventListener("mouseup", this._onMouseUp.bind(this), false);
50    this.element.addEventListener("contextmenu", this._onContextMenu.bind(this), false);
51    this.element.addEventListener("dblclick", this._onDoubleClick.bind(this), false);
52    this.element.addEventListener("click", this._onClick.bind(this), false);
53    this._elementsByLayerId = {};
54    this._rotateX = 0;
55    this._rotateY = 0;
56    this._scaleAdjustmentStylesheet = this.element.ownerDocument.head.createChild("style");
57    this._scaleAdjustmentStylesheet.disabled = true;
58    this._lastOutlinedElement = {};
59    this._layerImage = document.createElement("img");
60    WebInspector.settings.showPaintRects.addChangeListener(this._update, this);
61}
62
63/**
64 * @enum {string}
65 */
66WebInspector.Layers3DView.OutlineType = {
67    Hovered: "hovered",
68    Selected: "selected"
69}
70
71/**
72 * @enum {string}
73 */
74WebInspector.Layers3DView.Events = {
75    LayerHovered: "LayerHovered",
76    LayerSelected: "LayerSelected",
77    LayerSnapshotRequested: "LayerSnapshotRequested"
78}
79
80WebInspector.Layers3DView.PaintRectColors = [
81    WebInspector.Color.fromRGBA([0xFF, 0, 0]),
82    WebInspector.Color.fromRGBA([0xFF, 0, 0xFF]),
83    WebInspector.Color.fromRGBA([0, 0, 0xFF])
84]
85
86WebInspector.Layers3DView.prototype = {
87    onResize: function()
88    {
89        this._update();
90    },
91
92    willHide: function()
93    {
94        this._scaleAdjustmentStylesheet.disabled = true;
95    },
96
97    wasShown: function()
98    {
99        this._scaleAdjustmentStylesheet.disabled = false;
100        if (this._needsUpdate)
101            this._update();
102    },
103
104    /**
105     * @param {!WebInspector.Layers3DView.OutlineType} type
106     * @param {?WebInspector.Layer} layer
107     */
108    _setOutline: function(type, layer)
109    {
110        var element = layer ? this._elementForLayer(layer) : null;
111        var previousElement = this._lastOutlinedElement[type];
112        if (previousElement === element)
113            return;
114        this._lastOutlinedElement[type] = element;
115        if (previousElement) {
116            previousElement.classList.remove(type);
117            this._updateElementColor(previousElement);
118        }
119        if (element) {
120            element.classList.add(type);
121            this._updateElementColor(element);
122        }
123    },
124
125    /**
126     * @param {!WebInspector.Layer} layer
127     */
128    hoverLayer: function(layer)
129    {
130        this._setOutline(WebInspector.Layers3DView.OutlineType.Hovered, layer);
131    },
132
133    /**
134     * @param {!WebInspector.Layer} layer
135     */
136    selectLayer: function(layer)
137    {
138        this._setOutline(WebInspector.Layers3DView.OutlineType.Hovered, null);
139        this._setOutline(WebInspector.Layers3DView.OutlineType.Selected, layer);
140    },
141
142    /**
143     * @param {!WebInspector.Layer} layer
144     * @param {string=} imageURL
145     */
146    showImageForLayer: function(layer, imageURL)
147    {
148        var element = this._elementForLayer(layer);
149        this._layerImage.removeAttribute("src");
150        if (imageURL)
151            this._layerImage.src = imageURL;
152        element.appendChild(this._layerImage);
153    },
154
155    _scaleToFit: function()
156    {
157        var root = this._model.contentRoot();
158        if (!root)
159            return;
160        const padding = 40;
161        var scaleX = this._clientWidth / (root.width() + 2 * padding);
162        var scaleY = this._clientHeight / (root.height() + 2 * padding);
163        this._scale = Math.min(scaleX, scaleY);
164
165        const screenLayerSpacing = 20;
166        this._layerSpacing = Math.ceil(screenLayerSpacing / this._scale) + "px";
167        const screenLayerThickness = 4;
168        var layerThickness = Math.ceil(screenLayerThickness / this._scale) + "px";
169        var stylesheetContent = ".layer-container .side-wall { height: " + layerThickness + "; width: " + layerThickness + "; } " +
170            ".layer-container .back-wall { -webkit-transform: translateZ(-" + layerThickness + "); } " +
171            ".layer-container { -webkit-transform: translateZ(" + this._layerSpacing + "); }";
172        // Workaround for double style recalculation upon assignment to style sheet's text content.
173        var stylesheetTextNode = this._scaleAdjustmentStylesheet.firstChild;
174        if (!stylesheetTextNode || stylesheetTextNode.nodeType !== Node.TEXT_NODE || stylesheetTextNode.nextSibling)
175            this._scaleAdjustmentStylesheet.textContent = stylesheetContent;
176        else
177            stylesheetTextNode.nodeValue = stylesheetContent;
178        var element = this._elementForLayer(root);
179        element.style.webkitTransform = "scale3d(" + this._scale + "," + this._scale + "," + this._scale + ")";
180        element.style.webkitTransformOrigin = "";
181        element.style.left = ((this._clientWidth - root.width() * this._scale) >> 1) + "px";
182        element.style.top = ((this._clientHeight - root.height() * this._scale) >> 1) + "px";
183    },
184
185    _update: function()
186    {
187        if (!this.isShowing()) {
188            this._needsUpdate = true;
189            return;
190        }
191        if (!this._model.contentRoot()) {
192            this._emptyView.show(this.element);
193            this._rotatingContainerElement.removeChildren();
194            return;
195        }
196        this._emptyView.detach();
197
198        /**
199         * @this {WebInspector.Layers3DView}
200         */
201        function updateLayer(layer)
202        {
203            this._updateLayerElement(this._elementForLayer(layer));
204        }
205        this._clientWidth = this.element.clientWidth;
206        this._clientHeight = this.element.clientHeight;
207        for (var layerId in this._elementsByLayerId) {
208            if (this._model.layerById(layerId))
209                continue;
210            this._elementsByLayerId[layerId].remove();
211            delete this._elementsByLayerId[layerId];
212        }
213        this._scaleToFit();
214        this._model.forEachLayer(updateLayer.bind(this), this._model.contentRoot());
215        this._needsUpdate = false;
216    },
217
218    /**
219     * @param {!WebInspector.Event} event
220     */
221    _onLayerPainted: function(event)
222    {
223        var layer = /** @type {!WebInspector.Layer} */ (event.data);
224        this._updatePaintRect(this._elementForLayer(layer));
225    },
226
227    /**
228     * @param {!WebInspector.Layer} layer
229     * @return {!Element}
230     */
231    _elementForLayer: function(layer)
232    {
233        var element = this._elementsByLayerId[layer.id()];
234        if (element) {
235            // We might have missed an update were a layer with given id was gone and re-created,
236            // so update reference to point to proper layer object.
237            element.__layerDetails.layer = layer;
238            return element;
239        }
240        element = document.createElement("div");
241        element.className = "layer-container";
242        ["fill back-wall", "side-wall top", "side-wall right", "side-wall bottom", "side-wall left"].forEach(element.createChild.bind(element, "div"));
243        element.__layerDetails = new WebInspector.LayerDetails(layer, element.createChild("div", "paint-rect"));
244        this._elementsByLayerId[layer.id()] = element;
245        return element;
246    },
247
248    /**
249     * @param {!Element} element
250     */
251    _updateLayerElement: function(element)
252    {
253        var layer = element.__layerDetails.layer;
254        var style = element.style;
255        var isContentRoot = layer === this._model.contentRoot();
256        var parentElement = isContentRoot ? this._rotatingContainerElement : this._elementForLayer(layer.parent());
257        element.__layerDetails.depth = parentElement.__layerDetails ? parentElement.__layerDetails.depth + 1 : 0;
258        element.enableStyleClass("invisible", layer.invisible());
259        this._updateElementColor(element);
260        if (parentElement !== element.parentElement)
261            parentElement.appendChild(element);
262
263        style.width  = layer.width() + "px";
264        style.height  = layer.height() + "px";
265        this._updatePaintRect(element);
266        if (isContentRoot)
267            return;
268
269        style.left  = layer.offsetX() + "px";
270        style.top  = layer.offsetY() + "px";
271        var transform = layer.transform();
272        if (transform) {
273            // Avoid exponential notation in CSS.
274            style.webkitTransform = "matrix3d(" + transform.map(toFixed5).join(",") + ") translateZ(" + this._layerSpacing + ")";
275            var anchor = layer.anchorPoint();
276            style.webkitTransformOrigin = Math.round(anchor[0] * 100) + "% " + Math.round(anchor[1] * 100) + "% " + anchor[2];
277        } else {
278            style.webkitTransform = "";
279            style.webkitTransformOrigin = "";
280        }
281
282        function toFixed5(x)
283        {
284            return x.toFixed(5);
285        }
286    },
287
288    _updatePaintRect: function(element)
289    {
290        var details = element.__layerDetails;
291        var paintRect = details.layer.lastPaintRect();
292        var paintRectElement = details.paintRectElement;
293        if (!paintRect || !WebInspector.settings.showPaintRects.get()) {
294            paintRectElement.classList.add("hidden");
295            return;
296        }
297        paintRectElement.classList.remove("hidden");
298        if (details.paintCount === details.layer.paintCount())
299            return;
300        details.paintCount = details.layer.paintCount();
301        var style = paintRectElement.style;
302        style.left = paintRect.x + "px";
303        style.top = paintRect.y + "px";
304        style.width = paintRect.width + "px";
305        style.height = paintRect.height + "px";
306        var color = WebInspector.Layers3DView.PaintRectColors[details.paintCount % WebInspector.Layers3DView.PaintRectColors.length];
307        style.borderWidth = Math.ceil(1 / this._scale) + "px";
308        style.borderColor = color.toString(WebInspector.Color.Format.RGBA);
309    },
310
311    /**
312     * @param {!Element} element
313     */
314    _updateElementColor: function(element)
315    {
316        var color;
317        if (element === this._lastOutlinedElement[WebInspector.Layers3DView.OutlineType.Selected])
318            color = WebInspector.Color.PageHighlight.Content.toString(WebInspector.Color.Format.RGBA) || "";
319        else {
320            const base = 144;
321            var component = base + 20 * ((element.__layerDetails.depth - 1) % 5);
322            color = "rgba(" + component + "," + component + "," + component + ", 0.8)";
323        }
324        element.style.backgroundColor = color;
325    },
326
327    /**
328     * @param {?Event} event
329     */
330    _onMouseDown: function(event)
331    {
332        if (event.which !== 1)
333            return;
334        this._setReferencePoint(event);
335    },
336
337    /**
338     * @param {?Event} event
339     */
340    _setReferencePoint: function(event)
341    {
342        this._originX = event.clientX;
343        this._originY = event.clientY;
344        this._oldRotateX = this._rotateX;
345        this._oldRotateY = this._rotateY;
346    },
347
348    _resetReferencePoint: function()
349    {
350        delete this._originX;
351        delete this._originY;
352        delete this._oldRotateX;
353        delete this._oldRotateY;
354    },
355
356    /**
357     * @param {?Event} event
358     */
359    _onMouseUp: function(event)
360    {
361        if (event.which !== 1)
362            return;
363        this._resetReferencePoint();
364    },
365
366    /**
367     * @param {?Event} event
368     * @return {?WebInspector.Layer}
369     */
370    _layerFromEventPoint: function(event)
371    {
372        var element = this.element.ownerDocument.elementFromPoint(event.pageX, event.pageY);
373        if (!element)
374            return null;
375        element = element.enclosingNodeOrSelfWithClass("layer-container");
376        return element && element.__layerDetails && element.__layerDetails.layer;
377    },
378
379    /**
380     * @param {?Event} event
381     */
382    _onMouseMove: function(event)
383    {
384        if (!event.which) {
385            this.dispatchEventToListeners(WebInspector.Layers3DView.Events.LayerHovered, this._layerFromEventPoint(event));
386            return;
387        }
388        if (event.which === 1) {
389            // Set reference point if we missed mousedown.
390            if (typeof this._originX !== "number")
391                this._setReferencePoint(event);
392            this._rotateX = this._oldRotateX + (this._originY - event.clientY) / 2;
393            this._rotateY = this._oldRotateY - (this._originX - event.clientX) / 4;
394            // Translate well to front so that no matter how we turn the plane, no parts of it goes below  parent.
395            // This makes sure mouse events go to proper layers, not straight to the parent.
396            this._rotatingContainerElement.style.webkitTransform = "translateZ(10000px) rotateX(" + this._rotateX + "deg) rotateY(" + this._rotateY + "deg)";
397        }
398    },
399
400    /**
401     * @param {?Event} event
402     */
403    _onContextMenu: function(event)
404    {
405        var layer = this._layerFromEventPoint(event);
406        var nodeId = layer && layer.nodeId();
407        if (!nodeId)
408            return;
409        var domNode = WebInspector.domAgent.nodeForId(nodeId);
410        if (!domNode)
411            return;
412         var contextMenu = new WebInspector.ContextMenu(event);
413        contextMenu.appendApplicableItems(domNode);
414        contextMenu.show();
415    },
416
417    /**
418     * @param {?Event} event
419     */
420    _onClick: function(event)
421    {
422        this.dispatchEventToListeners(WebInspector.Layers3DView.Events.LayerSelected, this._layerFromEventPoint(event));
423    },
424
425    /**
426     * @param {?Event} event
427     */
428    _onDoubleClick: function(event)
429    {
430        var layer = this._layerFromEventPoint(event);
431        if (layer)
432            this.dispatchEventToListeners(WebInspector.Layers3DView.Events.LayerSnapshotRequested, layer);
433        event.stopPropagation();
434    },
435
436    __proto__: WebInspector.View.prototype
437}
438
439/**
440 * @constructor
441 * @param {!WebInspector.Layer} layer
442 * @param {!Element} paintRectElement
443 */
444WebInspector.LayerDetails = function(layer, paintRectElement)
445{
446    this.layer = layer;
447    this.depth = 0;
448    this.paintRectElement = paintRectElement;
449    this.paintCount = 0;
450}
451