--- a/browser/components/extensions/ext-theme.js
+++ b/browser/components/extensions/ext-theme.js
@@ -5,101 +5,220 @@ Cu.import("resource://gre/modules/Servic
// WeakMap[Extension -> Theme]
let themeMap = new WeakMap();
let styleSheetService = Components.classes["@mozilla.org/content/style-sheet-service;1"]
.getService(Components.interfaces.nsIStyleSheetService);
let ioService = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
+const kChromeThemeColorVarMap = new Map([
+ ["background_tab", ["--tab-background-color", ":root"]],
+ ["background_tab_text", ["--tabs-toolbar-color", ":root"]],
+ ["button_background", ["--chrome-nav-buttons-background", ":root"]],
+ ["frame", ["--chrome-background-color", ":root"]],
+ ["frame_inactive", ["--chrome-secondary-background-color", ":root"]],
+ ["tab_text", ["--tab-selection-color", ":root"]],
+ ["toolbar", ["--tab-background-color", ":root"]],
+ ["toolbar_bottom_separator", ["--chrome-nav-bar-separator-color", ":root"]],
+ ["toolbar_button_stroke", ["--toolbarbutton-active-bordercolor", ":root", ":root toolbar:-moz-lwtheme"]],
+ ["toolbar_button_stroke_inactive", ["--toolbarbutton-hover-bordercolor", ":root", ":root toolbar:-moz-lwtheme"]],
+ ["toolbar_top_separator", ["--chrome-navigator-toolbox-separator-color", ":root"]],
+ ["toolbar_vertical_separator", ["--urlbar-separator-color", ":root"]]
+]);
+const kChromeThemeTintsVarMap = new Map([
+ ["background_tab", ["--tab-hover-background-color", 1, ":root"]],
+ ["buttons", ["--toolbarbutton-hover-background", .2, ":root", ":root toolbar:-moz-lwtheme"]]
+]);
+const kChromeThemeGradientsVarMap = new Map([
+ ["toolbar_button", ["--toolbarbutton-hover-background", ":root", ":root toolbar:-moz-lwtheme"]],
+ ["toolbar_button_pressed", ["--toolbarbutton-active-background", ":root", ":root toolbar:-moz-lwtheme"]]
+]);
+const kTransparentGif = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
+
+function hueToRgb(p, q, t) {
+ if (t < 0) {
+ t += 1;
+ }
+ if (t > 1) {
+ t -= 1;
+ }
+ if (t < 1 / 6) {
+ return p + (q - p) * 6 * t;
+ }
+ if (t < 1 / 2) {
+ return q;
+ }
+ if (t < 2 / 3) {
+ return p + (q - p) * (2 / 3 - t) * 6;
+ }
+ return p;
+}
+
+function hslToRgb([h, s, l]) {
+ // Filter out unsupported -1 value that Chrome supports.
+ if (h < 0 || s < 0 || l < 0) {
+ return null;
+ }
+
+ let r, g, b;
+ if (s == 0) {
+ r = g = b = l; // Achromatic
+ } else {
+ let q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ let p = 2 * l - q;
+ r = hueToRgb(p, q, h + 1 / 3);
+ g = hueToRgb(p, q, h);
+ b = hueToRgb(p, q, h - 1 / 3);
+ }
+
+ return [ r * 255, g * 255, b * 255 ];
+}
+
+function addToCSSVars(cssVars, value, varName, ...selectors) {
+ for (let selector of selectors) {
+ if (!cssVars[selector]) {
+ cssVars[selector] = [];
+ }
+ cssVars[selector].push(`${varName}: ${value}`);
+ }
+}
+
class Theme {
constructor(manifest) {
this.userSheetURI = null;
this.LWTStyles = {};
+ this.aboutHomeCSSVars = {};
this.cssVars = {};
this.load(manifest.theme);
this.render();
}
load(theme) {
+ // Order of sections matters here!
if (theme.images) {
this.loadImages(theme.images);
}
if (theme.colors) {
this.loadColors(theme.colors);
}
- }
-
- loadImages(images) {
- // Use a temporary element to filter the CSS values that themes can provide.
- if (WindowManager.topWindow) {
- let temp = WindowManager.topWindow.document.createElement("temp");
- for (let image of Object.getOwnPropertyNames(images)) {
- if (images[image]) {
- let cssURL = 'url("' + images[image].replace(/"/g, '\\"') + '")';
- if (image == "theme_ntp_background") {
- temp.style.background = cssURL;
- this.cssVars["--page-background"] = `${temp.style.background} !important;`;
- } else if (image == "theme_frame" || image == "headerURL") {
- this.LWTStyles.headerURL = images[image];
- }
- }
- }
+ if (theme.tints) {
+ this.loadTints(theme.tints);
+ }
+ if (theme.gradients) {
+ this.loadGradients(theme.gradients);
}
}
loadColors(colors) {
for (let color of Object.getOwnPropertyNames(colors)) {
- let val = details.theme.colors[color];
+ let val = colors[color];
let cssColor = Array.isArray(val) ?
"rgb" + (val.length > 3 ? "a" : "") + "(" + val.join(",") + ")" : val;
switch (color) {
case "accentcolor":
case "frame":
this.LWTStyles.accentcolor = cssColor;
break;
case "tab_text":
case "textcolor":
this.LWTStyles.textcolor = cssColor;
break;
}
+
+ if (kChromeThemeColorVarMap.has(color)) {
+ addToCSSVars.apply(null, [this.cssVars, cssColor].concat(kChromeThemeColorVarMap.get(color)));
+ }
+ }
+ }
+
+ loadGradients(gradients) {
+ for (let gradient of Object.getOwnPropertyNames(gradients || {})) {
+ let val = gradients[gradient];
+ if (kChromeThemeGradientsVarMap.has(gradient)) {
+ addToCSSVars.apply(null, [this.cssVars, val].concat(kChromeThemeGradientsVarMap.get(gradient)));
+ }
+ }
+ }
+
+ loadImages(images) {
+ // Use a temporary element to filter the CSS values that themes can provide.
+ if (WindowManager.topWindow) {
+ let temp = WindowManager.topWindow.document.createElement("temp");
+ for (let image of Object.getOwnPropertyNames(images)) {
+ if (!images[image]) {
+ continue;
+ }
+ let cssURL = 'url("' + images[image].replace(/"/g, '\\"') + '")';
+ if (image == "theme_ntp_background") {
+ temp.style.background = cssURL;
+ this.aboutHomeCSSVars["--page-background"] = `${temp.style.background} !important;`;
+ } else if (image == "theme_frame" || image == "headerURL") {
+ this.LWTStyles.headerURL = images[image];
+ }
+ }
+ }
+ }
+
+ loadTints(tints) {
+ for (let tint of Object.getOwnPropertyNames(tints || {})) {
+ let val = tints[tint];
+ if (kChromeThemeTintsVarMap.has(tint)) {
+ let [varName, opacity, ...selectors] = kChromeThemeTintsVarMap.get(tint);
+ let cssColor = val;
+ if (Array.isArray(val)) {
+ cssColor = hslToRgb(val);
+ if (cssColor) {
+ cssColor = `rgba(${cssColor.join(",")},${opacity})`;
+ }
+ }
+ if (cssColor) {
+ addToCSSVars.apply(null, [this.cssVars, cssColor, varName].concat(selectors));
+ }
+ }
}
}
render() {
- if (this.cssVars) {
- let aboutHomeStyles = `
+ let styles = "";
+ if (Object.getOwnPropertyNames(this.aboutHomeCSSVars).length) {
+ styles = `
@-moz-document url("about:home"),
url("chrome://browser/content/abouthome/aboutHome.xhtml") {
:root {
- --page-background: ${this.cssVars["--page-background"]}
+ --page-background: ${this.aboutHomeCSSVars["--page-background"]}
}
}`;
- let dataURL = `data:text/css,${aboutHomeStyles}`;
+ }
+ for (let selector of Object.getOwnPropertyNames(this.cssVars)) {
+ styles += `
+ ${selector} {
+ ${this.cssVars[selector].join(" !important;")} !important;
+ }`;
+ }
+ let dataURL = `data:text/css,${styles}`;
- if (this.userSheetURI) {
- styleSheetService.unregisterSheet(this.userSheetURI, styleSheetService.USER_SHEET);
- }
-
- this.userSheetURI = ioService.newURI(dataURL, null, null);
- styleSheetService.loadAndRegisterSheet(this.userSheetURI, styleSheetService.USER_SHEET);
+ if (this.userSheetURI) {
+ styleSheetService.unregisterSheet(this.userSheetURI, styleSheetService.USER_SHEET);
}
- if (this.LWTStyles.headerURL) {
- Services.obs.notifyObservers(null, "lightweight-theme-styling-update", JSON.stringify(LWTStyles));
+ this.userSheetURI = ioService.newURI(dataURL, null, null);
+ styleSheetService.loadAndRegisterSheet(this.userSheetURI, styleSheetService.USER_SHEET);
+
+ if (!this.LWTStyles.headerURL) {
+ this.LWTStyles.headerURL = kTransparentGif;
}
+ Services.obs.notifyObservers(null, "lightweight-theme-styling-update", JSON.stringify(this.LWTStyles));
}
shutdown() {
if (this.userSheetURI) {
styleSheetService.unregisterSheet(this.userSheetURI, styleSheetService.USER_SHEET);
}
- if (this.LWTStyles.headerURL) {
- Services.obs.notifyObservers(null, "lightweight-theme-styling-update", null);
- }
+ Services.obs.notifyObservers(null, "lightweight-theme-styling-update", null);
}
}
/* eslint-disable mozilla/balanced-listeners */
extensions.on("manifest_theme", (type, directive, extension, manifest) => {
themeMap.set(extension, new Theme(manifest));
});
--- a/browser/components/extensions/schemas/theme.json
+++ b/browser/components/extensions/schemas/theme.json
@@ -31,33 +31,101 @@
"colors": {
"type": "object",
"optional": true,
"properties": {
"accentcolor": {
"type": "string",
"optional": true
},
+ "background_tab": {
+ "type": "array",
+ "optional": true
+ },
+ "background_tab_text": {
+ "type": "array",
+ "optional": true
+ },
+ "button_background": {
+ "type": "array",
+ "optional": true
+ },
"frame": {
"type": "array",
"items": {
"type": "integer"
},
"optional": true
},
+ "frame_inactive": {
+ "type": "array",
+ "optional": true
+ },
"tab_text": {
"type": "array",
"items": {
"type": "integer"
},
"optional": true
},
"textcolor": {
"type": "string",
"optional": true
+ },
+ "toolbar": {
+ "type": "array",
+ "optional": true
+ },
+ "toolbar_bottom_separator": {
+ "type": "array",
+ "optional": true
+ },
+ "toolbar_button_stroke": {
+ "type": "array",
+ "optional": true
+ },
+ "toolbar_button_stroke_inactive": {
+ "type": "array",
+ "optional": true
+ },
+ "toolbar_top_separator": {
+ "type": "array",
+ "optional": true
+ },
+ "toolbar_vertical_separator": {
+ "type": "array",
+ "optional": true
+ }
+ }
+ },
+ "gradients": {
+ "type": "object",
+ "optional": true,
+ "properties": {
+ "toolbar_button": {
+ "type": "string",
+ "optional": true
+ },
+ "toolbar_button_pressed": {
+ "type": "string",
+ "optional": true
+ }
+ }
+ },
+ "tints": {
+ "type": "object",
+ "optional": true,
+ "properties": {
+ "background_tab": {
+ "type": "array",
+ "optional": true
+ },
+ "buttons": {
+ "type": "array",
+ "optional": true
}
}
}
}
},
{
"$extend": "WebExtensionManifest",
"properties": {
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -82,16 +82,17 @@ tags = webextensions
[browser_ext_tabs_query.js]
[browser_ext_tabs_reload.js]
[browser_ext_tabs_reload_bypass_cache.js]
[browser_ext_tabs_sendMessage.js]
[browser_ext_tabs_update.js]
[browser_ext_tabs_zoom.js]
[browser_ext_tabs_update_url.js]
[browser_ext_theme_abouthomebackground.js]
+[browser_ext_theme_chromeThemeSupport.js]
[browser_ext_theme_lwtsupport.js]
[browser_ext_topwindowid.js]
[browser_ext_webNavigation_frameId0.js]
[browser_ext_webNavigation_getFrames.js]
[browser_ext_webNavigation_urlbar_transitions.js]
[browser_ext_windows.js]
[browser_ext_windows_create.js]
tags = fullscreen
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_theme_chromeThemeSupport.js
@@ -0,0 +1,68 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const inIDOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+
+add_task(function* testChromeThemeProperties() {
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home", true);
+
+ let manifest = {
+ manifest: {
+ "theme": {
+ "colors": {
+ // Color values copied from devedition.inc.css.
+ "background_tab": [39, 43, 53], // #272b35
+ "background_tab_text": [245, 247, 250], // #F5F7FA;
+ "button_background": [255, 255, 255], // #ffffff
+ "frame": [39, 43, 53], // #272b35
+ "frame_inactive": [57, 63, 76], // #393F4C
+ "tab_text": [245, 247, 250], // #f5f7fa
+ "toolbar": [39, 43, 53], // #272b35
+ "toolbar_bottom_separator": [0, 0, 0, .2], // rgba(0,0,0,.2)
+ "toolbar_button_stroke": [25, 33, 38, .8], // rgba(25,33,38,.8)
+ "toolbar_button_stroke_inactive": [25, 33, 38, .6], // rgba(25,33,38,.6)
+ "toolbar_top_separator": [0, 0, 0, .2], // rgba(0,0,0,.2)
+ "toolbar_vertical_separator": [95, 102, 112], // #5F6670
+ },
+ "tints": {
+ "background_tab": [.56, .18, .04], // #07090a
+ "buttons": [.64, .56, .22], // rgb(25,33,38)
+ },
+ "gradients": {
+ "toolbar_button": "linear-gradient(rgba(25, 33, 38, 0.6), rgba(25, 33, 38, 0.6))",
+ "toolbar_button_pressed": "linear-gradient(rgba(25, 33, 38, 1), rgba(25, 33, 38, 1))"
+ }
+ }
+ }
+ };
+ let extension = ExtensionTestUtils.loadExtension(manifest);
+
+ yield extension.startup();
+
+ let document = tab.ownerDocument;
+ let window = document.defaultView;
+
+ let theme = manifest.manifest.theme;
+ let style = window.getComputedStyle(document.documentElement);
+ Assert.ok(style.backgroundImage, "Expected background image");
+ Assert.equal(style.backgroundColor, "rgb(" + theme.colors.frame.join(", ") + ")",
+ "Expected correct background color");
+ Assert.equal(style.color, "rgb(" + theme.colors.tab_text.join(", ") + ")",
+ "Expected correct text color");
+
+// yield new Promise(resolve => window.setTimeout(resolve, 20000));
+
+ // Pick a button to test colors.
+ let button = document.getElementById("PanelUI-menu-button");
+ inIDOMUtils.addPseudoClassLock(button, ":hover");
+ style = window.getComputedStyle(button);
+ dump("style: " + style.borderColor + ", " + style.backgroundImage + "\n");
+ Assert.equal(style.backgroundImage, theme.gradients.toolbar_button,
+ "Expected a gradient background")
+ inIDOMUtils.clearPseudoClassLocks(button);
+
+ yield extension.unload();
+
+ yield BrowserTestUtils.removeTab(tab);
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_theme_lwtsupport.js
@@ -0,0 +1,98 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const kBackground = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+
+function hexToRGB(hex) {
+ hex = parseInt((hex.indexOf("#") > -1 ? hex.substring(1) : hex), 16);
+ return [ hex >> 16, (hex & 0x00FF00) >> 8, (hex & 0x0000FF) ];
+}
+
+add_task(function* testLWTSupportNewProperties() {
+ const kFrameColor = [71, 105, 91];
+ const kTabTextColor = [207, 221, 192, .9];
+
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home", true);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "theme": {
+ "images": {
+ "theme_frame": kBackground
+ },
+ "colors": {
+ "frame": kFrameColor,
+ "tab_text": kTabTextColor
+ }
+ }
+ }
+ });
+
+ yield extension.startup();
+
+ let win = tab.ownerDocument.defaultView;
+ let docEl = tab.ownerDocument.documentElement;
+ let style = win.getComputedStyle(docEl);
+
+ Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set");
+ Assert.equal(docEl.getAttribute("lwthemetextcolor"), "bright",
+ "LWT text color attribute should be set");
+
+ Assert.equal(style.backgroundImage, 'url("' + kBackground.replace(/"/g, '\\"') + '")',
+ "Expected background image");
+ Assert.equal(style.backgroundColor, "rgb(" + kFrameColor.join(", ") + ")",
+ "Expected correct background color");
+ Assert.equal(style.color, "rgba(" + kTabTextColor.join(", ") + ")",
+ "Expected correct text color");
+
+ yield extension.unload();
+
+ Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
+
+ yield BrowserTestUtils.removeTab(tab);
+});
+
+add_task(function* testLWTSupportLegacyProperties() {
+ const kAccentColor = "#a14040";
+ const kTextColor = "#fac96e";
+
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home", true);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "theme": {
+ "images": {
+ "headerURL": kBackground
+ },
+ "colors": {
+ "accentcolor": kAccentColor,
+ "textcolor": kTextColor
+ }
+ }
+ }
+ });
+
+ yield extension.startup();
+
+ let win = tab.ownerDocument.defaultView;
+ let docEl = tab.ownerDocument.documentElement;
+ let style = win.getComputedStyle(docEl);
+
+ Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set");
+ Assert.equal(docEl.getAttribute("lwthemetextcolor"), "bright",
+ "LWT text color attribute should be set");
+
+ Assert.equal(style.backgroundImage, 'url("' + kBackground.replace(/"/g, '\\"') + '")',
+ "Expected background image");
+ Assert.equal(style.backgroundColor, "rgb(" + hexToRGB(kAccentColor).join(", ") + ")",
+ "Expected correct background color");
+ Assert.equal(style.color, "rgb(" + hexToRGB(kTextColor).join(", ") + ")",
+ "Expected correct text color");
+
+ yield extension.unload();
+
+ Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
+
+ yield BrowserTestUtils.removeTab(tab);
+});