Bug 1282716 - Add static highlighters for ellipse and inset. r=pbro
authorMike Park <mikeparkms@gmail.com>
Tue, 16 May 2017 16:25:19 -0400
changeset 360828 e9e3bea0eddd364642cfae82c6fd185f8df542dd
parent 360827 28bd68d3022281ca77d8c3618d6d61867ffc5cf9
child 360829 a709c2aec00c22ab4555f650f2990c465006b1dd
push id31902
push userryanvm@gmail.com
push dateFri, 26 May 2017 19:43:26 +0000
treeherdermozilla-central@ba1a33add29d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro
bugs1282716
milestone55.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 1282716 - Add static highlighters for ellipse and inset. r=pbro Currently doesn't handle zooms or transforms, or rounded corners for inset(). MozReview-Commit-ID: J9ZTjhn9Ki0
devtools/client/inspector/test/browser_inspector_highlighter-cssshape_01.js
devtools/client/inspector/test/browser_inspector_highlighter-cssshape_02.js
devtools/server/actors/highlighters.css
devtools/server/actors/highlighters/shapes.js
--- a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_01.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_01.js
@@ -3,58 +3,75 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 // Test the creation of the CSS shapes highlighter.
 
 const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html";
 const HIGHLIGHTER_TYPE = "ShapesHighlighter";
+const SHAPE_IDS = ["polygon", "ellipse", "rect"];
+const SHAPE_TYPES = [
+  {
+    shapeName: "polygon",
+    highlighter: "polygon"
+  },
+  {
+    shapeName: "circle",
+    highlighter: "ellipse"
+  },
+  {
+    shapeName: "ellipse",
+    highlighter: "ellipse"
+  },
+  {
+    shapeName: "inset",
+    highlighter: "rect"
+  }
+];
 
 add_task(function* () {
   let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
   let front = inspector.inspector;
   let highlighter = yield front.getHighlighterByType(HIGHLIGHTER_TYPE);
 
   yield isHiddenByDefault(testActor, highlighter);
   yield isVisibleWhenShown(testActor, inspector, highlighter);
 
   yield highlighter.finalize();
 });
 
+function* getShapeHidden(testActor, highlighterFront) {
+  let hidden = {};
+  for (let shape of SHAPE_IDS) {
+    hidden[shape] = yield testActor.getHighlighterNodeAttribute(
+      "shapes-" + shape, "hidden", highlighterFront);
+  }
+  return hidden;
+}
+
 function* isHiddenByDefault(testActor, highlighterFront) {
   info("Checking that highlighter is hidden by default");
 
   let polygonHidden = yield testActor.getHighlighterNodeAttribute(
     "shapes-polygon", "hidden", highlighterFront);
   let ellipseHidden = yield testActor.getHighlighterNodeAttribute(
     "shapes-ellipse", "hidden", highlighterFront);
   ok(polygonHidden && ellipseHidden, "The highlighter is hidden by default");
 }
 
 function* isVisibleWhenShown(testActor, inspector, highlighterFront) {
-  info("Asking to show the highlighter on the polygon node");
-
-  let polygonNode = yield getNodeFront("#polygon", inspector);
-  yield highlighterFront.show(polygonNode, {mode: "cssClipPath"});
-
-  let polygonHidden = yield testActor.getHighlighterNodeAttribute(
-    "shapes-polygon", "hidden", highlighterFront);
-  ok(!polygonHidden, "The polygon highlighter is visible");
+  for (let { shapeName, highlighter } of SHAPE_TYPES) {
+    info(`Asking to show the highlighter on the ${shapeName} node`);
 
-  info("Asking to show the highlighter on the circle node");
-  let circleNode = yield getNodeFront("#circle", inspector);
-  yield highlighterFront.show(circleNode, {mode: "cssClipPath"});
+    let node = yield getNodeFront(`#${shapeName}`, inspector);
+    yield highlighterFront.show(node, {mode: "cssClipPath"});
 
-  let ellipseHidden = yield testActor.getHighlighterNodeAttribute(
-    "shapes-ellipse", "hidden", highlighterFront);
-  polygonHidden = yield testActor.getHighlighterNodeAttribute(
-    "shapes-polygon", "hidden", highlighterFront);
-  ok(!ellipseHidden, "The circle highlighter is visible");
-  ok(polygonHidden, "The polygon highlighter is no longer visible");
+    let hidden = yield getShapeHidden(testActor, highlighterFront);
+    ok(!hidden[highlighter], `The ${shapeName} highlighter is visible`);
+  }
 
   info("Hiding the highlighter");
   yield highlighterFront.hide();
 
-  polygonHidden = yield testActor.getHighlighterNodeAttribute(
-    "shapes-polygon", "hidden", highlighterFront);
-  ok(polygonHidden, "The highlighter is hidden");
+  let hidden = yield getShapeHidden(testActor, highlighterFront);
+  ok(hidden.polygon && hidden.ellipse && hidden.rect, "The highlighter is hidden");
 }
--- a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_02.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_02.js
@@ -11,16 +11,18 @@ const HIGHLIGHTER_TYPE = "ShapesHighligh
 
 add_task(function* () {
   let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
   let front = inspector.inspector;
   let highlighter = yield front.getHighlighterByType(HIGHLIGHTER_TYPE);
 
   yield polygonHasCorrectAttrs(testActor, inspector, highlighter);
   yield circleHasCorrectAttrs(testActor, inspector, highlighter);
+  yield ellipseHasCorrectAttrs(testActor, inspector, highlighter);
+  yield insetHasCorrectAttrs(testActor, inspector, highlighter);
 
   yield highlighter.finalize();
 });
 
 function* polygonHasCorrectAttrs(testActor, inspector, highlighterFront) {
   info("Checking polygon highlighter has correct points");
 
   let polygonNode = yield getNodeFront("#polygon", inspector);
@@ -48,8 +50,50 @@ function* circleHasCorrectAttrs(testActo
   let cy = yield testActor.getHighlighterNodeAttribute(
     "shapes-ellipse", "cy", highlighterFront);
 
   is(rx, 25, "Circle highlighter has correct rx");
   is(ry, 25, "Circle highlighter has correct ry");
   is(cx, 30, "Circle highlighter has correct cx");
   is(cy, 40, "Circle highlighter has correct cy");
 }
+
+function* ellipseHasCorrectAttrs(testActor, inspector, highlighterFront) {
+  info("Checking ellipse highlighter has correct attributes");
+
+  let ellipseNode = yield getNodeFront("#ellipse", inspector);
+  yield highlighterFront.show(ellipseNode, {mode: "cssClipPath"});
+
+  let rx = yield testActor.getHighlighterNodeAttribute(
+    "shapes-ellipse", "rx", highlighterFront);
+  let ry = yield testActor.getHighlighterNodeAttribute(
+    "shapes-ellipse", "ry", highlighterFront);
+  let cx = yield testActor.getHighlighterNodeAttribute(
+    "shapes-ellipse", "cx", highlighterFront);
+  let cy = yield testActor.getHighlighterNodeAttribute(
+    "shapes-ellipse", "cy", highlighterFront);
+
+  is(rx, 40, "Ellipse highlighter has correct rx");
+  is(ry, 30, "Ellipse highlighter has correct ry");
+  is(cx, 25, "Ellipse highlighter has correct cx");
+  is(cy, 75, "Ellipse highlighter has correct cy");
+}
+
+function* insetHasCorrectAttrs(testActor, inspector, highlighterFront) {
+  info("Checking rect highlighter has correct attributes");
+
+  let insetNode = yield getNodeFront("#inset", inspector);
+  yield highlighterFront.show(insetNode, {mode: "cssClipPath"});
+
+  let x = yield testActor.getHighlighterNodeAttribute(
+    "shapes-rect", "x", highlighterFront);
+  let y = yield testActor.getHighlighterNodeAttribute(
+    "shapes-rect", "y", highlighterFront);
+  let width = yield testActor.getHighlighterNodeAttribute(
+    "shapes-rect", "width", highlighterFront);
+  let height = yield testActor.getHighlighterNodeAttribute(
+    "shapes-rect", "height", highlighterFront);
+
+  is(x, 15, "Rect highlighter has correct x");
+  is(y, 25, "Rect highlighter has correct y");
+  is(width, 72.5, "Rect highlighter has correct width");
+  is(height, 45, "Rect highlighter has correct height");
+}
--- a/devtools/server/actors/highlighters.css
+++ b/devtools/server/actors/highlighters.css
@@ -605,14 +605,15 @@
   height: 10px;
   transform: translate(-5px, -5px);
   background: transparent;
   border-radius: 50%;
   color: var(--highlighter-bubble-background-color);
 }
 
 :-moz-native-anonymous .shapes-polygon,
-:-moz-native-anonymous .shapes-ellipse {
+:-moz-native-anonymous .shapes-ellipse,
+:-moz-native-anonymous .shapes-rect {
   fill: transparent;
   stroke: var(--highlighter-guide-color);
   shape-rendering: crispEdges;
   vector-effect: non-scaling-stroke;
 }
--- a/devtools/server/actors/highlighters/shapes.js
+++ b/devtools/server/actors/highlighters/shapes.js
@@ -87,21 +87,38 @@ class ShapesHighlighter extends AutoRefr
       attributes: {
         "id": "ellipse",
         "class": "ellipse",
         "hidden": true
       },
       prefix: this.ID_CLASS_PREFIX
     });
 
-    // TODO: Append different SVG objects for different shapes.
+    // Append a rect for inset().
+    createSVGNode(this.win, {
+      nodeType: "rect",
+      parent: mainSvg,
+      attributes: {
+        "id": "rect",
+        "class": "rect",
+        "hidden": true
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
 
     return container;
   }
 
+  get currentDimensions() {
+    return {
+      width: this.currentQuads.border[0].bounds.width,
+      height: this.currentQuads.border[0].bounds.height
+    };
+  }
+
   /**
    * Parses the CSS definition given and returns the shape type associated
    * with the definition and the coordinates necessary to draw the shape.
    * @param {String} definition the input CSS definition
    * @returns {Object} null if the definition is not of a known shape type,
    *          or an object of the type { shapeType, coordinates }, where
    *          shapeType is the name of the shape and coordinates are an array
    *          or object of the coordinates needed to draw the shape.
@@ -110,16 +127,24 @@ class ShapesHighlighter extends AutoRefr
     const types = [{
       name: "polygon",
       prefix: "polygon(",
       coordParser: this.polygonPoints.bind(this)
     }, {
       name: "circle",
       prefix: "circle(",
       coordParser: this.circlePoints.bind(this)
+    }, {
+      name: "ellipse",
+      prefix: "ellipse(",
+      coordParser: this.ellipsePoints.bind(this)
+    }, {
+      name: "inset",
+      prefix: "inset(",
+      coordParser: this.insetPoints.bind(this)
     }];
 
     for (let { name, prefix, coordParser } of types) {
       if (definition.includes(prefix)) {
         definition = definition.substring(prefix.length, definition.length - 1);
         return {
           shapeType: name,
           coordinates: coordParser(definition)
@@ -134,48 +159,35 @@ class ShapesHighlighter extends AutoRefr
    * Parses the definition of the CSS polygon() function and returns its points,
    * converted to percentages.
    * @param {String} definition the arguments of the polygon() function
    * @returns {Array} an array of the points of the polygon, with all values
    *          evaluated and converted to percentages
    */
   polygonPoints(definition) {
     return definition.split(",").map(coords => {
-      return splitCoords(coords).map((coord, i) => {
-        let size = i % 2 === 0 ? this.currentQuads.border[0].bounds.width
-                               : this.currentQuads.border[0].bounds.height;
-        if (coord.includes("calc(")) {
-          return evalCalcExpression(coord.substring(5, coord.length - 1), size);
-        }
-        return coordToPercent(coord, size);
-      });
+      return splitCoords(coords).map(this.convertCoordsToPercent.bind(this));
     });
   }
 
   /**
    * Parses the definition of the CSS circle() function and returns the x/y radiuses and
    * center coordinates, converted to percentages.
    * @param {String} definition the arguments of the circle() function
    * @returns {Object} an object of the form { rx, ry, cx, cy }, where rx and ry are the
    *          radiuses for the x and y axes, and cx and cy are the x/y coordinates for the
    *          center of the circle. All values are evaluated and converted to percentages.
    */
   circlePoints(definition) {
     // The computed value of circle() always has the keyword "at".
     let values = definition.split(" at ");
     let radius = values[0];
-    let elemWidth = this.currentQuads.border[0].bounds.width;
-    let elemHeight = this.currentQuads.border[0].bounds.height;
-    let center = splitCoords(values[1]).map((coord, i) => {
-      let size = i % 2 === 0 ? elemWidth : elemHeight;
-      if (coord.includes("calc(")) {
-        return evalCalcExpression(coord.substring(5, coord.length - 1), size);
-      }
-      return coordToPercent(coord, size);
-    });
+    let elemWidth = this.currentDimensions.width;
+    let elemHeight = this.currentDimensions.height;
+    let center = splitCoords(values[1]).map(this.convertCoordsToPercent.bind(this));
 
     if (radius === "closest-side") {
       // radius is the distance from center to closest side of reference box
       radius = Math.min(center[0], center[1], 100 - center[0], 100 - center[1]);
     } else if (radius === "farthest-side") {
       // radius is the distance from center to farthest side of reference box
       radius = Math.max(center[0], center[1], 100 - center[0], 100 - center[1]);
     } else {
@@ -193,16 +205,96 @@ class ShapesHighlighter extends AutoRefr
     let radiusX = radius / ratioX;
     let radiusY = radius / ratioY;
 
     // rx, ry, cx, ry
     return { rx: radiusX, ry: radiusY, cx: center[0], cy: center[1] };
   }
 
   /**
+   * Parses the definition of the CSS ellipse() function and returns the x/y radiuses and
+   * center coordinates, converted to percentages.
+   * @param {String} definition the arguments of the ellipse() function
+   * @returns {Object} an object of the form { rx, ry, cx, cy }, where rx and ry are the
+   *          radiuses for the x and y axes, and cx and cy are the x/y coordinates for the
+   *          center of the ellipse. All values are evaluated and converted to percentages
+   */
+  ellipsePoints(definition) {
+    let values = definition.split(" at ");
+    let elemWidth = this.currentDimensions.width;
+    let elemHeight = this.currentDimensions.height;
+    let center = splitCoords(values[1]).map(this.convertCoordsToPercent.bind(this));
+
+    let radii = values[0].trim().split(" ").map((radius, i) => {
+      let size = i % 2 === 0 ? elemWidth : elemHeight;
+      if (radius === "closest-side") {
+        // radius is the distance from center to closest x/y side of reference box
+        return i % 2 === 0 ? Math.min(center[0], 100 - center[0])
+                           : Math.min(center[1], 100 - center[1]);
+      } else if (radius === "farthest-side") {
+        // radius is the distance from center to farthest x/y side of reference box
+        return i % 2 === 0 ? Math.max(center[0], 100 - center[0])
+                           : Math.max(center[1], 100 - center[1]);
+      }
+      return coordToPercent(radius, size);
+    });
+
+    return { rx: radii[0], ry: radii[1], cx: center[0], cy: center[1] };
+  }
+
+  /**
+   * Parses the definition of the CSS inset() function and returns the x/y offsets and
+   * width/height of the shape, converted to percentages. Border radiuses (given after
+   * "round" in the definition) are currently ignored.
+   * @param {String} definition the arguments of the inset() function
+   * @returns {Object} an object of the form { x, y, width, height }, which are the top/
+   *          left positions and width/height of the shape.
+   */
+  insetPoints(definition) {
+    let values = definition.split(" round ");
+    let offsets = splitCoords(values[0]).map(this.convertCoordsToPercent.bind(this));
+
+    let x, y = 0;
+    let width = this.currentDimensions.width;
+    let height = this.currentDimensions.height;
+    // The offsets, like margin/padding/border, are in order: top, right, bottom, left.
+    if (offsets.length === 1) {
+      x = y = offsets[0];
+      width = height = 100 - 2 * x;
+    } else if (offsets.length === 2) {
+      y = offsets[0];
+      x = offsets[1];
+      height = 100 - 2 * y;
+      width = 100 - 2 * x;
+    } else if (offsets.length === 3) {
+      y = offsets[0];
+      x = offsets[1];
+      height = 100 - y - offsets[2];
+      width = 100 - 2 * x;
+    } else if (offsets.length === 4) {
+      y = offsets[0];
+      x = offsets[3];
+      height = 100 - y - offsets[2];
+      width = 100 - x - offsets[1];
+    }
+
+    return { x, y, width, height };
+  }
+
+  convertCoordsToPercent(coord, i) {
+    let elemWidth = this.currentDimensions.width;
+    let elemHeight = this.currentDimensions.height;
+    let size = i % 2 === 0 ? elemWidth : elemHeight;
+    if (coord.includes("calc(")) {
+      return evalCalcExpression(coord.substring(5, coord.length - 1), size);
+    }
+    return coordToPercent(coord, size);
+  }
+
+  /**
    * Destroy the nodes. Remove listeners.
    */
   destroy() {
     AutoRefreshHighlighter.prototype.destroy.call(this);
     this.markup.destroy();
   }
 
   /**
@@ -229,34 +321,40 @@ class ShapesHighlighter extends AutoRefr
   _hasMoved() {
     let hasMoved = AutoRefreshHighlighter.prototype._hasMoved.call(this);
 
     let oldShapeCoordinates = JSON.stringify(this.coordinates);
 
     // TODO: need other modes too.
     if (this.options.mode.startsWith("css")) {
       let property = shapeModeToCssPropertyName(this.options.mode);
+      let style = getComputedStyle(this.currentNode)[property];
 
-      let { coordinates, shapeType } =
-        this._parseCSSShapeValue(getComputedStyle(this.currentNode)[property]);
-      this.coordinates = coordinates;
-      this.shapeType = shapeType;
+      if (!style || style === "none") {
+        this.coordinates = [];
+        this.shapeType = "none";
+      } else {
+        let { coordinates, shapeType } = this._parseCSSShapeValue(style);
+        this.coordinates = coordinates;
+        this.shapeType = shapeType;
+      }
     }
 
     let newShapeCoordinates = JSON.stringify(this.coordinates);
 
     return hasMoved || oldShapeCoordinates !== newShapeCoordinates;
   }
 
   /**
    * Hide all elements used to highlight CSS different shapes.
    */
   _hideShapes() {
     this.getElement("ellipse").setAttribute("hidden", true);
     this.getElement("polygon").setAttribute("hidden", true);
+    this.getElement("rect").setAttribute("hidden", true);
   }
 
   /**
    * Update the highlighter for the current node. Called whenever the element's quads
    * or CSS shape has changed.
    * @returns {Boolean} whether the highlighter was successfully updated
    */
   _update() {
@@ -270,16 +368,20 @@ class ShapesHighlighter extends AutoRefr
 
     this._hideShapes();
     this.getElement("markers-container").setAttribute("style", "");
 
     if (this.shapeType === "polygon") {
       this._updatePolygonShape(top, left, width, height);
     } else if (this.shapeType === "circle") {
       this._updateCircleShape(top, left, width, height);
+    } else if (this.shapeType === "ellipse") {
+      this._updateEllipseShape(top, left, width, height);
+    } else if (this.shapeType === "inset") {
+      this._updateInsetShape(top, left, width, height);
     }
 
     setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement);
 
     return true;
   }
 
   /**
@@ -318,25 +420,69 @@ class ShapesHighlighter extends AutoRefr
     let ellipseEl = this.getElement("ellipse");
     ellipseEl.setAttribute("rx", rx);
     ellipseEl.setAttribute("ry", ry);
     ellipseEl.setAttribute("cx", cx);
     ellipseEl.setAttribute("cy", cy);
     ellipseEl.removeAttribute("hidden");
 
     let shadows = `${MARKER_SIZE + cx * width / 100}px
-      ${MARKER_SIZE + cy * height / 100}px 0 0,
-      ${MARKER_SIZE + (cx + rx) * width / 100}px
       ${MARKER_SIZE + cy * height / 100}px 0 0`;
 
     this.getElement("markers-container").setAttribute("style",
       `top:${top - MARKER_SIZE}px;left:${left - MARKER_SIZE}px;box-shadow:${shadows};`);
   }
 
   /**
+   * Update the SVG ellipse to fit the CSS ellipse.
+   * @param {Number} top the top bound of the element quads
+   * @param {Number} left the left bound of the element quads
+   * @param {Number} width the width of the element quads
+   * @param {Number} height the height of the element quads
+   */
+  _updateEllipseShape(top, left, width, height) {
+    let { rx, ry, cx, cy } = this.coordinates;
+    let ellipseEl = this.getElement("ellipse");
+    ellipseEl.setAttribute("rx", rx);
+    ellipseEl.setAttribute("ry", ry);
+    ellipseEl.setAttribute("cx", cx);
+    ellipseEl.setAttribute("cy", cy);
+    ellipseEl.removeAttribute("hidden");
+
+    let shadows = `${MARKER_SIZE + cx * width / 100}px
+      ${MARKER_SIZE + cy * height / 100}px 0 0,
+      ${MARKER_SIZE + (cx + rx) * height / 100}px
+      ${MARKER_SIZE + cy * height / 100}px 0 0,
+      ${MARKER_SIZE + cx * height / 100}px
+      ${MARKER_SIZE + (cy + ry) * height / 100}px 0 0`;
+
+    this.getElement("markers-container").setAttribute("style",
+      `top:${top - MARKER_SIZE}px;left:${left - MARKER_SIZE}px;box-shadow:${shadows};`);
+  }
+
+  /**
+   * Update the SVG rect to fit the CSS inset.
+   * @param {Number} top the top bound of the element quads
+   * @param {Number} left the left bound of the element quads
+   * @param {Number} width the width of the element quads
+   * @param {Number} height the height of the element quads
+   */
+  _updateInsetShape(top, left, width, height) {
+    let rectEl = this.getElement("rect");
+    rectEl.setAttribute("x", this.coordinates.x);
+    rectEl.setAttribute("y", this.coordinates.y);
+    rectEl.setAttribute("width", this.coordinates.width);
+    rectEl.setAttribute("height", this.coordinates.height);
+    rectEl.removeAttribute("hidden");
+
+    this.getElement("markers-container").setAttribute("style",
+      `top:${top - MARKER_SIZE}px;left:${left - MARKER_SIZE}px;box-shadow:none;`);
+  }
+
+  /**
    * Hide the highlighter, the outline and the infobar.
    */
   _hide() {
     setIgnoreLayoutChanges(true);
 
     this._hideShapes();
     this.getElement("markers-container").setAttribute("style", "");