Bug 1303171 - Adjust highlighters to account for writing mode and text dir. r=gl draft
authorJ. Ryan Stinnett <jryans@gmail.com>
Fri, 08 Dec 2017 22:18:21 -0600
changeset 721275 b5e758ea0f9d2577a6114f29ac2a07d922d691ad
parent 721208 b2cb61e83ac50115a28f04aaa8a32d4db90aad23
child 746276 4c8793cfb9e9f168806186d0a473d75faa0ad320
push id95775
push userbmo:jryans@gmail.com
push dateTue, 16 Jan 2018 23:31:18 +0000
reviewersgl
bugs1303171
milestone59.0a1
Bug 1303171 - Adjust highlighters to account for writing mode and text dir. r=gl The `getCurrentMatrix` function is used by grid and other highlighters to account for the element's position, transforms, etc. Here we extend it with extra logic to account for the element's writing mode and text direction. MozReview-Commit-ID: AZT4cJySLwk
devtools/client/preferences/devtools.js
devtools/server/actors/highlighters/css-grid.js
devtools/server/actors/highlighters/utils/canvas.js
devtools/shared/layout/dom-matrix-2d.js
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -77,16 +77,19 @@ pref("devtools.new-animationinspector.en
 
 // Grid highlighter preferences
 pref("devtools.gridinspector.gridOutlineMaxColumns", 50);
 pref("devtools.gridinspector.gridOutlineMaxRows", 50);
 pref("devtools.gridinspector.showGridAreas", false);
 pref("devtools.gridinspector.showGridLineNumbers", false);
 pref("devtools.gridinspector.showInfiniteLines", false);
 
+// Common highlighter preferences
+pref("devtools.highlighter.writingModeAdjust", false);
+
 // Whether or not the box model panel is opened in the computed view
 pref("devtools.computed.boxmodel.opened", true);
 // Whether or not the box model panel is opened in the layout view
 pref("devtools.layout.boxmodel.opened", true);
 // Whether or not the flexbox panel is opened in the layout view
 pref("devtools.layout.flexbox.opened", true);
 // Whether or not the grid inspector panel is opened in the layout view
 pref("devtools.layout.grid.opened", true);
--- a/devtools/server/actors/highlighters/css-grid.js
+++ b/devtools/server/actors/highlighters/css-grid.js
@@ -1460,18 +1460,18 @@ class CssGridHighlighter extends AutoRef
     // It also clear the <canvas>'s drawing context.
     updateCanvasElement(this.canvas, this._canvasPosition, this.win.devicePixelRatio);
 
     // Clear the grid area highlights.
     this.clearGridAreas();
     this.clearGridCell();
 
     // Update the current matrix used in our canvas' rendering.
-    let { currentMatrix, hasNodeTransformations } = getCurrentMatrix(this.currentNode,
-      this.win);
+    let { currentMatrix, hasNodeTransformations } =
+      getCurrentMatrix(this.currentNode, this.win);
     this.currentMatrix = currentMatrix;
     this.hasNodeTransformations = hasNodeTransformations;
 
     // Start drawing the grid fragments.
     for (let i = 0; i < this.gridData.length; i++) {
       this.renderFragment(this.gridData[i]);
     }
 
--- a/devtools/server/actors/highlighters/utils/canvas.js
+++ b/devtools/server/actors/highlighters/utils/canvas.js
@@ -1,20 +1,25 @@
 /* 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 DevToolsUtils = require("devtools/shared/DevToolsUtils");
+
 const {
   apply,
   getNodeTransformationMatrix,
   identity,
   isIdentity,
   multiply,
+  reflectAboutY,
+  rotate,
   scale,
   translate,
 } = require("devtools/shared/layout/dom-matrix-2d");
 const { getViewportDimensions } = require("devtools/shared/layout/utils");
 
 // A set of utility functions for highlighters that render their content to a <canvas>
 // element.
 
@@ -33,16 +38,21 @@ const { getViewportDimensions } = requir
 // the displayport API instead.
 //
 // Using a fixed value should also solve bug 1348293.
 const CANVAS_SIZE = 4096;
 
 // The default color used for the canvas' font, fill and stroke colors.
 const DEFAULT_COLOR = "#9400FF";
 
+// Boolean pref to enable adjustment for writing mode and RTL content.
+DevToolsUtils.defineLazyGetter(this, "WRITING_MODE_ADJUST_ENABLED", () => {
+  return Services.prefs.getBoolPref("devtools.highlighter.writingModeAdjust");
+});
+
 /**
  * Draws a rect to the context given and applies a transformation matrix if passed.
  * The coordinates are the start and end points of the rectangle's diagonal.
  *
  * @param  {CanvasRenderingContext2D} ctx
  *         The 2D canvas context.
  * @param  {Number} x1
  *         The x-axis coordinate of the rectangle's diagonal start point.
@@ -258,16 +268,18 @@ function getBoundsFromPoints(points) {
  * 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.)
+ *   6. Rotate, translate, and reflect as needed to match the writing mode and text
+ *      direction of the element.
  *
  *  The transformations of the element's ancestors are not currently computed (see
  *  bug 1355675).
  *
  * @param  {Element} element
  *         The current element.
  * @param  {Window} window
  *         The window object.
@@ -285,32 +297,40 @@ function getCurrentMatrix(element, windo
   let borderTop = parseFloat(computedStyle.borderTopWidth);
   let borderLeft = parseFloat(computedStyle.borderLeftWidth);
 
   let nodeMatrix = getNodeTransformationMatrix(element, window.document.documentElement);
 
   let currentMatrix = identity();
   let hasNodeTransformations = false;
 
-  // First, we scale based on the device pixel ratio.
+  // Scale based on the device pixel ratio.
   currentMatrix = multiply(currentMatrix, scale(window.devicePixelRatio));
 
-  // Then, we apply the current node's transformation matrix, relative to the
-  // inspected window's root element, but only if it's not a identity matrix.
+  // Apply the current node's transformation matrix, relative to the inspected window's
+  // root element, but only if it's not a identity matrix.
   if (isIdentity(nodeMatrix)) {
     hasNodeTransformations = false;
   } else {
     currentMatrix = multiply(currentMatrix, nodeMatrix);
     hasNodeTransformations = true;
   }
 
-  // Finally, we translate the origin based on the node's padding and border values.
+  // Translate the origin based on the node's padding and border values.
   currentMatrix = multiply(currentMatrix,
     translate(paddingLeft + borderLeft, paddingTop + borderTop));
 
+  if (WRITING_MODE_ADJUST_ENABLED) {
+    // Adjust as needed to match the writing mode and text direction of the element.
+    let writingModeMatrix = getWritingModeMatrix(element, computedStyle);
+    if (!isIdentity(writingModeMatrix)) {
+      currentMatrix = multiply(currentMatrix, writingModeMatrix);
+    }
+  }
+
   return { currentMatrix, hasNodeTransformations };
 }
 
 /**
  * Given an array of four points, returns a string represent a path description.
  *
  * @param  {Array} points
  *         An array with 4 pointer objects {x, y} representing the box quads.
@@ -350,16 +370,82 @@ function getPointsFromDiagonal(x1, y1, x
   ].map(point => {
     let transformedPoint = apply(matrix, point);
 
     return { x: transformedPoint[0], y: transformedPoint[1] };
   });
 }
 
 /**
+ * Returns the matrix to rotate, translate, and reflect (if needed) from the element's
+ * top-left origin into the actual writing mode and text direction applied to the element.
+ *
+ * @param  {Element} element
+ *         The current element.
+ * @param  {CSSStyleDeclaration} computedStyle
+ *         The computed style for the element.
+ * @return {Array}
+ *         The matrix with adjustments for writing mode and text direction, if any.
+ */
+function getWritingModeMatrix(element, computedStyle) {
+  let currentMatrix = identity();
+  let { direction, writingMode } = computedStyle;
+
+  switch (writingMode) {
+    case "horizontal-tb":
+      // This is the initial value.  No further adjustment needed.
+      break;
+    case "vertical-rl":
+      currentMatrix = multiply(
+        translate(element.offsetWidth, 0),
+        rotate(-Math.PI / 2)
+      );
+      break;
+    case "vertical-lr":
+      currentMatrix = multiply(
+        reflectAboutY(),
+        rotate(-Math.PI / 2)
+      );
+      break;
+    case "sideways-rl":
+      currentMatrix = multiply(
+        translate(element.offsetWidth, 0),
+        rotate(-Math.PI / 2)
+      );
+      break;
+    case "sideways-lr":
+      currentMatrix = multiply(
+        rotate(Math.PI / 2),
+        translate(-element.offsetHeight, 0)
+      );
+      break;
+    default:
+      console.error(`Unexpected writing-mode: ${writingMode}`);
+  }
+
+  switch (direction) {
+    case "ltr":
+      // This is the initial value.  No further adjustment needed.
+      break;
+    case "rtl":
+      let rowLength = element.offsetWidth;
+      if (writingMode != "horizontal-tb") {
+        rowLength = element.offsetHeight;
+      }
+      currentMatrix = multiply(currentMatrix, translate(rowLength, 0));
+      currentMatrix = multiply(currentMatrix, reflectAboutY());
+      break;
+    default:
+      console.error(`Unexpected direction: ${direction}`);
+  }
+
+  return currentMatrix;
+}
+
+/**
  * Updates the <canvas> element's style in accordance with the current window's
  * device pixel ratio, and the position calculated in `getCanvasPosition`. It also
  * clears the drawing context. This is called on canvas update after a scroll event where
  * `getCanvasPosition` updates the new canvasPosition.
  *
  * @param  {Canvas} canvas
  *         The <canvas> element.
  * @param  {Object} canvasPosition
--- a/devtools/shared/layout/dom-matrix-2d.js
+++ b/devtools/shared/layout/dom-matrix-2d.js
@@ -40,16 +40,30 @@ exports.scale = scale;
 const translate = (tx = 0, ty = tx) => [
   1, 0, tx,
   0, 1, ty,
   0, 0, 1
 ];
 exports.translate = translate;
 
 /**
+ * Returns a matrix that reflects about the Y axis.  For example, the point (x1, y1) would
+ * become (-x1, y1).
+ *
+ * @return {Array}
+ *         The new matrix.
+ */
+const reflectAboutY = () => [
+  -1, 0, 0,
+  0,  1, 0,
+  0,  0, 1,
+];
+exports.reflectAboutY = reflectAboutY;
+
+/**
  * Returns a matrix for the rotation given.
  * Calling `rotate()` or `rotate(0)` returns a new identity matrix.
  *
  * @param {Number} [angle = 0]
  *        The angle, in radians, for which to return a corresponding rotation matrix.
  *        If unspecified, it will equal `0`.
  * @return {Array}
  *         The new matrix.