Bug 1430918 - Rotate grid outline for writing mode. r=gl draft
authorJ. Ryan Stinnett <jryans@gmail.com>
Sat, 10 Feb 2018 22:03:12 -0600
changeset 753978 cc4fe98a1086ad123e7e02f98f010f719818fc10
parent 753977 ee4a6bae902d1ed7101e09876c7947e63d7d2cac
child 753988 d926d5e5579a73e2e39728297b557c5db73ed183
child 754050 b2ae6982239b1c7e64a281ccf207fd42685626aa
push id98727
push userbmo:jryans@gmail.com
push dateMon, 12 Feb 2018 18:15:41 +0000
reviewersgl
bugs1430918
milestone60.0a1
Bug 1430918 - Rotate grid outline for writing mode. r=gl Adjust the grid outline in the Inspector's Layout panel as needed to match the writing mode and text direction of the grid container. MozReview-Commit-ID: Ggcp1e4ZipE
devtools/client/inspector/grids/components/GridOutline.js
devtools/client/inspector/grids/grid-inspector.js
devtools/client/inspector/grids/test/browser.ini
devtools/client/inspector/grids/test/browser_grids_grid-outline-highlight-area.js
devtools/client/inspector/grids/test/browser_grids_grid-outline-highlight-cell.js
devtools/client/inspector/grids/test/browser_grids_grid-outline-selected-grid.js
devtools/client/inspector/grids/test/browser_grids_grid-outline-writing-mode.js
devtools/client/inspector/grids/test/head.js
devtools/client/inspector/grids/types.js
devtools/server/actors/highlighters/utils/canvas.js
devtools/server/actors/layout.js
devtools/shared/fronts/layout.js
devtools/shared/layout/dom-matrix-2d.js
--- a/devtools/client/inspector/grids/components/GridOutline.js
+++ b/devtools/client/inspector/grids/components/GridOutline.js
@@ -1,32 +1,42 @@
 /* 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 { PureComponent } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const { getStr } = require("devtools/client/inspector/layout/utils/l10n");
+const {
+  getWritingModeMatrix,
+  getCSSMatrixTransform,
+} = require("devtools/shared/layout/dom-matrix-2d");
 
 const Types = require("../types");
 
 // The delay prior to executing the grid cell highlighting.
 const GRID_HIGHLIGHTING_DEBOUNCE = 50;
 
 // Prefs for the max number of rows/cols a grid container can have for
 // the outline to display.
 const GRID_OUTLINE_MAX_ROWS_PREF =
   Services.prefs.getIntPref("devtools.gridinspector.gridOutlineMaxRows");
 const GRID_OUTLINE_MAX_COLUMNS_PREF =
   Services.prefs.getIntPref("devtools.gridinspector.gridOutlineMaxColumns");
 
+// 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");
+});
+
 // Move SVG grid to the right 100 units, so that it is not flushed against the edge of
 // layout border
 const TRANSLATE_X = 0;
 const TRANSLATE_Y = 0;
 
 const GRID_CELL_SCALE_FACTOR = 50;
 
 const VIEWPORT_MIN_HEIGHT = 100;
@@ -91,17 +101,17 @@ class GridOutline extends PureComponent 
     const {
       grids,
       onShowGridAreaHighlight,
       onShowGridCellHighlight,
     } = this.props;
     const name = target.dataset.gridAreaName;
     const id = target.dataset.gridId;
     const fragmentIndex = target.dataset.gridFragmentIndex;
-    const color = target.closest(".grid-cell-group").dataset.gridLineColor;
+    const color = target.closest(".grid-outline-group").dataset.gridLineColor;
     const rowNumber = target.dataset.gridRow;
     const columnNumber = target.dataset.gridColumn;
 
     onShowGridAreaHighlight(grids[id].nodeFront, null, color);
     onShowGridCellHighlight(grids[id].nodeFront, color);
 
     if (hide) {
       return;
@@ -177,16 +187,24 @@ class GridOutline extends PureComponent 
       height += GRID_CELL_SCALE_FACTOR * (rows.tracks[i].breadth / 100);
     }
 
     let width = 0;
     for (let i = 0; i < cols.lines.length - 1; i++) {
       width += GRID_CELL_SCALE_FACTOR * (cols.tracks[i].breadth / 100);
     }
 
+    if (WRITING_MODE_ADJUST_ENABLED) {
+      // All writing modes other than horizontal-tb (the initial value) involve a 90 deg
+      // rotation, so swap width and height.
+      if (grid.writingMode != "horizontal-tb") {
+        [ width, height ] = [ height, width ];
+      }
+    }
+
     return { width, height };
   }
 
   /**
    * Displays a message text "Cannot show outline for this grid".
    */
   renderCannotShowOutlineText() {
     return dom.div(
@@ -239,22 +257,40 @@ class GridOutline extends PureComponent 
         rectangles.push(gridCell);
         x += width;
       }
 
       x = 0;
       y += height;
     }
 
+    // Transform the cells as needed to match the grid container's writing mode.
+    let cellGroupStyle = {};
+
+    if (WRITING_MODE_ADJUST_ENABLED) {
+      let writingModeMatrix = getWritingModeMatrix(this.state, grid);
+      cellGroupStyle.transform = getCSSMatrixTransform(writingModeMatrix);
+    }
+
+    let cellGroup = dom.g(
+      {
+        id: "grid-cell-group",
+        style: cellGroupStyle,
+      },
+      rectangles
+    );
+
     // Draw a rectangle that acts as the grid outline border.
     const border = this.renderGridOutlineBorder(this.state.width, this.state.height,
                                                 color);
-    rectangles.unshift(border);
 
-    return rectangles;
+    return [
+      border,
+      cellGroup,
+    ];
   }
 
   /**
    * Renders the grid cell of a grid fragment.
    *
    * @param  {Number} id
    *         The grid id stored on the grid fragment
    * @param  {Number} gridFragmentIndex
@@ -273,18 +309,18 @@ class GridOutline extends PureComponent 
    *         The width of grid cell.
    * @param  {Number} height
    *         The height of the grid cell.
    */
   renderGridCell(id, gridFragmentIndex, x, y, rowNumber, columnNumber, color,
     gridAreaName, width, height) {
     return dom.rect(
       {
-        "key": `${id}-${rowNumber}-${columnNumber}`,
-        "className": "grid-outline-cell",
+        key: `${id}-${rowNumber}-${columnNumber}`,
+        className: "grid-outline-cell",
         "data-grid-area-name": gridAreaName,
         "data-grid-fragment-index": gridFragmentIndex,
         "data-grid-id": id,
         "data-grid-row": rowNumber,
         "data-grid-column": columnNumber,
         x,
         y,
         width,
@@ -296,20 +332,20 @@ class GridOutline extends PureComponent 
     );
   }
 
   renderGridOutline(grid) {
     let { color } = grid;
 
     return dom.g(
       {
-        id: "grid-cell-group",
-        "className": "grid-cell-group",
+        id: "grid-outline-group",
+        className: "grid-outline-group",
         "data-grid-line-color": color,
-        "style": { color }
+        style: { color }
       },
       this.renderGrid(grid)
     );
   }
 
   renderGridOutlineBorder(borderWidth, borderHeight, color) {
     return dom.rect(
       {
--- a/devtools/client/inspector/grids/grid-inspector.js
+++ b/devtools/client/inspector/grids/grid-inspector.js
@@ -327,19 +327,21 @@ class GridInspector {
 
       let fallbackColor = GRID_COLORS[i % GRID_COLORS.length];
       let color = this.getInitialGridColor(nodeFront, fallbackColor);
 
       grids.push({
         id: i,
         actorID: grid.actorID,
         color,
+        direction: grid.direction,
         gridFragments: grid.gridFragments,
         highlighted: nodeFront == this.highlighters.gridHighlighterShown,
         nodeFront,
+        writingMode: grid.writingMode,
       });
     }
 
     this.store.dispatch(updateGrids(grids));
   }
 
   /**
    * Handler for "grid-highlighter-shown" and "grid-highlighter-hidden" events emitted
--- a/devtools/client/inspector/grids/test/browser.ini
+++ b/devtools/client/inspector/grids/test/browser.ini
@@ -26,11 +26,12 @@ support-files =
 [browser_grids_grid-list-on-mutation-element-removed.js]
 [browser_grids_grid-list-toggle-multiple-grids.js]
 [browser_grids_grid-list-toggle-single-grid.js]
 [browser_grids_grid-outline-cannot-show-outline.js]
 [browser_grids_grid-outline-highlight-area.js]
 [browser_grids_grid-outline-highlight-cell.js]
 [browser_grids_grid-outline-selected-grid.js]
 [browser_grids_grid-outline-updates-on-grid-change.js]
+[browser_grids_grid-outline-writing-mode.js]
 [browser_grids_highlighter-setting-rules-grid-toggle.js]
 [browser_grids_number-of-css-grids-telemetry.js]
 [browser_grids_restored-after-reload.js]
--- a/devtools/client/inspector/grids/test/browser_grids_grid-outline-highlight-area.js
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-outline-highlight-area.js
@@ -37,35 +37,35 @@ add_task(function* () {
   // Don't track reflows since this might cause intermittent failures.
   inspector.reflowTracker.untrackReflows(gridInspector, gridInspector.onReflow);
 
   let gridList = doc.getElementById("grid-list");
   let checkbox = gridList.children[0].querySelector("input");
 
   info("Toggling ON the CSS grid highlighter from the layout panel.");
   let onHighlighterShown = highlighters.once("grid-highlighter-shown");
-  let onGridOutlineRendered = waitForDOM(doc, "#grid-cell-group rect", 3);
+  let onGridOutlineRendered = waitForDOM(doc, "#grid-cell-group rect", 2);
   let onCheckboxChange = waitUntilState(store, state =>
     state.grids.length == 1 &&
     state.grids[0].highlighted);
   checkbox.click();
   yield onCheckboxChange;
   yield onHighlighterShown;
   let elements = yield onGridOutlineRendered;
 
-  let gridCellA = elements[1];
+  let gridCellA = elements[0];
 
   info("Hovering over grid cell A in the grid outline.");
   let onCellAHighlight = highlighters.once("grid-highlighter-shown",
     (event, nodeFront, options) => {
       info("Checking the grid highlighter options for the show grid area" +
       "and cell parameters.");
       const { showGridCell, showGridArea } = options;
       const { gridFragmentIndex, rowNumber, columnNumber } = showGridCell;
 
       is(gridFragmentIndex, 0, "Should be the first grid fragment index.");
       is(rowNumber, 1, "Should be the first grid row.");
       is(columnNumber, 1, "Should be the first grid column.");
       is(showGridArea, "header", "Grid area name should be 'header'.");
     });
-  EventUtils.synthesizeMouse(gridCellA, 5, 5, {type: "mouseover"}, doc.defaultView);
+  EventUtils.synthesizeMouse(gridCellA, 1, 1, {type: "mouseover"}, doc.defaultView);
   yield onCellAHighlight;
 });
--- a/devtools/client/inspector/grids/test/browser_grids_grid-outline-highlight-cell.js
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-outline-highlight-cell.js
@@ -28,33 +28,33 @@ add_task(function* () {
   // Don't track reflows since this might cause intermittent failures.
   inspector.reflowTracker.untrackReflows(gridInspector, gridInspector.onReflow);
 
   let gridList = doc.getElementById("grid-list");
   let checkbox = gridList.children[0].querySelector("input");
 
   info("Toggling ON the CSS grid highlighter from the layout panel.");
   let onHighlighterShown = highlighters.once("grid-highlighter-shown");
-  let onGridOutlineRendered = waitForDOM(doc, "#grid-cell-group rect", 3);
+  let onGridOutlineRendered = waitForDOM(doc, "#grid-cell-group rect", 2);
   let onCheckboxChange = waitUntilState(store, state =>
     state.grids.length == 1 &&
     state.grids[0].highlighted);
   checkbox.click();
   yield onCheckboxChange;
   yield onHighlighterShown;
   let elements = yield onGridOutlineRendered;
 
-  let gridCellA = elements[1];
+  let gridCellA = elements[0];
 
   info("Hovering over grid cell A in the grid outline.");
   let onCellAHighlight = highlighters.once("grid-highlighter-shown",
     (event, nodeFront, options) => {
       info("Checking show grid cell options are correct.");
       const { showGridCell } = options;
       const { gridFragmentIndex, rowNumber, columnNumber } = showGridCell;
 
       is(gridFragmentIndex, 0, "Should be the first grid fragment index.");
       is(rowNumber, 1, "Should be the first grid row.");
       is(columnNumber, 1, "Should be the first grid column.");
     });
-  EventUtils.synthesizeMouse(gridCellA, 10, 5, {type: "mouseover"}, doc.defaultView);
+  EventUtils.synthesizeMouse(gridCellA, 1, 1, {type: "mouseover"}, doc.defaultView);
   yield onCellAHighlight;
 });
--- a/devtools/client/inspector/grids/test/browser_grids_grid-outline-selected-grid.js
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-outline-selected-grid.js
@@ -32,17 +32,17 @@ add_task(function* () {
   ok(!doc.getElementById("grid-outline-container"),
     "There should be no grid outline shown.");
 
   info("Toggling ON the CSS grid highlighter from the layout panel.");
   let onHighlighterShown = highlighters.once("grid-highlighter-shown");
   let onCheckboxChange = waitUntilState(store, state =>
     state.grids.length == 1 &&
     state.grids[0].highlighted);
-  let onGridOutlineRendered = waitForDOM(doc, "#grid-cell-group rect", 4);
+  let onGridOutlineRendered = waitForDOM(doc, "#grid-cell-group rect", 3);
   checkbox.click();
   yield onHighlighterShown;
   yield onCheckboxChange;
   let elements = yield onGridOutlineRendered;
 
   info("Checking the grid outline is shown.");
-  is(elements.length, 4, "Grid outline is shown.");
+  is(elements.length, 3, "Grid outline is shown.");
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/grids/test/browser_grids_grid-outline-writing-mode.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the grid outline adjusts to match the container's writing mode.
+
+const TEST_URI = `
+  <style type='text/css'>
+    .grid {
+      display: grid;
+      width: 400px;
+      height: 300px;
+    }
+    .rtl {
+      direction: rtl;
+    }
+    .v-rl {
+      writing-mode: vertical-rl;
+    }
+    .v-lr {
+      writing-mode: vertical-lr;
+    }
+    .s-rl {
+      writing-mode: sideways-rl;
+    }
+    .s-lr {
+      writing-mode: sideways-lr;
+    }
+  </style>
+  <div class="grid">
+    <div id="cella">Cell A</div>
+    <div id="cellb">Cell B</div>
+    <div id="cellc">Cell C</div>
+  </div>
+  <div class="grid rtl">
+    <div id="cella">Cell A</div>
+    <div id="cellb">Cell B</div>
+    <div id="cellc">Cell C</div>
+  </div>
+  <div class="grid v-rl">
+    <div id="cella">Cell A</div>
+    <div id="cellb">Cell B</div>
+    <div id="cellc">Cell C</div>
+  </div>
+  <div class="grid v-lr">
+    <div id="cella">Cell A</div>
+    <div id="cellb">Cell B</div>
+    <div id="cellc">Cell C</div>
+  </div>
+  <div class="grid s-rl">
+    <div id="cella">Cell A</div>
+    <div id="cellb">Cell B</div>
+    <div id="cellc">Cell C</div>
+  </div>
+  <div class="grid s-lr">
+    <div id="cella">Cell A</div>
+    <div id="cellb">Cell B</div>
+    <div id="cellc">Cell C</div>
+  </div>
+`;
+
+add_task(async function () {
+  await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+  let { inspector, gridInspector } = await openLayoutView();
+  let { document: doc } = gridInspector;
+  let { highlighters, store } = inspector;
+
+  info("Checking the initial state of the Grid Inspector.");
+  ok(!doc.getElementById("grid-outline-container"),
+    "There should be no grid outline shown.");
+
+  let elements;
+
+  elements = await enableGrid(doc, highlighters, store, 0);
+  is(elements[0].style.transform,
+    "matrix(1, 0, 0, 1, 0, 0)",
+    "Transform matches for horizontal-tb and ltr.");
+  await disableGrid(doc, highlighters, store, 0);
+
+  elements = await enableGrid(doc, highlighters, store, 1);
+  is(elements[0].style.transform,
+    "matrix(-1, 0, 0, 1, 200, 0)",
+    "Transform matches for horizontal-tb and rtl");
+  await disableGrid(doc, highlighters, store, 1);
+
+  elements = await enableGrid(doc, highlighters, store, 2);
+  is(elements[0].style.transform,
+    "matrix(6.12323e-17, 1, -1, 6.12323e-17, 200, 0)",
+    "Transform matches for vertical-rl and ltr");
+  await disableGrid(doc, highlighters, store, 2);
+
+  elements = await enableGrid(doc, highlighters, store, 3);
+  is(elements[0].style.transform,
+    "matrix(-6.12323e-17, 1, 1, 6.12323e-17, 0, 0)",
+    "Transform matches for vertical-lr and ltr");
+  await disableGrid(doc, highlighters, store, 3);
+
+  elements = await enableGrid(doc, highlighters, store, 4);
+  is(elements[0].style.transform,
+    "matrix(6.12323e-17, 1, -1, 6.12323e-17, 200, 0)",
+    "Transform matches for sideways-rl and ltr");
+  await disableGrid(doc, highlighters, store, 4);
+
+  elements = await enableGrid(doc, highlighters, store, 5);
+  is(elements[0].style.transform,
+    "matrix(6.12323e-17, -1, 1, 6.12323e-17, -9.18485e-15, 150)",
+    "Transform matches for sideways-lr and ltr");
+  await disableGrid(doc, highlighters, store, 5);
+});
+
+async function enableGrid(doc, highlighters, store, index) {
+  info(`Enabling the CSS grid highlighter for grid ${index}.`);
+  let onHighlighterShown = highlighters.once("grid-highlighter-shown");
+  let onCheckboxChange = waitUntilState(store, state =>
+    state.grids.length == 6 &&
+    state.grids[index].highlighted);
+  let onGridOutlineRendered = waitForDOM(doc, "#grid-cell-group");
+  let gridList = doc.getElementById("grid-list");
+  gridList.children[index].querySelector("input").click();
+  await onHighlighterShown;
+  await onCheckboxChange;
+  return onGridOutlineRendered;
+}
+
+async function disableGrid(doc, highlighters, store, index) {
+  info(`Disabling the CSS grid highlighter for grid ${index}.`);
+  let onHighlighterShown = highlighters.once("grid-highlighter-hidden");
+  let onCheckboxChange = waitUntilState(store, state =>
+    state.grids.length == 6 &&
+    !state.grids[index].highlighted);
+  let onGridOutlineRemoved = waitForDOM(doc, "#grid-cell-group", 0);
+  let gridList = doc.getElementById("grid-list");
+  gridList.children[index].querySelector("input").click();
+  await onHighlighterShown;
+  await onCheckboxChange;
+  return onGridOutlineRemoved;
+}
--- a/devtools/client/inspector/grids/test/head.js
+++ b/devtools/client/inspector/grids/test/head.js
@@ -11,18 +11,20 @@ Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
   this);
 
 // Load the shared Redux helpers into this compartment.
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/framework/test/shared-redux-head.js",
   this);
 
+Services.prefs.setBoolPref("devtools.highlighter.writingModeAdjust", true);
 Services.prefs.setIntPref("devtools.toolbox.footer.height", 350);
 registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("devtools.highlighter.writingModeAdjust");
   Services.prefs.clearUserPref("devtools.toolbox.footer.height");
 });
 
 const HIGHLIGHTER_TYPE = "CssGridHighlighter";
 
 /**
  * Simulate a color change in a given color picker tooltip.
  *
--- a/devtools/client/inspector/grids/types.js
+++ b/devtools/client/inspector/grids/types.js
@@ -11,24 +11,30 @@ const PropTypes = require("devtools/clie
  */
 exports.grid = {
   // The id of the grid
   id: PropTypes.number,
 
   // The color for the grid overlay highlighter
   color: PropTypes.string,
 
+  // The text direction of the grid container
+  direction: PropTypes.string,
+
   // The grid fragment object of the grid container
   gridFragments: PropTypes.array,
 
   // Whether or not the grid highlighter is highlighting the grid
   highlighted: PropTypes.bool,
 
   // The node front of the grid container
   nodeFront: PropTypes.object,
+
+  // The writing mode of the grid container
+  writingMode: PropTypes.string,
 };
 
 /**
  * The grid highlighter settings on what to display in its grid overlay in the document.
  */
 exports.highlighterSettings = {
   // Whether or not the grid highlighter should show the grid line numbers
   showGridLineNumbers: PropTypes.bool,
--- a/devtools/server/actors/highlighters/utils/canvas.js
+++ b/devtools/server/actors/highlighters/utils/canvas.js
@@ -5,21 +5,20 @@
 "use strict";
 
 const Services = require("Services");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 
 const {
   apply,
   getNodeTransformationMatrix,
+  getWritingModeMatrix,
   identity,
   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.
@@ -316,17 +315,21 @@ function getCurrentMatrix(element, windo
   }
 
   // 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);
+    let size = {
+      width: element.offsetWidth,
+      height: element.offsetHeight,
+    };
+    let writingModeMatrix = getWritingModeMatrix(size, computedStyle);
     if (!isIdentity(writingModeMatrix)) {
       currentMatrix = multiply(currentMatrix, writingModeMatrix);
     }
   }
 
   return { currentMatrix, hasNodeTransformations };
 }
 
@@ -371,82 +374,16 @@ 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/server/actors/layout.js
+++ b/devtools/server/actors/layout.js
@@ -97,19 +97,24 @@ const GridActor = ActorClassWithSpec(gri
       return this.actorID;
     }
 
     // Seralize the grid fragment data into JSON so protocol.js knows how to write
     // and read the data.
     let gridFragments = this.containerEl.getGridFragments();
     this.gridFragments = getStringifiableFragments(gridFragments);
 
+    // Record writing mode and text direction for use by the grid outline.
+    let { direction, writingMode } = CssLogic.getComputedStyle(this.containerEl);
+
     let form = {
       actor: this.actorID,
+      direction,
       gridFragments: this.gridFragments,
+      writingMode,
     };
 
     // If the WalkerActor already knows the container element, then also return its
     // ActorID so we avoid the client from doing another round trip to get it in many
     // cases.
     if (this.walker.hasNode(this.containerEl)) {
       form.containerNodeActorID = this.walker.getNode(this.containerEl).actorID;
     }
--- a/devtools/shared/fronts/layout.js
+++ b/devtools/shared/fronts/layout.js
@@ -46,20 +46,44 @@ const GridFront = FrontClassWithSpec(gri
     if (!this._form.containerNodeActorID) {
       return null;
     }
 
     return this.conn.getActor(this._form.containerNodeActorID);
   },
 
   /**
+   * Get the text direction of the grid container.
+   * Added in Firefox 60.
+   */
+  get direction() {
+    if (!this._form.direction) {
+      return "ltr";
+    }
+
+    return this._form.direction;
+  },
+
+  /**
    * Getter for the grid fragments data.
    */
   get gridFragments() {
     return this._form.gridFragments;
   },
+
+  /**
+   * Get the writing mode of the grid container.
+   * Added in Firefox 60.
+   */
+  get writingMode() {
+    if (!this._form.writingMode) {
+      return "horizontal-tb";
+    }
+
+    return this._form.writingMode;
+  },
 });
 
 const LayoutFront = FrontClassWithSpec(layoutSpec, {});
 
 exports.FlexboxFront = FlexboxFront;
 exports.GridFront = GridFront;
 exports.LayoutFront = LayoutFront;
--- a/devtools/shared/layout/dom-matrix-2d.js
+++ b/devtools/shared/layout/dom-matrix-2d.js
@@ -207,22 +207,112 @@ exports.changeMatrixBase = changeMatrixB
  * as second argument; considering the ancestor transformation too.
  * If no ancestor is specified, it will returns the transformation matrix relative to the
  * node's parent element.
  *
  * @param {DOMNode} node
  *        The node.
  * @param {DOMNode} ancestor
  *        The ancestor of the node given.
- ** @return {Array}
+ * @return {Array}
  *        The transformation matrix.
  */
 function getNodeTransformationMatrix(node, ancestor = node.parentElement) {
   let { a, b, c, d, e, f } = ancestor.getTransformToParent()
                                      .multiply(node.getTransformToAncestor(ancestor));
 
   return [
     a, c, e,
     b, d, f,
     0, 0, 1
   ];
 }
 exports.getNodeTransformationMatrix = getNodeTransformationMatrix;
+
+/**
+ * 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  {Object} size
+ *         An element's untransformed `width` and `height`.
+ * @param  {Object} style
+ *         The computed `writingMode` and `direction` properties for the element.
+ * @return {Array}
+ *         The matrix with adjustments for writing mode and text direction, if any.
+ */
+function getWritingModeMatrix(size, style) {
+  let currentMatrix = identity();
+  let { width, height } = size;
+  let { direction, writingMode } = style;
+
+  switch (writingMode) {
+    case "horizontal-tb":
+      // This is the initial value. No further adjustment needed.
+      break;
+    case "vertical-rl":
+      currentMatrix = multiply(
+        translate(width, 0),
+        rotate(-Math.PI / 2)
+      );
+      break;
+    case "vertical-lr":
+      currentMatrix = multiply(
+        reflectAboutY(),
+        rotate(-Math.PI / 2)
+      );
+      break;
+    case "sideways-rl":
+      currentMatrix = multiply(
+        translate(width, 0),
+        rotate(-Math.PI / 2)
+      );
+      break;
+    case "sideways-lr":
+      currentMatrix = multiply(
+        rotate(Math.PI / 2),
+        translate(-height, 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 = width;
+      if (writingMode != "horizontal-tb") {
+        rowLength = height;
+      }
+      currentMatrix = multiply(currentMatrix, translate(rowLength, 0));
+      currentMatrix = multiply(currentMatrix, reflectAboutY());
+      break;
+    default:
+      console.error(`Unexpected direction: ${direction}`);
+  }
+
+  return currentMatrix;
+}
+exports.getWritingModeMatrix = getWritingModeMatrix;
+
+/**
+ * Convert from the matrix format used in this module:
+ *   a, c, e,
+ *   b, d, f,
+ *   0, 0, 1
+ * to the format used by the `matrix()` CSS transform function:
+ *   a, b, c, d, e, f
+ *
+ * @param  {Array} M
+ *         The matrix in this module's 9 element format.
+ * @return {String}
+ *         The matching 6 element CSS transform function.
+ */
+function getCSSMatrixTransform(M) {
+  let [
+    a, c, e,
+    b, d, f,
+  ] = M;
+  return `matrix(${a}, ${b}, ${c}, ${d}, ${e}, ${f})`;
+}
+exports.getCSSMatrixTransform = getCSSMatrixTransform;