chrome-devtools-frontend
Version:
Chrome DevTools UI
339 lines (296 loc) • 11.6 kB
text/typescript
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/*
* Copyright (C) 2011 Google Inc. All rights reserved.
* Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
* Copyright (C) 2007 Matt Lilek (pewtermoose@gmail.com).
* Copyright (C) 2009 Joseph Pecoraro
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import * as Common from '../../../core/common/common.js';
import * as Platform from '../../../core/platform/platform.js';
import inspectorSyntaxHighlightStyles from '../inspectorSyntaxHighlight.css.legacy.js';
import inspectorSyntaxHighlightDarkStyles from '../inspectorSyntaxHighlightDark.css.legacy.js';
let themeSupportInstance: ThemeSupport;
export class ThemeSupport {
private readonly themeNameInternal: string;
private themableProperties: Set<string>;
private readonly cachedThemePatches: Map<string, string>;
private readonly setting: Common.Settings.Setting<string>;
private readonly customSheets: Set<string>;
private readonly computedRoot: () => symbol | CSSStyleDeclaration;
private injectingStyleSheet?: boolean;
private constructor(setting: Common.Settings.Setting<string>) {
const systemPreferredTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default';
this.themeNameInternal = setting.get() === 'systemPreferred' ? systemPreferredTheme : setting.get();
this.themableProperties = new Set([
'color',
'box-shadow',
'text-shadow',
'outline-color',
'background-image',
'background-color',
'border-left-color',
'border-right-color',
'border-top-color',
'border-bottom-color',
'-webkit-border-image',
'fill',
'stroke',
]);
this.cachedThemePatches = new Map();
this.setting = setting;
this.customSheets = new Set();
this.computedRoot = Common.Lazy.lazy(() => window.getComputedStyle(document.documentElement));
}
static hasInstance(): boolean {
return typeof themeSupportInstance !== 'undefined';
}
static instance(opts: {
forceNew: boolean|null,
setting: Common.Settings.Setting<string>|null,
} = {forceNew: null, setting: null}): ThemeSupport {
const {forceNew, setting} = opts;
if (!themeSupportInstance || forceNew) {
if (!setting) {
throw new Error(`Unable to create theme support: setting must be provided: ${new Error().stack}`);
}
themeSupportInstance = new ThemeSupport(setting);
}
return themeSupportInstance;
}
getComputedValue(variableName: string): string {
const computedRoot = this.computedRoot();
if (typeof computedRoot === 'symbol') {
throw new Error(`Computed value for property (${variableName}) could not be found on :root.`);
}
return computedRoot.getPropertyValue(variableName);
}
hasTheme(): boolean {
return this.themeNameInternal !== 'default';
}
themeName(): string {
return this.themeNameInternal;
}
injectHighlightStyleSheets(element: Element|ShadowRoot): void {
this.injectingStyleSheet = true;
this.appendStyle(element, inspectorSyntaxHighlightStyles);
if (this.themeNameInternal === 'dark') {
this.appendStyle(element, inspectorSyntaxHighlightDarkStyles);
}
this.injectingStyleSheet = false;
}
/**
* Note: this is a duplicate of the function in ui/utils. It exists here
* so there is no circular dependency between ui/utils and theme_support.
*/
private appendStyle(node: Node, {cssContent}: {cssContent: string}): void {
const styleElement = document.createElement('style');
styleElement.textContent = cssContent;
node.appendChild(styleElement);
}
injectCustomStyleSheets(element: Element|ShadowRoot): void {
for (const sheet of this.customSheets) {
const styleElement = document.createElement('style');
styleElement.textContent = sheet;
element.appendChild(styleElement);
}
}
isForcedColorsMode(): boolean {
return window.matchMedia('(forced-colors: active)').matches;
}
addCustomStylesheet(sheetText: string): void {
this.customSheets.add(sheetText);
}
applyTheme(document: Document): void {
if (!this.hasTheme() || this.isForcedColorsMode()) {
return;
}
if (this.themeNameInternal === 'dark') {
document.documentElement.classList.add('-theme-with-dark-background');
}
const styleSheets = document.styleSheets;
const result = [];
for (let i = 0; i < styleSheets.length; ++i) {
const href = styleSheets[i].href;
if (!href) {
continue;
}
result.push(this.patchForTheme(href, (styleSheets[i] as CSSStyleSheet)));
}
result.push('/*# sourceURL=inspector.css.theme */');
const styleElement = document.createElement('style');
styleElement.textContent = result.join('\n');
document.head.appendChild(styleElement);
}
themeStyleSheet(id: string, text: string): string {
if (!this.hasTheme() || this.injectingStyleSheet || this.isForcedColorsMode()) {
return '';
}
let patch = this.cachedThemePatches.get(id);
if (!patch) {
const styleElement = document.createElement('style');
styleElement.textContent = text;
document.body.appendChild(styleElement);
const {sheet} = styleElement;
if (!sheet) {
throw new Error('No sheet in stylesheet object');
}
patch = this.patchForTheme(id, sheet);
document.body.removeChild(styleElement);
}
return patch;
}
private patchForTheme(id: string, styleSheet: CSSStyleSheet): string {
const cached = this.cachedThemePatches.get(id);
if (cached) {
return cached;
}
try {
const rules = styleSheet.cssRules;
const result = [];
for (let j = 0; j < rules.length; ++j) {
const rule = rules[j];
if (rule instanceof CSSImportRule) {
result.push(this.patchForTheme(rule.styleSheet.href || '', rule.styleSheet));
continue;
}
if (!(rule instanceof CSSStyleRule)) {
continue;
}
const output: string[] = [];
const style = rule.style;
const selectorText = rule.selectorText;
for (let i = 0; style && i < style.length; ++i) {
this.patchProperty(selectorText, style, style[i], output);
}
if (output.length) {
result.push(rule.selectorText + '{' + output.join('') + '}');
}
}
const fullText = result.join('\n');
this.cachedThemePatches.set(id, fullText);
return fullText;
} catch (e) {
this.setting.set('default');
return '';
}
}
/**
* Theming API is primarily targeted at making dark theme look good.
* - If rule has ".-theme-preserve" in selector, it won't be affected.
* - One can create specializations for dark themes via body.-theme-with-dark-background selector in host context.
*/
private patchProperty(selectorText: string, style: CSSStyleDeclaration, name: string, output: string[]): void {
if (!this.themableProperties.has(name)) {
return;
}
const value = style.getPropertyValue(name);
if (!value || value === 'none' || value === 'inherit' || value === 'initial' || value === 'transparent') {
return;
}
if (name === 'background-image' && value.indexOf('gradient') === -1) {
return;
}
if (selectorText.indexOf('-theme-') !== -1) {
return;
}
let colorUsage = ThemeSupport.ColorUsage.Unknown;
if (name.indexOf('background') === 0 || name.indexOf('border') === 0) {
colorUsage |= ThemeSupport.ColorUsage.Background;
}
if (name.indexOf('background') === -1) {
colorUsage |= ThemeSupport.ColorUsage.Foreground;
}
output.push(name);
output.push(':');
if (/^var\(.*\)$/.test(value)) {
// Don't translate CSS variables.
output.push(value);
} else {
const items = value.replace(Common.Color.Regex, '\0$1\0').split('\0');
for (const item of items) {
output.push(this.patchColorText(item, (colorUsage as number)));
}
}
if (style.getPropertyPriority(name)) {
output.push(' !important');
}
output.push(';');
}
patchColorText(text: string, colorUsage: number): string {
const color = Common.Color.Color.parse(text);
if (!color) {
return text;
}
const outColor = this.patchColor(color, colorUsage);
let outText = outColor.asString(null);
if (!outText) {
outText = outColor.asString(outColor.hasAlpha() ? Common.Color.Format.RGBA : Common.Color.Format.RGB);
}
return outText || text;
}
patchColor(color: Common.Color.Color, colorUsage: number): Common.Color.Color {
const hsla = color.hsla();
this.patchHSLA(hsla, colorUsage);
const rgba: number[] = [];
Common.Color.Color.hsl2rgb(hsla, rgba);
return new Common.Color.Color(rgba, color.format());
}
private patchHSLA(hsla: number[], colorUsage: number): void {
const hue = hsla[0];
const sat = hsla[1];
let lit: number = hsla[2];
const alpha = hsla[3];
switch (this.themeNameInternal) {
case 'dark': {
const minCap = colorUsage & ThemeSupport.ColorUsage.Background ? 0.14 : 0;
const maxCap = colorUsage & ThemeSupport.ColorUsage.Foreground ? 0.9 : 1;
lit = 1 - lit;
if (lit < minCap * 2) {
lit = minCap + lit / 2;
} else if (lit > 2 * maxCap - 1) {
lit = maxCap - 1 / 2 + lit / 2;
}
break;
}
}
hsla[0] = Platform.NumberUtilities.clamp(hue, 0, 1);
hsla[1] = Platform.NumberUtilities.clamp(sat, 0, 1);
hsla[2] = Platform.NumberUtilities.clamp(lit, 0, 1);
hsla[3] = Platform.NumberUtilities.clamp(alpha, 0, 1);
}
}
export namespace ThemeSupport {
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum ColorUsage {
Unknown = 0,
Foreground = 1,
Background = 2,
}
}