Bug 1282726 - Simple CSS Grid highlighter that displays grid lines r=pbro
authorGabriel Luong <gabriel.luong@gmail.com>
Fri, 19 Aug 2016 10:26:39 -0700
changeset 310524 6b1f62a32be285d0d8956629b2860fb5212bc8de
parent 310424 216ef67f8bba14428b27439bb4d5db5da3037f55
child 310532 1720386f83dc19ec161f70ae46d370edbf1e5e5c
push id80894
push userryanvm@gmail.com
push dateMon, 22 Aug 2016 13:43:54 +0000
treeherdermozilla-inbound@e4d567ff5113 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro
bugs1282726
milestone51.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 1282726 - Simple CSS Grid highlighter that displays grid lines r=pbro This adds a new highlighter in devtools/server/actors/highlighters. For now this highlighter isn't used and can only display grid lines as provided by node.getGridFragments().
.eslintignore
devtools/client/inspector/test/browser.ini
devtools/client/inspector/test/browser_inspector_highlighter-cssgrid_01.js
devtools/server/actors/highlighters.css
devtools/server/actors/highlighters.js
devtools/server/actors/highlighters/css-grid.js
devtools/server/actors/highlighters/moz.build
--- a/.eslintignore
+++ b/.eslintignore
@@ -108,16 +108,17 @@ devtools/client/webide/**
 devtools/server/*.js
 devtools/server/*.jsm
 !devtools/server/child.js
 !devtools/server/css-logic.js
 !devtools/server/main.js
 !devtools/server/websocket-server.js
 devtools/server/actors/**
 !devtools/server/actors/inspector.js
+!devtools/server/actors/highlighters/css-grid.js
 !devtools/server/actors/highlighters/eye-dropper.js
 !devtools/server/actors/webbrowser.js
 !devtools/server/actors/webextension.js
 !devtools/server/actors/styles.js
 !devtools/server/actors/string.js
 !devtools/server/actors/csscoverage.js
 devtools/server/performance/**
 devtools/server/tests/**
--- a/devtools/client/inspector/test/browser.ini
+++ b/devtools/client/inspector/test/browser.ini
@@ -60,16 +60,17 @@ skip-if = os == "mac" # Full keyboard na
 [browser_inspector_expand-collapse.js]
 [browser_inspector_gcli-inspect-command.js]
 [browser_inspector_highlighter-01.js]
 [browser_inspector_highlighter-02.js]
 [browser_inspector_highlighter-03.js]
 [browser_inspector_highlighter-04.js]
 [browser_inspector_highlighter-by-type.js]
 [browser_inspector_highlighter-comments.js]
+[browser_inspector_highlighter-cssgrid_01.js]
 [browser_inspector_highlighter-csstransform_01.js]
 [browser_inspector_highlighter-csstransform_02.js]
 [browser_inspector_highlighter-embed.js]
 [browser_inspector_highlighter-eyedropper-clipboard.js]
 subsuite = clipboard
 [browser_inspector_highlighter-eyedropper-csp.js]
 [browser_inspector_highlighter-eyedropper-events.js]
 [browser_inspector_highlighter-eyedropper-label.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssgrid_01.js
@@ -0,0 +1,77 @@
+/* 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";
+
+// Test the creation of the canvas highlighter element of the css grid highlighter.
+
+const TEST_URL = `
+  <style type='text/css'>
+    #grid {
+      display: grid;
+    }
+    #cell1 {
+      grid-column: 1;
+      grid-row: 1;
+    }
+    #cell2 {
+      grid-column: 2;
+      grid-row: 1;
+    }
+    #cell3 {
+      grid-column: 1;
+      grid-row: 2;
+    }
+    #cell4 {
+      grid-column: 2;
+      grid-row: 2;
+    }
+  </style>
+  <div id="grid">
+    <div id="cell1">cell1</div>
+    <div id="cell2">cell2</div>
+    <div id="cell3">cell3</div>
+    <div id="cell4">cell4</div>
+  </div>
+`;
+
+const HIGHLIGHTER_TYPE = "CssGridHighlighter";
+
+add_task(function* () {
+  let {inspector, testActor} = yield openInspectorForURL(
+    "data:text/html;charset=utf-8," + encodeURIComponent(TEST_URL));
+  let front = inspector.inspector;
+  let highlighter = yield front.getHighlighterByType(HIGHLIGHTER_TYPE);
+
+  yield isHiddenByDefault(testActor, highlighter);
+  yield isVisibleWhenShown(testActor, inspector, highlighter);
+
+  yield highlighter.finalize();
+});
+
+function* isHiddenByDefault(testActor, highlighterFront) {
+  info("Checking that the highlighter is hidden by default");
+
+  let hidden = yield testActor.getHighlighterNodeAttribute(
+    "css-grid-canvas", "hidden", highlighterFront);
+  ok(hidden, "The highlighter is hidden by default");
+}
+
+function* isVisibleWhenShown(testActor, inspector, highlighterFront) {
+  info("Asking to show the highlighter on the test node");
+
+  let node = yield getNodeFront("#grid", inspector);
+  yield highlighterFront.show(node);
+
+  let hidden = yield testActor.getHighlighterNodeAttribute(
+    "css-grid-canvas", "hidden", highlighterFront);
+  ok(!hidden, "The highlighter is visible");
+
+  info("Hiding the highlighter");
+  yield highlighterFront.hide();
+
+  hidden = yield testActor.getHighlighterNodeAttribute(
+    "css-grid-canvas", "hidden", highlighterFront);
+  ok(hidden, "The highlighter is hidden");
+}
--- a/devtools/server/actors/highlighters.css
+++ b/devtools/server/actors/highlighters.css
@@ -399,16 +399,26 @@
 :-moz-native-anonymous .measuring-tool-highlighter-guide-right,
 :-moz-native-anonymous .measuring-tool-highlighter-guide-bottom,
 :-moz-native-anonymous .measuring-tool-highlighter-guide-left {
   stroke: var(--highlighter-guide-color);
   stroke-dasharray: 5 3;
   shape-rendering: crispEdges;
 }
 
+/* CSS Grid highlighter */
+
+:-moz-native-anonymous .css-grid-canvas {
+  position: absolute;
+  pointer-events: none;
+  top: 0;
+  left: 0;
+  image-rendering: -moz-crisp-edges;
+}
+
 /* Eye dropper */
 
 :-moz-native-anonymous .eye-dropper-root {
   --magnifier-width: 96px;
   --magnifier-height: 96px;
   /* Width accounts for all color formats (hsl being the longest) */
   --label-width: 160px;
   --label-height: 23px;
--- a/devtools/server/actors/highlighters.js
+++ b/devtools/server/actors/highlighters.js
@@ -651,16 +651,20 @@ HighlighterEnvironment.prototype = {
     this._win = null;
   }
 };
 
 const { BoxModelHighlighter } = require("./highlighters/box-model");
 register(BoxModelHighlighter);
 exports.BoxModelHighlighter = BoxModelHighlighter;
 
+const { CssGridHighlighter } = require("./highlighters/css-grid");
+register(CssGridHighlighter);
+exports.CssGridHighlighter = CssGridHighlighter;
+
 const { CssTransformHighlighter } = require("./highlighters/css-transform");
 register(CssTransformHighlighter);
 exports.CssTransformHighlighter = CssTransformHighlighter;
 
 const { SelectorHighlighter } = require("./highlighters/selector");
 register(SelectorHighlighter);
 exports.SelectorHighlighter = SelectorHighlighter;
 
new file mode 100644
--- /dev/null
+++ b/devtools/server/actors/highlighters/css-grid.js
@@ -0,0 +1,289 @@
+/* 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 { extend } = require("sdk/core/heritage");
+const { AutoRefreshHighlighter } = require("./auto-refresh");
+const { CanvasFrameAnonymousContentHelper, createNode } = require("./utils/markup");
+const {
+  getCurrentZoom,
+  setIgnoreLayoutChanges
+} = require("devtools/shared/layout/utils");
+const Services = require("Services");
+
+const CSS_GRID_ENABLED_PREF = "layout.css.grid.enabled";
+const LINE_DASH_ARRAY = [5, 3];
+const LINE_STROKE_STYLE = "#483D88";
+
+/**
+ * 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);
+ * h.hide();
+ * h.destroy();
+ *
+ * Available Options:
+ * - infiniteLines {Boolean}
+ *   Displays an infinite line to represent the grid lines
+ */
+function CssGridHighlighter(highlighterEnv) {
+  AutoRefreshHighlighter.call(this, highlighterEnv);
+
+  this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
+    this._buildMarkup.bind(this));
+}
+
+CssGridHighlighter.prototype = extend(AutoRefreshHighlighter.prototype, {
+  typeName: "CssGridHighlighter",
+
+  ID_CLASS_PREFIX: "css-grid-",
+
+  _buildMarkup() {
+    let container = createNode(this.win, {
+      attributes: {
+        "class": "highlighter-container"
+      }
+    });
+
+    // We use a <canvas> element so that we can draw an arbitrary number of lines
+    // which wouldn't be possible with HTML or SVG without having to insert and remove
+    // the whole markup on every update.
+    createNode(this.win, {
+      parent: container,
+      nodeType: "canvas",
+      attributes: {
+        "id": "canvas",
+        "class": "canvas",
+        "hidden": "true"
+      },
+      prefix: this.ID_CLASS_PREFIX
+    });
+
+    return container;
+  },
+
+  destroy() {
+    AutoRefreshHighlighter.prototype.destroy.call(this);
+    this.markup.destroy();
+  },
+
+  getElement(id) {
+    return this.markup.getElement(this.ID_CLASS_PREFIX + id);
+  },
+
+  get ctx() {
+    return this.canvas.getCanvasContext("2d");
+  },
+
+  get canvas() {
+    return this.getElement("canvas");
+  },
+
+  _show() {
+    if (Services.prefs.getBoolPref(CSS_GRID_ENABLED_PREF) && !this.isGrid()) {
+      this.hide();
+      return false;
+    }
+
+    return this._update();
+  },
+
+  /**
+   * Checks if the current node has a CSS Grid layout.
+   *
+   * @return  {Boolean} true if the current node has a CSS grid layout, false otherwise.
+   */
+  isGrid() {
+    return this.currentNode.getGridFragments().length > 0;
+  },
+
+  /**
+   * The AutoRefreshHighlighter's _hasMoved method returns true only if the
+   * element's quads have changed. Override it so it also returns true if the
+   * element's grid has changed (which can happen when you change the
+   * grid-template-* CSS properties with the highlighter displayed).
+   */
+  _hasMoved() {
+    let hasMoved = AutoRefreshHighlighter.prototype._hasMoved.call(this);
+
+    let oldGridData = stringifyGridFragments(this.gridData);
+    this.gridData = this.currentNode.getGridFragments();
+    let newGridData = stringifyGridFragments(this.gridData);
+
+    return hasMoved || oldGridData !== newGridData;
+  },
+
+  /**
+   * Update the highlighter on the current highlighted node (the one that was
+   * passed as an argument to show(node)).
+   * Should be called whenever node's geometry or grid changes
+   */
+  _update() {
+    setIgnoreLayoutChanges(true);
+
+    // Clear the canvas.
+    this.clearCanvas();
+
+    // And start drawing the fragments.
+    for (let i = 0; i < this.gridData.length; i++) {
+      let fragment = this.gridData[i];
+      let quad = this.currentQuads.content[i];
+      this.renderFragment(fragment, quad);
+    }
+
+    this._showGrid();
+
+    setIgnoreLayoutChanges(false, this.currentNode.ownerDocument.documentElement);
+    return true;
+  },
+
+  clearCanvas() {
+    let ratio = this.win.devicePixelRatio || 1;
+    let width = this.win.innerWidth;
+    let height = this.win.innerHeight;
+
+    // Resize the canvas taking the dpr into account so as to have crisp lines.
+    this.canvas.setAttribute("width", width * ratio);
+    this.canvas.setAttribute("height", height * ratio);
+    this.canvas.setAttribute("style", `width:${width}px;height:${height}px`);
+    this.ctx.scale(ratio, ratio);
+
+    this.ctx.clearRect(0, 0, width, height);
+  },
+
+  getFirstRowLinePos(fragment) {
+    return fragment.rows.lines[0].start;
+  },
+
+  getLastRowLinePos(fragment) {
+    return fragment.rows.lines[fragment.rows.lines.length - 1].start;
+  },
+
+  getFirstColLinePos(fragment) {
+    return fragment.cols.lines[0].start;
+  },
+
+  getLastColLinePos(fragment) {
+    return fragment.cols.lines[fragment.cols.lines.length - 1].start;
+  },
+
+  renderColLines(cols, {bounds}, startRowPos, endRowPos) {
+    let y1 = (bounds.top / getCurrentZoom(this.win)) + startRowPos;
+    let y2 = (bounds.top / getCurrentZoom(this.win)) + endRowPos;
+
+    if (this.options.infiniteLines) {
+      y1 = 0;
+      y2 = parseInt(this.canvas.getAttribute("height"), 10);
+    }
+
+    for (let i = 0; i < cols.lines.length; i++) {
+      let line = cols.lines[i];
+      let x = (bounds.left / getCurrentZoom(this.win)) + line.start;
+      this.renderLine(x, y1, x, y2);
+
+      // Render a second line to illustrate the gutter for non-zero breadth.
+      if (line.breadth > 0) {
+        x = x + line.breadth;
+        this.renderLine(x, y1, x, y2);
+      }
+    }
+  },
+
+  renderRowLines(rows, {bounds}, startColPos, endColPos) {
+    let x1 = (bounds.left / getCurrentZoom(this.win)) + startColPos;
+    let x2 = (bounds.left / getCurrentZoom(this.win)) + endColPos;
+
+    if (this.options.infiniteLines) {
+      x1 = 0;
+      x2 = parseInt(this.canvas.getAttribute("width"), 10);
+    }
+
+    for (let i = 0; i < rows.lines.length; i++) {
+      let line = rows.lines[i];
+      let y = (bounds.top / getCurrentZoom(this.win)) + line.start;
+      this.renderLine(x1, y, x2, y);
+
+      // Render a second line to illustrate the gutter for non-zero breadth.
+      if (line.breadth > 0) {
+        y = y + line.breadth;
+        this.renderLine(x1, y, x2, y);
+      }
+    }
+  },
+
+  renderLine(x1, y1, x2, y2) {
+    this.ctx.save();
+    this.ctx.setLineDash(LINE_DASH_ARRAY);
+    this.ctx.beginPath();
+    this.ctx.translate(.5, .5);
+    this.ctx.moveTo(x1, y1);
+    this.ctx.lineTo(x2, y2);
+    this.ctx.strokeStyle = LINE_STROKE_STYLE;
+    this.ctx.stroke();
+    this.ctx.restore();
+  },
+
+  renderFragment(fragment, quad) {
+    this.renderColLines(fragment.cols, quad,
+                        this.getFirstRowLinePos(fragment),
+                        this.getLastRowLinePos(fragment));
+
+    this.renderRowLines(fragment.rows, quad,
+                        this.getFirstColLinePos(fragment),
+                        this.getLastColLinePos(fragment));
+  },
+
+  _hide() {
+    setIgnoreLayoutChanges(true);
+    this._hideGrid();
+    setIgnoreLayoutChanges(false, this.currentNode.ownerDocument.documentElement);
+  },
+
+  _hideGrid() {
+    this.getElement("canvas").setAttribute("hidden", "true");
+  },
+
+  _showGrid() {
+    this.getElement("canvas").removeAttribute("hidden");
+  }
+});
+exports.CssGridHighlighter = CssGridHighlighter;
+
+/**
+ * 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 = []) {
+  return JSON.stringify(fragments.map(getStringifiableFragment));
+}
+
+function getStringifiableFragment(fragment) {
+  return {
+    cols: getStringifiableDimension(fragment.cols),
+    rows: getStringifiableDimension(fragment.rows)
+  };
+}
+
+function getStringifiableDimension(dimension) {
+  return {
+    lines: [...dimension.lines].map(getStringifiableLine),
+    tracks: [...dimension.tracks].map(getStringifiableTrack),
+  };
+}
+
+function getStringifiableLine({ breadth, number, start, names }) {
+  return { breadth, number, start, names };
+}
+
+function getStringifiableTrack({ breadth, start, state, type }) {
+  return { breadth, start, state, type };
+}
--- a/devtools/server/actors/highlighters/moz.build
+++ b/devtools/server/actors/highlighters/moz.build
@@ -6,16 +6,17 @@
 
 DIRS += [
     'utils',
 ]
 
 DevToolsModules(
     'auto-refresh.js',
     'box-model.js',
+    'css-grid.js',
     'css-transform.js',
     'eye-dropper.js',
     'geometry-editor.js',
     'measuring-tool.js',
     'rect.js',
     'rulers.js',
     'selector.js',
     'simple-outline.js'