Bug 1424275 - Show element quad outline for shape-outside in CSS shapes highlighter. r=gl draft
authorMike Park <mikeparkms@gmail.com>
Fri, 08 Dec 2017 14:38:40 -0500
changeset 710158 ebdf11c16ff852ca707b56c1dc3ed7801ef89d30
parent 709637 a461fe03fdb07218b7f50e92c59dde64b8f8a5b0
child 743525 275e7f755ef62daddc3b34631193bed7881f5e0e
push id92761
push userbmo:mpark@mozilla.com
push dateFri, 08 Dec 2017 20:12:50 +0000
reviewersgl
bugs1424275
milestone59.0a1
Bug 1424275 - Show element quad outline for shape-outside in CSS shapes highlighter. r=gl MozReview-Commit-ID: x1R9V0ZA3l
devtools/server/actors/highlighters.css
devtools/server/actors/highlighters/shapes.js
--- a/devtools/server/actors/highlighters.css
+++ b/devtools/server/actors/highlighters.css
@@ -617,17 +617,18 @@
   position: absolute;
   overflow: visible;
 }
 
 :-moz-native-anonymous .shapes-polygon,
 :-moz-native-anonymous .shapes-ellipse,
 :-moz-native-anonymous .shapes-rect,
 :-moz-native-anonymous .shapes-bounding-box,
-:-moz-native-anonymous .shapes-rotate-line {
+:-moz-native-anonymous .shapes-rotate-line,
+:-moz-native-anonymous .shapes-quad {
   fill: transparent;
   stroke: var(--highlighter-guide-color);
   shape-rendering: geometricPrecision;
   vector-effect: non-scaling-stroke;
 }
 
 :-moz-native-anonymous .shapes-markers {
   fill: #fff;
--- a/devtools/server/actors/highlighters/shapes.js
+++ b/devtools/server/actors/highlighters/shapes.js
@@ -115,16 +115,106 @@ class ShapesHighlighter extends AutoRefr
         "id": "shape-container",
         "class": "shape-container",
         "viewBox": "0 0 100 100",
         "preserveAspectRatio": "none"
       },
       prefix: this.ID_CLASS_PREFIX
     });
 
+    // This clipPath and its children make sure the element quad outline
+    // is only shown when the shape extends past the element quads.
+    let clipSvg = createSVGNode(this.win, {
+      nodeType: "clipPath",
+      parent: mainSvg,
+      attributes: {
+        "id": "clip-path",
+        "class": "clip-path",
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    createSVGNode(this.win, {
+      nodeType: "polygon",
+      parent: clipSvg,
+      attributes: {
+        "id": "clip-polygon",
+        "class": "clip-polygon",
+        "hidden": "true"
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    createSVGNode(this.win, {
+      nodeType: "ellipse",
+      parent: clipSvg,
+      attributes: {
+        "id": "clip-ellipse",
+        "class": "clip-ellipse",
+        "hidden": true
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    createSVGNode(this.win, {
+      nodeType: "rect",
+      parent: clipSvg,
+      attributes: {
+        "id": "clip-rect",
+        "class": "clip-rect",
+        "hidden": true
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    // Rectangle that displays the element quads. Only shown for shape-outside.
+    // Only the parts of the rectangle's outline that overlap with the shape is shown.
+    createSVGNode(this.win, {
+      nodeType: "rect",
+      parent: mainSvg,
+      attributes: {
+        "id": "quad",
+        "class": "quad",
+        "hidden": "true",
+        "clip-path": "url(#shapes-clip-path)",
+        "x": 0,
+        "y": 0,
+        "width": 100,
+        "height": 100
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    // clipPath that corresponds to the element's quads. Only applied for shape-outside.
+    // This ensures only the parts of the shape that are within the element's quads are
+    // outlined by a solid line.
+    let shapeClipSvg = createSVGNode(this.win, {
+      nodeType: "clipPath",
+      parent: mainSvg,
+      attributes: {
+        "id": "quad-clip-path",
+        "class": "quad-clip-path",
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    createSVGNode(this.win, {
+      nodeType: "rect",
+      parent: shapeClipSvg,
+      attributes: {
+        "id": "quad-clip",
+        "class": "quad-clip",
+        "x": -1,
+        "y": -1,
+        "width": 102,
+        "height": 102
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
     let mainGroup = createSVGNode(this.win, {
       nodeType: "g",
       parent: mainSvg,
       attributes: {
         "id": "group",
       },
       prefix: this.ID_CLASS_PREFIX
     });
@@ -160,16 +250,54 @@ class ShapesHighlighter extends AutoRefr
       attributes: {
         "id": "rect",
         "class": "rect",
         "hidden": true
       },
       prefix: this.ID_CLASS_PREFIX
     });
 
+    // Dashed versions of each shape. Only shown for the parts of the shape
+    // that extends past the element's quads.
+    createSVGNode(this.win, {
+      nodeType: "polygon",
+      parent: mainGroup,
+      attributes: {
+        "id": "dashed-polygon",
+        "class": "polygon",
+        "hidden": "true",
+        "stroke-dasharray": "5, 5",
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    createSVGNode(this.win, {
+      nodeType: "ellipse",
+      parent: mainGroup,
+      attributes: {
+        "id": "dashed-ellipse",
+        "class": "ellipse",
+        "hidden": "true",
+        "stroke-dasharray": "5, 5",
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    createSVGNode(this.win, {
+      nodeType: "rect",
+      parent: mainGroup,
+      attributes: {
+        "id": "dashed-rect",
+        "class": "rect",
+        "hidden": "true",
+        "stroke-dasharray": "5, 5",
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
     createSVGNode(this.win, {
       nodeType: "path",
       parent: mainGroup,
       attributes: {
         "id": "bounding-box",
         "class": "bounding-box",
         "stroke-dasharray": "5, 5",
         "hidden": true
@@ -314,24 +442,25 @@ class ShapesHighlighter extends AutoRefr
     let { target, type, pageX, pageY } = event;
 
     // For events on highlighted nodes in an iframe, when the event takes place
     // outside the iframe. Check if event target belongs to the iframe. If it doesn't,
     // adjust pageX/pageY to be relative to the iframe rather than the parent.
     let nodeDocument = this.currentNode.ownerDocument;
     if (target !== nodeDocument && target.ownerDocument !== nodeDocument) {
       let [xOffset, yOffset] = getFrameOffsets(target.ownerGlobal, this.currentNode);
+      let zoom = getCurrentZoom(this.win);
       // xOffset/yOffset are relative to the viewport, so first find the top/left
       // edges of the viewport relative to the page.
       let viewportLeft = pageX - event.clientX;
       let viewportTop = pageY - event.clientY;
       // Also adjust for scrolling in the iframe.
       let { scrollTop, scrollLeft } = nodeDocument.documentElement;
-      pageX -= viewportLeft + xOffset - scrollLeft;
-      pageY -= viewportTop + yOffset - scrollTop;
+      pageX -= viewportLeft + xOffset / zoom - scrollLeft;
+      pageY -= viewportTop + yOffset / zoom - scrollTop;
     }
 
     switch (type) {
       case "pagehide":
         // If a page hide event is triggered for current window's highlighter, hide the
         // highlighter.
         if (target.defaultView === this.win) {
           this.destroy();
@@ -2007,16 +2136,23 @@ class ShapesHighlighter extends AutoRefr
   _hideShapes() {
     this.getElement("ellipse").setAttribute("hidden", true);
     this.getElement("polygon").setAttribute("hidden", true);
     this.getElement("rect").setAttribute("hidden", true);
     this.getElement("bounding-box").setAttribute("hidden", true);
     this.getElement("markers").setAttribute("d", "");
     this.getElement("markers-outline").setAttribute("d", "");
     this.getElement("rotate-line").setAttribute("d", "");
+    this.getElement("quad").setAttribute("hidden", true);
+    this.getElement("clip-ellipse").setAttribute("hidden", true);
+    this.getElement("clip-polygon").setAttribute("hidden", true);
+    this.getElement("clip-rect").setAttribute("hidden", true);
+    this.getElement("dashed-polygon").setAttribute("hidden", true);
+    this.getElement("dashed-ellipse").setAttribute("hidden", true);
+    this.getElement("dashed-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() {
@@ -2041,16 +2177,32 @@ class ShapesHighlighter extends AutoRefr
     } else if (this.shapeType === "circle") {
       this._updateCircleShape(width, height, zoom);
     } else if (this.shapeType === "ellipse") {
       this._updateEllipseShape(width, height, zoom);
     } else if (this.shapeType === "inset") {
       this._updateInsetShape(width, height, zoom);
     }
 
+    if (this.property === "shape-outside") {
+      // For shape-outside, the element's quads are displayed for the parts that overlap
+      // with the shape, and the parts of the shape that extend past the element's quads
+      // are shown with a dashed line.
+      let quadRect = this.getElement("quad");
+      quadRect.removeAttribute("hidden");
+
+      this.getElement("polygon").setAttribute("clip-path", "url(#shapes-quad-clip-path)");
+      this.getElement("ellipse").setAttribute("clip-path", "url(#shapes-quad-clip-path)");
+      this.getElement("rect").setAttribute("clip-path", "url(#shapes-quad-clip-path)");
+    } else {
+      this.getElement("polygon").removeAttribute("clip-path");
+      this.getElement("ellipse").removeAttribute("clip-path");
+      this.getElement("rect").removeAttribute("clip-path");
+    }
+
     let { width: winWidth, height: winHeight } = this._winDimensions;
     root.removeAttribute("hidden");
     root.setAttribute("style",
       `position:absolute; width:${winWidth}px;height:${winHeight}px; overflow:hidden;`);
 
     this._handleMarkerHover(this.hoveredPoint);
 
     setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement);
@@ -2079,27 +2231,49 @@ class ShapesHighlighter extends AutoRefr
 
     if (this.shapeType === "polygon") {
       let points = this.coordinates.map(point => point.join(",")).join(" ");
 
       let polygonEl = this.getElement("polygon");
       polygonEl.setAttribute("points", points);
       polygonEl.removeAttribute("hidden");
 
+      let clipPolygon = this.getElement("clip-polygon");
+      clipPolygon.setAttribute("points", points);
+      clipPolygon.removeAttribute("hidden");
+
+      let dashedPolygon = this.getElement("dashed-polygon");
+      dashedPolygon.setAttribute("points", points);
+      dashedPolygon.removeAttribute("hidden");
+
       markerPoints.push(rotatePoint);
       let rotateLine = `M ${center.join(" ")} L ${rotatePoint.join(" ")}`;
       this.getElement("rotate-line").setAttribute("d", rotateLine);
     } else if (this.shapeType === "circle" || this.shapeType === "ellipse") {
       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 clipEllipse = this.getElement("clip-ellipse");
+      clipEllipse.setAttribute("rx", rx);
+      clipEllipse.setAttribute("ry", ry);
+      clipEllipse.setAttribute("cx", cx);
+      clipEllipse.setAttribute("cy", cy);
+      clipEllipse.removeAttribute("hidden");
+
+      let dashedEllipse = this.getElement("dashed-ellipse");
+      dashedEllipse.setAttribute("rx", rx);
+      dashedEllipse.setAttribute("ry", ry);
+      dashedEllipse.setAttribute("cx", cx);
+      dashedEllipse.setAttribute("cy", cy);
+      dashedEllipse.removeAttribute("hidden");
     }
     this._drawMarkers(markerPoints, width, height, zoom);
   }
 
   /**
    * Update the SVG polygon to fit the CSS polygon.
    * @param {Number} width the width of the element quads
    * @param {Number} height the height of the element quads
@@ -2108,16 +2282,23 @@ class ShapesHighlighter extends AutoRefr
   _updatePolygonShape(width, height, zoom) {
     // Draw and show the polygon.
     let points = this.coordinates.map(point => point.join(",")).join(" ");
 
     let polygonEl = this.getElement("polygon");
     polygonEl.setAttribute("points", points);
     polygonEl.removeAttribute("hidden");
 
+    let clipPolygon = this.getElement("clip-polygon");
+    clipPolygon.setAttribute("points", points);
+    clipPolygon.removeAttribute("hidden");
+
+    let dashedPolygon = this.getElement("dashed-polygon");
+    dashedPolygon.setAttribute("points", points);
+    dashedPolygon.removeAttribute("hidden");
     this._drawMarkers(this.coordinates, width, height, zoom);
   }
 
   /**
    * Update the SVG ellipse to fit the CSS circle.
    * @param {Number} width the width of the element quads
    * @param {Number} height the height of the element quads
    * @param {Number} zoom the zoom level of the window
@@ -2126,16 +2307,30 @@ class ShapesHighlighter extends AutoRefr
     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 clipEllipse = this.getElement("clip-ellipse");
+    clipEllipse.setAttribute("rx", rx);
+    clipEllipse.setAttribute("ry", ry);
+    clipEllipse.setAttribute("cx", cx);
+    clipEllipse.setAttribute("cy", cy);
+    clipEllipse.removeAttribute("hidden");
+
+    let dashedEllipse = this.getElement("dashed-ellipse");
+    dashedEllipse.setAttribute("rx", rx);
+    dashedEllipse.setAttribute("ry", ry);
+    dashedEllipse.setAttribute("cx", cx);
+    dashedEllipse.setAttribute("cy", cy);
+    dashedEllipse.removeAttribute("hidden");
+
     this._drawMarkers([[cx, cy], [cx + rx, cy]], width, height, zoom);
   }
 
   /**
    * Update the SVG ellipse to fit the CSS ellipse.
    * @param {Number} width the width of the element quads
    * @param {Number} height the height of the element quads
    * @param {Number} zoom the zoom level of the window
@@ -2144,16 +2339,29 @@ class ShapesHighlighter extends AutoRefr
     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 clipEllipse = this.getElement("clip-ellipse");
+    clipEllipse.setAttribute("rx", rx);
+    clipEllipse.setAttribute("ry", ry);
+    clipEllipse.setAttribute("cx", cx);
+    clipEllipse.setAttribute("cy", cy);
+    clipEllipse.removeAttribute("hidden");
+
+    let dashedEllipse = this.getElement("dashed-ellipse");
+    dashedEllipse.setAttribute("rx", rx);
+    dashedEllipse.setAttribute("ry", ry);
+    dashedEllipse.setAttribute("cx", cx);
+    dashedEllipse.setAttribute("cy", cy);
+    dashedEllipse.removeAttribute("hidden");
     let markerCoords = [ [cx, cy], [cx + rx, cy], [cx, cy + ry] ];
     this._drawMarkers(markerCoords, width, height, zoom);
   }
 
   /**
    * Update the SVG rect to fit the CSS inset.
    * @param {Number} width the width of the element quads
    * @param {Number} height the height of the element quads
@@ -2163,16 +2371,30 @@ class ShapesHighlighter extends AutoRefr
     let { top, left, right, bottom } = this.coordinates;
     let rectEl = this.getElement("rect");
     rectEl.setAttribute("x", left);
     rectEl.setAttribute("y", top);
     rectEl.setAttribute("width", 100 - left - right);
     rectEl.setAttribute("height", 100 - top - bottom);
     rectEl.removeAttribute("hidden");
 
+    let clipRect = this.getElement("clip-rect");
+    clipRect.setAttribute("x", left);
+    clipRect.setAttribute("y", top);
+    clipRect.setAttribute("width", 100 - left - right);
+    clipRect.setAttribute("height", 100 - top - bottom);
+    clipRect.removeAttribute("hidden");
+
+    let dashedRect = this.getElement("dashed-rect");
+    dashedRect.setAttribute("x", left);
+    dashedRect.setAttribute("y", top);
+    dashedRect.setAttribute("width", 100 - left - right);
+    dashedRect.setAttribute("height", 100 - top - bottom);
+    dashedRect.removeAttribute("hidden");
+
     let centerX = (left + (100 - right)) / 2;
     let centerY = (top + (100 - bottom)) / 2;
     let markerCoords = [[centerX, top], [100 - right, centerY],
                         [centerX, 100 - bottom], [left, centerY]];
     this._drawMarkers(markerCoords, width, height, zoom);
   }
 
   /**