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