Bug 1297100 - Part 1: Display CSS Grid layouts when highlighting elements in the page r=pbro
authorGabriel Luong <gabriel.luong@gmail.com>
Thu, 13 Oct 2016 16:30:26 -0400
changeset 317838 183b9d08d2648d4ed50fa1d354b50bd43c11a891
parent 317837 62ccb498db3ec05bef1b6427d5b1b4b0e501cad5
child 317839 3a35f6cea288558cd0433f1b8b00c02e8bc3e4da
push id33170
push usercbook@mozilla.com
push dateFri, 14 Oct 2016 10:37:07 +0000
treeherderautoland@0d101ebfd95c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro
bugs1297100
milestone52.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 1297100 - Part 1: Display CSS Grid layouts when highlighting elements in the page r=pbro
devtools/server/actors/highlighters/box-model.js
devtools/server/actors/highlighters/css-grid.js
--- a/devtools/server/actors/highlighters/box-model.js
+++ b/devtools/server/actors/highlighters/box-model.js
@@ -1,38 +1,43 @@
  /* 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 { Cu } = require("chrome");
 const { extend } = require("sdk/core/heritage");
 const { AutoRefreshHighlighter } = require("./auto-refresh");
+const { CssGridHighlighter } = require("./css-grid");
 const {
   CanvasFrameAnonymousContentHelper,
   createNode,
   createSVGNode,
   getBindingElementAndPseudo,
   hasPseudoClassLock,
   isNodeValid,
   moveInfobar,
 } = require("./utils/markup");
 const { setIgnoreLayoutChanges } = require("devtools/shared/layout/utils");
 const inspector = require("devtools/server/actors/inspector");
 const nodeConstants = require("devtools/shared/dom-node-constants");
+const Services = require("Services");
 
 // Note that the order of items in this array is important because it is used
 // for drawing the BoxModelHighlighter's path elements correctly.
 const BOX_MODEL_REGIONS = ["margin", "border", "padding", "content"];
 const BOX_MODEL_SIDES = ["top", "right", "bottom", "left"];
 // Width of boxmodelhighlighter guides
 const GUIDE_STROKE_WIDTH = 1;
 // FIXME: add ":visited" and ":link" after bug 713106 is fixed
 const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
 
+const GRID_ENABLED_PREF = "layout.css.grid.enabled";
+
 /**
  * The BoxModelHighlighter draws the box model regions on top of a node.
  * If the node is a block box, then each region will be displayed as 1 polygon.
  * If the node is an inline box though, each region may be represented by 1 or
  * more polygons, depending on how many line boxes the inline element has.
  *
  * Usage example:
  *
@@ -87,16 +92,20 @@ const PSEUDO_CLASSES = [":hover", ":acti
  *       <div class="box-model-infobar-arrow box-model-infobar-arrow-bottom"/>
  *     </div>
  *   </div>
  * </div>
  */
 function BoxModelHighlighter(highlighterEnv) {
   AutoRefreshHighlighter.call(this, highlighterEnv);
 
+  if (Services.prefs.getBoolPref(GRID_ENABLED_PREF)) {
+    this.cssGridHighlighter = new CssGridHighlighter(this.highlighterEnv);
+  }
+
   this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
     this._buildMarkup.bind(this));
 
   /**
    * Optionally customize each region's fill color by adding an entry to the
    * regionFill property: `highlighter.regionFill.margin = "red";
    */
   this.regionFill = {};
@@ -249,16 +258,21 @@ BoxModelHighlighter.prototype = extend(A
   },
 
   /**
    * Destroy the nodes. Remove listeners.
    */
   destroy: function () {
     AutoRefreshHighlighter.prototype.destroy.call(this);
     this.markup.destroy();
+
+    if (this.cssGridHighlighter) {
+      this.cssGridHighlighter.destroy();
+      this.cssGridHighlighter = null;
+    }
   },
 
   getElement: function (id) {
     return this.markup.getElement(this.ID_CLASS_PREFIX + id);
   },
 
   /**
    * Override the AutoRefreshHighlighter's _isNodeValid method to also return true for
@@ -273,16 +287,38 @@ BoxModelHighlighter.prototype = extend(A
   /**
    * Show the highlighter on a given node
    */
   _show: function () {
     if (BOX_MODEL_REGIONS.indexOf(this.options.region) == -1) {
       this.options.region = "content";
     }
 
+    // Show the CSS grid highlighter if the current node is a grid container or grid item.
+    if (this.cssGridHighlighter) {
+      this.cssGridHighlighter.hide();
+      let gridNode;
+      if (this.currentNode &&
+          this.currentNode.getGridFragments &&
+          this.currentNode.getGridFragments().length) {
+        gridNode = this.currentNode;
+      } else if (this.currentNode.parentNode &&
+                 this.currentNode.parentNode.getGridFragments &&
+                 this.currentNode.parentNode.getGridFragments().length) {
+        gridNode = this.currentNode.parentNode;
+      }
+
+      if (gridNode) {
+        // Display the grid highlighter for the grid container and
+        // hide the box model guides.
+        this.cssGridHighlighter.show(gridNode);
+        this.options.hideGuides = true;
+      }
+    }
+
     let shown = this._update();
     this._trackMutations();
     this.emit("ready");
     return shown;
   },
 
   /**
    * Track the current node markup mutations so that the node info bar can be
@@ -337,16 +373,20 @@ BoxModelHighlighter.prototype = extend(A
    */
   _hide: function () {
     setIgnoreLayoutChanges(true);
 
     this._untrackMutations();
     this._hideBoxModel();
     this._hideInfobar();
 
+    if (this.cssGridHighlighter) {
+      this.cssGridHighlighter.hide();
+    }
+
     setIgnoreLayoutChanges(false, this.currentNode.ownerDocument.documentElement);
   },
 
   /**
    * Hide the infobar
    */
   _hideInfobar: function () {
     this.getElement("infobar-container").setAttribute("hidden", "true");
--- a/devtools/server/actors/highlighters/css-grid.js
+++ b/devtools/server/actors/highlighters/css-grid.js
@@ -1,14 +1,15 @@
 /* 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 { Cu } = require("chrome");
 const { extend } = require("sdk/core/heritage");
 const { AutoRefreshHighlighter } = require("./auto-refresh");
 const {
   CanvasFrameAnonymousContentHelper,
   createNode,
   createSVGNode,
   moveInfobar,
 } = require("./utils/markup");
@@ -40,17 +41,21 @@ const GRID_LINES_PROPERTIES = {
 const GRID_GAP_PATTERN_WIDTH = 14;
 const GRID_GAP_PATTERN_HEIGHT = 14;
 const GRID_GAP_PATTERN_LINE_DASH = [5, 3];
 const GRID_GAP_PATTERN_STROKE_STYLE = "#9370DB";
 
 /**
  * Cached used by `CssGridHighlighter.getGridGapPattern`.
  */
-const gCachedGridPattern = new Map();
+const gCachedGridPattern = new WeakMap();
+// WeakMap key for the Row grid pattern.
+const ROW_KEY = {};
+// WeakMap key for the Column grid pattern.
+const COLUMN_KEY = {};
 
 /**
  * The CssGridHighlighter is the class that overlays a visual grid on top of
  * display:grid elements.
  *
  * Usage example:
  * let h = new CssGridHighlighter(env);
  * h.show(node, options);
@@ -88,16 +93,19 @@ const gCachedGridPattern = new Map();
  *   </div>
  * </div>
  */
 function CssGridHighlighter(highlighterEnv) {
   AutoRefreshHighlighter.call(this, highlighterEnv);
 
   this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
     this._buildMarkup.bind(this));
+
+  this.onNavigate = this.onNavigate.bind(this);
+  this.highlighterEnv.on("navigate", this.onNavigate);
 }
 
 CssGridHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
   typeName: "CssGridHighlighter",
 
   ID_CLASS_PREFIX: "css-grid-",
 
   _buildMarkup() {
@@ -198,19 +206,19 @@ CssGridHighlighter.prototype = extend(Au
       },
       prefix: this.ID_CLASS_PREFIX
     });
 
     return container;
   },
 
   destroy() {
-    AutoRefreshHighlighter.prototype.destroy.call(this);
+    this.highlighterEnv.off("navigate", this.onNavigate);
     this.markup.destroy();
-    gCachedGridPattern.clear();
+    AutoRefreshHighlighter.prototype.destroy.call(this);
   },
 
   getElement(id) {
     return this.markup.getElement(this.ID_CLASS_PREFIX + id);
   },
 
   get ctx() {
     return this.canvas.getCanvasContext("2d");
@@ -218,51 +226,61 @@ CssGridHighlighter.prototype = extend(Au
 
   get canvas() {
     return this.getElement("canvas");
   },
 
   /**
    * Gets the grid gap pattern used to render the gap regions.
    *
-   * @param  {String} dimensionType
-   *         The grid dimension type which is either the constant COLUMNS or ROWS.
+   * @param  {Object} dimension
+   *         Refers to the WeakMap key for the grid dimension type which is either the
+   *         constant COLUMN or ROW.
    * @return {CanvasPattern} grid gap pattern.
    */
-  getGridGapPattern(dimensionType) {
-    if (gCachedGridPattern.has(dimensionType)) {
-      return gCachedGridPattern.get(dimensionType);
+  getGridGapPattern(dimension) {
+    if (gCachedGridPattern.has(dimension)) {
+      return gCachedGridPattern.get(dimension);
     }
 
     // Create the diagonal lines pattern for the rendering the grid gaps.
     let canvas = createNode(this.win, { nodeType: "canvas" });
     canvas.width = GRID_GAP_PATTERN_WIDTH;
     canvas.height = GRID_GAP_PATTERN_HEIGHT;
 
     let ctx = canvas.getContext("2d");
     ctx.setLineDash(GRID_GAP_PATTERN_LINE_DASH);
     ctx.beginPath();
     ctx.translate(.5, .5);
 
-    if (dimensionType === COLUMNS) {
+    if (dimension === COLUMN_KEY) {
       ctx.moveTo(0, 0);
       ctx.lineTo(GRID_GAP_PATTERN_WIDTH, GRID_GAP_PATTERN_HEIGHT);
     } else {
       ctx.moveTo(GRID_GAP_PATTERN_WIDTH, 0);
       ctx.lineTo(0, GRID_GAP_PATTERN_HEIGHT);
     }
 
     ctx.strokeStyle = GRID_GAP_PATTERN_STROKE_STYLE;
     ctx.stroke();
 
     let pattern = ctx.createPattern(canvas, "repeat");
-    gCachedGridPattern.set(dimensionType, pattern);
+    gCachedGridPattern.set(dimension, pattern);
     return pattern;
   },
 
+  /**
+   * Called when the page navigates. Used to clear the cached gap patterns and avoid
+   * using DeadWrapper objects as gap patterns the next time.
+   */
+  onNavigate() {
+    gCachedGridPattern.delete(ROW_KEY);
+    gCachedGridPattern.delete(COLUMN_KEY);
+  },
+
   _show() {
     if (Services.prefs.getBoolPref(CSS_GRID_ENABLED_PREF) && !this.isGrid()) {
       this.hide();
       return false;
     }
 
     return this._update();
   },
@@ -597,21 +615,22 @@ CssGridHighlighter.prototype = extend(Au
    *         The end position of the cross side of the grid line.
    * @param  {Number} breadth
    *         The grid line breadth value.
    * @param  {String} dimensionType
    *         The grid dimension type which is either the constant COLUMNS or ROWS.
    */
   renderGridGap(linePos, startPos, endPos, breadth, dimensionType) {
     this.ctx.save();
-    this.ctx.fillStyle = this.getGridGapPattern(dimensionType);
 
     if (dimensionType === COLUMNS) {
+      this.ctx.fillStyle = this.getGridGapPattern(COLUMN_KEY);
       this.ctx.fillRect(linePos, startPos, breadth, endPos - startPos);
     } else {
+      this.ctx.fillStyle = this.getGridGapPattern(ROW_KEY);
       this.ctx.fillRect(startPos, linePos, endPos - startPos, breadth);
     }
 
     this.ctx.restore();
   },
 
   /**
    * Render the grid area highlight for the given area name or for all the grid areas.
@@ -705,16 +724,20 @@ exports.CssGridHighlighter = CssGridHigh
  * Stringify CSS Grid data as returned by node.getGridFragments.
  * This is useful to compare grid state at each update and redraw the highlighter if
  * needed.
  *
  * @param  {Object} Grid Fragments
  * @return {String} representation of the CSS grid fragment data.
  */
 function stringifyGridFragments(fragments = []) {
+  if (fragments[0] && Cu.isDeadWrapper(fragments[0])) {
+    return {};
+  }
+
   return JSON.stringify(fragments.map(getStringifiableFragment));
 }
 
 function getStringifiableFragment(fragment) {
   return {
     cols: getStringifiableDimension(fragment.cols),
     rows: getStringifiableDimension(fragment.rows)
   };