Skip to content

Commit 95dbba2

Browse files
pfaffeDevtools-frontend LUCI CQ
authored andcommitted
[css value tracing] Evaluate color-mix()
This makes mixed colors appear as evaluation steps in color traces. Bug: 396080529 Change-Id: I3c465387cb8783ca4a4b2ab2bf17540a216f233f Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6308808 Commit-Queue: Philip Pfaffe <[email protected]> Reviewed-by: Eric Leese <[email protected]>
1 parent 8430d87 commit 95dbba2

File tree

7 files changed

+109
-32
lines changed

7 files changed

+109
-32
lines changed

front_end/panels/elements/ComputedStyleWidget.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,11 @@ function renderPropertyContents(
116116
}
117117
const name = Renderer.renderNameElement(propertyName);
118118
name.slot = 'name';
119-
const value = Renderer.renderValueElement(
120-
propertyName, propertyValue, matchProperty(propertyName, propertyValue),
121-
[new ColorRenderer(), new URLRenderer(null, node), new StringRenderer()]);
119+
const value = Renderer
120+
.renderValueElement(
121+
propertyName, propertyValue, matchProperty(propertyName, propertyValue),
122+
[new ColorRenderer(), new URLRenderer(null, node), new StringRenderer()])
123+
.valueElement;
122124
value.slot = 'value';
123125
propertyContentsCache.set(cacheKey, {name, value});
124126
return {name, value};
@@ -155,7 +157,7 @@ const createTraceElement =
155157
linkifier: Components.Linkifier.Linkifier): ElementsComponents.ComputedStyleTrace.ComputedStyleTrace => {
156158
const trace = new ElementsComponents.ComputedStyleTrace.ComputedStyleTrace();
157159

158-
const valueElement = Renderer.renderValueElement(
160+
const {valueElement} = Renderer.renderValueElement(
159161
property.name, property.value, matchProperty(property.name, property.value),
160162
[new ColorRenderer(), new URLRenderer(null, node), new StringRenderer()]);
161163
valueElement.slot = 'trace-value';

front_end/panels/elements/PropertyRenderer.test.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,17 @@ describeWithEnvironment('PropertyRenderer', () => {
2424
// Prevent normaliztaion to get an accurate representation of the parser result.
2525
sinon.stub(Element.prototype, 'normalize');
2626
assert.deepEqual(
27-
textFragments(Array.from(renderValueElement('--p', 'var(--v)').childNodes)), ['var', '(', '--v', ')']);
27+
textFragments(Array.from(renderValueElement('--p', 'var(--v)').valueElement.childNodes)),
28+
['var', '(', '--v', ')']);
2829

2930
assert.deepEqual(
30-
textFragments(Array.from(renderValueElement('--p', '/* comments are text */ 1px solid 4').childNodes)),
31+
textFragments(
32+
Array.from(renderValueElement('--p', '/* comments are text */ 1px solid 4').valueElement.childNodes)),
3133
['/* comments are text */', ' ', '1px', ' ', 'solid', ' ', '4']);
3234
assert.deepEqual(
3335
textFragments(Array.from(
3436
renderValueElement('--p', '2px var(--double, var(--fallback, black)) #32a1ce rgb(124 125 21 0)')
35-
.childNodes)),
37+
.valueElement.childNodes)),
3638
[
3739
'2px', ' ', 'var', '(', '--double', ',', ' ', 'var', '(', '--fallback', ',', ' ', 'black', ')',
3840
')', ' ', '#32a1ce', ' ', 'rgb', '(', '124', ' ', '125', ' ', '21', ' ', '0', ')',
@@ -67,12 +69,14 @@ describeWithEnvironment('PropertyRenderer', () => {
6769

6870
it('renders trailing comments', () => {
6971
const property = '/* color: red */ blue /* color: red */';
70-
assert.strictEqual(textFragments(Array.from(renderValueElement('--p', property).childNodes)).join(''), property);
72+
assert.strictEqual(
73+
textFragments(Array.from(renderValueElement('--p', property).valueElement.childNodes)).join(''), property);
7174
});
7275

7376
it('renders malformed comments', () => {
7477
const property = 'red /* foo: bar';
75-
assert.strictEqual(textFragments(Array.from(renderValueElement('--p', property).childNodes)).join(''), property);
78+
assert.strictEqual(
79+
textFragments(Array.from(renderValueElement('--p', property).valueElement.childNodes)).join(''), property);
7680
});
7781
});
7882
});

front_end/panels/elements/PropertyRenderer.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,8 @@ export class Renderer extends SDK.CSSPropertyParser.TreeWalker {
293293
// unmatched text and around rendered matching results.
294294
static renderValueElement(
295295
name: string, value: string, matchedResult: SDK.CSSPropertyParser.BottomUpTreeMatching|null,
296-
renderers: Array<MatchRenderer<SDK.CSSPropertyParser.Match>>, tracing?: TracingContext): HTMLElement {
296+
renderers: Array<MatchRenderer<SDK.CSSPropertyParser.Match>>,
297+
tracing?: TracingContext): {valueElement: HTMLElement, cssControls: SDK.CSSPropertyParser.CSSControlMap} {
297298
const valueElement = document.createElement('span');
298299
valueElement.setAttribute(
299300
'jslog', `${VisualLogging.value().track({
@@ -305,7 +306,7 @@ export class Renderer extends SDK.CSSPropertyParser.TreeWalker {
305306

306307
if (!matchedResult) {
307308
valueElement.appendChild(document.createTextNode(value));
308-
return valueElement;
309+
return {valueElement, cssControls: new Map()};
309310
}
310311
const rendererMap = new Map<
311312
SDK.CSSPropertyParser.Constructor<SDK.CSSPropertyParser.Match>, MatchRenderer<SDK.CSSPropertyParser.Match>>();
@@ -314,10 +315,10 @@ export class Renderer extends SDK.CSSPropertyParser.TreeWalker {
314315
}
315316

316317
const context = new RenderingContext(matchedResult.ast, rendererMap, matchedResult, undefined, {}, tracing);
317-
Renderer.render([matchedResult.ast.tree, ...matchedResult.ast.trailingNodes], context)
318-
.nodes.forEach(node => valueElement.appendChild(node));
318+
const {nodes, cssControls} = Renderer.render([matchedResult.ast.tree, ...matchedResult.ast.trailingNodes], context);
319+
nodes.forEach(node => valueElement.appendChild(node));
319320
valueElement.normalize();
320-
return valueElement;
321+
return {valueElement, cssControls};
321322
}
322323
}
323324

front_end/panels/elements/StylePropertyTreeElement.test.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as Bindings from '../../models/bindings/bindings.js';
1111
import * as Workspace from '../../models/workspace/workspace.js';
1212
import {renderElementIntoDOM} from '../../testing/DOMHelpers.js';
1313
import {createTarget, stubNoopSettings} from '../../testing/EnvironmentHelpers.js';
14-
import {expectCall} from '../../testing/ExpectStubCall.js';
14+
import {expectCall, spyCall} from '../../testing/ExpectStubCall.js';
1515
import {describeWithMockConnection, setMockConnectionResponseHandler} from '../../testing/MockConnection.js';
1616
import {
1717
getMatchedStylesWithBlankRule,
@@ -272,6 +272,30 @@ describeWithMockConnection('StylePropertyTreeElement', () => {
272272
assert.deepEqual(
273273
handler.args[1][0].data, {text: 'color-mix(in srgb, color-mix(in oklch, #ff0000, green), blue)'});
274274
});
275+
276+
it('supports evaluation during tracing', async () => {
277+
const property = addProperty('color', 'color-mix(in srgb, black, white)');
278+
setMockConnectionResponseHandler(
279+
'CSS.resolveValues',
280+
(request: Protocol.CSS.ResolveValuesRequest) =>
281+
({results: request.values.map(v => v === property.value ? 'grey' : v)}));
282+
const matchedResult = property.parseValue(matchedStyles, new Map());
283+
284+
const context = new Elements.PropertyRenderer.TracingContext();
285+
assert.isTrue(context.nextEvaluation());
286+
const {valueElement} = Elements.PropertyRenderer.Renderer.renderValueElement(
287+
property.name, property.value, matchedResult,
288+
Elements.StylePropertyTreeElement.getPropertyRenderers(
289+
matchedStyles.nodeStyles()[0], stylesSidebarPane, matchedStyles, null, new Map()),
290+
context);
291+
292+
const colorSwatch = valueElement.querySelector('devtools-color-swatch');
293+
assert.exists(colorSwatch);
294+
const setColorTextCall = await spyCall(colorSwatch, 'setColorText');
295+
296+
assert.strictEqual(setColorTextCall.args[0].asString(), '#808080');
297+
assert.strictEqual(valueElement.textContent, '#808080');
298+
});
275299
});
276300

277301
describe('animation-name', () => {
@@ -495,8 +519,9 @@ describeWithMockConnection('StylePropertyTreeElement', () => {
495519
stylePropertyTreeElement.updateTitle();
496520

497521
const varSwatch =
498-
renderValueSpy.returnValues.find(value => value.firstChild instanceof InlineEditor.LinkSwatch.CSSVarSwatch)
499-
?.firstChild as InlineEditor.LinkSwatch.CSSVarSwatch |
522+
renderValueSpy.returnValues
523+
.find(value => value.valueElement.firstChild instanceof InlineEditor.LinkSwatch.CSSVarSwatch)
524+
?.valueElement.firstChild as InlineEditor.LinkSwatch.CSSVarSwatch |
500525
undefined;
501526
assert.exists(varSwatch);
502527
const revealPropertySpy = sinon.spy(stylesSidebarPane, 'revealProperty');
@@ -541,8 +566,9 @@ describeWithMockConnection('StylePropertyTreeElement', () => {
541566
stylePropertyTreeElement.updateTitle();
542567

543568
const varSwatch =
544-
renderValueSpy.returnValues.find(value => value.firstChild instanceof InlineEditor.LinkSwatch.CSSVarSwatch)
545-
?.firstChild as InlineEditor.LinkSwatch.CSSVarSwatch |
569+
renderValueSpy.returnValues
570+
.find(value => value.valueElement.firstChild instanceof InlineEditor.LinkSwatch.CSSVarSwatch)
571+
?.valueElement.firstChild as InlineEditor.LinkSwatch.CSSVarSwatch |
546572
undefined;
547573
assert.exists(varSwatch);
548574
const jumpToPropertySpy = sinon.spy(stylesSidebarPane, 'jumpToProperty');
@@ -1632,7 +1658,6 @@ describeWithMockConnection('StylePropertyTreeElement', () => {
16321658

16331659
describe('AutoBaseRenderer', () => {
16341660
it('strikes out non-selected values', async () => {
1635-
16361661
const stylePropertyTreeElement = getTreeElement('display', '-internal-auto-base(inline, block)');
16371662

16381663
stylePropertyTreeElement.updateTitle();

front_end/panels/elements/StylePropertyTreeElement.ts

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -232,13 +232,14 @@ export class VariableRenderer extends rendererBase(SDK.CSSPropertyParserMatchers
232232
const substitution = context.tracing?.substitution();
233233
if (substitution) {
234234
if (declaration?.declaration instanceof SDK.CSSProperty.CSSProperty) {
235-
const valueElement = Renderer.renderValueElement(
235+
const {valueElement, cssControls} = Renderer.renderValueElement(
236236
declaration.declaration.name, declaration.declaration.value,
237237
declaration.declaration.parseValue(this.#matchedStyles, this.#computedStyles),
238238
getPropertyRenderers(
239239
declaration.declaration.ownerStyle, this.#stylesPane, this.#matchedStyles, this.#treeElement,
240240
this.#computedStyles),
241241
substitution);
242+
cssControls.forEach((value, key) => value.forEach(control => context.addControl(key, control)));
242243
return [valueElement];
243244
}
244245
if (!declaration && match.fallback.length > 0) {
@@ -562,9 +563,11 @@ export class LightDarkColorRenderer extends rendererBase(SDK.CSSPropertyParserMa
562563
export class ColorMixRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.ColorMixMatch) {
563564
// clang-format on
564565
readonly #pane: StylesSidebarPane;
565-
constructor(pane: StylesSidebarPane) {
566+
#treeElement: StylePropertyTreeElement|null;
567+
constructor(pane: StylesSidebarPane, treeElement: StylePropertyTreeElement|null) {
566568
super();
567569
this.#pane = pane;
570+
this.#treeElement = treeElement;
568571
}
569572

570573
override render(match: SDK.CSSPropertyParserMatchers.ColorMixMatch, context: RenderingContext): Node[] {
@@ -587,30 +590,55 @@ export class ColorMixRenderer extends rendererBase(SDK.CSSPropertyParserMatchers
587590
return false;
588591
};
589592

593+
const childTracingContexts = context.tracing?.evaluation([match.space, match.color1, match.color2]);
594+
const childRenderingContexts =
595+
childTracingContexts?.map(ctx => ctx.renderingContext(context)) ?? [context, context, context];
596+
590597
const contentChild = document.createElement('span');
591598
contentChild.appendChild(document.createTextNode('color-mix('));
592-
Renderer.renderInto(match.space, context, contentChild);
599+
Renderer.renderInto(match.space, childRenderingContexts[0], contentChild);
593600
contentChild.appendChild(document.createTextNode(', '));
594-
const color1 = Renderer.renderInto(match.color1, context, contentChild).cssControls.get('color') ?? [];
601+
const color1 =
602+
Renderer.renderInto(match.color1, childRenderingContexts[1], contentChild).cssControls.get('color') ?? [];
595603
contentChild.appendChild(document.createTextNode(', '));
596-
const color2 = Renderer.renderInto(match.color2, context, contentChild).cssControls.get('color') ?? [];
604+
const color2 =
605+
Renderer.renderInto(match.color2, childRenderingContexts[2], contentChild).cssControls.get('color') ?? [];
597606
contentChild.appendChild(document.createTextNode(')'));
598607

599608
if (context.matchedResult.hasUnresolvedVars(match.node) || color1.length !== 1 || color2.length !== 1) {
600609
return [contentChild];
601610
}
602611

612+
const space = match.space.map(space => context.matchedResult.getComputedText(space)).join(' ');
613+
const color1Text = match.color1.map(color => context.matchedResult.getComputedText(color)).join(' ');
614+
const color2Text = match.color2.map(color => context.matchedResult.getComputedText(color)).join(' ');
615+
const colorMixText = `color-mix(${space}, ${color1Text}, ${color2Text})`;
616+
617+
if (childTracingContexts && context.tracing?.applyEvaluation(childTracingContexts)) {
618+
const initialColor = Common.Color.parse('#000') as Common.Color.Color;
619+
const swatch = new ColorRenderer(this.#pane, this.#treeElement).renderColorSwatch(initialColor);
620+
context.addControl('color', swatch);
621+
const nodeId = this.#pane.node()?.id;
622+
if (nodeId !== undefined) {
623+
void this.#pane.cssModel()?.resolveValues(nodeId, colorMixText).then(results => {
624+
if (results) {
625+
const color = Common.Color.parse(results[0]);
626+
if (color) {
627+
swatch.setColorText(color.as(Common.Color.Format.HEXA));
628+
}
629+
}
630+
});
631+
return [swatch];
632+
}
633+
}
634+
603635
const swatch = new InlineEditor.ColorMixSwatch.ColorMixSwatch();
604636
if (!hookUpColorArg(color1[0], text => swatch.setFirstColor(text)) ||
605637
!hookUpColorArg(color2[0], text => swatch.setSecondColor(text))) {
606638
return [contentChild];
607639
}
608-
609-
const space = match.space.map(space => context.matchedResult.getComputedText(space)).join(' ');
610-
const color1Text = match.color1.map(color => context.matchedResult.getComputedText(color)).join(' ');
611-
const color2Text = match.color2.map(color => context.matchedResult.getComputedText(color)).join(' ');
612640
swatch.tabIndex = -1;
613-
swatch.setColorMixText(`color-mix(${space}, ${color1Text}, ${color2Text})`);
641+
swatch.setColorMixText(colorMixText);
614642
this.#pane.addPopover(swatch, {
615643
contents: () => {
616644
const color = swatch.mixedColor();
@@ -1439,7 +1467,7 @@ export function getPropertyRenderers(
14391467
return [
14401468
new VariableRenderer(stylesPane, treeElement, matchedStyles, computedStyles),
14411469
new ColorRenderer(stylesPane, treeElement),
1442-
new ColorMixRenderer(stylesPane),
1470+
new ColorMixRenderer(stylesPane, treeElement),
14431471
new URLRenderer(style.parentRule, stylesPane.node()),
14441472
new AngleRenderer(treeElement),
14451473
new LinkableNameRenderer(matchedStyles, stylesPane),
@@ -1886,7 +1914,7 @@ export class StylePropertyTreeElement extends UI.TreeOutline.TreeElement {
18861914
}
18871915
this.listItemElement.removeChildren();
18881916
const matchedResult = this.property.parseValue(this.matchedStyles(), this.computedStyles);
1889-
this.valueElement = Renderer.renderValueElement(this.name, this.value, matchedResult, renderers);
1917+
this.valueElement = Renderer.renderValueElement(this.name, this.value, matchedResult, renderers).valueElement;
18901918
this.nameElement = Renderer.renderNameElement(this.name);
18911919
if (this.property.name.startsWith('--') && this.nameElement) {
18921920
this.parentPaneInternal.addPopover(this.nameElement, {

front_end/testing/ExpectStubCall.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,19 @@ export function expectCalled<TArgs extends any[] = any[], TReturnValue = any>(
2727
}
2828
return expectCall(stub, {...options, callCount: remainingCalls});
2929
}
30+
31+
type Args<T> = T extends(...args: infer TArgs) => unknown ? TArgs : never;
32+
type Ret<T> = T extends(...args: infer TArgs) => infer TRet ? TRet : never;
33+
34+
export function spyCall<T, Fn extends keyof T>(obj: T, method: Fn): Promise<{args: Args<T[Fn]>, result: Ret<T[Fn]>}> {
35+
const {promise, resolve} = Promise.withResolvers<{args: Args<T[Fn]>, result: Ret<T[Fn]>}>();
36+
37+
const original = obj[method] as (...args: Args<T[Fn]>) => Ret<T[Fn]>;
38+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
39+
sinon.stub(obj, method).callsFake(function(this: any, ...args: any) {
40+
const result = original.apply(this, args);
41+
resolve({args, result});
42+
});
43+
44+
return promise;
45+
}

front_end/ui/legacy/components/inline_editor/colorMixSwatch.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
grid: [stack] 1fr / [stack] 1fr;
1212
margin-left: 1px;
1313
margin-right: 1px;
14+
vertical-align: -1px;
1415
color: var(--color);
1516
}
1617

0 commit comments

Comments
 (0)