Skip to content

Commit 1d5d955

Browse files
pfaffeDevtools-frontend LUCI CQ
authored andcommitted
[css] Add a css value trace view
This CL adds a view for tracing the computation of a CSS value. Bug: 396080529 Change-Id: I73c5be6ceb170028aac62886d549f4a690d7b213 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6269196 Reviewed-by: Ergün Erdoğmuş <[email protected]> Commit-Queue: Philip Pfaffe <[email protected]>
1 parent 874c7e0 commit 1d5d955

File tree

7 files changed

+194
-3
lines changed

7 files changed

+194
-3
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1369,6 +1369,7 @@ grd_files_debug_sources = [
13691369
"front_end/panels/elements/AccessibilityTreeView.js",
13701370
"front_end/panels/elements/CSSRuleValidator.js",
13711371
"front_end/panels/elements/CSSRuleValidatorHelper.js",
1372+
"front_end/panels/elements/CSSValueTraceView.js",
13721373
"front_end/panels/elements/ClassesPaneWidget.js",
13731374
"front_end/panels/elements/ColorSwatchPopoverIcon.js",
13741375
"front_end/panels/elements/ComputedStyleModel.js",
@@ -1434,6 +1435,7 @@ grd_files_debug_sources = [
14341435
"front_end/panels/elements/components/queryContainer.css.js",
14351436
"front_end/panels/elements/components/stylePropertyEditor.css.js",
14361437
"front_end/panels/elements/computedStyleSidebarPane.css.js",
1438+
"front_end/panels/elements/cssValueTraceView.css.js",
14371439
"front_end/panels/elements/domLinkifier.css.js",
14381440
"front_end/panels/elements/elementStatePaneWidget.css.js",
14391441
"front_end/panels/elements/elementsPanel.css.js",

front_end/panels/elements/BUILD.gn

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ generate_css("css_files") {
1313
"accessibilityTreeView.css",
1414
"classesPaneWidget.css",
1515
"computedStyleSidebarPane.css",
16+
"cssValueTraceView.css",
1617
"domLinkifier.css",
1718
"elementStatePaneWidget.css",
1819
"elementsPanel.css",
@@ -33,6 +34,7 @@ devtools_module("elements") {
3334
"AccessibilityTreeView.ts",
3435
"CSSRuleValidator.ts",
3536
"CSSRuleValidatorHelper.ts",
37+
"CSSValueTraceView.ts",
3638
"ClassesPaneWidget.ts",
3739
"ColorSwatchPopoverIcon.ts",
3840
"ComputedStyleModel.ts",
@@ -153,6 +155,7 @@ ts_library("unittests") {
153155
sources = [
154156
"AccessibilityTreeView.test.ts",
155157
"CSSRuleValidator.test.ts",
158+
"CSSValueTraceView.test.ts",
156159
"ClassesPaneWidget.test.ts",
157160
"ColorSwatchPopoverIcon.test.ts",
158161
"ComputedStyleModel.test.ts",
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import * as Elements from './elements.js';
6+
7+
describe('CSSValueTraceView', () => {
8+
it('works', async () => {
9+
const view = new Elements.CSSValueTraceView.CSSValueTraceView();
10+
view.showTrace(
11+
[[document.createTextNode('sub 1')], [document.createTextNode('sub 2')]],
12+
[[document.createTextNode('eval 1')], [document.createTextNode('eval 2')]], [document.createTextNode('final')]);
13+
const {performUpdate} = view;
14+
const performUpdatePromise = Promise.withResolvers<void>();
15+
sinon.stub(view, 'performUpdate').callsFake(function(this: unknown) {
16+
performUpdate.call(this);
17+
performUpdatePromise.resolve();
18+
});
19+
await performUpdatePromise.promise;
20+
assert.deepEqual(
21+
view.contentElement.textContent?.split('\n').map(l => l.trim()).filter(l => l),
22+
['\u21B3sub 1\u21B3sub 2', '=eval 1', '=eval 2', '=final']);
23+
});
24+
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import * as Lit from '../../third_party/lit/lit.js';
6+
import * as UI from '../../ui/legacy/legacy.js';
7+
8+
import cssValueTraceViewStyles from './cssValueTraceView.css.js';
9+
10+
const {html, render} = Lit;
11+
12+
export interface ViewInput {
13+
substitutions: Node[][];
14+
evaluations: Node[][];
15+
finalResult: Node[]|undefined;
16+
onToggle: () => void;
17+
}
18+
export interface ViewOutput {}
19+
20+
export type View = (
21+
input: ViewInput,
22+
output: ViewOutput,
23+
target: HTMLElement,
24+
) => void;
25+
26+
export class CSSValueTraceView extends UI.Widget.VBox {
27+
#view: View;
28+
#finalResult: Node[]|undefined = undefined;
29+
#evaluations: Node[][] = [];
30+
#substitutions: Node[][] = [];
31+
32+
constructor(
33+
view: View = (input, output, target):
34+
void => {
35+
const substitutionIcon = html`<span class=trace-line-icon aria-label="resolved to">\u21B3</span>`;
36+
const evalIcon = html`<span class=trace-line-icon aria-label="is equal to">\u003D</span>`;
37+
const [firstEvaluation, ...intermediateEvaluations] = input.evaluations;
38+
render(
39+
// clang-format off
40+
html`
41+
<div class="css-value-trace monospace">
42+
${input.substitutions.map(line => html`${substitutionIcon}<span class="trace-line">${line}</span>`)}
43+
${
44+
firstEvaluation && intermediateEvaluations.length === 0
45+
? html`${evalIcon}<span class="trace-line">${firstEvaluation}</span>`
46+
: html`<details
47+
@toggle=${input.onToggle}
48+
?hidden=${!firstEvaluation || intermediateEvaluations.length === 0}>
49+
<summary>
50+
${evalIcon}<devtools-icon class=marker></devtools-icon><span class="trace-line">${firstEvaluation}</span>
51+
</summary>
52+
<div>
53+
${intermediateEvaluations.map(evaluation => html`${evalIcon}<span class="trace-line">${evaluation}</span>`)}
54+
</div>
55+
</details>`
56+
}
57+
${!input.finalResult ? '' : html`${evalIcon}<span class="trace-line">${input.finalResult}</span>`}
58+
</div>
59+
`,
60+
// clang-format on
61+
target,
62+
);
63+
},
64+
) {
65+
super(true);
66+
this.registerRequiredCSS(cssValueTraceViewStyles);
67+
this.#view = view;
68+
this.requestUpdate();
69+
}
70+
71+
showTrace(
72+
substitutions: Node[][],
73+
evaluations: Node[][],
74+
finalResult: Node[]|undefined,
75+
): void {
76+
this.#substitutions = substitutions;
77+
this.#evaluations = evaluations;
78+
this.#finalResult = finalResult;
79+
this.requestUpdate();
80+
}
81+
82+
override performUpdate(): void {
83+
const viewInput: ViewInput = {
84+
substitutions: this.#substitutions,
85+
evaluations: this.#evaluations,
86+
finalResult: this.#finalResult,
87+
onToggle: () => this.onResize(),
88+
};
89+
const viewOutput = {};
90+
this.#view(viewInput, viewOutput, this.contentElement);
91+
}
92+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2025 The Chromium Authors. All rights reserved.
3+
* Use of this source code is governed by a BSD-style license that can be
4+
* found in the LICENSE file.
5+
*/
6+
7+
.css-value-trace {
8+
--cell-width: 2em;
9+
10+
display: grid;
11+
grid-template-columns: var(--cell-width) 1fr;
12+
margin: var(--sys-size-3);
13+
14+
& .trace-line-icon {
15+
grid-column-start: 1;
16+
width: var(--sys-size-9);
17+
height: var(--sys-size-9);
18+
font-size: var(--sys-size-9);
19+
text-align: center;
20+
color: var(--icon-default);
21+
padding-top: var(--sys-size-4);
22+
}
23+
24+
details {
25+
height: min-content;
26+
grid-column: 1 / 4;
27+
28+
summary {
29+
display: grid;
30+
grid-template-columns: var(--cell-width) var(--cell-width) 1fr;
31+
32+
&::marker {
33+
display: none;
34+
content: "";
35+
}
36+
}
37+
38+
div {
39+
devtools-icon, .trace-line-icon {
40+
grid-column-start: 2;
41+
}
42+
43+
display: grid;
44+
grid-template-columns: var(--cell-width) var(--cell-width) 1fr;
45+
}
46+
47+
.trace-line {
48+
grid-column: 3 / 4;
49+
}
50+
51+
.marker {
52+
grid-column-start: 2;
53+
54+
--icon-url: var(--image-file-triangle-right);
55+
56+
padding-top: var(--sys-size-3);
57+
}
58+
59+
&[open] .marker {
60+
--icon-url: var(--image-file-triangle-down);
61+
}
62+
}
63+
64+
& .trace-line {
65+
place-self: center start;
66+
margin-top: var(--sys-size-2);
67+
padding: var(--sys-size-3);
68+
grid-column: 2 / 3;
69+
}
70+
}

front_end/panels/elements/elements.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import * as ColorSwatchPopoverIcon from './ColorSwatchPopoverIcon.js';
3737
import * as ComputedStyleModel from './ComputedStyleModel.js';
3838
import * as ComputedStyleWidget from './ComputedStyleWidget.js';
3939
import * as CSSRuleValidator from './CSSRuleValidator.js';
40+
import * as CSSValueTraceView from './CSSValueTraceView.js';
4041
import * as DOMLinkifier from './DOMLinkifier.js';
4142
import * as DOMPath from './DOMPath.js';
4243
import * as ElementsPanel from './ElementsPanel.js';
@@ -71,6 +72,7 @@ export {
7172
ComputedStyleModel,
7273
ComputedStyleWidget,
7374
CSSRuleValidator,
75+
CSSValueTraceView,
7476
DOMLinkifier,
7577
DOMPath,
7678
ElementsPanel,

front_end/ui/components/icon_button/icon.css

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,8 @@ span {
3333
width: 100%;
3434
height: 100%;
3535
background-color: currentcolor;
36-
mask: var(--icon-url) center / contain no-repeat;
37-
3836
/* Default to a (scaled) 1x1 filled mask image, so that the `Icon` renders as transparent until a "name" is set */
39-
--icon-url: url("data:image/svg+xml,%3Csvg width='1' height='1' fill='%23000' xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E ");
37+
mask: var(--icon-url, url("data:image/svg+xml,%3Csvg width='1' height='1' fill='%23000' xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E ")) center / contain no-repeat;
4038
}
4139

4240
@media (forced-colors: active) {

0 commit comments

Comments
 (0)