UNPKG

chrome-devtools-frontend

Version:
339 lines (296 loc) • 11.6 kB
// 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, } }