UNPKG

chrome-devtools-frontend

Version:
310 lines (273 loc) 10.6 kB
// 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 {Directives, html, nothing, render, type TemplateResult} from '../../ui/lit/lit.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.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_); const {ref, repeat} = Directives; export interface LayerTreeNode { layer: SDK.LayerTreeBase.Layer; isExpanded: boolean; children: LayerTreeNode[]; } export interface ViewInput { treeData: LayerTreeNode[]; selectedLayer: SDK.LayerTreeBase.Layer|null; hoveredLayer: SDK.LayerTreeBase.Layer|null; layerCount: number; totalLayerMemory: number; onSelect: (layer: SDK.LayerTreeBase.Layer) => void; onHover: (layer: SDK.LayerTreeBase.Layer|null) => void; onContextMenu: (event: MouseEvent, layer: SDK.LayerTreeBase.Layer|null) => void; } export interface ViewOutput { focusTree?: () => void; revealLayer?: (layer: SDK.LayerTreeBase.Layer) => void; } export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void; export const DEFAULT_VIEW: View = (input, output, target) => { const renderNode = (node: LayerTreeNode): TemplateResult => { const layer = node.layer; const domNode = layer.nodeForSelfOrAncestor(); const titleText = domNode ? domNode.simpleSelector() : '#' + layer.id(); const detailsText = i18nString(UIStrings.updateChildDimension, {PH1: layer.width(), PH2: layer.height()}); const isSelected = layer === input.selectedLayer; const isHovered = layer === input.hoveredLayer; // clang-format off return html` <li role="treeitem" data-layer-id=${layer.id()} class=${isHovered ? 'hovered' : ''} ?selected=${isSelected} aria-expanded=${node.isExpanded ? 'true' : 'false'} @select=${() => input.onSelect(layer)} @mouseenter=${() => input.onHover(layer)} @mouseleave=${() => input.onHover(null)}> <span class="tree-node-title" @contextmenu=${(event: MouseEvent) => { input.onContextMenu(event, layer); event.stopPropagation(); }}> ${titleText} <span class="dimmed">${detailsText}</span> </span> ${node.children.length > 0 ? html`<ul role="group">${repeat(node.children, n => n.layer.id(), renderNode)}</ul>` : nothing} </li> `; // clang-format on }; // clang-format off render(html` <style>${layerTreeOutlineStyles}</style> <div class="vbox layer-tree-wrapper" jslog=${VisualLogging.pane('layers-tree')}> <devtools-tree class="layer-tree overflow-auto" @contextmenu=${(event: MouseEvent) => input.onContextMenu(event, null)} ${ref((el: Element | undefined) => { if (el) { output.focusTree = () => { (el as UI.TreeOutline.TreeViewElement).focus(); }; output.revealLayer = (layer: SDK.LayerTreeBase.Layer) => { const li = el.querySelector(`li[data-layer-id="${layer.id()}"]`); if (li) { // Toggling the selected attribute forces TreeViewElement to scroll the node into view. li.removeAttribute('selected'); li.setAttribute('selected', ''); } }; } })} .template=${html` <style>${layerTreeOutlineStyles}</style> <ul role="tree" aria-label=${i18nString(UIStrings.layersTreePane)}> ${input.treeData.map(renderNode)} </ul> `}> </devtools-tree> <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); // clang-format on }; 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 layerTree?: SDK.LayerTreeBase.LayerTreeBase|null; private layerSnapshotMap?: Map<SDK.LayerTreeBase.Layer, SnapshotSelection>; #layerCount = 0; #totalLayerMemory = 0; #treeData: LayerTreeNode[] = []; #viewOutput: ViewOutput = {}; #view: View; #hoveredLayer: SDK.LayerTreeBase.Layer|null = null; constructor(layerViewHost: LayerViewHost, view: View = DEFAULT_VIEW) { super(); this.#view = view; this.layerViewHost = layerViewHost; this.layerViewHost.registerView(this); this.layerViewHost.showInternalLayersSetting().addChangeListener(this.update, this); } override wasShown(): void { super.wasShown(); this.requestUpdate(); } override performUpdate(): void { this.#view({ treeData: this.#treeData, selectedLayer: this.layerViewHost.selection()?.layer() || null, hoveredLayer: this.#hoveredLayer, layerCount: this.#layerCount, totalLayerMemory: this.#totalLayerMemory, onSelect: this.onSelect.bind(this), onHover: this.onHover.bind(this), onContextMenu: this.onContextMenu.bind(this), }, this.#viewOutput, this.contentElement); } override focus(): void { this.#viewOutput.focusTree?.(); } selectObject(selection: Selection|null): void { this.hoverObject(null); this.requestUpdate(); if (selection) { // Defer revealLayer to ensure Lit has finished rendering DOM updates queueMicrotask(() => { this.#viewOutput.revealLayer?.(selection.layer()); }); } } hoverObject(selection: Selection|null): void { this.#hoveredLayer = selection?.layer() || null; this.requestUpdate(); } setLayerTree(layerTree: SDK.LayerTreeBase.LayerTreeBase|null): void { this.layerTree = layerTree; this.update(); } private update(): void { let layerCount = 0; let totalLayerMemory = 0; const showInternalLayers = this.layerViewHost.showInternalLayersSetting().get(); let root: SDK.LayerTreeBase.Layer|null = null; if (this.layerTree) { if (!showInternalLayers) { root = this.layerTree.contentRoot(); } if (!root) { root = this.layerTree.root(); } } 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 makeTreeNode = (layer: SDK.LayerTreeBase.Layer): LayerTreeNode => { const children = childrenMap.get(layer) || []; return { layer, isExpanded: !layer.drawsContent(), children: children.map(makeTreeNode), }; }; this.#treeData = root && (root.drawsContent() || showInternalLayers) ? [makeTreeNode(root)] : []; this.#layerCount = layerCount; this.#totalLayerMemory = totalLayerMemory; this.requestUpdate(); if (!this.layerViewHost.selection() && this.layerTree && this.#treeData.length > 0) { const elementToSelect = this.layerTree.contentRoot() || this.layerTree.root(); if (elementToSelect) { this.layerViewHost.selectObject(new LayerSelection(elementToSelect)); } } } private onHover(layer: SDK.LayerTreeBase.Layer|null): void { this.layerViewHost.hoverObject(layer ? new LayerSelection(layer) : null); } private onSelect(layer: SDK.LayerTreeBase.Layer): void { if (this.layerViewHost.selection()?.layer() === layer) { return; } this.layerViewHost.selectObject(new LayerSelection(layer)); } private onContextMenu(event: MouseEvent, layer: SDK.LayerTreeBase.Layer|null): void { const selection = layer ? new LayerSelection(layer) : null; const contextMenu = new UI.ContextMenu.ContextMenu(event); this.layerSnapshotMap = this.layerViewHost.getLayerSnapshotMap(); if (layer && this.layerSnapshotMap.has(layer) && selection) { contextMenu.defaultSection().appendItem( i18nString(UIStrings.showPaintProfiler), () => this.dispatchEventToListeners(Events.PAINT_PROFILER_REQUESTED, selection), {jslogContext: 'layers.paint-profiler'}); } this.layerViewHost.showContextMenu(contextMenu, selection); } } export const enum Events { PAINT_PROFILER_REQUESTED = 'PaintProfilerRequested', } export interface EventTypes { [Events.PAINT_PROFILER_REQUESTED]: Selection; }