diff options
Diffstat (limited to 'toolkit/modules/LightweightThemeConsumer.jsm')
-rw-r--r-- | toolkit/modules/LightweightThemeConsumer.jsm | 478 |
1 files changed, 478 insertions, 0 deletions
diff --git a/toolkit/modules/LightweightThemeConsumer.jsm b/toolkit/modules/LightweightThemeConsumer.jsm new file mode 100644 index 0000000000..52da2fe9a7 --- /dev/null +++ b/toolkit/modules/LightweightThemeConsumer.jsm @@ -0,0 +1,478 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["LightweightThemeConsumer"]; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const DEFAULT_THEME_ID = "default-theme@mozilla.org"; + +ChromeUtils.defineModuleGetter( + this, + "AppConstants", + "resource://gre/modules/AppConstants.jsm" +); +// Get the theme variables from the app resource directory. +// This allows per-app variables. +ChromeUtils.defineModuleGetter( + this, + "ThemeContentPropertyList", + "resource:///modules/ThemeVariableMap.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "ThemeVariableMap", + "resource:///modules/ThemeVariableMap.jsm" +); + +const toolkitVariableMap = [ + [ + "--lwt-accent-color", + { + lwtProperty: "accentcolor", + processColor(rgbaChannels, element) { + if (!rgbaChannels || rgbaChannels.a == 0) { + return "white"; + } + // Remove the alpha channel + const { r, g, b } = rgbaChannels; + return `rgb(${r}, ${g}, ${b})`; + }, + }, + ], + [ + "--lwt-text-color", + { + lwtProperty: "textcolor", + processColor(rgbaChannels, element) { + if (!rgbaChannels) { + rgbaChannels = { r: 0, g: 0, b: 0 }; + } + // Remove the alpha channel + const { r, g, b } = rgbaChannels; + element.setAttribute( + "lwthemetextcolor", + _isColorDark(r, g, b) ? "dark" : "bright" + ); + return `rgba(${r}, ${g}, ${b})`; + }, + }, + ], + [ + "--arrowpanel-background", + { + lwtProperty: "popup", + }, + ], + [ + "--arrowpanel-color", + { + lwtProperty: "popup_text", + processColor(rgbaChannels, element) { + const disabledColorVariable = "--panel-disabled-color"; + const descriptionColorVariable = "--panel-description-color"; + + if (!rgbaChannels) { + element.removeAttribute("lwt-popup-brighttext"); + element.style.removeProperty(disabledColorVariable); + element.style.removeProperty(descriptionColorVariable); + return null; + } + + let { r, g, b, a } = rgbaChannels; + + if (_isColorDark(r, g, b)) { + element.removeAttribute("lwt-popup-brighttext"); + } else { + element.setAttribute("lwt-popup-brighttext", "true"); + } + + element.style.setProperty( + disabledColorVariable, + `rgba(${r}, ${g}, ${b}, 0.5)` + ); + element.style.setProperty( + descriptionColorVariable, + `rgba(${r}, ${g}, ${b}, 0.65)` + ); + return `rgba(${r}, ${g}, ${b}, ${a})`; + }, + }, + ], + [ + "--arrowpanel-border-color", + { + lwtProperty: "popup_border", + }, + ], + [ + "--lwt-toolbar-field-background-color", + { + lwtProperty: "toolbar_field", + }, + ], + [ + "--lwt-toolbar-field-color", + { + lwtProperty: "toolbar_field_text", + processColor(rgbaChannels, element) { + if (!rgbaChannels) { + element.removeAttribute("lwt-toolbar-field-brighttext"); + return null; + } + const { r, g, b, a } = rgbaChannels; + if (_isColorDark(r, g, b)) { + element.removeAttribute("lwt-toolbar-field-brighttext"); + } else { + element.setAttribute("lwt-toolbar-field-brighttext", "true"); + } + return `rgba(${r}, ${g}, ${b}, ${a})`; + }, + }, + ], + [ + "--lwt-toolbar-field-border-color", + { + lwtProperty: "toolbar_field_border", + }, + ], + [ + "--lwt-toolbar-field-focus", + { + lwtProperty: "toolbar_field_focus", + fallbackProperty: "toolbar_field", + processColor(rgbaChannels, element, propertyOverrides) { + // Ensure minimum opacity as this is used behind address bar results. + if (!rgbaChannels) { + propertyOverrides.set("toolbar_field_text_focus", "black"); + return "white"; + } + const min_opacity = 0.9; + let { r, g, b, a } = rgbaChannels; + if (a < min_opacity) { + propertyOverrides.set( + "toolbar_field_text_focus", + _isColorDark(r, g, b) ? "white" : "black" + ); + return `rgba(${r}, ${g}, ${b}, ${min_opacity})`; + } + return `rgba(${r}, ${g}, ${b}, ${a})`; + }, + }, + ], + [ + "--lwt-toolbar-field-focus-color", + { + lwtProperty: "toolbar_field_text_focus", + fallbackProperty: "toolbar_field_text", + processColor(rgbaChannels, element) { + if (!rgbaChannels) { + element.removeAttribute("lwt-toolbar-field-focus-brighttext"); + return null; + } + const { r, g, b, a } = rgbaChannels; + if (_isColorDark(r, g, b)) { + element.removeAttribute("lwt-toolbar-field-focus-brighttext"); + } else { + element.setAttribute("lwt-toolbar-field-focus-brighttext", "true"); + } + return `rgba(${r}, ${g}, ${b}, ${a})`; + }, + }, + ], + [ + "--toolbar-field-focus-border-color", + { + lwtProperty: "toolbar_field_border_focus", + }, + ], + [ + "--lwt-toolbar-field-highlight", + { + lwtProperty: "toolbar_field_highlight", + processColor(rgbaChannels, element) { + if (!rgbaChannels) { + element.removeAttribute("lwt-selection"); + return null; + } + element.setAttribute("lwt-selection", "true"); + const { r, g, b, a } = rgbaChannels; + return `rgba(${r}, ${g}, ${b}, ${a})`; + }, + }, + ], + [ + "--lwt-toolbar-field-highlight-text", + { + lwtProperty: "toolbar_field_highlight_text", + }, + ], +]; + +function LightweightThemeConsumer(aDocument) { + this._doc = aDocument; + this._win = aDocument.defaultView; + this._winId = this._win.docShell.outerWindowID; + + Services.obs.addObserver(this, "lightweight-theme-styling-update"); + + // In Linux, the default theme picks up the right colors from dark GTK themes. + if (AppConstants.platform != "linux") { + this.darkThemeMediaQuery = this._win.matchMedia("(-moz-system-dark-theme)"); + this.darkThemeMediaQuery.addListener(this); + } + + const { LightweightThemeManager } = ChromeUtils.import( + "resource://gre/modules/LightweightThemeManager.jsm" + ); + this._update(LightweightThemeManager.themeData); + + this._win.addEventListener("unload", this, { once: true }); +} + +LightweightThemeConsumer.prototype = { + _lastData: null, + + observe(aSubject, aTopic, aData) { + if (aTopic != "lightweight-theme-styling-update") { + return; + } + + let data = aSubject.wrappedJSObject; + if (data.window && data.window !== this._winId) { + return; + } + + this._update(data); + }, + + handleEvent(aEvent) { + if (aEvent.target == this.darkThemeMediaQuery) { + this._update(this._lastData); + return; + } + + switch (aEvent.type) { + case "unload": + Services.obs.removeObserver(this, "lightweight-theme-styling-update"); + Services.ppmm.sharedData.delete(`theme/${this._winId}`); + this._win = this._doc = null; + if (this.darkThemeMediaQuery) { + this.darkThemeMediaQuery.removeListener(this); + this.darkThemeMediaQuery = null; + } + break; + } + }, + + get darkMode() { + return this.darkThemeMediaQuery && this.darkThemeMediaQuery.matches; + }, + + _update(themeData) { + this._lastData = themeData; + + let theme = themeData.theme; + if (themeData.darkTheme && this.darkMode) { + theme = themeData.darkTheme; + } + if (!theme) { + theme = { id: DEFAULT_THEME_ID }; + } + + let active = (this._active = Object.keys(theme).length); + + let root = this._doc.documentElement; + + if (active && theme.headerURL) { + root.setAttribute("lwtheme-image", "true"); + } else { + root.removeAttribute("lwtheme-image"); + } + + this._setExperiment(active, themeData.experiment, theme.experimental); + _setImage(root, active, "--lwt-header-image", theme.headerURL); + _setImage( + root, + active, + "--lwt-additional-images", + theme.additionalBackgrounds + ); + _setProperties(root, active, theme); + + if (theme.id != DEFAULT_THEME_ID || this.darkMode) { + root.setAttribute("lwtheme", "true"); + } else { + root.removeAttribute("lwtheme"); + root.removeAttribute("lwthemetextcolor"); + } + if (theme.id == DEFAULT_THEME_ID && this.darkMode) { + root.setAttribute("lwt-default-theme-in-dark-mode", "true"); + } else { + root.removeAttribute("lwt-default-theme-in-dark-mode"); + } + + let contentThemeData = _getContentProperties(this._doc, active, theme); + Services.ppmm.sharedData.set(`theme/${this._winId}`, contentThemeData); + + this._win.dispatchEvent(new CustomEvent("windowlwthemeupdate")); + }, + + _setExperiment(active, experiment, properties) { + const root = this._doc.documentElement; + if (this._lastExperimentData) { + const { stylesheet, usedVariables } = this._lastExperimentData; + if (stylesheet) { + stylesheet.remove(); + } + if (usedVariables) { + for (const [variable] of usedVariables) { + _setProperty(root, false, variable); + } + } + } + + this._lastExperimentData = {}; + + if (!active || !experiment) { + return; + } + + let usedVariables = []; + if (properties.colors) { + for (const property in properties.colors) { + const cssVariable = experiment.colors[property]; + const value = _rgbaToString( + _cssColorToRGBA(root.ownerDocument, properties.colors[property]) + ); + usedVariables.push([cssVariable, value]); + } + } + + if (properties.images) { + for (const property in properties.images) { + const cssVariable = experiment.images[property]; + usedVariables.push([ + cssVariable, + `url(${properties.images[property]})`, + ]); + } + } + if (properties.properties) { + for (const property in properties.properties) { + const cssVariable = experiment.properties[property]; + usedVariables.push([cssVariable, properties.properties[property]]); + } + } + for (const [variable, value] of usedVariables) { + _setProperty(root, true, variable, value); + } + this._lastExperimentData.usedVariables = usedVariables; + + if (experiment.stylesheet) { + /* Stylesheet URLs are validated using WebExtension schemas */ + let stylesheetAttr = `href="${experiment.stylesheet}" type="text/css"`; + let stylesheet = this._doc.createProcessingInstruction( + "xml-stylesheet", + stylesheetAttr + ); + this._doc.insertBefore(stylesheet, root); + this._lastExperimentData.stylesheet = stylesheet; + } + }, +}; + +function _getContentProperties(doc, active, data) { + if (!active) { + return {}; + } + let properties = {}; + for (let property in data) { + if (ThemeContentPropertyList.includes(property)) { + properties[property] = _cssColorToRGBA(doc, data[property]); + } + } + return properties; +} + +function _setImage(aRoot, aActive, aVariableName, aURLs) { + if (aURLs && !Array.isArray(aURLs)) { + aURLs = [aURLs]; + } + _setProperty( + aRoot, + aActive, + aVariableName, + aURLs && aURLs.map(v => `url("${v.replace(/"/g, '\\"')}")`).join(",") + ); +} + +function _setProperty(elem, active, variableName, value) { + if (active && value) { + elem.style.setProperty(variableName, value); + } else { + elem.style.removeProperty(variableName); + } +} + +function _setProperties(root, active, themeData) { + let propertyOverrides = new Map(); + + for (let map of [toolkitVariableMap, ThemeVariableMap]) { + for (let [cssVarName, definition] of map) { + const { + lwtProperty, + fallbackProperty, + optionalElementID, + processColor, + isColor = true, + } = definition; + let elem = optionalElementID + ? root.ownerDocument.getElementById(optionalElementID) + : root; + let val = propertyOverrides.get(lwtProperty) || themeData[lwtProperty]; + if (isColor) { + val = _cssColorToRGBA(root.ownerDocument, val); + if (!val && fallbackProperty) { + val = _cssColorToRGBA( + root.ownerDocument, + themeData[fallbackProperty] + ); + } + if (processColor) { + val = processColor(val, elem, propertyOverrides); + } else { + val = _rgbaToString(val); + } + } + _setProperty(elem, active, cssVarName, val); + } + } +} + +const kInvalidColor = { r: 0, g: 0, b: 0, a: 1 }; + +function _cssColorToRGBA(doc, cssColor) { + if (!cssColor) { + return null; + } + return ( + doc.defaultView.InspectorUtils.colorToRGBA(cssColor, doc) || kInvalidColor + ); +} + +function _rgbaToString(parsedColor) { + if (!parsedColor) { + return null; + } + let { r, g, b, a } = parsedColor; + if (a == 1) { + return `rgb(${r}, ${g}, ${b})`; + } + return `rgba(${r}, ${g}, ${b}, ${a})`; +} + +function _isColorDark(r, g, b) { + return 0.2125 * r + 0.7154 * g + 0.0721 * b <= 110; +} |