Bug 1332049 - Add a new working copy of the Spectrum widget for the color picker refresh. r=pbro
☠☠ backed out by fce1a1dde304 ☠ ☠
authorGabriel Luong <gabriel.luong@gmail.com>
Thu, 19 Jan 2017 09:01:04 -0500
changeset 375119 b3338b6713e03abd0fd7a5ae00d2d0d3b0a58a6b
parent 375118 5abe47a8bfe0fd31d5cedc5bf01bdfdbae9ed7bf
child 375120 fb476a85015b45db1e8bcd45439c7767dcfa13a1
push id6996
push userjlorenzo@mozilla.com
push dateMon, 06 Mar 2017 20:48:21 +0000
treeherdermozilla-beta@d89512dab048 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro
bugs1332049
milestone53.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 1332049 - Add a new working copy of the Spectrum widget for the color picker refresh. r=pbro
devtools/client/jar.mn
devtools/client/preferences/devtools.js
devtools/client/shared/widgets/ColorWidget.js
devtools/client/shared/widgets/color-widget.css
devtools/client/shared/widgets/moz.build
devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -113,16 +113,17 @@ devtools.jar:
     content/inspector/inspector.xhtml (inspector/inspector.xhtml)
     content/framework/connect/connect.xhtml (framework/connect/connect.xhtml)
     content/framework/connect/connect.css (framework/connect/connect.css)
     content/framework/connect/connect.js (framework/connect/connect.js)
     content/shared/widgets/graphs-frame.xhtml (shared/widgets/graphs-frame.xhtml)
     content/shared/widgets/cubic-bezier.css (shared/widgets/cubic-bezier.css)
     content/shared/widgets/mdn-docs.css (shared/widgets/mdn-docs.css)
     content/shared/widgets/filter-widget.css (shared/widgets/filter-widget.css)
+    content/shared/widgets/color-widget.css (shared/widgets/color-widget.css)
     content/shared/widgets/spectrum.css (shared/widgets/spectrum.css)
     content/aboutdebugging/aboutdebugging.xhtml (aboutdebugging/aboutdebugging.xhtml)
     content/aboutdebugging/aboutdebugging.css (aboutdebugging/aboutdebugging.css)
     content/aboutdebugging/initializer.js (aboutdebugging/initializer.js)
     content/responsive.html/index.xhtml (responsive.html/index.xhtml)
     content/responsive.html/index.js (responsive.html/index.js)
     content/dom/dom.html (dom/dom.html)
     content/dom/main.js (dom/main.js)
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -59,16 +59,18 @@ pref("devtools.inspector.show_pseudo_ele
 // The default size for image preview tooltips in the rule-view/computed-view/markup-view
 pref("devtools.inspector.imagePreviewTooltipSize", 300);
 // Enable user agent style inspection in rule-view
 pref("devtools.inspector.showUserAgentStyles", false);
 // Show all native anonymous content (like controls in <video> tags)
 pref("devtools.inspector.showAllAnonymousContent", false);
 // Enable the MDN docs tooltip
 pref("devtools.inspector.mdnDocsTooltip.enabled", true);
+// Enable the new color widget
+pref("devtools.inspector.colorWidget.enabled", false);
 
 // Enable the Font Inspector
 pref("devtools.fontinspector.enabled", true);
 
 // Enable the Layout View
 pref("devtools.layoutview.enabled", false);
 
 // Grid highlighter preferences
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/widgets/ColorWidget.js
@@ -0,0 +1,341 @@
+/* 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/. */
+
+/**
+ * This file is a new working copy of Spectrum.js for the purposes of refreshing the color
+ * widget. It is hidden behind a pref("devtools.inspector.colorWidget.enabled").
+ */
+
+"use strict";
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * ColorWidget creates a color picker widget in any container you give it.
+ *
+ * Simple usage example:
+ *
+ * const {ColorWidget} = require("devtools/client/shared/widgets/ColorWidget");
+ * let s = new ColorWidget(containerElement, [255, 126, 255, 1]);
+ * s.on("changed", (event, rgba, color) => {
+ *   console.log("rgba(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + ", " +
+ *     rgba[3] + ")");
+ * });
+ * s.show();
+ * s.destroy();
+ *
+ * Note that the color picker is hidden by default and you need to call show to
+ * make it appear. This 2 stages initialization helps in cases you are creating
+ * the color picker in a parent element that hasn't been appended anywhere yet
+ * or that is hidden. Calling show() when the parent element is appended and
+ * visible will allow spectrum to correctly initialize its various parts.
+ *
+ * Fires the following events:
+ * - changed : When the user changes the current color
+ */
+function ColorWidget(parentEl, rgb) {
+  EventEmitter.decorate(this);
+
+  this.element = parentEl.ownerDocument.createElementNS(XHTML_NS, "div");
+  this.parentEl = parentEl;
+
+  this.element.className = "spectrum-container";
+  this.element.innerHTML = `
+    <div class="spectrum-top">
+      <div class="spectrum-fill"></div>
+      <div class="spectrum-top-inner">
+        <div class="spectrum-color spectrum-box">
+          <div class="spectrum-sat">
+            <div class="spectrum-val">
+              <div class="spectrum-dragger"></div>
+            </div>
+          </div>
+        </div>
+        <div class="spectrum-hue spectrum-box">
+          <div class="spectrum-slider spectrum-slider-control"></div>
+        </div>
+      </div>
+    </div>
+    <div class="spectrum-alpha spectrum-checker spectrum-box">
+      <div class="spectrum-alpha-inner">
+        <div class="spectrum-alpha-handle spectrum-slider-control"></div>
+      </div>
+    </div>
+  `;
+
+  this.onElementClick = this.onElementClick.bind(this);
+  this.element.addEventListener("click", this.onElementClick);
+
+  this.parentEl.appendChild(this.element);
+
+  this.slider = this.element.querySelector(".spectrum-hue");
+  this.slideHelper = this.element.querySelector(".spectrum-slider");
+  ColorWidget.draggable(this.slider, this.onSliderMove.bind(this));
+
+  this.dragger = this.element.querySelector(".spectrum-color");
+  this.dragHelper = this.element.querySelector(".spectrum-dragger");
+  ColorWidget.draggable(this.dragger, this.onDraggerMove.bind(this));
+
+  this.alphaSlider = this.element.querySelector(".spectrum-alpha");
+  this.alphaSliderInner = this.element.querySelector(".spectrum-alpha-inner");
+  this.alphaSliderHelper = this.element.querySelector(".spectrum-alpha-handle");
+  ColorWidget.draggable(this.alphaSliderInner, this.onAlphaSliderMove.bind(this));
+
+  if (rgb) {
+    this.rgb = rgb;
+    this.updateUI();
+  }
+}
+
+module.exports.ColorWidget = ColorWidget;
+
+ColorWidget.hsvToRgb = function (h, s, v, a) {
+  let r, g, b;
+
+  let i = Math.floor(h * 6);
+  let f = h * 6 - i;
+  let p = v * (1 - s);
+  let q = v * (1 - f * s);
+  let t = v * (1 - (1 - f) * s);
+
+  switch (i % 6) {
+    case 0: r = v; g = t; b = p; break;
+    case 1: r = q; g = v; b = p; break;
+    case 2: r = p; g = v; b = t; break;
+    case 3: r = p; g = q; b = v; break;
+    case 4: r = t; g = p; b = v; break;
+    case 5: r = v; g = p; b = q; break;
+  }
+
+  return [r * 255, g * 255, b * 255, a];
+};
+
+ColorWidget.rgbToHsv = function (r, g, b, a) {
+  r = r / 255;
+  g = g / 255;
+  b = b / 255;
+
+  let max = Math.max(r, g, b), min = Math.min(r, g, b);
+  let h, s, v = max;
+
+  let d = max - min;
+  s = max == 0 ? 0 : d / max;
+
+  if (max == min) {
+    // achromatic
+    h = 0;
+  } else {
+    switch (max) {
+      case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+      case g: h = (b - r) / d + 2; break;
+      case b: h = (r - g) / d + 4; break;
+    }
+    h /= 6;
+  }
+  return [h, s, v, a];
+};
+
+ColorWidget.draggable = function (element, onmove, onstart, onstop) {
+  onmove = onmove || function () {};
+  onstart = onstart || function () {};
+  onstop = onstop || function () {};
+
+  let doc = element.ownerDocument;
+  let dragging = false;
+  let offset = {};
+  let maxHeight = 0;
+  let maxWidth = 0;
+
+  function prevent(e) {
+    e.stopPropagation();
+    e.preventDefault();
+  }
+
+  function move(e) {
+    if (dragging) {
+      if (e.buttons === 0) {
+        // The button is no longer pressed but we did not get a mouseup event.
+        stop();
+        return;
+      }
+      let pageX = e.pageX;
+      let pageY = e.pageY;
+
+      let dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth));
+      let dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight));
+
+      onmove.apply(element, [dragX, dragY]);
+    }
+  }
+
+  function start(e) {
+    let rightclick = e.which === 3;
+
+    if (!rightclick && !dragging) {
+      if (onstart.apply(element, arguments) !== false) {
+        dragging = true;
+        maxHeight = element.offsetHeight;
+        maxWidth = element.offsetWidth;
+
+        offset = element.getBoundingClientRect();
+
+        move(e);
+
+        doc.addEventListener("selectstart", prevent);
+        doc.addEventListener("dragstart", prevent);
+        doc.addEventListener("mousemove", move);
+        doc.addEventListener("mouseup", stop);
+
+        prevent(e);
+      }
+    }
+  }
+
+  function stop() {
+    if (dragging) {
+      doc.removeEventListener("selectstart", prevent);
+      doc.removeEventListener("dragstart", prevent);
+      doc.removeEventListener("mousemove", move);
+      doc.removeEventListener("mouseup", stop);
+      onstop.apply(element, arguments);
+    }
+    dragging = false;
+  }
+
+  element.addEventListener("mousedown", start);
+};
+
+ColorWidget.prototype = {
+  set rgb(color) {
+    this.hsv = ColorWidget.rgbToHsv(color[0], color[1], color[2], color[3]);
+  },
+
+  get rgb() {
+    let rgb = ColorWidget.hsvToRgb(this.hsv[0], this.hsv[1], this.hsv[2],
+      this.hsv[3]);
+    return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]),
+            Math.round(rgb[3] * 100) / 100];
+  },
+
+  get rgbNoSatVal() {
+    let rgb = ColorWidget.hsvToRgb(this.hsv[0], 1, 1);
+    return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]), rgb[3]];
+  },
+
+  get rgbCssString() {
+    let rgb = this.rgb;
+    return "rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " +
+      rgb[3] + ")";
+  },
+
+  show: function () {
+    this.element.classList.add("spectrum-show");
+
+    this.slideHeight = this.slider.offsetHeight;
+    this.dragWidth = this.dragger.offsetWidth;
+    this.dragHeight = this.dragger.offsetHeight;
+    this.dragHelperHeight = this.dragHelper.offsetHeight;
+    this.slideHelperHeight = this.slideHelper.offsetHeight;
+    this.alphaSliderWidth = this.alphaSliderInner.offsetWidth;
+    this.alphaSliderHelperWidth = this.alphaSliderHelper.offsetWidth;
+
+    this.updateUI();
+  },
+
+  onElementClick: function (e) {
+    e.stopPropagation();
+  },
+
+  onSliderMove: function (dragX, dragY) {
+    this.hsv[0] = (dragY / this.slideHeight);
+    this.updateUI();
+    this.onChange();
+  },
+
+  onDraggerMove: function (dragX, dragY) {
+    this.hsv[1] = dragX / this.dragWidth;
+    this.hsv[2] = (this.dragHeight - dragY) / this.dragHeight;
+    this.updateUI();
+    this.onChange();
+  },
+
+  onAlphaSliderMove: function (dragX, dragY) {
+    this.hsv[3] = dragX / this.alphaSliderWidth;
+    this.updateUI();
+    this.onChange();
+  },
+
+  onChange: function () {
+    this.emit("changed", this.rgb, this.rgbCssString);
+  },
+
+  updateHelperLocations: function () {
+    // If the UI hasn't been shown yet then none of the dimensions will be
+    // correct
+    if (!this.element.classList.contains("spectrum-show")) {
+      return;
+    }
+
+    let h = this.hsv[0];
+    let s = this.hsv[1];
+    let v = this.hsv[2];
+
+    // Placing the color dragger
+    let dragX = s * this.dragWidth;
+    let dragY = this.dragHeight - (v * this.dragHeight);
+    let helperDim = this.dragHelperHeight / 2;
+
+    dragX = Math.max(
+      -helperDim,
+      Math.min(this.dragWidth - helperDim, dragX - helperDim)
+    );
+    dragY = Math.max(
+      -helperDim,
+      Math.min(this.dragHeight - helperDim, dragY - helperDim)
+    );
+
+    this.dragHelper.style.top = dragY + "px";
+    this.dragHelper.style.left = dragX + "px";
+
+    // Placing the hue slider
+    let slideY = (h * this.slideHeight) - this.slideHelperHeight / 2;
+    this.slideHelper.style.top = slideY + "px";
+
+    // Placing the alpha slider
+    let alphaSliderX = (this.hsv[3] * this.alphaSliderWidth) -
+      (this.alphaSliderHelperWidth / 2);
+    this.alphaSliderHelper.style.left = alphaSliderX + "px";
+  },
+
+  updateUI: function () {
+    this.updateHelperLocations();
+
+    let rgb = this.rgb;
+    let rgbNoSatVal = this.rgbNoSatVal;
+
+    let flatColor = "rgb(" + rgbNoSatVal[0] + ", " + rgbNoSatVal[1] + ", " +
+      rgbNoSatVal[2] + ")";
+
+    this.dragger.style.backgroundColor = flatColor;
+
+    let rgbNoAlpha = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")";
+    let rgbAlpha0 = "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ", 0)";
+    let alphaGradient = "linear-gradient(to right, " + rgbAlpha0 + ", " +
+      rgbNoAlpha + ")";
+    this.alphaSliderInner.style.background = alphaGradient;
+  },
+
+  destroy: function () {
+    this.element.removeEventListener("click", this.onElementClick);
+
+    this.parentEl.removeChild(this.element);
+
+    this.slider = null;
+    this.dragger = null;
+    this.alphaSlider = this.alphaSliderInner = this.alphaSliderHelper = null;
+    this.parentEl = null;
+    this.element = null;
+  }
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/widgets/color-widget.css
@@ -0,0 +1,155 @@
+/* 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/. */
+
+#eyedropper-button {
+  margin-inline-start: 5px;
+  display: block;
+}
+
+#eyedropper-button::before {
+  background-image: url(chrome://devtools/skin/images/command-eyedropper.svg);
+}
+
+/* Mix-in classes */
+
+.spectrum-checker {
+  background-color: #eee;
+  background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc),
+    linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc);
+  background-size: 12px 12px;
+  background-position: 0 0, 6px 6px;
+}
+
+.spectrum-slider-control {
+  cursor: pointer;
+  box-shadow: 0 0 2px rgba(0,0,0,.6);
+  background: #fff;
+  border-radius: 10px;
+  opacity: .8;
+}
+
+.spectrum-box {
+  border: 1px solid rgba(0,0,0,0.2);
+  border-radius: 2px;
+  background-clip: content-box;
+}
+
+/* Elements */
+
+#spectrum-tooltip {
+  padding: 4px;
+}
+
+.spectrum-container {
+  position: relative;
+  display: none;
+  top: 0;
+  left: 0;
+  border-radius: 0;
+  width: 200px;
+  padding: 5px;
+}
+
+.spectrum-show {
+  display: inline-block;
+}
+
+/* Keep aspect ratio:
+http://www.briangrinstead.com/blog/keep-aspect-ratio-with-html-and-css */
+.spectrum-top {
+  position: relative;
+  width: 100%;
+  display: inline-block;
+}
+
+.spectrum-top-inner {
+  position: absolute;
+  top:0;
+  left:0;
+  bottom:0;
+  right:0;
+}
+
+.spectrum-color {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 20%;
+}
+
+.spectrum-hue {
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 83%;
+}
+
+.spectrum-fill {
+  /* Same as spectrum-color width */
+  margin-top: 85%;
+}
+
+.spectrum-sat, .spectrum-val {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+}
+
+.spectrum-dragger, .spectrum-slider {
+  -moz-user-select: none;
+}
+
+.spectrum-alpha {
+  position: relative;
+  height: 8px;
+  margin-top: 3px;
+}
+
+.spectrum-alpha-inner {
+  height: 100%;
+}
+
+.spectrum-alpha-handle {
+  position: absolute;
+  top: -3px;
+  bottom: -3px;
+  width: 5px;
+  left: 50%;
+}
+
+.spectrum-sat {
+  background-image: linear-gradient(to right, #FFF, rgba(204, 154, 129, 0));
+}
+
+.spectrum-val {
+  background-image: linear-gradient(to top, #000000, rgba(204, 154, 129, 0));
+}
+
+.spectrum-hue {
+  background: linear-gradient(to bottom, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
+}
+
+.spectrum-dragger {
+  position: absolute;
+  top: 0px;
+  left: 0px;
+  cursor: pointer;
+  border-radius: 50%;
+  height: 8px;
+  width: 8px;
+  border: 1px solid white;
+  box-shadow: 0 0 2px rgba(0,0,0,.6);
+}
+
+.spectrum-slider {
+  position: absolute;
+  top: 0;
+  height: 5px;
+  left: -3px;
+  right: -3px;
+}
--- a/devtools/client/shared/widgets/moz.build
+++ b/devtools/client/shared/widgets/moz.build
@@ -8,16 +8,17 @@ DIRS += [
     'tooltip',
 ]
 
 DevToolsModules(
     'AbstractTreeItem.jsm',
     'BarGraphWidget.js',
     'BreadcrumbsWidget.jsm',
     'Chart.js',
+    'ColorWidget.js',
     'CubicBezierPresets.js',
     'CubicBezierWidget.js',
     'FastListWidget.js',
     'FilterWidget.js',
     'FlameGraph.js',
     'Graphs.js',
     'GraphsWorker.js',
     'LineGraphWidget.js',
--- a/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
+++ b/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js
@@ -1,23 +1,26 @@
 /* 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 Services = require("Services");
 const {Task} = require("devtools/shared/task");
 const {colorUtils} = require("devtools/shared/css/color");
 const {Spectrum} = require("devtools/client/shared/widgets/Spectrum");
 const SwatchBasedEditorTooltip = require("devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip");
 const {LocalizationHelper} = require("devtools/shared/l10n");
 const L10N = new LocalizationHelper("devtools/client/locales/inspector.properties");
 
 const Heritage = require("sdk/core/heritage");
 
+const colorWidgetPref = "devtools.inspector.colorWidget.enabled";
+const NEW_COLOR_WIDGET = Services.prefs.getBoolPref(colorWidgetPref);
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
 
 /**
  * The swatch color picker tooltip class is a specific class meant to be used
  * along with output-parser's generated color swatches.
  * It extends the parent SwatchBasedEditorTooltip class.
  * It just wraps a standard Tooltip and sets its content with an instance of a
  * color picker.
@@ -29,17 +32,19 @@ const XHTML_NS = "http://www.w3.org/1999
  * @param {InspectorPanel} inspector
  *        The inspector panel, needed for the eyedropper.
  * @param {Function} supportsCssColor4ColorFunction
  *        A function for checking the supporting of css-color-4 color function.
  */
 function SwatchColorPickerTooltip(document,
                                   inspector,
                                   {supportsCssColor4ColorFunction}) {
-  let stylesheet = "chrome://devtools/content/shared/widgets/spectrum.css";
+  let stylesheet = NEW_COLOR_WIDGET ?
+    "chrome://devtools/content/shared/widgets/color-widget.css" :
+    "chrome://devtools/content/shared/widgets/spectrum.css";
   SwatchBasedEditorTooltip.call(this, document, stylesheet);
 
   this.inspector = inspector;
 
   // Creating a spectrum instance. this.spectrum will always be a promise that
   // resolves to the spectrum instance
   this.spectrum = this.setColorPickerContent([0, 0, 0, 1]);
   this._onSpectrumColorChange = this._onSpectrumColorChange.bind(this);
@@ -66,17 +71,23 @@ SwatchColorPickerTooltip.prototype = Her
     /* pointerEvents for eyedropper has to be set auto to display tooltip when
      * eyedropper is disabled in non-HTML documents.
      */
     eyedropper.style.pointerEvents = "auto";
     container.appendChild(eyedropper);
 
     this.tooltip.setContent(container, { width: 218, height: 224 });
 
-    let spectrum = new Spectrum(spectrumNode, color);
+    let spectrum;
+    if (NEW_COLOR_WIDGET) {
+      const {ColorWidget} = require("devtools/client/shared/widgets/ColorWidget");
+      spectrum = new ColorWidget(spectrumNode, color);
+    } else {
+      spectrum = new Spectrum(spectrumNode, color);
+    }
 
     // Wait for the tooltip to be shown before calling spectrum.show
     // as it expect to be visible in order to compute DOM element sizes.
     this.tooltip.once("shown", () => {
       spectrum.show();
     });
 
     return spectrum;