Bug 1310144 - add support for an initial mapping of Chrome theme manifest properties, including a test manifest that uses DevEdition colors. r?jaws draft
authorMike de Boer <mdeboer@mozilla.com>
Tue, 25 Oct 2016 14:51:10 +0200
changeset 429195 226c7fe30759d50a764b3482bf5456e7da9a310c
parent 429194 56035eb2cff2f2420a88f8f50bb21d64ff48c46f
child 534919 2d87991c029ad84bdadbc6c55ebc32b246ffd0b1
push id33507
push usermdeboer@mozilla.com
push dateTue, 25 Oct 2016 12:54:01 +0000
reviewersjaws
bugs1310144
milestone52.0a1
Bug 1310144 - add support for an initial mapping of Chrome theme manifest properties, including a test manifest that uses DevEdition colors. r?jaws MozReview-Commit-ID: 5a9cIdY0ZIM
browser/components/extensions/ext-theme.js
browser/components/extensions/schemas/theme.json
browser/components/extensions/test/browser/browser.ini
browser/components/extensions/test/browser/browser_ext_theme_chromeThemeSupport.js
browser/components/extensions/test/browser/browser_ext_theme_lwtsupport.js
--- 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);
+});