Bug 1064373 - Add dark theme for timeline's memory and marker overviews. r=vp
authorJordan Santell <jsantell@gmail.com>
Mon, 05 Jan 2015 14:57:00 -0500
changeset 248213 6001258c480c27faa1b48adb6498b2cf5dc6a49a
parent 248212 2537c1b51a123e400eac807b348ccd9a729ac9f1
child 248214 7b4b861a642e79c1731945d75181252b592e6b0e
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvp
bugs1064373
milestone37.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1064373 - Add dark theme for timeline's memory and marker overviews. r=vp
browser/devtools/shared/test/browser_css_color.js
browser/devtools/timeline/test/browser.ini
browser/devtools/timeline/test/browser_timeline_overview-theme.js
browser/devtools/timeline/timeline.js
browser/devtools/timeline/widgets/markers-overview.js
browser/devtools/timeline/widgets/memory-overview.js
toolkit/devtools/css-color.js
--- a/browser/devtools/shared/test/browser_css_color.js
+++ b/browser/devtools/shared/test/browser_css_color.js
@@ -48,16 +48,17 @@ function testColorUtils() {
     is(color.hex, hex, "color.hex === hex");
     is(color.hsl, hsl, "color.hsl === hsl");
     is(color.rgb, rgb, "color.rgb === rgb");
 
     testToString(color, name, hex, hsl, rgb);
     testColorMatch(name, hex, hsl, rgb, color.rgba);
   }
   testProcessCSSString();
+  testSetAlpha();
   finishUp();
 }
 
 function testToString(color, name, hex, hsl, rgb) {
   switchColorUnit(colorUtils.CssColor.COLORUNIT.name);
   is(color.toString(), name, "toString() with authored type");
 
   switchColorUnit(colorUtils.CssColor.COLORUNIT.hex);
@@ -133,16 +134,39 @@ function testProcessCSSString() {
                  "color #0F0; font-weight: bold; " +
                  "background-color: transparent; " +
                  "border-top-color: rgba(0, 0, 255, 0.5);";
   let after = colorUtils.processCSSString(before);
 
   is(after, expected, "CSS string processed correctly");
 }
 
+function testSetAlpha() {
+  let values = [
+    ["longhex", "#ff0000", 0.5, "rgba(255, 0, 0, 0.5)"],
+    ["hex", "#f0f", 0.2, "rgba(255, 0, 255, 0.2)"],
+    ["rgba", "rgba(120, 34, 23, 1)", 0.25, "rgba(120, 34, 23, 0.25)"],
+    ["rgb", "rgb(120, 34, 23)", 0.25, "rgba(120, 34, 23, 0.25)"],
+    ["hsl", "hsl(208, 100%, 97%)", 0.75, "rgba(239, 247, 255, 0.75)"],
+    ["hsla", "hsla(208, 100%, 97%, 1)", 0.75, "rgba(239, 247, 255, 0.75)"]
+  ];
+  values.forEach(([type, value, alpha, expected]) => {
+    is(colorUtils.setAlpha(value, alpha), expected, "correctly sets alpha value for " + type);
+  });
+
+  try {
+    colorUtils.setAlpha("rgb(24, 25, 45, 1)", 1);
+    ok(false, "Should fail when passing in an invalid color.");
+  } catch (e) {
+    ok(true, "Fails when setAlpha receives an invalid color.");
+  }
+
+  is(colorUtils.setAlpha("#fff"), "rgba(255, 255, 255, 1)", "sets alpha to 1 if invalid.");
+}
+
 function finishUp() {
   Services = colorUtils = Loader = null;
   gBrowser.removeCurrentTab();
   finish();
 }
 
 function getTestData() {
   return [
--- a/browser/devtools/timeline/test/browser.ini
+++ b/browser/devtools/timeline/test/browser.ini
@@ -5,15 +5,16 @@ support-files =
   head.js
 
 [browser_timeline_aaa_run_first_leaktest.js]
 [browser_timeline_blueprint.js]
 [browser_timeline_filters.js]
 [browser_timeline_overview-initial-selection-01.js]
 [browser_timeline_overview-initial-selection-02.js]
 [browser_timeline_overview-update.js]
+[browser_timeline_overview-theme.js]
 [browser_timeline_panels.js]
 [browser_timeline_recording-without-memory.js]
 [browser_timeline_recording.js]
 [browser_timeline_waterfall-background.js]
 [browser_timeline_waterfall-generic.js]
 [browser_timeline_waterfall-styles.js]
 [browser_timeline_waterfall-sidebar.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/timeline/test/browser_timeline_overview-theme.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the markers and memory overviews render with the correct
+ * theme on load, and rerenders when changed.
+ */
+
+const LIGHT_BG = "#fcfcfc";
+const DARK_BG = "#14171a";
+
+add_task(function*() {
+  let { target, panel } = yield initTimelinePanel("about:blank");
+  let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
+
+  $("#memory-checkbox").checked = true;
+
+  setTheme("dark");
+
+  yield TimelineController.updateMemoryRecording();
+  is(TimelineView.markersOverview.backgroundColor, DARK_BG,
+    "correct theme on load for markers.");
+  is(TimelineView.memoryOverview.backgroundColor, DARK_BG,
+    "correct theme on load for memory.");
+
+  yield TimelineController.toggleRecording();
+  ok(true, "Recording has started.");
+
+  yield TimelineController.toggleRecording();
+  ok(true, "Recording has ended.");
+
+  let refreshed = Promise.all([
+    once(TimelineView.markersOverview, "refresh"),
+    once(TimelineView.memoryOverview, "refresh"),
+  ]);
+
+  setTheme("light");
+  yield refreshed;
+
+  ok(true, "Both memory and markers were rerendered after theme change.");
+  is(TimelineView.markersOverview.backgroundColor, LIGHT_BG,
+    "correct theme on after toggle for markers.");
+  is(TimelineView.memoryOverview.backgroundColor, LIGHT_BG,
+    "correct theme on after toggle for memory.");
+
+  refreshed = Promise.all([
+    once(TimelineView.markersOverview, "refresh"),
+    once(TimelineView.memoryOverview, "refresh"),
+  ]);
+
+  setTheme("dark");
+  yield refreshed;
+
+  ok(true, "Both memory and markers were rerendered after theme change.");
+  is(TimelineView.markersOverview.backgroundColor, DARK_BG,
+    "correct theme on after toggle for markers once more.");
+  is(TimelineView.memoryOverview.backgroundColor, DARK_BG,
+    "correct theme on after toggle for memory once more.");
+
+  refreshed = Promise.all([
+    once(TimelineView.markersOverview, "refresh"),
+    once(TimelineView.memoryOverview, "refresh"),
+  ]);
+
+  // Set theme back to light
+  setTheme("light");
+  yield refreshed;
+});
+
+/**
+ * Mimics selecting the theme selector in the toolbox;
+ * sets the preference and emits an event on gDevTools to trigger
+ * the themeing.
+ */
+function setTheme (newTheme) {
+  let oldTheme = Services.prefs.getCharPref("devtools.theme");
+  info("Setting `devtools.theme` to \"" + newTheme + "\"");
+  Services.prefs.setCharPref("devtools.theme", newTheme);
+  gDevTools.emit("pref-changed", {
+    pref: "devtools.theme",
+    newValue: newTheme,
+    oldValue: oldTheme
+  });
+}
--- a/browser/devtools/timeline/timeline.js
+++ b/browser/devtools/timeline/timeline.js
@@ -1,18 +1,20 @@
 /* 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/. */
 "use strict";
 
 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
+Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/devtools/Loader.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+Cu.import("resource:///modules/devtools/gDevTools.jsm");
 
 devtools.lazyRequireGetter(this, "promise");
 devtools.lazyRequireGetter(this, "EventEmitter",
   "devtools/toolkit/event-emitter");
 
 devtools.lazyRequireGetter(this, "MarkersOverview",
   "devtools/timeline/markers-overview", true);
 devtools.lazyRequireGetter(this, "MemoryOverview",
@@ -93,16 +95,17 @@ let TimelineController = {
   /**
    * Initialization function, called when the tool is started.
    */
   initialize: function() {
     this._onRecordingTick = this._onRecordingTick.bind(this);
     this._onMarkers = this._onMarkers.bind(this);
     this._onMemory = this._onMemory.bind(this);
     this._onFrames = this._onFrames.bind(this);
+
     gFront.on("markers", this._onMarkers);
     gFront.on("memory", this._onMemory);
     gFront.on("frames", this._onFrames);
   },
 
   /**
    * Destruction function, called when the tool is closed.
    */
@@ -288,37 +291,44 @@ let TimelineView = {
    * Initialization function, called when the tool is started.
    */
   initialize: Task.async(function*() {
     let blueprint = this._getFilteredBluePrint();
     this.markersOverview = new MarkersOverview($("#markers-overview"), blueprint);
     this.waterfall = new Waterfall($("#timeline-waterfall"), $("#timeline-pane"), blueprint);
     this.markerDetails = new MarkerDetails($("#timeline-waterfall-details"), $("#timeline-waterfall-container > splitter"));
 
+    this._onThemeChange = this._onThemeChange.bind(this);
     this._onSelecting = this._onSelecting.bind(this);
     this._onRefresh = this._onRefresh.bind(this);
+
+    gDevTools.on("pref-changed", this._onThemeChange);
     this.markersOverview.on("selecting", this._onSelecting);
     this.markersOverview.on("refresh", this._onRefresh);
     this.markerDetails.on("resize", this._onRefresh);
 
     this._onMarkerSelected = this._onMarkerSelected.bind(this);
     this.waterfall.on("selected", this._onMarkerSelected);
     this.waterfall.on("unselected", this._onMarkerSelected);
 
+    let theme = Services.prefs.getCharPref("devtools.theme");
+    this.markersOverview.setTheme(theme);
+
     yield this.markersOverview.ready();
 
     yield this.waterfall.recalculateBounds();
 
     this._buildFilterPopup();
   }),
 
   /**
    * Destruction function, called when the tool is closed.
    */
   destroy: function() {
+    gDevTools.off("pref-changed", this._onThemeChange);
     this.markerDetails.off("resize", this._onRefresh);
     this.markerDetails.destroy();
     this.waterfall.off("selected", this._onMarkerSelected);
     this.waterfall.off("unselected", this._onMarkerSelected);
     this.waterfall.destroy();
     this.markersOverview.off("selecting", this._onSelecting);
     this.markersOverview.off("refresh", this._onRefresh);
     this.markersOverview.destroy();
@@ -328,17 +338,20 @@ let TimelineView = {
       this.memoryOverview.destroy();
     }
   },
 
   /**
    * Shows the memory overview graph.
    */
   showMemoryOverview: Task.async(function*() {
+    let theme = Services.prefs.getCharPref("devtools.theme");
+
     this.memoryOverview = new MemoryOverview($("#memory-overview"));
+    this.memoryOverview.setTheme(theme);
     yield this.memoryOverview.ready();
 
     let interval = TimelineController.getInterval();
     let memory = TimelineController.getMemory();
     this.memoryOverview.setData({ interval, memory });
 
     CanvasGraphUtils.linkAnimation(this.markersOverview, this.memoryOverview);
     CanvasGraphUtils.linkSelection(this.markersOverview, this.memoryOverview);
@@ -570,16 +583,30 @@ let TimelineView = {
       // Style used by pseudo element ::before in timeline.css.in
       let bulletStyle = `--bullet-bg: ${markerDetails.fill};`
       bulletStyle += `--bullet-border: ${markerDetails.stroke}`;
       menuitem.setAttribute("style", bulletStyle);
 
       popup.appendChild(menuitem);
     }
   },
+
+  /*
+   * Called when the developer tools theme changes. Redraws
+   * the graphs with the new theme setting.
+   */
+  _onThemeChange: function (_, theme) {
+    if (this.memoryOverview) {
+      this.memoryOverview.setTheme(theme.newValue);
+      this.memoryOverview.refresh({ force: true });
+    }
+
+    this.markersOverview.setTheme(theme.newValue);
+    this.markersOverview.refresh({ force: true });
+  }
 };
 
 /**
  * Convenient way of emitting events from the panel window.
  */
 EventEmitter.decorate(this);
 
 /**
--- a/browser/devtools/timeline/widgets/markers-overview.js
+++ b/browser/devtools/timeline/widgets/markers-overview.js
@@ -9,70 +9,65 @@
  * markers are visible in the "waterfall".
  */
 
 const {Cc, Ci, Cu, Cr} = require("chrome");
 
 Cu.import("resource:///modules/devtools/Graphs.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
+const { colorUtils: { setAlpha }} = require("devtools/css-color");
+const { getColor } = require("devtools/shared/theme");
+
 loader.lazyRequireGetter(this, "L10N",
   "devtools/timeline/global", true);
 
-const HTML_NS = "http://www.w3.org/1999/xhtml";
-
 const OVERVIEW_HEADER_HEIGHT = 14; // px
 const OVERVIEW_ROW_HEIGHT = 11; // row height
 
-const OVERVIEW_BACKGROUND_COLOR = "#fff";
-const OVERVIEW_CLIPHEAD_LINE_COLOR = "#666";
-const OVERVIEW_SELECTION_LINE_COLOR = "#555";
-const OVERVIEW_SELECTION_BACKGROUND_COLOR = "rgba(76,158,217,0.25)";
-const OVERVIEW_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
+const OVERVIEW_BODY_HEIGHT = 55; // 11px * 5 groups
+const OVERVIEW_SELECTION_LINE_COLOR = "#666";
+const OVERVIEW_CLIPHEAD_LINE_COLOR = "#555";
 
 const OVERVIEW_HEADER_TICKS_MULTIPLE = 100; // ms
 const OVERVIEW_HEADER_TICKS_SPACING_MIN = 75; // px
 const OVERVIEW_HEADER_SAFE_BOUNDS = 50; // px
-const OVERVIEW_HEADER_BACKGROUND = "#fff";
-const OVERVIEW_HEADER_TEXT_COLOR = "#18191a";
 const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9; // px
 const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
 const OVERVIEW_HEADER_TEXT_PADDING_LEFT = 6; // px
 const OVERVIEW_HEADER_TEXT_PADDING_TOP = 1; // px
-const OVERVIEW_TIMELINE_STROKES = "#ccc";
 const OVERVIEW_MARKERS_COLOR_STOPS = [0, 0.1, 0.75, 1];
 const OVERVIEW_MARKER_DURATION_MIN = 4; // ms
 const OVERVIEW_GROUP_VERTICAL_PADDING = 5; // px
-const OVERVIEW_GROUP_ALTERNATING_BACKGROUND = "rgba(0,0,0,0.05)";
 
 /**
  * An overview for the markers data.
  *
  * @param nsIDOMNode parent
  *        The parent node holding the overview.
  * @param Object blueprint
  *        List of names and colors defining markers.
  */
 function MarkersOverview(parent, blueprint, ...args) {
   AbstractCanvasGraph.apply(this, [parent, "markers-overview", ...args]);
 
+  this.setTheme();
+
   // Set the list of names, properties and colors used to paint this overview.
   this.setBlueprint(blueprint);
 
   this.once("ready", () => {
     // Populate this overview with some dummy initial data.
     this.setData({ interval: { startTime: 0, endTime: 1000 }, markers: [] });
   });
 }
 
 MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
   clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
   selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
-  selectionBackgroundColor: OVERVIEW_SELECTION_BACKGROUND_COLOR,
-  selectionStripesColor: OVERVIEW_SELECTION_STRIPES_COLOR,
   headerHeight: OVERVIEW_HEADER_HEIGHT,
   rowHeight: OVERVIEW_ROW_HEIGHT,
   groupPadding: OVERVIEW_GROUP_VERTICAL_PADDING,
 
   /**
    * Compute the height of the overview.
    */
   get fixedHeight() {
@@ -133,25 +128,25 @@ MarkersOverview.prototype = Heritage.ext
     let groupHeight = this.rowHeight * this._pixelRatio;
     let groupPadding = this.groupPadding * this._pixelRatio;
 
     let totalTime = (endTime - startTime) || 0;
     let dataScale = this.dataScaleX = availableWidth / totalTime;
 
     // Draw the header and overview background.
 
-    ctx.fillStyle = OVERVIEW_HEADER_BACKGROUND;
+    ctx.fillStyle = this.headerBackgroundColor;
     ctx.fillRect(0, 0, canvasWidth, headerHeight);
 
-    ctx.fillStyle = OVERVIEW_BACKGROUND_COLOR;
+    ctx.fillStyle = this.backgroundColor;
     ctx.fillRect(0, headerHeight, canvasWidth, canvasHeight);
 
     // Draw the alternating odd/even group backgrounds.
 
-    ctx.fillStyle = OVERVIEW_GROUP_ALTERNATING_BACKGROUND;
+    ctx.fillStyle = this.alternatingBackgroundColor;
     ctx.beginPath();
 
     for (let i = 0; i < totalGroups; i += 2) {
       let top = headerHeight + i * groupHeight;
       ctx.rect(0, top, canvasWidth, groupHeight);
     }
 
     ctx.fill();
@@ -161,18 +156,18 @@ MarkersOverview.prototype = Heritage.ext
     let fontSize = OVERVIEW_HEADER_TEXT_FONT_SIZE * this._pixelRatio;
     let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
     let textPaddingLeft = OVERVIEW_HEADER_TEXT_PADDING_LEFT * this._pixelRatio;
     let textPaddingTop = OVERVIEW_HEADER_TEXT_PADDING_TOP * this._pixelRatio;
     let tickInterval = this._findOptimalTickInterval(dataScale);
 
     ctx.textBaseline = "middle";
     ctx.font = fontSize + "px " + fontFamily;
-    ctx.fillStyle = OVERVIEW_HEADER_TEXT_COLOR;
-    ctx.strokeStyle = OVERVIEW_TIMELINE_STROKES;
+    ctx.fillStyle = this.headerTextColor;
+    ctx.strokeStyle = this.headerTimelineStrokeColor;
     ctx.beginPath();
 
     for (let x = 0; x < availableWidth; x += tickInterval) {
       let lineLeft = x;
       let textLeft = lineLeft + textPaddingLeft;
       let time = Math.round(x / dataScale);
       let label = L10N.getFormatStr("timeline.tick", time);
       ctx.fillText(label, textLeft, headerHeight / 2 + textPaddingTop);
@@ -227,12 +222,28 @@ MarkersOverview.prototype = Heritage.ext
     while (true) {
       let scaledStep = dataScale * timingStep;
       if (scaledStep < spacingMin) {
         timingStep <<= 1;
         continue;
       }
       return scaledStep;
     }
+  },
+
+  /**
+   * Sets the theme via `theme` to either "light" or "dark",
+   * and updates the internal styling to match. Requires a redraw
+   * to see the effects.
+   */
+  setTheme: function (theme) {
+    theme = theme || "light";
+    this.backgroundColor = getColor("body-background", theme);
+    this.selectionBackgroundColor = setAlpha(getColor("selection-background", theme), 0.25);
+    this.selectionStripesColor = setAlpha("#fff", 0.1);
+    this.headerBackgroundColor = getColor("body-background", theme);
+    this.headerTextColor = getColor("body-color", theme);
+    this.headerTimelineStrokeColor = setAlpha(getColor("body-color-alt", theme), 0.1);
+    this.alternatingBackgroundColor = setAlpha(getColor("body-color", theme), 0.05);
   }
 });
 
 exports.MarkersOverview = MarkersOverview;
--- a/browser/devtools/timeline/widgets/memory-overview.js
+++ b/browser/devtools/timeline/widgets/memory-overview.js
@@ -8,67 +8,56 @@
  * of all the memory measurements taken while streaming the timeline data.
  */
 
 const {Cc, Ci, Cu, Cr} = require("chrome");
 
 Cu.import("resource:///modules/devtools/Graphs.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
+const { colorUtils: { setAlpha }} = require("devtools/css-color");
+const { getColor } = require("devtools/shared/theme");
+
 loader.lazyRequireGetter(this, "L10N",
   "devtools/timeline/global", true);
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
 const OVERVIEW_HEIGHT = 30; // px
-
-const OVERVIEW_BACKGROUND_COLOR = "#fff";
-const OVERVIEW_BACKGROUND_GRADIENT_START = "rgba(0,136,204,0.1)";
-const OVERVIEW_BACKGROUND_GRADIENT_END = "rgba(0,136,204,0.0)";
 const OVERVIEW_STROKE_WIDTH = 1; // px
-const OVERVIEW_STROKE_COLOR = "rgba(0,136,204,1)";
-const OVERVIEW_CLIPHEAD_LINE_COLOR = "#666";
-const OVERVIEW_SELECTION_LINE_COLOR = "#555";
 const OVERVIEW_MAXIMUM_LINE_COLOR = "rgba(0,136,204,0.4)";
 const OVERVIEW_AVERAGE_LINE_COLOR = "rgba(0,136,204,0.7)";
 const OVERVIEW_MINIMUM_LINE_COLOR = "rgba(0,136,204,0.9)";
-
-const OVERVIEW_SELECTION_BACKGROUND_COLOR = "rgba(76,158,217,0.25)";
-const OVERVIEW_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
+const OVERVIEW_CLIPHEAD_LINE_COLOR = "#666";
+const OVERVIEW_SELECTION_LINE_COLOR = "#555";
 
 /**
  * An overview for the memory data.
  *
  * @param nsIDOMNode parent
  *        The parent node holding the overview.
  */
 function MemoryOverview(parent) {
   LineGraphWidget.call(this, parent, L10N.getStr("graphs.memory"));
-
+  this.setTheme();
   this.once("ready", () => {
     // Populate this overview with some dummy initial data.
     this.setData({ interval: { startTime: 0, endTime: 1000 }, memory: [] });
   });
 }
 
 MemoryOverview.prototype = Heritage.extend(LineGraphWidget.prototype, {
   dampenValuesFactor: 0.95,
   fixedHeight: OVERVIEW_HEIGHT,
-  backgroundColor: OVERVIEW_BACKGROUND_COLOR,
-  backgroundGradientStart: OVERVIEW_BACKGROUND_GRADIENT_START,
-  backgroundGradientEnd: OVERVIEW_BACKGROUND_GRADIENT_END,
-  strokeColor: OVERVIEW_STROKE_COLOR,
   strokeWidth: OVERVIEW_STROKE_WIDTH,
   maximumLineColor: OVERVIEW_MAXIMUM_LINE_COLOR,
   averageLineColor: OVERVIEW_AVERAGE_LINE_COLOR,
   minimumLineColor: OVERVIEW_MINIMUM_LINE_COLOR,
   clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
   selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
-  selectionBackgroundColor: OVERVIEW_SELECTION_BACKGROUND_COLOR,
-  selectionStripesColor: OVERVIEW_SELECTION_STRIPES_COLOR,
   withTooltipArrows: false,
   withFixedTooltipPositions: true,
 
   /**
    * Disables selection and empties this graph.
    */
   clearView: function() {
     this.selectionEnabled = false;
@@ -77,12 +66,27 @@ MemoryOverview.prototype = Heritage.exte
   },
 
   /**
    * Sets the data source for this graph.
    */
   setData: function({ interval, memory }) {
     this.dataOffsetX = interval.startTime;
     LineGraphWidget.prototype.setData.call(this, memory);
+  },
+
+  /**
+   * Sets the theme via `theme` to either "light" or "dark",
+   * and updates the internal styling to match. Requires a redraw
+   * to see the effects.
+   */
+  setTheme: function (theme) {
+    theme = theme || "light";
+    this.backgroundColor = getColor("body-background", theme);
+    this.backgroundGradientStart = setAlpha(getColor("highlight-blue", theme), 0.1);
+    this.backgroundGradientEnd = setAlpha(getColor("highlight-blue", theme), 0);
+    this.strokeColor = getColor("highlight-blue", theme);
+    this.selectionBackgroundColor = setAlpha(getColor("selection-background", theme), 0.25);
+    this.selectionStripesColor = "rgba(255, 255, 255, 0.1)";
   }
 });
 
 exports.MemoryOverview = MemoryOverview;
--- a/toolkit/devtools/css-color.js
+++ b/toolkit/devtools/css-color.js
@@ -74,17 +74,18 @@ const SPECIALVALUES = new Set([
 
 function CssColor(colorValue) {
   this.newColor(colorValue);
 }
 
 module.exports.colorUtils = {
   CssColor: CssColor,
   processCSSString: processCSSString,
-  rgbToHsl: rgbToHsl
+  rgbToHsl: rgbToHsl,
+  setAlpha: setAlpha
 };
 
 /**
  * Values used in COLOR_UNIT_PREF
  */
 CssColor.COLORUNIT = {
   "authored": "authored",
   "hex": "hex",
@@ -398,11 +399,40 @@ function rgbToHsl([r,g,b]) {
     if (h < 0) {
       h += 360;
     }
   }
 
   return [Math.round(h), Math.round(s * 100), Math.round(l * 100)];
 }
 
+/**
+ * Takes a color value of any type (hex, hsl, hsla, rgb, rgba)
+ * and an alpha value to generate an rgba string with the correct
+ * alpha value.
+ *
+ * @param  {String} colorValue
+ *         Color in the form of hex, hsl, hsla, rgb, rgba.
+ * @param  {Number} alpha
+ *         Alpha value for the color, between 0 and 1.
+ * @return {String}
+ *         Converted color with `alpha` value in rgba form.
+ */
+function setAlpha(colorValue, alpha) {
+  let color = new CssColor(colorValue);
+
+  // Throw if the color supplied is not valid.
+  if (!color.valid) {
+    throw new Error("Invalid color.");
+  }
+
+  // If an invalid alpha valid, just set to 1.
+  if (!(alpha >= 0 && alpha <= 1)) {
+    alpha = 1;
+  }
+
+  let { r, g, b } = color._getRGBATuple();
+  return "rgba(" + r + ", " + g + ", " + b + ", " + alpha + ")";
+}
+
 loader.lazyGetter(this, "DOMUtils", function () {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 });