c818f0cbaac2aacf069896d0a2348ad197e8f6ce
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / Layers3DContentView.js
1 /*
2  * Copyright (C) 2017 Sony Interactive Entertainment Inc.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.Layers3DContentView = class Layers3DContentView extends WI.ContentView
27 {
28     constructor()
29     {
30         super();
31
32         this.element.classList.add("layers-3d");
33
34         this._compositingBordersButtonNavigationItem = new WI.ActivateButtonNavigationItem("layer-borders", WI.UIString("Show compositing borders"), WI.UIString("Hide compositing borders"), "Images/LayerBorders.svg", 13, 13);
35         this._compositingBordersButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._toggleCompositingBorders, this);
36         this._compositingBordersButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
37
38         WI.showPaintRectsSetting.addEventListener(WI.Setting.Event.Changed, this._showPaintRectsSettingChanged, this);
39         this._paintFlashingButtonNavigationItem = new WI.ActivateButtonNavigationItem("paint-flashing", WI.UIString("Enable paint flashing"), WI.UIString("Disable paint flashing"), "Images/Paint.svg", 16, 16);
40         this._paintFlashingButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._togglePaintFlashing, this);
41         this._paintFlashingButtonNavigationItem.enabled = !!PageAgent.setShowPaintRects;
42         this._paintFlashingButtonNavigationItem.activated = PageAgent.setShowPaintRects && WI.showPaintRectsSetting.value;
43         this._paintFlashingButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low;
44
45         this._layers = [];
46         this._layerGroupsById = new Map;
47         this._selectedLayerGroup = null;
48         this._candidateSelection = null;
49         this._nodeToSelect = null;
50
51         this._renderer = null;
52         this._camera = null;
53         this._controls = null;
54         this._scene = null;
55         this._boundingBox = null;
56         this._raycaster = null;
57         this._animationFrameRequestId = null;
58         this._documentNode = null;
59
60         this._layerInfoElement = null;
61         this._compositedDimensionsElement = null;
62         this._visibleDimensionsElement = null;
63         this._reasonsListElement = null;
64     }
65
66     // Public
67
68     get navigationItems()
69     {
70         return [this._compositingBordersButtonNavigationItem, this._paintFlashingButtonNavigationItem];
71     }
72
73     get supplementalRepresentedObjects()
74     {
75         return this._layers;
76     }
77
78     shown()
79     {
80         super.shown();
81
82         this._updateCompositingBordersButtonState();
83
84         this.updateLayout();
85         WI.layerTreeManager.addEventListener(WI.LayerTreeManager.Event.LayerTreeDidChange, this._layerTreeDidChange, this);
86
87         if (this.didInitialLayout)
88             this._animate();
89     }
90
91     hidden()
92     {
93         WI.layerTreeManager.removeEventListener(WI.LayerTreeManager.Event.LayerTreeDidChange, this._layerTreeDidChange, this);
94         this._stopAnimation();
95
96         super.hidden();
97     }
98
99     closed()
100     {
101         WI.showPaintRectsSetting.removeEventListener(WI.Setting.Event.Changed, this._showPaintRectsSettingChanged, this);
102
103         super.closed();
104     }
105
106     selectLayerById(layerId)
107     {
108         let layerGroup = this._layerGroupsById.get(layerId);
109         this._updateLayerGroupSelection(layerGroup);
110         this._updateLayerInfoElement();
111         this._centerOnSelection();
112     }
113
114     selectLayerForNode(node)
115     {
116         if (!this._layers.length) {
117             this._nodeToSelect = node;
118             return;
119         }
120
121         this._nodeToSelect = null;
122
123         let layer = null;
124         while (node && !layer) {
125             layer = this._layers.find((layer) => layer.nodeId === node.id);
126             if (!layer)
127                 node = node.parentNode;
128         }
129
130         console.assert(layer, "There should always be a top level (document) layer");
131         if (!layer)
132             return;
133
134         this.selectLayerById(layer.layerId);
135
136         this.dispatchEventToListeners(WI.Layers3DContentView.Event.SelectedLayerChanged, {layerId: layer.layerId});
137     }
138
139     // Protected
140
141     initialLayout()
142     {
143         super.initialLayout();
144
145         this._renderer = new THREE.WebGLRenderer({antialias: true});
146         const backgroundColor = window.getComputedStyle(document.documentElement).getPropertyValue("--background-color-content").trim();
147         this._renderer.setClearColor(backgroundColor);
148         this._renderer.setSize(this.element.offsetWidth, this.element.offsetHeight);
149
150         this._camera = new THREE.PerspectiveCamera(45, this.element.offsetWidth / this.element.offsetHeight, 1, 100000);
151
152         this._controls = new THREE.OrbitControls(this._camera, this._renderer.domElement);
153         this._controls.enableDamping = true;
154         this._controls.panSpeed = 0.5;
155         this._controls.enableKeys = false;
156         this._controls.zoomSpeed = 0.5;
157         this._controls.minDistance = 1000;
158         this._controls.rotateSpeed = 0.5;
159         this._controls.minAzimuthAngle = -Math.PI / 2;
160         this._controls.maxAzimuthAngle = Math.PI / 2;
161         this._controls.screenSpacePanning = true;
162         this._renderer.domElement.addEventListener("contextmenu", (event) => { event.stopPropagation(); });
163
164         this._scene = new THREE.Scene;
165         this._boundingBox = new THREE.Box3;
166
167         this._raycaster = new THREE.Raycaster;
168         this._renderer.domElement.addEventListener("mousedown", this._canvasMouseDown.bind(this));
169         this._renderer.domElement.addEventListener("mouseup", this._canvasMouseUp.bind(this));
170
171         this.element.appendChild(this._renderer.domElement);
172
173         this._animate();
174     }
175
176     layout()
177     {
178         if (this.layoutReason === WI.View.LayoutReason.Resize)
179             return;
180
181         WI.domManager.requestDocument((node) => {
182             let documentWasUpdated = this._updateDocument(node);
183
184             WI.layerTreeManager.layersForNode(node, (layers) => {
185                 this._updateLayers(layers);
186
187                 if (documentWasUpdated)
188                     this._resetCamera();
189
190                 if (this._nodeToSelect)
191                     this.selectLayerForNode(this._nodeToSelect);
192             });
193         });
194     }
195
196     sizeDidChange()
197     {
198         super.sizeDidChange();
199
200         this._stopAnimation();
201         this._camera.aspect = this.element.offsetWidth / this.element.offsetHeight;
202         this._camera.updateProjectionMatrix();
203         this._renderer.setSize(this.element.offsetWidth, this.element.offsetHeight);
204         this._animate();
205     }
206
207     // Private
208
209     _layerTreeDidChange(event)
210     {
211         this.needsLayout();
212     }
213
214     _animate()
215     {
216         this._controls.update();
217         this._restrictPan();
218         this._renderer.render(this._scene, this._camera);
219         this._animationFrameRequestId = requestAnimationFrame(() => { this._animate(); });
220     }
221
222     _stopAnimation()
223     {
224         cancelAnimationFrame(this._animationFrameRequestId);
225         this._animationFrameRequestId = null;
226     }
227
228     _updateDocument(documentNode)
229     {
230         if (documentNode === this._documentNode)
231             return false;
232
233         this._scene.children.length = 0;
234         this._layerGroupsById.clear();
235         this._layers.length = 0;
236
237         this._documentNode = documentNode;
238
239         return true;
240     }
241
242     _updateLayers(newLayers)
243     {
244         // FIXME: This should be made into the basic usage of the manager, if not the agent itself.
245         //        At that point, we can remove this duplication from the visualization and sidebar.
246         let {removals, additions} = WI.layerTreeManager.layerTreeMutations(this._layers, newLayers);
247
248         for (let layer of removals) {
249             let layerGroup = this._layerGroupsById.get(layer.layerId);
250             this._scene.remove(layerGroup);
251             this._layerGroupsById.delete(layer.layerId);
252         }
253
254         if (this._selectedLayerGroup && !this._layerGroupsById.get(this._selectedLayerGroup.userData.layer.layerId))
255             this.selectedLayerGroup = null;
256
257         for (let layer of additions) {
258             let layerGroup = this._createLayerGroup(layer);
259             this._layerGroupsById.set(layer.layerId, layerGroup);
260             this._scene.add(layerGroup);
261         }
262
263         // FIXME: Update the backend to provide a literal "layer tree" so we can decide z-indices less naively.
264         const zInterval = 25;
265         newLayers.forEach((layer, index) => {
266             let layerGroup = this._layerGroupsById.get(layer.layerId);
267             layerGroup.position.set(layer.bounds.x, -layer.bounds.y, index * zInterval);
268         });
269
270         this._boundingBox.setFromObject(this._scene);
271         this._controls.maxDistance = this._boundingBox.max.z + WI.Layers3DContentView._zPadding;
272
273         this._layers = newLayers;
274         this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange);
275     }
276
277     _createLayerGroup(layer) {
278         let layerGroup = new THREE.Group;
279         layerGroup.userData.layer = layer;
280         layerGroup.add(this._createLayerMesh(layer.bounds), this._createLayerMesh(layer.compositedBounds, true));
281         return layerGroup;
282     }
283
284     _createLayerMesh({width, height}, isOutline = false)
285     {
286         let geometry = new THREE.Geometry;
287         geometry.vertices.push(
288             new THREE.Vector3(0,     0,       0),
289             new THREE.Vector3(0,     -height, 0),
290             new THREE.Vector3(width, -height, 0),
291             new THREE.Vector3(width, 0,       0),
292         );
293
294         if (isOutline) {
295             let material = new THREE.LineBasicMaterial({color: WI.Layers3DContentView._layerColor.stroke});
296             return new THREE.LineLoop(geometry, material);
297         }
298
299         geometry.faces.push(new THREE.Face3(0, 1, 3), new THREE.Face3(1, 2, 3));
300
301         let material = new THREE.MeshBasicMaterial({
302             color: WI.Layers3DContentView._layerColor.fill,
303             transparent: true,
304             opacity: 0.4,
305             side: THREE.DoubleSide,
306             depthWrite: false,
307         });
308
309         return new THREE.Mesh(geometry, material);
310     }
311
312     _canvasMouseDown(event)
313     {
314         let x = (event.offsetX / event.target.offsetWidth) * 2 - 1;
315         let y = -(event.offsetY / event.target.offsetHeight) * 2 + 1;
316         this._raycaster.setFromCamera(new THREE.Vector2(x, y), this._camera);
317
318         const recursive = true;
319         let intersects = this._raycaster.intersectObjects(this._scene.children, recursive);
320         let layerGroup = intersects.length ? intersects[0].object.parent : null;
321         this._candidateSelection = {layerGroup};
322
323         let canvasMouseMove = (event) => {
324             this._candidateSelection = null;
325             this._renderer.domElement.removeEventListener("mousemove", canvasMouseMove);
326         };
327
328         this._renderer.domElement.addEventListener("mousemove", canvasMouseMove);
329     }
330
331     _canvasMouseUp(event)
332     {
333         if (!this._candidateSelection)
334             return;
335
336         let selection = this._candidateSelection.layerGroup;
337         if (selection && selection === this._selectedLayerGroup) {
338             if (!event.metaKey)
339                 return;
340
341             selection = null;
342         }
343
344         this._updateLayerGroupSelection(selection);
345         this._updateLayerInfoElement();
346
347         let layerId = selection ? selection.userData.layer.layerId : null;
348         this.dispatchEventToListeners(WI.Layers3DContentView.Event.SelectedLayerChanged, {layerId});
349     }
350
351     _updateLayerGroupSelection(layerGroup)
352     {
353         let setColor = ({fill, stroke}) => {
354             let [plane, outline] = this._selectedLayerGroup.children;
355             plane.material.color.set(fill);
356             outline.material.color.set(stroke);
357         };
358
359         if (this._selectedLayerGroup)
360             setColor(WI.Layers3DContentView._layerColor);
361
362         this._selectedLayerGroup = layerGroup;
363
364         if (this._selectedLayerGroup)
365             setColor(WI.Layers3DContentView._selectedLayerColor);
366     }
367
368     _centerOnSelection()
369     {
370         if (!this._selectedLayerGroup)
371             return;
372
373         let {x, y, width, height} = this._selectedLayerGroup.userData.layer.bounds;
374         this._controls.target.set(x + (width / 2), -y - (height / 2), 0);
375         this._camera.position.set(x + (width / 2), -y - (height / 2), this._selectedLayerGroup.position.z + WI.Layers3DContentView._zPadding / 2);
376     }
377
378     _resetCamera()
379     {
380         let {x, y, width, height} = this._layers[0].bounds;
381         this._controls.target.set(x + (width / 2), -y - (height / 2), 0);
382         this._camera.position.set(x + (width / 2), -y - (height / 2), this._controls.maxDistance - WI.Layers3DContentView._zPadding / 2);
383     }
384
385     _restrictPan()
386     {
387         let delta = new THREE.Vector3;
388         this._boundingBox.clampPoint(this._controls.target, delta).setZ(0).sub(this._controls.target);
389         this._controls.target.add(delta);
390         this._camera.position.add(delta);
391     }
392
393     _showPaintRectsSettingChanged(event)
394     {
395         console.assert(PageAgent.setShowPaintRects);
396
397         this._paintFlashingButtonNavigationItem.activated = WI.showPaintRectsSetting.value;
398         PageAgent.setShowPaintRects(this._paintFlashingButtonNavigationItem.activated);
399     }
400
401     _togglePaintFlashing(event)
402     {
403         WI.showPaintRectsSetting.value = !WI.showPaintRectsSetting.value;
404     }
405
406     _updateCompositingBordersButtonState()
407     {
408         // This value can be changed outside of Web Inspector.
409         // FIXME: Have PageAgent dispatch a change event instead?
410         PageAgent.getCompositingBordersVisible((error, compositingBordersVisible) => {
411             this._compositingBordersButtonNavigationItem.activated = error ? false : compositingBordersVisible;
412             this._compositingBordersButtonNavigationItem.enabled = error !== "unsupported";
413         });
414     }
415
416     _toggleCompositingBorders(event)
417     {
418         this._compositingBordersButtonNavigationItem.activated = !this._compositingBordersButtonNavigationItem.activated;
419         PageAgent.setCompositingBordersVisible(this._compositingBordersButtonNavigationItem.activated);
420     }
421
422     _buildLayerInfoElement()
423     {
424         this._layerInfoElement = this._element.appendChild(document.createElement("div"));
425         this._layerInfoElement.classList.add("layer-info", "hidden");
426
427         let content = this._layerInfoElement.appendChild(document.createElement("div"));
428         content.className = "content";
429
430         let dimensionsTitle = content.appendChild(document.createElement("div"));
431         dimensionsTitle.textContent = WI.UIString("Dimensions");
432         let dimensionsTable = content.appendChild(document.createElement("table"));
433
434         let compositedRow = dimensionsTable.appendChild(document.createElement("tr"));
435         let compositedLabel = compositedRow.appendChild(document.createElement("td"));
436         compositedLabel.textContent = WI.UIString("Composited");
437         this._compositedDimensionsElement = compositedRow.appendChild(document.createElement("td"));
438
439         let visibleRow = dimensionsTable.appendChild(document.createElement("tr"));
440         let visibleLabel = visibleRow.appendChild(document.createElement("td"));
441         visibleLabel.textContent = WI.UIString("Visible");
442         this._visibleDimensionsElement = visibleRow.appendChild(document.createElement("td"));
443
444         let reasonsTitle = content.appendChild(document.createElement("div"));
445         reasonsTitle.textContent = WI.UIString("Reasons for compositing");
446         this._reasonsListElement = content.appendChild(document.createElement("ul"));
447     }
448
449     _updateLayerInfoElement()
450     {
451         if (!this._layerInfoElement)
452             this._buildLayerInfoElement();
453
454         if (!this._selectedLayerGroup) {
455             this._layerInfoElement.classList.add("hidden");
456             return;
457         }
458
459         let layer = this._selectedLayerGroup.userData.layer;
460         this._compositedDimensionsElement.textContent = `${layer.compositedBounds.width}px ${multiplicationSign} ${layer.compositedBounds.height}px`;
461         this._visibleDimensionsElement.textContent = `${layer.bounds.width}px ${multiplicationSign} ${layer.bounds.height}px`;
462
463         WI.layerTreeManager.reasonsForCompositingLayer(layer, (compositingReasons) => {
464             this._updateReasonsList(compositingReasons);
465             this._layerInfoElement.classList.remove("hidden");
466         });
467     }
468
469     _updateReasonsList(compositingReasons)
470     {
471         this._reasonsListElement.removeChildren();
472
473         let addReason = (reason) => {
474             let item = this._reasonsListElement.appendChild(document.createElement("li"));
475             item.textContent = reason;
476         };
477
478         if (compositingReasons.transform3D)
479             addReason(WI.UIString("Element has a 3D transform"));
480         if (compositingReasons.video)
481             addReason(WI.UIString("Element is <video>"));
482         if (compositingReasons.canvas)
483             addReason(WI.UIString("Element is <canvas>"));
484         if (compositingReasons.plugin)
485             addReason(WI.UIString("Element is a plug-in"));
486         if (compositingReasons.iFrame)
487             addReason(WI.UIString("Element is <iframe>"));
488         if (compositingReasons.backfaceVisibilityHidden)
489             addReason(WI.UIString("Element has “backface-visibility: hidden” style"));
490         if (compositingReasons.clipsCompositingDescendants)
491             addReason(WI.UIString("Element clips compositing descendants"));
492         if (compositingReasons.animation)
493             addReason(WI.UIString("Element is animated"));
494         if (compositingReasons.filters)
495             addReason(WI.UIString("Element has CSS filters applied"));
496         if (compositingReasons.positionFixed)
497             addReason(WI.UIString("Element has “position: fixed” style"));
498         if (compositingReasons.positionSticky)
499             addReason(WI.UIString("Element has “position: sticky” style"));
500         if (compositingReasons.overflowScrollingTouch)
501             addReason(WI.UIString("Element has “-webkit-overflow-scrolling: touch” style"));
502         if (compositingReasons.stacking)
503             addReason(WI.UIString("Element may overlap another compositing element"));
504         if (compositingReasons.overlap)
505             addReason(WI.UIString("Element overlaps other compositing element"));
506         if (compositingReasons.negativeZIndexChildren)
507             addReason(WI.UIString("Element has children with a negative z-index"));
508         if (compositingReasons.transformWithCompositedDescendants)
509             addReason(WI.UIString("Element has a 2D transform and composited descendants"));
510         if (compositingReasons.opacityWithCompositedDescendants)
511             addReason(WI.UIString("Element has opacity applied and composited descendants"));
512         if (compositingReasons.maskWithCompositedDescendants)
513             addReason(WI.UIString("Element is masked and has composited descendants"));
514         if (compositingReasons.reflectionWithCompositedDescendants)
515             addReason(WI.UIString("Element has a reflection and composited descendants"));
516         if (compositingReasons.filterWithCompositedDescendants)
517             addReason(WI.UIString("Element has CSS filters applied and composited descendants"));
518         if (compositingReasons.blendingWithCompositedDescendants)
519             addReason(WI.UIString("Element has CSS blending applied and composited descendants"));
520         if (compositingReasons.isolatesCompositedBlendingDescendants)
521             addReason(WI.UIString("Element is a stacking context and has composited descendants with CSS blending applied"));
522         if (compositingReasons.perspective)
523             addReason(WI.UIString("Element has perspective applied"));
524         if (compositingReasons.preserve3D)
525             addReason(WI.UIString("Element has “transform-style: preserve-3d” style"));
526         if (compositingReasons.willChange)
527             addReason(WI.UIString("Element has “will-change” style which includes opacity, transform, transform-style, perspective, filter or backdrop-filter"));
528         if (compositingReasons.root)
529             addReason(WI.UIString("Element is the root element"));
530         if (compositingReasons.blending)
531             addReason(WI.UIString("Element has “blend-mode” style"));
532     }
533 };
534
535 WI.Layers3DContentView._zPadding = 3000;
536
537 WI.Layers3DContentView._layerColor = {
538     fill: "hsl(76, 49%, 75%)",
539     stroke: "hsl(79, 45%, 50%)"
540 };
541
542 WI.Layers3DContentView._selectedLayerColor = {
543     fill: "hsl(208, 66%, 79%)",
544     stroke: "hsl(202, 57%, 68%)"
545 };
546
547 WI.Layers3DContentView.Event = {
548     SelectedLayerChanged: "selected-layer-changed"
549 };