Bug 1359794 - grid cell highlighting now are properly transformed, infobars are better positioned; r=gl
authorMatteo Ferretti <mferretti@mozilla.com>
Mon, 05 Jun 2017 16:22:30 +0200
changeset 410593 dc53ed710486d80d6d3029d0b51440982bab2e43
parent 410592 97ce14cb28f98838664b6f8576b577ba65df14ee
child 410594 71fbf74857a39bdf2565ce466b5d254bd8199c8e
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgl
bugs1359794
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 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
@@ -95,16 +95,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.
@@ -136,28 +207,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.
@@ -803,56 +869,58 @@ 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 = LAYOUT_L10N.getFormatStr("layout.rowColumnPositions",
                    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.
    *
@@ -865,44 +933,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();
@@ -986,18 +1028,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.)
    *
@@ -1497,49 +1539,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(" "));
   },
 
@@ -1563,33 +1615,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.