chrome-devtools-frontend
Version:
Chrome DevTools UI
342 lines (298 loc) • 11.4 kB
text/typescript
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import type * as SDK from '../../core/sdk/sdk.js';
import * as UI from '../../ui/legacy/legacy.js';
import {html, render} from '../../ui/lit/lit.js';
import layerTreeOutlineStyles from './layerTreeOutline.css.js';
import {
LayerSelection,
type LayerView,
type LayerViewHost,
type Selection,
type SnapshotSelection,
} from './LayerViewHost.js';
const UIStrings = {
/**
* @description A count of the number of rendering layers in Layer Tree Outline of the Layers panel
* @example {10} PH1
*/
layerCount: '{PH1} layers',
/**
* @description Label for layers sidepanel tree
*/
layersTreePane: 'Layers Tree Pane',
/**
* @description A context menu item in the DView of the Layers panel
*/
showPaintProfiler: 'Show Paint Profiler',
/**
* @description Details text content in Layer Tree Outline of the Layers panel
* @example {10} PH1
* @example {10} PH2
*/
updateChildDimension: ' ({PH1} × {PH2})',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/layer_viewer/LayerTreeOutline.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export interface ViewInput {
treeOutlineElement: HTMLElement;
layerCount: number;
totalLayerMemory: number;
}
export type View = (input: ViewInput, output: object, target: HTMLElement) => void;
const DEFAULT_VIEW: View = (input, _output, target) => {
render(html`
<style>${layerTreeOutlineStyles}</style>
<div class="vbox layer-tree-wrapper">
<div style="flex-grow: 1; overflow: auto; display: flex;">
${input.treeOutlineElement}
</div>
<div class="hbox layer-summary">
<span class="layer-count">${i18nString(UIStrings.layerCount, {
PH1: input.layerCount
})}</span>
<span>${i18n.ByteUtilities.bytesToString(input.totalLayerMemory)}</span>
</div>
</div>
`,
target);
};
export class LayerTreeOutline extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Widget.Widget>(
UI.Widget.Widget) implements Common.EventTarget.EventTarget<EventTypes>, LayerView {
private layerViewHost: LayerViewHost;
private treeOutline: UI.TreeOutline.TreeOutlineInShadow;
private lastHoveredNode: LayerTreeElement|null;
private layerTree?: SDK.LayerTreeBase.LayerTreeBase|null;
private layerSnapshotMap?: Map<SDK.LayerTreeBase.Layer, SnapshotSelection>;
#view: View;
#layerCount = 0;
#totalLayerMemory = 0;
constructor(layerViewHost: LayerViewHost, view: View = DEFAULT_VIEW) {
super();
this.layerViewHost = layerViewHost;
this.layerViewHost.registerView(this);
this.#view = view;
this.treeOutline = new UI.TreeOutline.TreeOutlineInShadow();
this.treeOutline.element.classList.add('layer-tree', 'overflow-auto');
this.treeOutline.element.addEventListener('mousemove', this.onMouseMove.bind(this) as EventListener, false);
this.treeOutline.element.addEventListener('mouseout', this.onMouseMove.bind(this) as EventListener, false);
this.treeOutline.element.addEventListener('contextmenu', this.onContextMenu.bind(this) as EventListener, true);
UI.ARIAUtils.setLabel(this.treeOutline.contentElement, i18nString(UIStrings.layersTreePane));
this.lastHoveredNode = null;
this.layerViewHost.showInternalLayersSetting().addChangeListener(this.update, this);
}
override wasShown(): void {
super.wasShown();
this.requestUpdate();
}
override performUpdate(): void {
this.#view({
treeOutlineElement: this.treeOutline.element,
layerCount: this.#layerCount,
totalLayerMemory: this.#totalLayerMemory,
},
{}, this.contentElement);
}
override focus(): void {
this.treeOutline.focus();
}
selectObject(selection: Selection|null): void {
this.hoverObject(null);
const layer = selection?.layer();
const node = layer && layerToTreeElement.get(layer);
if (node) {
node.revealAndSelect(true);
} else if (this.treeOutline.selectedTreeElement) {
this.treeOutline.selectedTreeElement.deselect();
}
}
hoverObject(selection: Selection|null): void {
const layer = selection?.layer();
const node = layer && layerToTreeElement.get(layer);
if (node === this.lastHoveredNode) {
return;
}
if (this.lastHoveredNode) {
this.lastHoveredNode.setHovered(false);
}
if (node) {
node.setHovered(true);
}
this.lastHoveredNode = node as LayerTreeElement;
}
setLayerTree(layerTree: SDK.LayerTreeBase.LayerTreeBase|null): void {
this.layerTree = layerTree;
this.update();
}
private update(): void {
const showInternalLayers = this.layerViewHost.showInternalLayersSetting().get();
const seenLayers = new Set<SDK.LayerTreeBase.Layer>();
let root: (SDK.LayerTreeBase.Layer|null)|null = null;
if (this.layerTree) {
if (!showInternalLayers) {
root = this.layerTree.contentRoot();
}
if (!root) {
root = this.layerTree.root();
}
}
let layerCount = 0;
let totalLayerMemory = 0;
const childrenMap = new Map<SDK.LayerTreeBase.Layer, SDK.LayerTreeBase.Layer[]>();
if (this.layerTree && root) {
const buildTree = (layer: SDK.LayerTreeBase.Layer): void => {
if (!layer.drawsContent() && !showInternalLayers) {
return;
}
layerCount++;
totalLayerMemory += layer.gpuMemoryUsage();
if (layer === root) {
return;
}
let parentLayer = layer.parent();
// Skip till nearest visible ancestor.
while (parentLayer && parentLayer !== root && !parentLayer.drawsContent() && !showInternalLayers) {
parentLayer = parentLayer.parent();
}
if (parentLayer) {
let children = childrenMap.get(parentLayer);
if (!children) {
children = [];
childrenMap.set(parentLayer, children);
}
children.push(layer);
} else {
console.assert(false, 'Internal error: multiple root layers');
}
};
this.layerTree.forEachLayer(buildTree, root);
}
const syncNode =
(layer: SDK.LayerTreeBase.Layer, parent: UI.TreeOutline.TreeOutline|UI.TreeOutline.TreeElement): void => {
seenLayers.add(layer);
let node: LayerTreeElement|null = layerToTreeElement.get(layer) || null;
if (!node) {
node = new LayerTreeElement(this, layer);
parent.appendChild(node);
// Expand all new non-content layers to expose content layers better.
if (!layer.drawsContent()) {
node.expand();
}
} else {
if (node.parent !== parent) {
const oldSelection = this.treeOutline.selectedTreeElement;
if (node.parent) {
node.parent.removeChild(node);
}
parent.appendChild(node);
if (oldSelection && oldSelection !== this.treeOutline.selectedTreeElement) {
oldSelection.select();
}
}
node.update();
}
const children = childrenMap.get(layer) || [];
for (const child of children) {
syncNode(child, node);
}
};
if (root && (root.drawsContent() || showInternalLayers)) {
syncNode(root, this.treeOutline.rootElement());
}
// Clean up layers that don't exist anymore from tree.
const rootElement = this.treeOutline.rootElement();
for (let node = rootElement.firstChild(); node instanceof LayerTreeElement && !node.root;) {
if (seenLayers.has(node.layer)) {
node = node.traverseNextTreeElement(false);
} else {
const nextNode = node.nextSibling || node.parent;
if (node.parent) {
node.parent.removeChild(node);
}
if (node === this.lastHoveredNode) {
this.lastHoveredNode = null;
}
node = nextNode;
}
}
if (!this.treeOutline.selectedTreeElement && this.layerTree) {
const elementToSelect = this.layerTree.contentRoot() || this.layerTree.root();
if (elementToSelect) {
const layer = layerToTreeElement.get(elementToSelect);
if (layer) {
layer.revealAndSelect(true);
}
}
}
this.#layerCount = layerCount;
this.#totalLayerMemory = totalLayerMemory;
this.requestUpdate();
}
private onMouseMove(event: MouseEvent): void {
const node = this.treeOutline.treeElementFromEvent(event) as LayerTreeElement | null;
if (node === this.lastHoveredNode) {
return;
}
this.layerViewHost.hoverObject(this.selectionForNode(node));
}
selectedNodeChanged(node: LayerTreeElement): void {
this.layerViewHost.selectObject(this.selectionForNode(node));
}
private onContextMenu(event: MouseEvent): void {
const selection = this.selectionForNode(this.treeOutline.treeElementFromEvent(event) as LayerTreeElement | null);
const contextMenu = new UI.ContextMenu.ContextMenu(event);
const layer = selection?.layer();
if (selection && layer) {
this.layerSnapshotMap = this.layerViewHost.getLayerSnapshotMap();
if (this.layerSnapshotMap.has(layer)) {
contextMenu.defaultSection().appendItem(
i18nString(UIStrings.showPaintProfiler),
() => this.dispatchEventToListeners(Events.PAINT_PROFILER_REQUESTED, selection),
{jslogContext: 'layers.paint-profiler'});
}
}
this.layerViewHost.showContextMenu(contextMenu, selection);
}
private selectionForNode(node: LayerTreeElement|null): Selection|null {
return node?.layer ? new LayerSelection(node.layer) : null;
}
}
export const enum Events {
PAINT_PROFILER_REQUESTED = 'PaintProfilerRequested',
}
export interface EventTypes {
[Events.PAINT_PROFILER_REQUESTED]: Selection;
}
export class LayerTreeElement extends UI.TreeOutline.TreeElement {
// Watch out: This is different from treeOutline that
// LayerTreeElement inherits from UI.TreeOutline.TreeElement.
#treeOutline: LayerTreeOutline;
layer: SDK.LayerTreeBase.Layer;
constructor(tree: LayerTreeOutline, layer: SDK.LayerTreeBase.Layer) {
super();
this.#treeOutline = tree;
this.layer = layer;
layerToTreeElement.set(layer, this);
this.update();
}
update(): void {
const node = this.layer.nodeForSelfOrAncestor();
const title = document.createDocumentFragment();
UI.UIUtils.createTextChild(title, node ? node.simpleSelector() : '#' + this.layer.id());
const details = title.createChild('span', 'dimmed');
details.textContent =
i18nString(UIStrings.updateChildDimension, {PH1: this.layer.width(), PH2: this.layer.height()});
this.title = title;
}
override onselect(): boolean {
this.#treeOutline.selectedNodeChanged(this);
return false;
}
setHovered(hovered: boolean): void {
this.listItemElement.classList.toggle('hovered', hovered);
}
}
export const layerToTreeElement = new WeakMap<SDK.LayerTreeBase.Layer, LayerTreeElement>();