--- a/devtools/server/actors/highlighters/css-grid.js
+++ b/devtools/server/actors/highlighters/css-grid.js
@@ -12,18 +12,28 @@ const {
createNode,
createSVGNode,
moveInfobar,
} = require("./utils/markup");
const {
getCurrentZoom,
getDisplayPixelRatio,
setIgnoreLayoutChanges,
+ getNodeBounds,
getViewportDimensions,
} = require("devtools/shared/layout/utils");
+const {
+ identity,
+ apply,
+ translate,
+ multiply,
+ scale,
+ getNodeTransformationMatrix,
+ getNodeTransformOrigin
+} = require("devtools/shared/layout/dom-matrix-2d");
const { stringifyGridFragments } = require("devtools/server/actors/utils/css-grid-utils");
const CSS_GRID_ENABLED_PREF = "layout.css.grid.enabled";
const DEFAULT_GRID_COLOR = "#4B0082";
const COLUMNS = "cols";
const ROWS = "rows";
@@ -68,46 +78,109 @@ const gCachedGridPattern = new Map();
//
// Note:
// Once bug 1232491 lands, we could try to refactor this code to use the values from
// the displayport API instead.
//
// Using a fixed value should also solve bug 1348293.
const CANVAS_SIZE = 4096;
+// This constant is used as value to draw infinite lines on canvas; since we cannot use
+// the canvas boundaries as coordinates to draw the lines, and then applying
+// transformations on top of them (the resulting coordinates might ending before reaching
+// the viewport's edges, therefore the lines won't looks as "infinite").
+const CANVAS_INFINITY = CANVAS_SIZE << 8;
+
+/**
+ * Draws a line to the context given, applying a transformation matrix if passed.
+ *
+ * @param {CanvasRenderingContext2D} ctx
+ * The 2d canvas context.
+ * @param {Number} x1
+ * The x-axis of the coordinate for the begin of the line.
+ * @param {Number} y1
+ * The y-axis of the coordinate for the begin of the line.
+ * @param {Number} x2
+ * The x-axis of the coordinate for the end of the line.
+ * @param {Number} y2
+ * The y-axis of the coordinate for the end of the line.
+ * @param {Array} [matrix=identity()]
+ * The transformation matrix to apply.
+ */
+function drawLine(ctx, x1, y1, x2, y2, matrix = identity()) {
+ let fromPoint = apply(matrix, [x1, y1]);
+ let toPoint = apply(matrix, [x2, y2]);
+
+ ctx.moveTo(Math.round(fromPoint[0]), Math.round(fromPoint[1]));
+ ctx.lineTo(Math.round(toPoint[0]), Math.round(toPoint[1]));
+}
+
+/**
+ * Draws a rect to the context given, applying a transformation matrix if passed.
+ * The coordinates are the start and end points of the rectangle's diagonal.
+ *
+ * @param {CanvasRenderingContext2D} ctx
+ * The 2d canvas context.
+ * @param {Number} x1
+ * The x-axis coordinate of the rectangle's diagonal start point.
+ * @param {Number} y1
+ * The y-axis coordinate of the rectangle's diagonal start point.
+ * @param {Number} x2
+ * The x-axis coordinate of the rectangle's diagonal end point.
+ * @param {Number} y2
+ * The y-axis coordinate of the rectangle's diagonal end point.
+ * @param {Array} [matrix=identity()]
+ * The transformation matrix to apply.
+ */
+function drawRect(ctx, x1, y1, x2, y2, matrix = identity()) {
+ let p = [
+ [x1, y1],
+ [x2, y1],
+ [x2, y2],
+ [x1, y2]
+ ].map(point => apply(matrix, point).map(Math.round));
+
+ ctx.beginPath();
+ ctx.moveTo(p[0][0], p[0][1]);
+ ctx.lineTo(p[1][0], p[1][1]);
+ ctx.lineTo(p[2][0], p[2][1]);
+ ctx.lineTo(p[3][0], p[3][1]);
+ ctx.closePath();
+}
+
/**
* Utility method to draw a rounded rectangle in the provided canvas context.
*
* @param {CanvasRenderingContext2D} ctx
* The 2d canvas context.
* @param {Number} x
* The x-axis origin of the rectangle.
* @param {Number} y
* The y-axis origin of the rectangle.
* @param {Number} width
* The width of the rectangle.
* @param {Number} height
* The height of the rectangle.
* @param {Number} radius
* The radius of the rounding.
*/
-const roundedRect = function (ctx, x, y, width, height, radius) {
+function drawRoundedRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x, y + radius);
ctx.lineTo(x, y + height - radius);
ctx.arcTo(x, y + height, x + radius, y + height, radius);
ctx.lineTo(x + width - radius, y + height);
ctx.arcTo(x + width, y + height, x + width, y + height - radius, radius);
ctx.lineTo(x + width, y + radius);
ctx.arcTo(x + width, y, x + width - radius, y, radius);
ctx.lineTo(x + radius, y);
ctx.arcTo(x, y, x, y + radius, radius);
ctx.stroke();
ctx.fill();
-};
+}
/**
* The CssGridHighlighter is the class that overlays a visual grid on top of
* display:[inline-]grid elements.
*
* Usage example:
* let h = new CssGridHighlighter(env);
* h.show(node, options);
@@ -657,21 +730,23 @@ CssGridHighlighter.prototype = extend(Au
// Updates the <canvas> element's position and size.
// It also clear the <canvas>'s drawing context.
this.updateCanvasElement();
// Clear the grid area highlights.
this.clearGridAreas();
this.clearGridCell();
+ // Update the current matrix used in our canvas' rendering
+ this.updateCurrentMatrix();
+
// Start drawing the grid 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.renderFragment(fragment);
}
// Display the grid area highlights if needed.
if (this.options.showAllGridAreas) {
this.showAllGridAreas();
} else if (this.options.showGridArea) {
this.showGridArea(this.options.showGridArea);
}
@@ -871,16 +946,59 @@ CssGridHighlighter.prototype = extend(Au
// Resize the canvas taking the dpr into account so as to have crisp lines, and
// translating it to give the perception that it always covers the viewport.
this.canvas.setAttribute("style",
`width:${size}px;height:${size}px; transform: translate(${x}px, ${y}px);`);
this.ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE);
},
+ /**
+ * Updates the current matrix taking in account the following transformations, in this
+ * order:
+ * 1. The scale given by the display pixel ratio.
+ * 2. The translation to the top left corner of the element.
+ * 3. The scale given by the current zoom.
+ * 4. Any CSS transformation applied directly to the element (only 2D
+ * transformation; the 3D transformation are flattened, see `dom-matrix-2d` module
+ * for further details.)
+ *
+ * The transformations of the element's ancestors are not currently computed (see
+ * bug 1355675).
+ */
+ updateCurrentMatrix() {
+ let origin = getNodeTransformOrigin(this.currentNode);
+ let bounds = getNodeBounds(this.win, this.currentNode);
+ let nodeMatrix = getNodeTransformationMatrix(this.currentNode);
+
+ let ox = origin[0];
+ let oy = origin[1];
+
+ let m = identity();
+
+ // First, we scale based on the display's current pixel ratio.
+ m = multiply(m, scale(getDisplayPixelRatio(this.win)));
+ // Then we translate the origin to the node's top left corner.
+ m = multiply(m, translate(bounds.p1.x, bounds.p1.y));
+ // And scale based on the current zoom factor.
+ m = multiply(m, scale(getCurrentZoom(this.win)));
+ // Finally, we can apply the current node's transformation matrix, taking in account
+ // the `transform-origin` property.
+ if (nodeMatrix) {
+ m = multiply(m, translate(ox, oy));
+ m = multiply(m, nodeMatrix);
+ m = multiply(m, translate(-ox, -oy));
+ this.hasNodeTransformations = true;
+ } else {
+ this.hasNodeTransformations = false;
+ }
+
+ this.currentMatrix = m;
+ },
+
getFirstRowLinePos(fragment) {
return fragment.rows.lines[0].start;
},
getLastRowLinePos(fragment) {
return fragment.rows.lines[fragment.rows.lines.length - 1].start;
},
@@ -906,29 +1024,29 @@ CssGridHighlighter.prototype = extend(Au
while (trackIndex >= 0 && tracks[trackIndex].type != "explicit") {
trackIndex--;
}
// The grid line index is the grid track index + 1.
return trackIndex + 1;
},
- renderFragment(fragment, quad) {
- this.renderLines(fragment.cols, quad, COLUMNS, "left", "top", "height",
+ renderFragment(fragment) {
+ this.renderLines(fragment.cols, COLUMNS, "left", "top", "height",
this.getFirstRowLinePos(fragment),
this.getLastRowLinePos(fragment));
- this.renderLines(fragment.rows, quad, ROWS, "top", "left", "width",
+ this.renderLines(fragment.rows, ROWS, "top", "left", "width",
this.getFirstColLinePos(fragment),
this.getLastColLinePos(fragment));
// Line numbers are rendered in a 2nd step to avoid overlapping with existing lines.
if (this.options.showGridLineNumbers) {
- this.renderLineNumbers(fragment.cols, quad, COLUMNS, "left", "top",
+ this.renderLineNumbers(fragment.cols, COLUMNS, "left", "top",
this.getFirstRowLinePos(fragment));
- this.renderLineNumbers(fragment.rows, quad, ROWS, "top", "left",
+ this.renderLineNumbers(fragment.rows, ROWS, "top", "left",
this.getFirstColLinePos(fragment));
}
},
/**
* Render the grid lines given the grid dimension information of the
* column or row lines.
*
@@ -947,32 +1065,31 @@ CssGridHighlighter.prototype = extend(Au
* @param {String} mainSize
* The main size of the given grid dimension - "width" for rows and
* "height" for columns.
* @param {Number} startPos
* The start position of the cross side of the grid dimension.
* @param {Number} endPos
* The end position of the cross side of the grid dimension.
*/
- renderLines(gridDimension, {bounds}, dimensionType, mainSide, crossSide,
+ renderLines(gridDimension, dimensionType, mainSide, crossSide,
mainSize, startPos, endPos) {
- let currentZoom = getCurrentZoom(this.win);
- let lineStartPos = (bounds[crossSide] / currentZoom) + startPos;
- let lineEndPos = (bounds[crossSide] / currentZoom) + endPos;
+ let lineStartPos = startPos;
+ let lineEndPos = endPos;
if (this.options.showInfiniteLines) {
lineStartPos = 0;
lineEndPos = Infinity;
}
let lastEdgeLineIndex = this.getLastEdgeLineIndex(gridDimension.tracks);
for (let i = 0; i < gridDimension.lines.length; i++) {
let line = gridDimension.lines[i];
- let linePos = (bounds[mainSide] / currentZoom) + line.start;
+ let linePos = line.start;
if (i == 0 || i == lastEdgeLineIndex) {
this.renderLine(linePos, lineStartPos, lineEndPos, dimensionType, "edge");
} else {
this.renderLine(linePos, lineStartPos, lineEndPos, dimensionType,
gridDimension.tracks[i - 1].type);
}
@@ -987,27 +1104,23 @@ CssGridHighlighter.prototype = extend(Au
},
/**
* Render the grid lines given the grid dimension information of the
* column or row lines.
*
* see @param for renderLines.
*/
- renderLineNumbers(gridDimension, {bounds}, dimensionType, mainSide, crossSide,
+ renderLineNumbers(gridDimension, dimensionType, mainSide, crossSide,
startPos) {
- let zoom = getCurrentZoom(this.win);
- let lineStartPos = (bounds[crossSide] / zoom) + startPos;
- if (this.options.showInfiniteLines) {
- lineStartPos = 0;
- }
+ let lineStartPos = startPos;
for (let i = 0; i < gridDimension.lines.length; i++) {
let line = gridDimension.lines[i];
- let linePos = (bounds[mainSide] / zoom) + line.start;
+ let linePos = line.start;
this.renderGridLineNumber(line.number, linePos, lineStartPos, line.breadth,
dimensionType);
}
},
/**
* Render the grid line on the css grid highlighter canvas.
*
@@ -1026,33 +1139,41 @@ CssGridHighlighter.prototype = extend(Au
renderLine(linePos, startPos, endPos, dimensionType, lineType) {
let { devicePixelRatio } = this.win;
let lineWidth = getDisplayPixelRatio(this.win);
let offset = (lineWidth / 2) % 1;
let x = Math.round(this._canvasPosition.x * devicePixelRatio);
let y = Math.round(this._canvasPosition.y * devicePixelRatio);
- linePos = Math.round(linePos * devicePixelRatio);
- startPos = Math.round(startPos * devicePixelRatio);
+ linePos = Math.round(linePos);
+ startPos = Math.round(startPos);
this.ctx.save();
this.ctx.setLineDash(GRID_LINES_PROPERTIES[lineType].lineDash);
this.ctx.beginPath();
this.ctx.translate(offset - x, offset - y);
this.ctx.lineWidth = lineWidth;
if (dimensionType === COLUMNS) {
- endPos = isFinite(endPos) ? endPos * devicePixelRatio : CANVAS_SIZE + y;
- this.ctx.moveTo(linePos, startPos);
- this.ctx.lineTo(linePos, endPos);
+ if (isFinite(endPos)) {
+ endPos = Math.round(endPos);
+ } else {
+ endPos = CANVAS_INFINITY;
+ startPos = -endPos;
+ }
+ drawLine(this.ctx, linePos, startPos, linePos, endPos, this.currentMatrix);
} else {
- endPos = isFinite(endPos) ? endPos * devicePixelRatio : CANVAS_SIZE + x;
- this.ctx.moveTo(startPos, linePos);
- this.ctx.lineTo(endPos, linePos);
+ if (isFinite(endPos)) {
+ endPos = Math.round(endPos);
+ } else {
+ endPos = CANVAS_INFINITY;
+ startPos = -endPos;
+ }
+ drawLine(this.ctx, startPos, linePos, endPos, linePos, this.currentMatrix);
}
this.ctx.strokeStyle = this.color;
this.ctx.globalAlpha = GRID_LINES_PROPERTIES[lineType].alpha;
this.ctx.stroke();
this.ctx.restore();
},
@@ -1068,32 +1189,33 @@ CssGridHighlighter.prototype = extend(Au
* @param {Number} startPos
* The start 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.
*/
renderGridLineNumber(lineNumber, linePos, startPos, breadth, dimensionType) {
+ let displayPixelRatio = getDisplayPixelRatio(this.win);
let { devicePixelRatio } = this.win;
- let displayPixelRatio = getDisplayPixelRatio(this.win);
+ let offset = (displayPixelRatio / 2) % 1;
- linePos = Math.round(linePos * devicePixelRatio);
- startPos = Math.round(startPos * devicePixelRatio);
- breadth = Math.round(breadth * devicePixelRatio);
+ linePos = Math.round(linePos);
+ startPos = Math.round(startPos);
+ breadth = Math.round(breadth);
if (linePos + breadth < 0) {
// The line is not visible on screen, don't render the line number
return;
}
this.ctx.save();
let canvasX = Math.round(this._canvasPosition.x * devicePixelRatio);
let canvasY = Math.round(this._canvasPosition.y * devicePixelRatio);
- this.ctx.translate(.5 - canvasX, .5 - canvasY);
+ this.ctx.translate(offset - canvasX, offset - canvasY);
let fontSize = (GRID_FONT_SIZE * displayPixelRatio);
this.ctx.font = fontSize + "px " + GRID_FONT_FAMILY;
let textWidth = this.ctx.measureText(lineNumber).width;
// The width of the character 'm' approximates the height of the text.
let textHeight = this.ctx.measureText("m").width;
@@ -1102,36 +1224,42 @@ CssGridHighlighter.prototype = extend(Au
let padding = 3 * displayPixelRatio;
let boxWidth = textWidth + 2 * padding;
let boxHeight = textHeight + 2 * padding;
// Calculate the x & y coordinates for the line number container, so that it is
// centered on the line, and in the middle of the gap if there is any.
let x, y;
+
if (dimensionType === COLUMNS) {
- x = linePos - boxWidth / 2;
- y = startPos - boxHeight / 2;
- x += breadth / 2;
+ x = linePos + breadth / 2;
+ y = startPos;
} else {
- x = startPos - boxWidth / 2;
- y = linePos - boxHeight / 2;
- y += breadth / 2;
+ x = startPos;
+ y = linePos + breadth / 2;
}
- x = Math.max(x, padding);
- y = Math.max(y, padding);
+ [x, y] = apply(this.currentMatrix, [x, y]);
+
+ x -= boxWidth / 2;
+ y -= boxHeight / 2;
- // Draw a rounded rectangle with a border width of 4 pixels, a border color matching
+ if (!this.hasNodeTransformations) {
+ x = Math.max(x, padding);
+ y = Math.max(y, padding);
+ }
+
+ // Draw a rounded rectangle with a border width of 2 pixels, a border color matching
// the grid color and a white background (the line number will be written in black).
this.ctx.lineWidth = 2 * displayPixelRatio;
this.ctx.strokeStyle = this.color;
this.ctx.fillStyle = "white";
let radius = 2 * displayPixelRatio;
- roundedRect(this.ctx, x, y, boxWidth, boxHeight, radius);
+ drawRoundedRect(this.ctx, x, y, boxWidth, boxHeight, radius);
// Write the line number inside of the rectangle.
this.ctx.fillStyle = "black";
this.ctx.fillText(lineNumber, x + padding, y + textHeight + padding);
this.ctx.restore();
},
@@ -1147,34 +1275,50 @@ 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) {
let { devicePixelRatio } = this.win;
- let x = Math.round(this._canvasPosition.x * devicePixelRatio);
- let y = Math.round(this._canvasPosition.y * devicePixelRatio);
+ let displayPixelRatio = getDisplayPixelRatio(this.win);
+ let offset = (displayPixelRatio / 2) % 1;
- linePos = Math.round(linePos * devicePixelRatio);
- startPos = Math.round(startPos * devicePixelRatio);
- breadth = Math.round(breadth * devicePixelRatio);
+ let canvasX = Math.round(this._canvasPosition.x * devicePixelRatio);
+ let canvasY = Math.round(this._canvasPosition.y * devicePixelRatio);
+
+ linePos = Math.round(linePos);
+ startPos = Math.round(startPos);
+ breadth = Math.round(breadth);
this.ctx.save();
this.ctx.fillStyle = this.getGridGapPattern(devicePixelRatio, dimensionType);
- this.ctx.translate(.5 - x, .5 - y);
+ this.ctx.translate(offset - canvasX, offset - canvasY);
if (dimensionType === COLUMNS) {
- endPos = isFinite(endPos) ? Math.round(endPos * devicePixelRatio) : CANVAS_SIZE + y;
- this.ctx.fillRect(linePos, startPos, breadth, endPos - startPos);
+ if (isFinite(endPos)) {
+ endPos = Math.round(endPos);
+ } else {
+ endPos = this._winDimensions.height;
+ startPos = -endPos;
+ }
+ drawRect(this.ctx, linePos, startPos, linePos + breadth, endPos,
+ this.currentMatrix);
} else {
- endPos = isFinite(endPos) ? Math.round(endPos * devicePixelRatio) : CANVAS_SIZE + x;
- this.ctx.fillRect(startPos, linePos, endPos - startPos, breadth);
+ if (isFinite(endPos)) {
+ endPos = Math.round(endPos);
+ } else {
+ endPos = this._winDimensions.width;
+ startPos = -endPos;
+ }
+ drawRect(this.ctx, startPos, linePos, endPos, linePos + breadth,
+ this.currentMatrix);
}
+ this.ctx.fill();
this.ctx.restore();
},
/**
* Render the grid area highlight for the given area name or for all the grid areas.
*
* @param {String} areaName
* Name of the grid area to be highlighted. If no area name is provided, all