Bug 1313325 - support many more theme properties and browser overrides to support more Chrome themes, video files as background for about:home and about:newtab and much more. r?jaws draft
authorMike de Boer <mdeboer@mozilla.com>
Mon, 14 Nov 2016 16:07:37 +0100
changeset 438440 dc61d12d971e86e9fc349d0e14dffda0be2390d6
parent 438399 b32d6eefe4a871d5966bbe765b1f77eaed788ef1
child 536915 424b8f9a8e0dd7d8545077f20dfe899cbd03c218
push id35720
push usermdeboer@mozilla.com
push dateMon, 14 Nov 2016 15:08:13 +0000
reviewersjaws
bugs1313325
milestone52.0a1
Bug 1313325 - support many more theme properties and browser overrides to support more Chrome themes, video files as background for about:home and about:newtab and much more. r?jaws MozReview-Commit-ID: 5A7585ONI7N
browser/base/content/abouthome/aboutHome.css
browser/base/content/newtab/newTab.css
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_extreme.js
browser/themes/shared/newtab/newTab.inc.css
toolkit/modules/LightweightThemeConsumer.jsm
--- a/browser/base/content/abouthome/aboutHome.css
+++ b/browser/base/content/abouthome/aboutHome.css
@@ -1,23 +1,36 @@
 %if 0
 /* 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/. */
 %endif
 
 :root {
-  --page-background: hsl(0,0%,95%);
+  --page-background-color: hsl(0,0%,95%);
+  --page-background-image: none;
+  --page-background-position: inherit;
+  --page-background-repeat: no-repeat;
+  --text-color: #000;
+  --launchbutton-text-color: #525c66;
+  --link-color: -moz-nativehyperlinktext;
+  --link-hover-color: -moz-nativehyperlinktext;
+  --section-color: #3c3c3c;
+  --section-link-color: inherit;
+  --section-link-hover-color: inherit;
 }
 
 html {
+  color: var(--text-color);
+  background-color: var(--page-background-color);
+  background-image: var(--page-background-image);
+  background-position: var(--page-background-position);
+  background-repeat: var(--page-background-repeat);
   font: message-box;
   font-size: 100%;
-  background: var(--page-background);
-  color: #000;
   height: 100%;
 }
 
 body {
   margin: 0;
   display: -moz-box;
   -moz-box-orient: vertical;
   width: 100%;
@@ -26,20 +39,24 @@ body {
 
 input,
 button {
   font-size: inherit;
   font-family: inherit;
 }
 
 a {
-  color: -moz-nativehyperlinktext;
+  color: var(--link-color);
   text-decoration: none;
 }
 
+a:hover {
+  color: var(--link-hover-color);
+}
+
 .spacer {
   -moz-box-flex: 1;
 }
 
 #topSection {
   text-align: center;
 }
 
@@ -198,26 +215,34 @@ a {
 #defaultSnippet2 {
   background-image: url("chrome://browser/content/abouthome/snippet2.png");
 }
 
 #snippets {
   display: inline-block;
   text-align: start;
   margin: 12px 0;
-  color: #3c3c3c;
+  color: var(--section-color);
   font-size: 75%;
   /* 12px is the computed font size, 15px the computed line height of the snippets
      with Segoe UI on a default Windows 7 setup. The 15/12 multiplier approximately
      converts em from units of font-size to units of line-height. The goal is to
      preset the height of a three-line snippet to avoid visual moving/flickering as
      the snippets load. */
   min-height: calc(15/12 * 3em);
 }
 
+#snippets a {
+  color: var(--section-link-color);
+}
+
+#snippets a:hover {
+  color: var(--section-link-hover-color);
+}
+
 #launcher {
   display: -moz-box;
   -moz-box-align: center;
   -moz-box-pack: center;
   width: 100%;
   background-color: hsla(0,0%,0%,.03);
   border-top: 1px solid hsla(0,0%,0%,.03);
   box-shadow: 0 1px 2px hsla(0,0%,0%,.02) inset,
@@ -239,17 +264,17 @@ body[narrow] #launcher[session] {
   min-width: 88px;
   max-width: 176px;
   max-height: 85px;
   vertical-align: top;
   white-space: normal;
   background: transparent padding-box;
   border: 1px solid transparent;
   border-radius: 2px;
-  color: #525c66;
+  color: var(--launchbutton-text-color);
   font-size: 75%;
   cursor: pointer;
   transition-property: background-color, border-color, box-shadow;
   transition-duration: 150ms;
 }
 
 body[narrow] #launcher[session] > .launchButton {
   margin: 4px 1px;
--- a/browser/base/content/newtab/newTab.css
+++ b/browser/base/content/newtab/newTab.css
@@ -1,24 +1,37 @@
 /* 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/. */
 
+:root {
+  --page-background-color: #F9F9F9;
+  --page-background-image: none;
+  --page-background-position: inherit;
+  --page-background-repeat: no-repeat;
+  --text-color: message-box;
+  --section-color: #F2F2F2;
+}
+
 html {
   width: 100%;
   height: 100%;
 }
 
 body {
   font: message-box;
+  color: var(--text-color);
   width: 100%;
   height: 100%;
   padding: 0;
   margin: 0;
-  background-color: #F9F9F9;
+  background-color: var(--page-background-color);
+  background-image: var(--page-background-image);
+  background-position: var(--page-background-position);
+  background-repeat: var(--page-background-repeat);
   display: -moz-box;
   position: relative;
   -moz-box-flex: 1;
   -moz-user-focus: normal;
   -moz-box-orient: vertical;
 }
 
 input {
@@ -177,17 +190,17 @@ input[type=button] {
 .newtab-sponsored,
 .newtab-title {
   bottom: 0;
   white-space: nowrap;
   text-overflow: ellipsis;
   font-size: 13px;
   line-height: 30px;
   vertical-align: middle;
-  background-color: #F2F2F2;
+  background-color: var(--section-color);
 }
 
 .newtab-suggested {
   border: 1px solid transparent;
   border-radius: 2px;
   font-size: 12px;
   height: 17px;
   line-height: 17px;
--- a/browser/components/extensions/ext-theme.js
+++ b/browser/components/extensions/ext-theme.js
@@ -17,17 +17,28 @@ let ioService = Components.classes["@moz
 const kIsMac = AppConstants.platform == "macosx";
 const kChromeThemeColorsVarMap = new Map([
   ["background_tab", ["--tab-selection-background-color", ":root"]],
   ["background_tab_inactive", ["--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"]],
+  ["frame_text", "--chrome-color"],
+  ["ntp_background", ["--page-background-color", ":root"]],
+  ["ntp_text", ["--text-color", ":root"]],
+  ["ntp_link", ["--link-color", ":root"]],
+  ["ntp_link_underline", ["--link-hover-color", ":root"]],
+  ["ntp_header", ["--launchbutton-text-color", ":root"]],
+  ["ntp_section", ["--snippet-color", ":root"]],
+  ["ntp_section_text", ["--snippet-color", ":root"]],
+  ["ntp_section_link", ["--snippet-link-color", ":root"]],
+  ["ntp_section_link_underline", ["--snippet-link-hover-color", ":root"]],
   ["tab_text", ["--tab-selection-color", ":root"]],
+  ["tab_background_text", ["--tabs-toolbar-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_input_control", ["--url-and-searchbar-background-color", ":root"]],
   ["toolbar_input_control_stroke", ["--chrome-nav-bar-controls-border-color", ":root"]],
   ["toolbar_top_separator", ["--chrome-navigator-toolbox-separator-color", ":root"]],
   ["toolbar_vertical_separator", ["--urlbar-separator-color", ":root"]],
@@ -36,16 +47,19 @@ 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 kThemePropertiesVarMap = new Map([
+  ["ntp_background_alignment", ["--page-background-position", ":root"]],
+  ["ntp_background_repeat", ["--page-background-repeat", ":root"]],
+  ["ntp_background_tiling", ["--page-background-repeat", ":root"]],
   ["square_backbutton", ["--backbutton-urlbar-overlap", ":root"]],
   ["space_above_tabbar", ["--space-above-tabbar", ":root"]],
   ["square_tabs", ["--tab-curve-width", "#TabsToolbar"]],
   ["toolbar_button_shadow", ["--toolbarbutton-active-boxshadow", ":root"]],
   ["toolbar_button_shadow_inactive", ["--toolbarbutton-hover-boxshadow", ":root"]],
   ["toolbar_navbar_overlap", ["--tab-toolbar-navbar-overlap", ":root"]],
   ["toolbar_navbar_highlight_overlap", ["--navbar-tab-toolbar-highlight-overlap", ":root"]],
   ["toolbar_text_shadow", ["--toolbarbutton-text-shadow", ":root"]],
@@ -77,17 +91,17 @@ const kBrowserOverrideColorsVarMap = new
   ["frame_inactive", [
     ["#navigator-toolbox > toolbar:not(#TabsToolbar):not(#toolbar-menubar), .browserContainer > findbar, #browser-bottombox",
       {"background-color": "--chrome-secondary-background-color", "background-image": "none"}],
   ]],
   ["space_above_tabbar", [
     ["#main-window[tabsintitlebar][customizing]", {"--space-above-tabbar": "--space-above-tabbar"}],
   ]],
   ["tab_text", [
-    [":root", {"--chrome-color": "--tab-selection-color"}],
+    [":root", {"--chrome-color": "--tabs-toolbar-color"}],
     [".tabbrowser-tab[visuallyselected]", {"color": "--tab-selection-color"}],
     ["#tabbrowser-tabs, #TabsToolbar, #browser-panel", {"color": "--chrome-color"}],
     ["#navigator-toolbox > toolbar:not(#TabsToolbar):not(#toolbar-menubar), .browserContainer > findbar, #browser-bottombox",
       {"color": "--chrome-color"}],
     ["#navigator-toolbox .toolbarbutton-1, .browserContainer > findbar .findbar-button, #PlacesToolbar toolbarbutton.bookmark-item",
       {"color": "--chrome-color"}],
     // Using toolbar[brighttext] instead of important to override linux:
     ["toolbar[brighttext] #downloads-indicator-counter", {"color": "--chrome-color"}],
@@ -99,16 +113,26 @@ const kBrowserOverrideColorsVarMap = new
       "background-image": "none",
       "color": "inherit",
       "border": "1px solid",
       "border-color": "--chrome-nav-bar-controls-border-color",
       "box-shadow": "none",
     }],
   ]],
 ]);
+const kBrowserOverrideImagesVarMap = new Map([
+  ["theme_toolbar", [
+    ["#navigator-toolbox > toolbar:not(#TabsToolbar):-moz-lwtheme",
+      {"background-image": "--toolbar-background-image"}],
+    ["#navigator-toolbox > toolbar:not(#toolbar-menubar):not(#TabsToolbar)",
+      {"background-image": "--toolbar-background-image"}],
+    ["#navigator-toolbox > toolbar:not(#toolbar-menubar):not(#TabsToolbar):not(#addon-bar):-moz-lwtheme",
+      {"background-image": "--toolbar-background-image"}],
+  ]],
+]);
 const kBrowserOverrideTintsVarMap = new Map([
   ["background_tab", [
     [".tabbrowser-arrowscrollbox > .scrollbutton-down:not([disabled]):hover, " +
      ".tabbrowser-arrowscrollbox > .scrollbutton-up:not([disabled]):hover, .tabbrowser-tab:hover:not([visuallyselected])",
       {"background-color": "--tab-hover-background-color"}],
     [".tabs-newtab-button:hover", {"background-color": "--tab-hover-background-color", "background-image": "none"}],
   ]],
 ]);
@@ -125,16 +149,17 @@ const kBrowserOverridePropertiesVarMap =
   ]],
   ["space_above_tabbar", [
     // Give some space to drag the window around while customizing (normal space
     // to left and right of tabs doesn't work in this case)
     ["#main-window[tabsintitlebar][customizing]", {"--space-above-tabbar": "9px"}],
   ]],
 ]);
 const kTransparentGif = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
+const kVideoElementID = "__WebExtThemeVideoElement__";
 
 function hueToRgb(p, q, t) {
   if (t < 0) {
     t += 1;
   }
   if (t > 1) {
     t -= 1;
   }
@@ -199,23 +224,80 @@ function addToBrowserOverrides(browserSt
           value = value.replace(/\$\{val(:?ue)?\}/g, propValue);
         }
       }
       browserStyleOverrides[selector].push(`${style}: ${value}`);
     }
   }
 }
 
+var gContentScriptLoaded = false;
+var gContentScript = `function() {
+  var Theme = {
+    init() {
+      addEventListener("DOMContentLoaded", this);
+      addEventListener("pagehide", this);
+      addEventListener("pageshow", this);
+      addMessageListener("Ext:Theme:Render", this);
+    },
+
+    handleEvent(event) {
+      if (event.target != content.document) {
+        return;
+      }
+      let uri = content.document.documentURI;
+      if (uri != "about:home" && uri != "about:newtab") {
+        return;
+      }
+
+      switch (event.type) {
+        case "DOMContentLoaded":
+          sendAsyncMessage("Ext:Theme:RequestRender", {});
+          break;
+        case "pagehide":
+          if (this.video && !this.video.paused)
+            this.video.pause();
+          break;
+        case "pageshow":
+          if (this.video && this.video.paused)
+            this.video.play();
+          break;
+      }
+    },
+
+    receiveMessage(message) {
+      if (!message.data.video)
+        return;
+
+      if (this.video) {
+        try {
+          this.video.remove();
+        } catch (ex) {}
+      }
+
+      let document = content.document;
+      this.video = document.createElement("video");
+      this.video.autoplay = this.video.loop = this.video.muted = true;
+      document.mozSetImageElement("${kVideoElementID}", this.video);
+      this.video.src = message.data.video;
+    }
+  };
+  Theme.init();
+}`;
+
 class Theme {
-  constructor(manifest) {
+  constructor(manifest, baseURI) {
+    this.loadContentScript();
+    this.baseURI = baseURI;
     this.userSheetURI = null;
     this.LWTStyles = {};
     this.aboutHomeCSSVars = {};
     this.browserStyleOverrides = {};
     this.cssVars = {};
+    this.ntp_video = null;
     this.load(manifest.theme);
     this.render();
   }
 
   load(theme) {
     // Order of sections matters here!
     if (theme.images) {
       this.loadImages(theme.images);
@@ -236,77 +318,113 @@ class Theme {
 
   loadColors(colors) {
     for (let color of Object.getOwnPropertyNames(colors)) {
       let val = colors[color];
       // Since values are optional, they may be `null`.
       if (val === null) {
         continue;
       }
+      let cssVarMap = this.cssVars;
+      if (color.startsWith("ntp_")) {
+        cssVarMap = this.aboutHomeCSSVars;
+      }
+
       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 (kChromeThemeColorsVarMap.has(color)) {
-        addToCSSVars.apply(null, [this.cssVars, cssColor].concat(kChromeThemeColorsVarMap.get(color)));
+        addToCSSVars.apply(null, [cssVarMap, cssColor].concat(kChromeThemeColorsVarMap.get(color)));
       }
       if (kBrowserOverrideColorsVarMap.has(color)) {
         addToBrowserOverrides(this.browserStyleOverrides, cssColor, kBrowserOverrideColorsVarMap.get(color));
       }
     }
   }
 
+  loadContentScript() {
+    if (!gContentScriptLoaded) {
+      gContentScriptLoaded = true;
+      Services.mm.loadFrameScript("data:,(" + gContentScript + ")();", true);
+    }
+    Services.mm.addMessageListener("Ext:Theme:RequestRender", this);
+  }
+
   loadGradients(gradients) {
     for (let gradient of Object.getOwnPropertyNames(gradients || {})) {
       let val = gradients[gradient];
       // Since values are optional, they may be `null`.
       if (val === null) {
         continue;
       }
       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;
+    if (!WindowManager.topWindow) {
+      return;
+    }
+
+    let temp = WindowManager.topWindow.document.createElement("temp");
+    for (let image of Object.getOwnPropertyNames(images)) {
+      let val = images[image];
+      if (!val) {
+        continue;
+      }
+      val = this.baseURI.resolve(val);
+      let cssURL = 'url("' + val.replace(/"/g, '\\"') + '")';
+      if (image == "theme_ntp_background") {
+        if (val.endsWith(".ogv") || val.endsWith(".mp4") || val.endsWith(".webm")) {
+          this.ntp_video = val;
+          addToCSSVars(this.aboutHomeCSSVars, `-moz-element(#${kVideoElementID})`,
+            "--page-background-image", ":root");
+        } else {
+          temp.style.backgroundImage = cssURL;
+          addToCSSVars(this.aboutHomeCSSVars, `${temp.style.backgroundImage}`,
+            "--page-background-image", ":root");
         }
-        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];
-        }
+      } else if (image == "theme_frame" || image == "headerURL") {
+        this.LWTStyles.headerURL = val;
+      } else if (image == "theme_toolbar") {
+        temp.style.backgroundImage = cssURL;
+        addToCSSVars(this.cssVars, `${temp.style.backgroundImage}`,
+          "--toolbar-background-image", ":root");
+      }
+      if (kBrowserOverrideImagesVarMap.has(image)) {
+        addToBrowserOverrides(this.browserStyleOverrides, cssURL, kBrowserOverrideImagesVarMap.get(image));
       }
     }
   }
 
   loadProperties(properties) {
     for (let property of Object.getOwnPropertyNames(properties || {})) {
       let val = properties[property];
       // Since values are optional, they may be `null`.
       if (val === null) {
         continue;
       }
+      let cssVarMap = this.cssVars;
+      if (property.startsWith("ntp_")) {
+        cssVarMap = this.aboutHomeCSSVars;
+      }
+
       switch (property) {
         case "space_above_tabbar":
           if (val === 0 && kIsMac) {
             addToBrowserOverrides(this.browserStyleOverrides, val + "px", [
               // Include extra space on left/right for dragging since there is no
               // space above the tabs.
               ["#main-window[tabsintitlebar] #TabsToolbar", {
                 "padding-left": "50px",
@@ -401,17 +519,17 @@ class Theme {
         case "tab_curve_half_width":
         case "toolbar_navbar_highlight_overlap":
         case "toolbar_navbar_overlap":
           val += "px";
           break;
       }
       if (typeof val != "boolean") {
         if (kThemePropertiesVarMap.has(property)) {
-          addToCSSVars.apply(null, [this.cssVars, val].concat(kThemePropertiesVarMap.get(property)));
+          addToCSSVars.apply(null, [cssVarMap, val].concat(kThemePropertiesVarMap.get(property)));
         }
         if (kBrowserOverridePropertiesVarMap.has(property)) {
           addToBrowserOverrides(this.browserStyleOverrides, val, kBrowserOverridePropertiesVarMap.get(property));
         }
       }
     }
   }
 
@@ -436,27 +554,50 @@ class Theme {
           if (kBrowserOverrideTintsVarMap.has(tint)) {
             addToBrowserOverrides(this.browserStyleOverrides, cssColor, kBrowserOverrideTintsVarMap.get(tint));
           }
         }
       }
     }
   }
 
-  render() {
-    let browserStyles = [];
-    if (Object.getOwnPropertyNames(this.aboutHomeCSSVars).length) {
+  receiveMessage(message) {
+    if (message.name != "Ext:Theme:RequestRender") {
+      return;
+    }
+
+    if (this.ntp_video) {
+      Services.mm.broadcastAsyncMessage("Ext:Theme:Render", {
+        video: this.ntp_video,
+      });
+    }
+  }
+
+  render({asUpdate} = {asUpdate: false}) {
+    let browserStyles = [`
+      @-moz-document url("about:home"),
+                     url("about:newtab"),
+                     url("chrome://browser/content/abouthome/aboutHome.xhtml"),
+                     url("chrome://browser/content/newtab/newTab.xhtml") {`];
+    for (let selector of Object.getOwnPropertyNames(this.aboutHomeCSSVars)) {
       browserStyles.push(`
-        @-moz-document url("about:home"),
-                       url("chrome://browser/content/abouthome/aboutHome.xhtml") {
-          :root {
-            --page-background: ${this.aboutHomeCSSVars["--page-background"]}
-          }
+        ${selector} {
+          ${this.aboutHomeCSSVars[selector].join(" !important;")} !important;
         }`);
     }
+    if (this.ntp_video) {
+      browserStyles.push(`
+        body {
+          background-size: 100% !important;
+        }`);
+    }
+    // Close the '@-moz-document' block.
+    browserStyles.push(`
+      }`);
+
     for (let selector of Object.getOwnPropertyNames(this.cssVars)) {
       browserStyles.push(`
         ${selector} {
           ${this.cssVars[selector].join(" !important;")} !important;
         }`);
     }
     for (let selector of Object.getOwnPropertyNames(this.browserStyleOverrides)) {
       let styles = this.browserStyleOverrides[selector];
@@ -465,40 +606,43 @@ class Theme {
       }
       browserStyles.push(`
         ${selector} {
           ${styles.join(" !important;")} !important;
         }`);
     }
     let dataURL = `data:text/css,${browserStyles.join("")}`;
 
-    if (this.userSheetURI) {
-      styleSheetService.unregisterSheet(this.userSheetURI, styleSheetService.USER_SHEET);
+    let whichSheet = asUpdate ? "userSheetUpdateURI" : "userSheetURI";
+    if (this[whichSheet]) {
+      styleSheetService.unregisterSheet(this[whichSheet], styleSheetService.USER_SHEET);
     }
-
-    this.userSheetURI = ioService.newURI(dataURL.replace(/#/g, "%23"), null, null);
-    styleSheetService.loadAndRegisterSheet(this.userSheetURI, styleSheetService.USER_SHEET);
+    this[whichSheet] = ioService.newURI(dataURL.replace(/#/g, "%23"), null, null);
+    styleSheetService.loadAndRegisterSheet(this[whichSheet], 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.userSheetUpdateURI) {
+      styleSheetService.unregisterSheet(this.userSheetUpdateURI, styleSheetService.USER_SHEET);
+    }
     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));
+  themeMap.set(extension, new Theme(manifest, extension.baseURI));
 });
 
 extensions.on("shutdown", (type, extension) => {
   if (themeMap.has(extension)) {
     themeMap.get(extension).shutdown();
     themeMap.delete(extension);
   }
 });
@@ -506,14 +650,14 @@ extensions.on("shutdown", (type, extensi
 
 extensions.registerSchemaAPI("theme", "addon_parent", context => {
   let {extension} = context;
   return {
     theme: {
       update(details) {
         let theme = themeMap.get(extension);
         theme.load(details);
-        theme.render();
+        theme.render({asUpdate: true});
         return Promise.resolve();
       },
     },
   };
 });
--- a/browser/components/extensions/schemas/theme.json
+++ b/browser/components/extensions/schemas/theme.json
@@ -20,16 +20,20 @@
               },
               "theme_frame": {
                 "type": "string",
                 "optional": true
               },
               "theme_ntp_background": {
                 "type": "string",
                 "optional": true
+              },
+              "theme_toolbar": {
+                "type": "string",
+                "optional": true
               }
             }
           },
           "colors": {
             "type": "object",
             "optional": true,
             "properties": {
               "accentcolor": {
@@ -52,31 +56,112 @@
                },
               "background_tab_text": {
                 "type": "array",
                 "items": {
                   "type": "number"
                 },
                 "optional": true
               },
+              "bookmark_text": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "optional": true
+              },
               "button_background": {
                 "type": "array",
                 "items": {
                   "type": "number"
                 },
                 "optional": true
               },
               "frame": {
+                "type": "any",
+                "optional": true
+              },
+              "frame_inactive": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "optional": true
+              },
+              "ntp_background": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "optional": true
+              },
+              "ntp_background": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "optional": true
+              },
+              "ntp_header": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "optional": true
+              },
+              "ntp_link": {
                 "type": "array",
                 "items": {
                   "type": "number"
                 },
                 "optional": true
               },
-              "frame_inactive": {
+              "ntp_link_underline": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "optional": true
+              },
+              "ntp_section": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "optional": true
+              },
+              "ntp_section_link": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "optional": true
+              },
+              "ntp_section_link_underline": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "optional": true
+              },
+              "ntp_section_text": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "optional": true
+              },
+              "ntp_text": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "optional": true
+              },
+              "tab_background_text": {
                 "type": "array",
                 "items": {
                   "type": "number"
                 },
                 "optional": true
               },
               "tab_text": {
                 "type": "array",
@@ -164,16 +249,31 @@
           "properties": {
             "type": "object",
             "optional": true,
             "properties": {
               "navbar_padding": {
                 "type": "number",
                 "optional": true
               },
+              "ntp_background_alignment": {
+                "type": "string",
+                "enum": ["bottom", "center", "left", "right", "top"],
+                "optional": true
+              },
+              "ntp_background_repeat": {
+                "type": "string",
+                "enum": ["no-repeat", "repeat", "repeat-x", "repeat-y"],
+                "optional": true
+              },
+              "ntp_background_tiling": {
+                "type": "string",
+                "enum": ["no-repeat", "repeat", "repeat-x", "repeat-y"],
+                "optional": true
+              },
               "space_above_tabbar": {
                 "type": "number",
                 "optional": true
               },
               "square_backbutton": {
                 "type": "boolean",
                 "optional": true
               },
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -91,16 +91,17 @@ tags = webextensions
 [browser_ext_tabs_sendMessage.js]
 [browser_ext_tabs_cookieStoreId.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_devEdition.js]
+[browser_ext_theme_extreme.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_allowScriptsToClose.js]
 [browser_ext_windows_create.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_theme_extreme.js
@@ -0,0 +1,142 @@
+/* -*- 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* testExtremeThemeProperties() {
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home", true);
+
+  let manifest = {
+    manifest: {
+      "theme": {
+        "colors": {
+          "background_tab": [76, 158, 217], // #4c9ed9
+          "background_tab_inactive": [39, 43, 53], // #272b35
+          "background_tab_text": [245, 247, 250], // #F5F7FA
+          "button_background": [37, 44, 51], // #252C33
+          "frame": [39, 43, 53], // #272b35
+          "frame_inactive": [57, 63, 76], // #393F4C
+          "ntp_background": [255, 170, 255],
+          "ntp_text": [0, 85, 187],
+          "ntp_link": [170, 255, 255],
+          "ntp_link_underline": [0, 85, 187],
+          "ntp_header": [0, 85, 187],
+          "ntp_section": [0, 85, 187],
+          "ntp_section_text": [0, 85, 187],
+          "ntp_section_link": [0, 85, 187],
+          "ntp_section_link_underline": [0, 85, 187],
+          "tab_text": [245, 247, 250], // #f5f7fa
+          "toolbar": [39, 43, 53], // #272b35
+          "toolbar_bottom_separator": [0, 0, 0, 0.2], // rgba(0,0,0,.2)
+          "toolbar_button_stroke": [25, 33, 38, 0.8], // rgba(25,33,38,.8)
+          "toolbar_button_stroke_inactive": [25, 33, 38, 0.6], // rgba(25,33,38,.6)
+          "toolbar_input_control": [23, 27, 31], // #171B1F
+          "toolbar_input_control_stroke": [29, 35, 40], // #1D2328
+          "toolbar_top_separator": [0, 0, 0, 0.2], // rgba(0,0,0,.2)
+          "toolbar_vertical_separator": [95, 102, 112], // #5F6670
+        },
+        "tints": {
+          "background_tab": [0.56, 0.18, 0.04], // #07090a
+          "buttons": [0.64, 0.56, 0.22], // rgb(25,33,38)
+        },
+        "gradients": {
+          "toolbar_button": "rgba(25,33, 38,.6) linear-gradient(rgba(25,33,38,.6), rgba(25,33,38,.6)) padding-box",
+          "toolbar_button_pressed": "rgba(25,33,38,1) linear-gradient(rgba(25,33,38,1), rgba(25,33,38,1)) border-box",
+        },
+        "properties": {
+          "navbar_padding": 0,
+          "space_above_tabbar": 0,
+          "square_tabs": true,
+          "square_backbutton": true,
+          "toolbar_button_shadow": "none",
+          "toolbar_button_shadow_inactive": "none",
+          "toolbar_navbar_highlight_overlap": 0,
+          "toolbar_navbar_overlap": 0,
+          "toolbar_text_shadow": "none",
+        },
+      },
+    },
+    background() {
+      browser.test.onMessage.addListener((message) => {
+        if (message !== "update-theme") {
+          return;
+        }
+
+        const kGradient = ["#ffaaff linear-gradient(", "deg, #ffaaff, #aaffff)"];
+        const kTimeoutMs = 100;
+
+        let gDegrees = 135;
+
+        browser.theme.update({
+          "colors": {
+            "background_tab": [255, 255, 255, 0.6],
+            "background_tab_inactive": [255, 255, 255, 0.2],
+            "background_tab_text": [0, 85, 187],
+            "button_background": [255, 255, 255, 0.05],
+            "frame": kGradient.join(gDegrees),
+            "frame_inactive": [238, 68, 255, 0.1],
+            "tab_text": [0, 85, 187], // #0055bb
+            "toolbar": [255, 255, 255, .05],
+            "toolbar_bottom_separator": [238, 68, 255, .2], // #ee44ff
+            "toolbar_button_stroke": [238, 68, 255, .8], // #ee44ff
+            "toolbar_button_stroke_inactive": [238, 68, 255, .6], // #ee44ff
+            "toolbar_input_control": [255, 255, 255, .95],
+            "toolbar_input_control_stroke": [238, 68, 255], // #ee44ff
+            "toolbar_top_separator": [255, 255, 255, 0.2],
+            "toolbar_vertical_separator": [238, 68, 255], // #ee44ff
+          },
+          "tints": {
+            "background_tab": [0, 0, 1, 0.25],
+            "buttons": [0, 0, 1, 0.05],
+          },
+          "gradients": {
+            "toolbar_button": "none",
+            "toolbar_button_pressed": "none",
+          },
+        });
+
+        setTimeout(() => {
+          gDegrees++;
+          browser.theme.update({
+            colors: {frame: kGradient.join(gDegrees)},
+          });
+
+          browser.test.sendMessage("theme-updated");
+        }, kTimeoutMs);
+      });
+    },
+  };
+  let extension = ExtensionTestUtils.loadExtension(manifest);
+
+  yield extension.startup();
+
+  let document = tab.ownerDocument;
+  let window = document.defaultView;
+
+  let style = window.getComputedStyle(document.documentElement);
+
+  Assert.ok(style.backgroundImage, "Expected background image");
+  Assert.equal(style.color, "rgb(245, 247, 250)", "Expected correct text color");
+
+  extension.sendMessage("update-theme");
+  yield extension.awaitMessage("theme-updated");
+
+  style = window.getComputedStyle(document.documentElement);
+  // HALP! The following style fetch keeps returning the old, initial value. Why?
+  // Is this a bug?
+  // Assert.equal(style.backgroundImage, "linear-gradient(136deg, #ffaaff, #aaffff)",
+  //   "Expected correct background image");
+  Assert.equal(style.color, "rgb(0, 85, 187)", "Expected correct text color");
+
+  // Pick a button to test updated colors.
+  let button = document.getElementById("PanelUI-menu-button");
+  inIDOMUtils.addPseudoClassLock(button, ":hover");
+  style = window.getComputedStyle(button);
+  Assert.equal(style.backgroundImage, "none", "Expected explicitly empty background");
+  inIDOMUtils.clearPseudoClassLocks(button);
+
+  yield extension.unload();
+
+  yield BrowserTestUtils.removeTab(tab);
+});
--- a/browser/themes/shared/newtab/newTab.inc.css
+++ b/browser/themes/shared/newtab/newTab.inc.css
@@ -1,16 +1,18 @@
 /* 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/. */
 
 :root {
   -moz-appearance: none;
   font-size: 75%;
   background-color: transparent;
+  --section-link-color: #5c5c5c;
+  --section-link-hover-color: white;
 }
 
 /* UNDO */
 #newtab-undo-container {
   padding: 4px 3px;
   border: 1px solid;
   border-color: rgba(8,22,37,.12) rgba(8,22,37,.14) rgba(8,22,37,.16);
   background-color: rgba(255,255,255,.4);
@@ -165,30 +167,30 @@
   background-position: center center;
   background-size: auto;
 }
 
 /* TITLES */
 .newtab-sponsored,
 .newtab-title,
 .newtab-suggested  {
-  color: #5c5c5c;
+  color: var(--section-link-color);
 }
 
 .newtab-suggested[active] {
   background-color: rgba(51, 51, 51, 0.95);
   border: 0;
   color: white;
 }
 
 .newtab-site:hover .newtab-title {
-  color: white;
+  color: var(--section-link-hover-color);
   background-color: #333;
   border: 1px solid #333;
-  border-top: 1px solid white;
+  border-top: 1px solid var(--section-link-hover-color);
 }
 
 .newtab-site[pinned] .newtab-title {
   padding-inline-start: 24px;
 }
 
 .newtab-site[pinned] .newtab-title::before {
   background-image: -moz-image-rect(url("chrome://browser/skin/newtab/controls.svg"), 7, 278, 28, 266);
--- a/toolkit/modules/LightweightThemeConsumer.jsm
+++ b/toolkit/modules/LightweightThemeConsumer.jsm
@@ -111,20 +111,20 @@ LightweightThemeConsumer.prototype = {
     let root = this._doc.documentElement;
     let active = !!aData.headerURL;
     let stateChanging = (active != this._active);
 
     // We need to clear these either way: either because the theme is being removed,
     // or because we are applying a new theme and the data might be bogus CSS,
     // so if we don't reset first, it'll keep the old value.
     root.style.removeProperty("color");
-    root.style.removeProperty("background-color");
+    root.style.removeProperty("background");
     if (active) {
       root.style.color = aData.textcolor || "black";
-      root.style.backgroundColor = aData.accentcolor || "white";
+      root.style.background = aData.accentcolor || "white";
       let [r, g, b] = _parseRGB(this._doc.defaultView.getComputedStyle(root, "").color);
       let luminance = 0.2125 * r + 0.7154 * g + 0.0721 * b;
       root.setAttribute("lwthemetextcolor", luminance <= 110 ? "dark" : "bright");
       root.setAttribute("lwtheme", "true");
     } else {
       root.removeAttribute("lwthemetextcolor");
       root.removeAttribute("lwtheme");
     }