Bug 1359794 - grid cell highlighting now are properly transformed, infobars are better positioned; r=gl draft
authorMatteo Ferretti <mferretti@mozilla.com>
Mon, 05 Jun 2017 16:22:30 +0200
changeset 589240 7649accf6ef75c2a4d29f92cc046ee48c72d1d73
parent 588376 b9f544b6a419e9dbe6b86624bf68f53109cf2398
child 631804 731d0f083c4cf81b8606f0df94f1fe1f16c97492
push id62291
push userbmo:zer0@mozilla.com
push dateMon, 05 Jun 2017 22:01:26 +0000
reviewersgl
bugs1359794
milestone55.0a1
Bug 1359794 - grid cell highlighting now are properly transformed, infobars are better positioned; r=gl The Grid Cell highlighting should take transformation into account Added utility functions for a better handling of points and boundaries; especially when a transformation is applied. The SVG path description's points are now properly transformed. Grid's infobars are now positioned in the same way of box model infobar, since we pass a more accurate `bounds` object. MozReview-Commit-ID: CNknsC0ufZj
devtools/server/actors/highlighters/css-grid.js
--- a/devtools/server/actors/highlighters/css-grid.js
+++ b/devtools/server/actors/highlighters/css-grid.js
@@ -91,16 +91,87 @@ const CANVAS_SIZE = 4096;
 
 // This constant is used as value to draw infinite lines on canvas; since we cannot use
 // the canvas boundaries as coordinates to draw the lines, and then applying
 // transformations on top of them (the resulting coordinates might ending before reaching
 // the viewport's edges, therefore the lines won't looks as "infinite").
 const CANVAS_INFINITY = CANVAS_SIZE << 8;
 
 /**
+ * Returns an array containing the four coordinates of a rectangle, given its diagonal
+ * as input; optionally applying a matrix, and a function to each of the coordinates'
+ * value.
+ *
+ * @param {Number} x1
+ *        The x-axis coordinate of the rectangle's diagonal start point.
+ * @param {Number} y1
+ *        The y-axis coordinate of the rectangle's diagonal start point.
+ * @param {Number} x2
+ *        The x-axis coordinate of the rectangle's diagonal end point.
+ * @param {Number} y2
+ *        The y-axis coordinate of the rectangle's diagonal end point.
+ * @param {Array} [matrix=identity()]
+ *        A transformation matrix to apply.
+ * @return {Array}
+ *        The rect four corners' points transformed by the matrix given.
+ */
+function getPointsFromDiagonal(x1, y1, x2, y2, matrix = identity()) {
+  return [
+    [x1, y1],
+    [x2, y1],
+    [x2, y2],
+    [x1, y2]
+  ].map(point => {
+    let transformedPoint = apply(matrix, point);
+
+    return {x: transformedPoint[0], y: transformedPoint[1]};
+  });
+}
+
+/**
+ * Takes an array of four points and returns a DOMRect-like object, represent the
+ * boundaries defined by the points given.
+ *
+ * @param {Array} points
+ *        The four points.
+ * @return {Object}
+ *         A DOMRect-like object.
+ */
+function getBoundsFromPoints(points) {
+  let bounds = {};
+
+  bounds.left = Math.min(points[0].x, points[1].x, points[2].x, points[3].x);
+  bounds.right = Math.max(points[0].x, points[1].x, points[2].x, points[3].x);
+  bounds.top = Math.min(points[0].y, points[1].y, points[2].y, points[3].y);
+  bounds.bottom = Math.max(points[0].y, points[1].y, points[2].y, points[3].y);
+
+  bounds.x = bounds.left;
+  bounds.y = bounds.top;
+  bounds.width = bounds.right - bounds.left;
+  bounds.height = bounds.bottom - bounds.top;
+
+  return bounds;
+}
+
+/**
+ * Takes an array of four points and returns a string represent a path description.
+ *
+ * @param {Array} points
+ *        The four points.
+ * @return {String}
+ *         A Path Description that can be used in svg's <path> element.
+ */
+function getPathDescriptionFromPoints(points) {
+  return "M" + points[0].x + "," + points[0].y + " " +
+         "L" + points[1].x + "," + points[1].y + " " +
+         "L" + points[2].x + "," + points[2].y + " " +
+         "L" + points[3].x + "," + points[3].y;
+}
+
+/**
  * Draws a line to the context given, applying a transformation matrix if passed.
  *
  * @param {CanvasRenderingContext2D} ctx
  *        The 2d canvas context.
  * @param {Number} x1
  *        The x-axis of the coordinate for the begin of the line.
  * @param {Number} y1
  *        The y-axis of the coordinate for the begin of the line.
@@ -132,28 +203,23 @@ function drawLine(ctx, x1, y1, x2, y2, m
  * @param {Number} x2
  *        The x-axis coordinate of the rectangle's diagonal end point.
  * @param {Number} y2
  *        The y-axis coordinate of the rectangle's diagonal end point.
  * @param {Array} [matrix=identity()]
  *        The transformation matrix to apply.
  */
 function drawRect(ctx, x1, y1, x2, y2, matrix = identity()) {
-  let p = [
-    [x1, y1],
-    [x2, y1],
-    [x2, y2],
-    [x1, y2]
-  ].map(point => apply(matrix, point).map(Math.round));
+  let p = getPointsFromDiagonal(x1, y1, x2, y2, matrix);
 
   ctx.beginPath();
-  ctx.moveTo(p[0][0], p[0][1]);
-  ctx.lineTo(p[1][0], p[1][1]);
-  ctx.lineTo(p[2][0], p[2][1]);
-  ctx.lineTo(p[3][0], p[3][1]);
+  ctx.moveTo(Math.round(p[0].x), Math.round(p[0].y));
+  ctx.lineTo(Math.round(p[1].x), Math.round(p[1].y));
+  ctx.lineTo(Math.round(p[2].x), Math.round(p[2].y));
+  ctx.lineTo(Math.round(p[3].x), Math.round(p[3].y));
   ctx.closePath();
 }
 
 /**
  * Utility method to draw a rounded rectangle in the provided canvas context.
  *
  * @param  {CanvasRenderingContext2D} ctx
  *         The 2d canvas context.
@@ -792,55 +858,57 @@ CssGridHighlighter.prototype = extend(Au
     return true;
   },
 
   /**
    * Update the grid information displayed in the grid area info bar.
    *
    * @param  {GridArea} area
    *         The grid area object.
-   * @param  {Number} x1
-   *         The first x-coordinate of the grid area rectangle.
-   * @param  {Number} x2
-   *         The second x-coordinate of the grid area rectangle.
-   * @param  {Number} y1
-   *         The first y-coordinate of the grid area rectangle.
-   * @param  {Number} y2
-   *         The second y-coordinate of the grid area rectangle.
+   * @param  {Object} bounds
+   *          A DOMRect-like object represent the grid area rectangle.
    */
-  _updateGridAreaInfobar(area, x1, x2, y1, y2) {
-    let width = x2 - x1;
-    let height = y2 - y1;
+  _updateGridAreaInfobar(area, bounds) {
+    let { width, height } = bounds;
     let dim = parseFloat(width.toPrecision(6)) +
               " \u00D7 " +
               parseFloat(height.toPrecision(6));
 
     this.getElement("area-infobar-name").setTextContent(area.name);
     this.getElement("area-infobar-dimensions").setTextContent(dim);
 
     let container = this.getElement("area-infobar-container");
-    this._moveInfobar(container, x1, x2, y1, y2, {
+    moveInfobar(container, bounds, this.win, {
       position: "bottom",
       hideIfOffscreen: true
     });
   },
 
-  _updateGridCellInfobar(rowNumber, columnNumber, x1, x2, y1, y2) {
-    let width = x2 - x1;
-    let height = y2 - y1;
+  /**
+   * Update the grid information displayed in the grid cell info bar.
+   *
+   * @param  {Number} rowNumber
+   *         The grid cell's row number.
+   * @param  {Number} columnNumber
+   *         The grid cell's column number.
+   * @param  {Object} bounds
+   *          A DOMRect-like object represent the grid cell rectangle.
+   */
+  _updateGridCellInfobar(rowNumber, columnNumber, bounds) {
+    let { width, height } = bounds;
     let dim = parseFloat(width.toPrecision(6)) +
               " \u00D7 " +
               parseFloat(height.toPrecision(6));
     let position = `${rowNumber}\/${columnNumber}`;
 
     this.getElement("cell-infobar-position").setTextContent(position);
     this.getElement("cell-infobar-dimensions").setTextContent(dim);
 
     let container = this.getElement("cell-infobar-container");
-    this._moveInfobar(container, x1, x2, y1, y2, {
+    moveInfobar(container, bounds, this.win, {
       position: "top",
       hideIfOffscreen: true
     });
   },
 
   /**
    * Update the grid information displayed in the grid line info bar.
    *
@@ -853,44 +921,18 @@ CssGridHighlighter.prototype = extend(Au
    * @param  {Number} y
    *         The y-coordinate of the grid line.
    */
   _updateGridLineInfobar(gridLineNames, gridLineNumber, x, y) {
     this.getElement("line-infobar-number").setTextContent(gridLineNumber);
     this.getElement("line-infobar-names").setTextContent(gridLineNames);
 
     let container = this.getElement("line-infobar-container");
-    this._moveInfobar(container, x, x, y, y);
-  },
-
-  /**
-   * Move the given grid infobar to the right place in the highlighter.
-   *
-   * @param  {Number} x1
-   *         The first x-coordinate of the grid rectangle.
-   * @param  {Number} x2
-   *         The second x-coordinate of the grid rectangle.
-   * @param  {Number} y1
-   *         The first y-coordinate of the grid rectangle.
-   * @param  {Number} y2
-   *         The second y-coordinate of the grid rectangle.
-   */
-  _moveInfobar(container, x1, x2, y1, y2, options) {
-    let bounds = {
-      bottom: y2,
-      height: y2 - y1,
-      left: x1,
-      right: x2,
-      top: y1,
-      width: x2 - x1,
-      x: x1,
-      y: y1,
-    };
-
-    moveInfobar(container, bounds, this.win, options);
+    moveInfobar(container,
+      getBoundsFromPoints([{x, y}, {x, y}, {x, y}, {x, y}]), this.win);
   },
 
   /**
    * The <canvas>'s position needs to be updated if the page scrolls too much, in order
    * to give the illusion that it always covers the viewport.
    */
   _scrollUpdate() {
     let hasPositionChanged = this.calculateCanvasPosition();
@@ -974,18 +1016,18 @@ CssGridHighlighter.prototype = extend(Au
     // translating it to give the perception that it always covers the viewport.
     this.canvas.setAttribute("style",
       `width:${size}px;height:${size}px; transform: translate(${x}px, ${y}px);`);
 
     this.ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE);
   },
 
   /**
-   * Updates the current matrix taking in account the following transformations, in this
-   * order:
+   * Updates the current matrices for both canvas drawing and SVG, taking in account the
+   * following transformations, in this order:
    *   1. The scale given by the display pixel ratio.
    *   2. The translation to the top left corner of the element.
    *   3. The scale given by the current zoom.
    *   4. The translation given by the top and left padding of the element.
    *   5. Any CSS transformation applied directly to the element (only 2D
    *      transformation; the 3D transformation are flattened, see `dom-matrix-2d` module
    *      for further details.)
    *
@@ -1464,49 +1506,59 @@ CssGridHighlighter.prototype = extend(Au
    * Render the grid area highlight for the given area name or for all the grid areas.
    *
    * @param  {String} areaName
    *         Name of the grid area to be highlighted. If no area name is provided, all
    *         the grid areas should be highlighted.
    */
   renderGridArea(areaName) {
     let paths = [];
-    let currentZoom = getCurrentZoom(this.win);
+    let { devicePixelRatio } = this.win;
+    let displayPixelRatio = getDisplayPixelRatio(this.win);
 
     for (let i = 0; i < this.gridData.length; i++) {
       let fragment = this.gridData[i];
-      let {bounds} = this.currentQuads.content[i];
 
       for (let area of fragment.areas) {
         if (areaName && areaName != area.name) {
           continue;
         }
 
         let rowStart = fragment.rows.lines[area.rowStart - 1];
         let rowEnd = fragment.rows.lines[area.rowEnd - 1];
         let columnStart = fragment.cols.lines[area.columnStart - 1];
         let columnEnd = fragment.cols.lines[area.columnEnd - 1];
 
-        let x1 = columnStart.start + columnStart.breadth +
-          (bounds.left / currentZoom);
-        let x2 = columnEnd.start + (bounds.left / currentZoom);
-        let y1 = rowStart.start + rowStart.breadth +
-          (bounds.top / currentZoom);
-        let y2 = rowEnd.start + (bounds.top / currentZoom);
+        let x1 = columnStart.start + columnStart.breadth;
+        let y1 = rowStart.start + rowStart.breadth;
+        let x2 = columnEnd.start;
+        let y2 = rowEnd.start;
+
+        let points = getPointsFromDiagonal(x1, y1, x2, y2, this.currentMatrix);
 
-        let path = "M" + x1 + "," + y1 + " " +
-                   "L" + x2 + "," + y1 + " " +
-                   "L" + x2 + "," + y2 + " " +
-                   "L" + x1 + "," + y2;
-        paths.push(path);
+        // Scale down by `devicePixelRatio` since SVG element already take them into
+        // account.
+        let svgPoints = points.map(point => ({
+          x: Math.round(point.x / devicePixelRatio),
+          y: Math.round(point.y / devicePixelRatio)
+        }));
+
+        // Scale down by `displayPixelRatio` since infobar's HTML elements already take it
+        // into account; and the zoom scaling is handled by `moveInfobar`.
+        let bounds = getBoundsFromPoints(points.map(point => ({
+          x: Math.round(point.x / displayPixelRatio),
+          y: Math.round(point.y / displayPixelRatio)
+        })));
+
+        paths.push(getPathDescriptionFromPoints(svgPoints));
 
         // Update and show the info bar when only displaying a single grid area.
         if (areaName) {
           this._showGridAreaInfoBar();
-          this._updateGridAreaInfobar(area, x1, x2, y1, y2);
+          this._updateGridAreaInfobar(area, bounds);
         }
       }
     }
 
     let areas = this.getElement("areas");
     areas.setAttribute("d", paths.join(" "));
   },
 
@@ -1530,33 +1582,44 @@ CssGridHighlighter.prototype = extend(Au
 
     let row = fragment.rows.tracks[rowNumber - 1];
     let column = fragment.cols.tracks[columnNumber - 1];
 
     if (!row || !column) {
       return;
     }
 
-    let currentZoom = getCurrentZoom(this.win);
-    let {bounds} = this.currentQuads.content[gridFragmentIndex];
+    let x1 = column.start;
+    let y1 = row.start;
+    let x2 = column.start + column.breadth;
+    let y2 = row.start + row.breadth;
+
+    let { devicePixelRatio } = this.win;
+    let displayPixelRatio = getDisplayPixelRatio(this.win);
+
+    let points = getPointsFromDiagonal(x1, y1, x2, y2, this.currentMatrix);
 
-    let x1 = column.start + (bounds.left / currentZoom);
-    let x2 = column.start + column.breadth + (bounds.left / currentZoom);
-    let y1 = row.start + (bounds.top / currentZoom);
-    let y2 = row.start + row.breadth + (bounds.top / currentZoom);
+    // Scale down by `devicePixelRatio` since SVG element already take them into account.
+    let svgPoints = points.map(point => ({
+      x: Math.round(point.x / devicePixelRatio),
+      y: Math.round(point.y / devicePixelRatio)
+    }));
 
-    let path = "M" + x1 + "," + y1 + " " +
-               "L" + x2 + "," + y1 + " " +
-               "L" + x2 + "," + y2 + " " +
-               "L" + x1 + "," + y2;
+    // Scale down by `displayPixelRatio` since infobar's HTML elements already take it
+    // into account, and the zoom scaling is handled by `moveInfobar`.
+    let bounds = getBoundsFromPoints(points.map(point => ({
+      x: Math.round(point.x / displayPixelRatio),
+      y: Math.round(point.y / displayPixelRatio)
+    })));
+
     let cells = this.getElement("cells");
-    cells.setAttribute("d", path);
+    cells.setAttribute("d", getPathDescriptionFromPoints(svgPoints));
 
     this._showGridCellInfoBar();
-    this._updateGridCellInfobar(rowNumber, columnNumber, x1, x2, y1, y2);
+    this._updateGridCellInfobar(rowNumber, columnNumber, bounds);
   },
 
   /**
    * Render the grid line name highlight for the given grid fragment index, lineNumber,
    * and dimensionType.
    *
    * @param  {Number} gridFragmentIndex
    *         Index of the grid fragment to render the grid line highlight.