diff --git a/.eslintignore b/.eslintignore index 246d0c85d2ba..7a23df037109 100644 --- a/.eslintignore +++ b/.eslintignore @@ -113,6 +113,7 @@ devtools/server/*.jsm !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 diff --git a/devtools/client/inspector/test/browser.ini b/devtools/client/inspector/test/browser.ini index 43315a3f6519..5140cc86ada0 100644 --- a/devtools/client/inspector/test/browser.ini +++ b/devtools/client/inspector/test/browser.ini @@ -65,6 +65,7 @@ skip-if = os == "mac" # Full keyboard navigation on OSX only works if Full Keybo [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] diff --git a/devtools/client/inspector/test/browser_inspector_highlighter-cssgrid_01.js b/devtools/client/inspector/test/browser_inspector_highlighter-cssgrid_01.js new file mode 100644 index 000000000000..ef21b88c9a3f --- /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 = ` + +
+
cell1
+
cell2
+
cell3
+
cell4
+
+`; + +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"); +} diff --git a/devtools/server/actors/highlighters.css b/devtools/server/actors/highlighters.css index 83733ccc8fd5..e4156e41b5ce 100644 --- a/devtools/server/actors/highlighters.css +++ b/devtools/server/actors/highlighters.css @@ -404,6 +404,16 @@ 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 { diff --git a/devtools/server/actors/highlighters.js b/devtools/server/actors/highlighters.js index a72fa4f3450c..f943be036ba3 100644 --- a/devtools/server/actors/highlighters.js +++ b/devtools/server/actors/highlighters.js @@ -656,6 +656,10 @@ 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; diff --git a/devtools/server/actors/highlighters/css-grid.js b/devtools/server/actors/highlighters/css-grid.js new file mode 100644 index 000000000000..7564c6ec185d --- /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 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 }; +} diff --git a/devtools/server/actors/highlighters/moz.build b/devtools/server/actors/highlighters/moz.build index b85d69f8ca53..317d0832caf9 100644 --- a/devtools/server/actors/highlighters/moz.build +++ b/devtools/server/actors/highlighters/moz.build @@ -11,6 +11,7 @@ DIRS += [ DevToolsModules( 'auto-refresh.js', 'box-model.js', + 'css-grid.js', 'css-transform.js', 'eye-dropper.js', 'geometry-editor.js',