Bug 1430916 - Rotate grid line numbers for writing mode. r=pbro draft
authorJ. Ryan Stinnett <jryans@gmail.com>
Tue, 30 Jan 2018 23:31:42 -0500
changeset 750418 6996b4d4f7e12718c6ae406cbcbd53f13746c2bd
parent 748520 9a3b6d64a64b328ed0de3d6503b99f20d1c94cfb
push id97650
push userbmo:jryans@gmail.com
push dateFri, 02 Feb 2018 03:30:13 +0000
reviewerspbro
bugs1430916
milestone60.0a1
Bug 1430916 - Rotate grid line numbers for writing mode. r=pbro Adjust grid highlighter line numbers to rotate to the expected position outside the grid container even in the presence of writing mode and text direction. This is also refactors and preserves the recently added behavior to flip line numbers inside the container if they are near the viewport edge. MozReview-Commit-ID: 4zFjJY4Iqdz
devtools/server/actors/highlighters/css-grid.js
devtools/server/actors/highlighters/utils/canvas.js
--- a/devtools/server/actors/highlighters/css-grid.js
+++ b/devtools/server/actors/highlighters/css-grid.js
@@ -1,14 +1,16 @@
 /* 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 { AutoRefreshHighlighter } = require("./auto-refresh");
 const {
   CANVAS_SIZE,
   DEFAULT_COLOR,
   drawBubbleRect,
   drawLine,
   drawRect,
   drawRoundedRect,
@@ -18,16 +20,17 @@ const {
   getPointsFromDiagonal,
   updateCanvasElement,
   updateCanvasPosition,
 } = require("./utils/canvas");
 const {
   CanvasFrameAnonymousContentHelper,
   createNode,
   createSVGNode,
+  getComputedStyle,
   moveInfobar,
 } = require("./utils/markup");
 const { apply } = require("devtools/shared/layout/dom-matrix-2d");
 const {
   getCurrentZoom,
   getDisplayPixelRatio,
   setIgnoreLayoutChanges,
 } = require("devtools/shared/layout/utils");
@@ -68,16 +71,60 @@ const GRID_GAP_PATTERN_WIDTH = 14; // px
 const GRID_GAP_PATTERN_HEIGHT = 14; // px
 const GRID_GAP_PATTERN_LINE_DASH = [5, 3]; // px
 const GRID_GAP_ALPHA = 0.5;
 
 // 25 is a good margin distance between the document grid container edge without cutting
 // off parts of the arrow box container.
 const OFFSET_FROM_EDGE = 25;
 
+// 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");
+});
+
+/**
+ * Given an `edge` of a box, return the name of the edge one move to the right.
+ */
+function rotateEdgeRight(edge) {
+  switch (edge) {
+    case "top": return "right";
+    case "right": return "bottom";
+    case "bottom": return "left";
+    case "left": return "top";
+    default: return edge;
+  }
+}
+
+/**
+ * Given an `edge` of a box, return the name of the edge one move to the left.
+ */
+function rotateEdgeLeft(edge) {
+  switch (edge) {
+    case "top": return "left";
+    case "right": return "top";
+    case "bottom": return "right";
+    case "left": return "bottom";
+    default: return edge;
+  }
+}
+
+/**
+ * Given an `edge` of a box, return the name of the opposite edge.
+ */
+function reflectEdge(edge) {
+  switch (edge) {
+    case "top": return "bottom";
+    case "right": return "left";
+    case "bottom": return "top";
+    case "left": return "right";
+    default: return edge;
+  }
+}
+
 /**
  * Cached used by `CssGridHighlighter.getGridGapPattern`.
  */
 const gCachedGridPattern = new Map();
 
 /**
  * The CssGridHighlighter is the class that overlays a visual grid on top of
  * display:[inline-]grid elements.
@@ -1180,119 +1227,134 @@ class CssGridHighlighter extends AutoRef
     this.ctx.strokeStyle = this.color;
     this.ctx.fillStyle = "white";
 
     // See param definitions of drawBubbleRect.
     let radius = 2 * displayPixelRatio;
     let margin = 2 * displayPixelRatio;
     let arrowSize = 8 * displayPixelRatio;
 
-    let minOffsetFromEdge = OFFSET_FROM_EDGE * displayPixelRatio;
-
     let minBoxSize = arrowSize * 2 + padding;
     boxWidth = Math.max(boxWidth, minBoxSize);
     boxHeight = Math.max(boxHeight, minBoxSize);
 
-    let { width, height } = this._winDimensions;
-
-    let boxAlignment;
-    let textCenterPos;
-
+    // Determine default box edge to aim the line number arrow at.
+    let boxEdge;
     if (dimensionType === COLUMNS) {
       if (lineNumber > 0) {
-        boxAlignment = "top";
-        textCenterPos = (boxHeight + arrowSize + radius) - boxHeight / 2;
-
-        // If there is not enough space for the box number, flip its alignment and
-        // its text position.
-        if (y <= minOffsetFromEdge) {
-          boxAlignment = "bottom";
-          textCenterPos = -((boxHeight + arrowSize + radius) - boxHeight / 2);
-
-          // Maintain a consistent margin between the pointer and viewport edge when the
-          // grid edge falls out of view. We can use the arrow text's padding value to
-          // maintain this distance.
-          if (y + padding < 0 || y === padding) {
-            y = padding;
-          } else {
-            // If the grid edge is still visible, increment the pointer position by its
-            // arrow size so that the it does not cross over edge.
-            y += arrowSize;
-          }
-        }
-
-        drawBubbleRect(this.ctx, x, y, boxWidth, boxHeight, radius, margin, arrowSize,
-          boxAlignment);
-
-        // After drawing the number box, we need to center the x/y coordinates of the
-        // number text written it.
-        y -= textCenterPos;
+        boxEdge = "top";
       } else {
-        boxAlignment = "bottom";
-        textCenterPos = (boxHeight + arrowSize + radius) - boxHeight / 2;
-
-        // Flip the negative number when its position reaches 95% of the document's
-        // height/width.
-        if (y / displayPixelRatio >= height * .95) {
-          boxAlignment = "top";
-          textCenterPos = -((boxHeight + arrowSize + radius) - boxHeight / 2);
-
-          if (y + padding > height) {
-            y -= arrowSize;
-          }
-        }
-
-        drawBubbleRect(this.ctx, x, y, boxWidth, boxHeight, radius, margin, arrowSize,
-                       boxAlignment);
-
-        y += textCenterPos;
+        boxEdge = "bottom";
+      }
+    }
+    if (dimensionType === ROWS) {
+      if (lineNumber > 0) {
+        boxEdge = "left";
+      } else {
+        boxEdge = "right";
       }
     }
 
-    if (dimensionType === ROWS) {
-      if (lineNumber > 0) {
-        boxAlignment = "left";
-        textCenterPos = (boxWidth + arrowSize + radius) - boxWidth / 2;
-
-        if (x <= minOffsetFromEdge) {
-          boxAlignment = "right";
-          textCenterPos = -((boxWidth + arrowSize + radius) - boxWidth / 2);
+    // Rotate box edge as needed for writing mode and text direction.
+    if (WRITING_MODE_ADJUST_ENABLED) {
+      let { direction, writingMode } = getComputedStyle(this.currentNode);
 
-          // See comment above for maintaining a consistent distance between the arrow box
-          // pointer and the edge of the viewport.
-          if (x + padding < 0 || x === padding) {
-            x = padding;
+      switch (writingMode) {
+        case "horizontal-tb":
+          // This is the initial value.  No further adjustment needed.
+          break;
+        case "vertical-rl":
+          boxEdge = rotateEdgeRight(boxEdge);
+          break;
+        case "vertical-lr":
+          if (dimensionType === COLUMNS) {
+            boxEdge = rotateEdgeLeft(boxEdge);
           } else {
-            x += arrowSize;
+            boxEdge = rotateEdgeRight(boxEdge);
           }
-        }
+          break;
+        case "sideways-rl":
+          boxEdge = rotateEdgeRight(boxEdge);
+          break;
+        case "sideways-lr":
+          boxEdge = rotateEdgeLeft(boxEdge);
+          break;
+        default:
+          console.error(`Unexpected writing-mode: ${writingMode}`);
+      }
 
-        drawBubbleRect(this.ctx, x, y, boxWidth, boxHeight, radius, margin, arrowSize,
-                       boxAlignment);
+      switch (direction) {
+        case "ltr":
+          // This is the initial value.  No further adjustment needed.
+          break;
+        case "rtl":
+          if (dimensionType === ROWS) {
+            boxEdge = reflectEdge(boxEdge);
+          }
+          break;
+        default:
+          console.error(`Unexpected direction: ${direction}`);
+      }
+    }
+
+    // Default to drawing outside the edge, but move inside when close to viewport.
+    let minOffsetFromEdge = OFFSET_FROM_EDGE * displayPixelRatio;
+    let { width, height } = this._winDimensions;
+    width *= displayPixelRatio;
+    height *= displayPixelRatio;
 
-        x -= textCenterPos;
-      } else {
-        boxAlignment = "right";
-        textCenterPos = (boxWidth + arrowSize + radius) - boxWidth / 2;
-
-        // See above comment for flipping negative numbers .
-        if (x / displayPixelRatio >= width * .95) {
-          boxAlignment = "left";
-          textCenterPos = -((boxWidth + arrowSize + radius) - boxWidth / 2);
+    // Check if the x or y position of the line number's arrow is too close to the edge
+    // of the window.  If it is too close, flip the arrow around (by reflecting the box
+    // edge we're thinking of) and adjust the position by 2 x padding since we're now
+    // going the opposite direction.
+    switch (boxEdge) {
+      case "left":
+        if (x < minOffsetFromEdge) {
+          boxEdge = reflectEdge(boxEdge);
+          x += 2 * padding;
+        }
+        break;
+      case "right":
+        if ((width - x) < minOffsetFromEdge) {
+          boxEdge = reflectEdge(boxEdge);
+          x -= 2 * padding;
+        }
+        break;
+      case "top":
+        if (y < minOffsetFromEdge) {
+          boxEdge = reflectEdge(boxEdge);
+          y += 2 * padding;
+        }
+        break;
+      case "bottom":
+        if ((height - y) < minOffsetFromEdge) {
+          boxEdge = reflectEdge(boxEdge);
+          y -= 2 * padding;
+        }
+        break;
+    }
 
-          if (x + padding > width) {
-            x -= arrowSize;
-          }
-        }
+    // Draw the bubble rect to show the arrow.
+    drawBubbleRect(this.ctx, x, y, boxWidth, boxHeight, radius, margin, arrowSize,
+                   boxEdge);
 
-        drawBubbleRect(this.ctx, x, y, boxWidth, boxHeight, radius, margin, arrowSize,
-                       boxAlignment);
-
-        x += textCenterPos;
-      }
+    // Adjust position based on the edge.
+    switch (boxEdge) {
+      case "left":
+        x -= (boxWidth + arrowSize + radius) - boxWidth / 2;
+        break;
+      case "right":
+        x += (boxWidth + arrowSize + radius) - boxWidth / 2;
+        break;
+      case "top":
+        y -= (boxHeight + arrowSize + radius) - boxHeight / 2;
+        break;
+      case "bottom":
+        y += (boxHeight + arrowSize + radius) - boxHeight / 2;
+        break;
     }
 
     // Write the line number inside of the rectangle.
     this.ctx.textAlign = "center";
     this.ctx.textBaseline = "middle";
     this.ctx.fillStyle = "black";
     const numberText = stackedLineIndex ? "" : lineNumber;
     this.ctx.fillText(numberText, x, y);
--- a/devtools/server/actors/highlighters/utils/canvas.js
+++ b/devtools/server/actors/highlighters/utils/canvas.js
@@ -14,16 +14,17 @@ const {
   isIdentity,
   multiply,
   reflectAboutY,
   rotate,
   scale,
   translate,
 } = require("devtools/shared/layout/dom-matrix-2d");
 const { getViewportDimensions } = require("devtools/shared/layout/utils");
+const { getComputedStyle } = require("./markup");
 
 // A set of utility functions for highlighters that render their content to a <canvas>
 // element.
 
 // We create a <canvas> element that has always 4096x4096 physical pixels, to displays
 // our grid's overlay.
 // Then, we move the element around when needed, to give the perception that it always
 // covers the screen (See bug 1345434).
@@ -285,17 +286,17 @@ function getBoundsFromPoints(points) {
  *         The window object.
  * @return {Object} An object with the following properties:
  *         - {Array} currentMatrix
  *           The current matrix.
  *         - {Boolean} hasNodeTransformations
  *           true if the node has transformed and false otherwise.
  */
 function getCurrentMatrix(element, window) {
-  let computedStyle = element.ownerGlobal.getComputedStyle(element);
+  let computedStyle = getComputedStyle(element);
 
   let paddingTop = parseFloat(computedStyle.paddingTop);
   let paddingLeft = parseFloat(computedStyle.paddingLeft);
   let borderTop = parseFloat(computedStyle.borderTopWidth);
   let borderLeft = parseFloat(computedStyle.borderLeftWidth);
 
   let nodeMatrix = getNodeTransformationMatrix(element, window.document.documentElement);