forked from mirrors/gecko-dev
1840 lines
59 KiB
JavaScript
1840 lines
59 KiB
JavaScript
/* 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 { AutoRefreshHighlighter } = require("./auto-refresh");
|
|
const {
|
|
CanvasFrameAnonymousContentHelper,
|
|
createNode,
|
|
createSVGNode,
|
|
moveInfobar,
|
|
} = require("./utils/markup");
|
|
const {
|
|
getCurrentZoom,
|
|
getDisplayPixelRatio,
|
|
setIgnoreLayoutChanges,
|
|
getViewportDimensions,
|
|
} = require("devtools/shared/layout/utils");
|
|
const {
|
|
identity,
|
|
apply,
|
|
translate,
|
|
multiply,
|
|
scale,
|
|
isIdentity,
|
|
getNodeTransformationMatrix,
|
|
} = require("devtools/shared/layout/dom-matrix-2d");
|
|
const { stringifyGridFragments } = require("devtools/server/actors/utils/css-grid-utils");
|
|
const { LocalizationHelper } = require("devtools/shared/l10n");
|
|
|
|
const LAYOUT_STRINGS_URI = "devtools/client/locales/layout.properties";
|
|
const LAYOUT_L10N = new LocalizationHelper(LAYOUT_STRINGS_URI);
|
|
|
|
const CSS_GRID_ENABLED_PREF = "layout.css.grid.enabled";
|
|
const NEGATIVE_LINE_NUMBERS_PREF = "devtools.gridinspector.showNegativeLineNumbers";
|
|
|
|
const DEFAULT_GRID_COLOR = "#4B0082";
|
|
|
|
const COLUMNS = "cols";
|
|
const ROWS = "rows";
|
|
|
|
const GRID_FONT_SIZE = 10;
|
|
const GRID_FONT_FAMILY = "sans-serif";
|
|
const GRID_AREA_NAME_FONT_SIZE = "20";
|
|
|
|
const GRID_LINES_PROPERTIES = {
|
|
"edge": {
|
|
lineDash: [0, 0],
|
|
alpha: 1,
|
|
},
|
|
"explicit": {
|
|
lineDash: [5, 3],
|
|
alpha: 0.75,
|
|
},
|
|
"implicit": {
|
|
lineDash: [2, 2],
|
|
alpha: 0.5,
|
|
},
|
|
"areaEdge": {
|
|
lineDash: [0, 0],
|
|
alpha: 1,
|
|
lineWidth: 3,
|
|
}
|
|
};
|
|
|
|
const GRID_GAP_PATTERN_WIDTH = 14; // px
|
|
const GRID_GAP_PATTERN_HEIGHT = 14; // px
|
|
const GRID_GAP_PATTERN_LINE_DASH = [5, 3]; // px
|
|
const GRID_GAP_ALPHA = 0.5;
|
|
|
|
/**
|
|
* Cached used by `CssGridHighlighter.getGridGapPattern`.
|
|
*/
|
|
const gCachedGridPattern = new Map();
|
|
|
|
// We create a <canvas> element that has always 4096x4096 physical pixels, to displays
|
|
// our grid's overlay.
|
|
// Then, we move the element around when needed, to give the perception that it always
|
|
// covers the screen (See bug 1345434).
|
|
//
|
|
// This canvas size value is the safest we can use because most GPUs can handle it.
|
|
// It's also far from the maximum canvas memory allocation limit (4096x4096x4 is
|
|
// 67.108.864 bytes, where the limit is 500.000.000 bytes, see:
|
|
// http://searchfox.org/mozilla-central/source/gfx/thebes/gfxPrefs.h#401).
|
|
//
|
|
// 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;
|
|
|
|
/**
|
|
* Returns an array containing the four coordinates of a rectangle, given its diagonal
|
|
* as input; optionally applying a matrix, and a function to each of the coordinates'
|
|
* value.
|
|
*
|
|
* @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()]
|
|
* A transformation matrix to apply.
|
|
* @return {Array}
|
|
* The rect four corners' points transformed by the matrix given.
|
|
*/
|
|
function getPointsFromDiagonal(x1, y1, x2, y2, matrix = identity()) {
|
|
return [
|
|
[x1, y1],
|
|
[x2, y1],
|
|
[x2, y2],
|
|
[x1, y2]
|
|
].map(point => {
|
|
let transformedPoint = apply(matrix, point);
|
|
|
|
return {x: transformedPoint[0], y: transformedPoint[1]};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Takes an array of four points and returns a DOMRect-like object, represent the
|
|
* boundaries defined by the points given.
|
|
*
|
|
* @param {Array} points
|
|
* The four points.
|
|
* @return {Object}
|
|
* A DOMRect-like object.
|
|
*/
|
|
function getBoundsFromPoints(points) {
|
|
let bounds = {};
|
|
|
|
bounds.left = Math.min(points[0].x, points[1].x, points[2].x, points[3].x);
|
|
bounds.right = Math.max(points[0].x, points[1].x, points[2].x, points[3].x);
|
|
bounds.top = Math.min(points[0].y, points[1].y, points[2].y, points[3].y);
|
|
bounds.bottom = Math.max(points[0].y, points[1].y, points[2].y, points[3].y);
|
|
|
|
bounds.x = bounds.left;
|
|
bounds.y = bounds.top;
|
|
bounds.width = bounds.right - bounds.left;
|
|
bounds.height = bounds.bottom - bounds.top;
|
|
|
|
return bounds;
|
|
}
|
|
|
|
/**
|
|
* Takes an array of four points and returns a string represent a path description.
|
|
*
|
|
* @param {Array} points
|
|
* The four points.
|
|
* @return {String}
|
|
* A Path Description that can be used in svg's <path> element.
|
|
*/
|
|
function getPathDescriptionFromPoints(points) {
|
|
return "M" + points[0].x + "," + points[0].y + " " +
|
|
"L" + points[1].x + "," + points[1].y + " " +
|
|
"L" + points[2].x + "," + points[2].y + " " +
|
|
"L" + points[3].x + "," + points[3].y;
|
|
}
|
|
|
|
/**
|
|
* 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 {Object} [options]
|
|
* The options object.
|
|
* @param {Array} [options.matrix=identity()]
|
|
* The transformation matrix to apply.
|
|
* @param {Array} [options.extendToBoundaries]
|
|
* If set, the line will be extended to reach the boundaries specified.
|
|
*/
|
|
function drawLine(ctx, x1, y1, x2, y2, options) {
|
|
let matrix = options.matrix || identity();
|
|
|
|
let p1 = apply(matrix, [x1, y1]);
|
|
let p2 = apply(matrix, [x2, y2]);
|
|
|
|
x1 = p1[0];
|
|
y1 = p1[1];
|
|
x2 = p2[0];
|
|
y2 = p2[1];
|
|
|
|
if (options.extendToBoundaries) {
|
|
if (p1[1] === p2[1]) {
|
|
x1 = options.extendToBoundaries[0];
|
|
x2 = options.extendToBoundaries[2];
|
|
} else {
|
|
y1 = options.extendToBoundaries[1];
|
|
x1 = (p2[0] - p1[0]) * (y1 - p1[1]) / (p2[1] - p1[1]) + p1[0];
|
|
y2 = options.extendToBoundaries[3];
|
|
x2 = (p2[0] - p1[0]) * (y2 - p1[1]) / (p2[1] - p1[1]) + p1[0];
|
|
}
|
|
}
|
|
|
|
ctx.moveTo(Math.round(x1), Math.round(y1));
|
|
ctx.lineTo(Math.round(x2), Math.round(y2));
|
|
}
|
|
|
|
/**
|
|
* 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 = getPointsFromDiagonal(x1, y1, x2, y2, matrix);
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(Math.round(p[0].x), Math.round(p[0].y));
|
|
ctx.lineTo(Math.round(p[1].x), Math.round(p[1].y));
|
|
ctx.lineTo(Math.round(p[2].x), Math.round(p[2].y));
|
|
ctx.lineTo(Math.round(p[3].x), Math.round(p[3].y));
|
|
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.
|
|
*/
|
|
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);
|
|
* h.hide();
|
|
* h.destroy();
|
|
*
|
|
* Available Options:
|
|
* - color(colorValue)
|
|
* @param {String} colorValue
|
|
* The color that should be used to draw the highlighter for this grid.
|
|
* - showAllGridAreas(isShown)
|
|
* @param {Boolean} isShown
|
|
* Shows all the grid area highlights for the current grid if isShown is true.
|
|
* - showGridArea(areaName)
|
|
* @param {String} areaName
|
|
* Shows the grid area highlight for the given area name.
|
|
* - showGridAreasOverlay(isShown)
|
|
* @param {Boolean} isShown
|
|
* Displays an overlay of all the grid areas for the current grid container if
|
|
* isShown is true.
|
|
* - showGridCell({ gridFragmentIndex: Number, rowNumber: Number, columnNumber: Number })
|
|
* @param {Object} { gridFragmentIndex: Number, rowNumber: Number,
|
|
* columnNumber: Number }
|
|
* An object containing the grid fragment index, row and column numbers to the
|
|
* corresponding grid cell to highlight for the current grid.
|
|
* - showGridLineNames({ gridFragmentIndex: Number, lineNumber: Number,
|
|
* type: String })
|
|
* @param {Object} { gridFragmentIndex: Number, lineNumber: Number }
|
|
* An object containing the grid fragment index and line number to the
|
|
* corresponding grid line to highlight for the current grid.
|
|
* - showGridLineNumbers(isShown)
|
|
* @param {Boolean} isShown
|
|
* Displays the grid line numbers on the grid lines if isShown is true.
|
|
* - showInfiniteLines(isShown)
|
|
* @param {Boolean} isShown
|
|
* Displays an infinite line to represent the grid lines if isShown is true.
|
|
*
|
|
* Structure:
|
|
* <div class="highlighter-container">
|
|
* <canvas id="css-grid-canvas" class="css-grid-canvas">
|
|
* <svg class="css-grid-elements" hidden="true">
|
|
* <g class="css-grid-regions">
|
|
* <path class="css-grid-areas" points="..." />
|
|
* <path class="css-grid-cells" points="..." />
|
|
* </g>
|
|
* </svg>
|
|
* <div class="css-grid-area-infobar-container">
|
|
* <div class="css-grid-infobar">
|
|
* <div class="css-grid-infobar-text">
|
|
* <span class="css-grid-area-infobar-name">Grid Area Name</span>
|
|
* <span class="css-grid-area-infobar-dimensions">Grid Area Dimensions></span>
|
|
* </div>
|
|
* </div>
|
|
* </div>
|
|
* <div class="css-grid-cell-infobar-container">
|
|
* <div class="css-grid-infobar">
|
|
* <div class="css-grid-infobar-text">
|
|
* <span class="css-grid-cell-infobar-position">Grid Cell Position</span>
|
|
* <span class="css-grid-cell-infobar-dimensions">Grid Cell Dimensions></span>
|
|
* </div>
|
|
* </div>
|
|
* <div class="css-grid-line-infobar-container">
|
|
* <div class="css-grid-infobar">
|
|
* <div class="css-grid-infobar-text">
|
|
* <span class="css-grid-line-infobar-number">Grid Line Number</span>
|
|
* <span class="css-grid-line-infobar-names">Grid Line Names></span>
|
|
* </div>
|
|
* </div>
|
|
* </div>
|
|
* </div>
|
|
*/
|
|
class CssGridHighlighter extends AutoRefreshHighlighter {
|
|
constructor(highlighterEnv) {
|
|
super(highlighterEnv);
|
|
|
|
this.ID_CLASS_PREFIX = "css-grid-";
|
|
|
|
this.markup = new CanvasFrameAnonymousContentHelper(this.highlighterEnv,
|
|
this._buildMarkup.bind(this));
|
|
|
|
this.onPageHide = this.onPageHide.bind(this);
|
|
this.onWillNavigate = this.onWillNavigate.bind(this);
|
|
|
|
this.highlighterEnv.on("will-navigate", this.onWillNavigate);
|
|
|
|
let { pageListenerTarget } = highlighterEnv;
|
|
pageListenerTarget.addEventListener("pagehide", this.onPageHide);
|
|
|
|
// Initialize the <canvas> position to the top left corner of the page
|
|
this._canvasPosition = {
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
|
|
// Calling `calculateCanvasPosition` anyway since the highlighter could be initialized
|
|
// on a page that has scrolled already.
|
|
this.calculateCanvasPosition();
|
|
}
|
|
|
|
_buildMarkup() {
|
|
let container = createNode(this.win, {
|
|
attributes: {
|
|
"class": "highlighter-container"
|
|
}
|
|
});
|
|
|
|
let root = createNode(this.win, {
|
|
parent: container,
|
|
attributes: {
|
|
"id": "root",
|
|
"class": "root"
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
|
|
// 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: root,
|
|
nodeType: "canvas",
|
|
attributes: {
|
|
"id": "canvas",
|
|
"class": "canvas",
|
|
"hidden": "true",
|
|
"width": CANVAS_SIZE,
|
|
"height": CANVAS_SIZE
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
|
|
// Build the SVG element
|
|
let svg = createSVGNode(this.win, {
|
|
nodeType: "svg",
|
|
parent: root,
|
|
attributes: {
|
|
"id": "elements",
|
|
"width": "100%",
|
|
"height": "100%",
|
|
"hidden": "true"
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
|
|
let regions = createSVGNode(this.win, {
|
|
nodeType: "g",
|
|
parent: svg,
|
|
attributes: {
|
|
"class": "regions"
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
|
|
createSVGNode(this.win, {
|
|
nodeType: "path",
|
|
parent: regions,
|
|
attributes: {
|
|
"class": "areas",
|
|
"id": "areas"
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
|
|
createSVGNode(this.win, {
|
|
nodeType: "path",
|
|
parent: regions,
|
|
attributes: {
|
|
"class": "cells",
|
|
"id": "cells"
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
|
|
// Building the grid area infobar markup
|
|
let areaInfobarContainer = createNode(this.win, {
|
|
parent: container,
|
|
attributes: {
|
|
"class": "area-infobar-container",
|
|
"id": "area-infobar-container",
|
|
"position": "top",
|
|
"hidden": "true"
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
|
|
let areaInfobar = createNode(this.win, {
|
|
parent: areaInfobarContainer,
|
|
attributes: {
|
|
"class": "infobar"
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
|
|
let areaTextbox = createNode(this.win, {
|
|
parent: areaInfobar,
|
|
attributes: {
|
|
"class": "infobar-text"
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
createNode(this.win, {
|
|
nodeType: "span",
|
|
parent: areaTextbox,
|
|
attributes: {
|
|
"class": "area-infobar-name",
|
|
"id": "area-infobar-name"
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
createNode(this.win, {
|
|
nodeType: "span",
|
|
parent: areaTextbox,
|
|
attributes: {
|
|
"class": "area-infobar-dimensions",
|
|
"id": "area-infobar-dimensions"
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
|
|
// Building the grid cell infobar markup
|
|
let cellInfobarContainer = createNode(this.win, {
|
|
parent: container,
|
|
attributes: {
|
|
"class": "cell-infobar-container",
|
|
"id": "cell-infobar-container",
|
|
"position": "top",
|
|
"hidden": "true"
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
|
|
let cellInfobar = createNode(this.win, {
|
|
parent: cellInfobarContainer,
|
|
attributes: {
|
|
"class": "infobar"
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
|
|
let cellTextbox = createNode(this.win, {
|
|
parent: cellInfobar,
|
|
attributes: {
|
|
"class": "infobar-text"
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
createNode(this.win, {
|
|
nodeType: "span",
|
|
parent: cellTextbox,
|
|
attributes: {
|
|
"class": "cell-infobar-position",
|
|
"id": "cell-infobar-position"
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
createNode(this.win, {
|
|
nodeType: "span",
|
|
parent: cellTextbox,
|
|
attributes: {
|
|
"class": "cell-infobar-dimensions",
|
|
"id": "cell-infobar-dimensions"
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
|
|
// Building the grid line infobar markup
|
|
let lineInfobarContainer = createNode(this.win, {
|
|
parent: container,
|
|
attributes: {
|
|
"class": "line-infobar-container",
|
|
"id": "line-infobar-container",
|
|
"position": "top",
|
|
"hidden": "true"
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
|
|
let lineInfobar = createNode(this.win, {
|
|
parent: lineInfobarContainer,
|
|
attributes: {
|
|
"class": "infobar"
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
|
|
let lineTextbox = createNode(this.win, {
|
|
parent: lineInfobar,
|
|
attributes: {
|
|
"class": "infobar-text"
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
createNode(this.win, {
|
|
nodeType: "span",
|
|
parent: lineTextbox,
|
|
attributes: {
|
|
"class": "line-infobar-number",
|
|
"id": "line-infobar-number"
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
createNode(this.win, {
|
|
nodeType: "span",
|
|
parent: lineTextbox,
|
|
attributes: {
|
|
"class": "line-infobar-names",
|
|
"id": "line-infobar-names"
|
|
},
|
|
prefix: this.ID_CLASS_PREFIX
|
|
});
|
|
|
|
return container;
|
|
}
|
|
|
|
destroy() {
|
|
let { highlighterEnv } = this;
|
|
highlighterEnv.off("will-navigate", this.onWillNavigate);
|
|
|
|
let { pageListenerTarget } = highlighterEnv;
|
|
if (pageListenerTarget) {
|
|
pageListenerTarget.removeEventListener("pagehide", this.onPageHide);
|
|
}
|
|
|
|
this.markup.destroy();
|
|
|
|
// Clear the pattern cache to avoid dead object exceptions (Bug 1342051).
|
|
this._clearCache();
|
|
AutoRefreshHighlighter.prototype.destroy.call(this);
|
|
}
|
|
|
|
getElement(id) {
|
|
return this.markup.getElement(this.ID_CLASS_PREFIX + id);
|
|
}
|
|
|
|
get ctx() {
|
|
return this.canvas.getCanvasContext("2d");
|
|
}
|
|
|
|
get canvas() {
|
|
return this.getElement("canvas");
|
|
}
|
|
|
|
get color() {
|
|
return this.options.color || DEFAULT_GRID_COLOR;
|
|
}
|
|
|
|
/**
|
|
* Gets the grid gap pattern used to render the gap regions based on the device
|
|
* pixel ratio given.
|
|
*
|
|
* @param {Number} devicePixelRatio
|
|
* The device pixel ratio we want the pattern for.
|
|
* @param {Object} dimension
|
|
* Refers to the Map key for the grid dimension type which is either the
|
|
* constant COLUMNS or ROWS.
|
|
* @return {CanvasPattern} grid gap pattern.
|
|
*/
|
|
getGridGapPattern(devicePixelRatio, dimension) {
|
|
let gridPatternMap = null;
|
|
|
|
if (gCachedGridPattern.has(devicePixelRatio)) {
|
|
gridPatternMap = gCachedGridPattern.get(devicePixelRatio);
|
|
} else {
|
|
gridPatternMap = new Map();
|
|
}
|
|
|
|
if (gridPatternMap.has(dimension)) {
|
|
return gridPatternMap.get(dimension);
|
|
}
|
|
|
|
// Create the diagonal lines pattern for the rendering the grid gaps.
|
|
let canvas = createNode(this.win, { nodeType: "canvas" });
|
|
let width = canvas.width = GRID_GAP_PATTERN_WIDTH * devicePixelRatio;
|
|
let height = canvas.height = GRID_GAP_PATTERN_HEIGHT * devicePixelRatio;
|
|
|
|
let ctx = canvas.getContext("2d");
|
|
ctx.save();
|
|
ctx.setLineDash(GRID_GAP_PATTERN_LINE_DASH);
|
|
ctx.beginPath();
|
|
ctx.translate(.5, .5);
|
|
|
|
if (dimension === COLUMNS) {
|
|
ctx.moveTo(0, 0);
|
|
ctx.lineTo(width, height);
|
|
} else {
|
|
ctx.moveTo(width, 0);
|
|
ctx.lineTo(0, height);
|
|
}
|
|
|
|
ctx.strokeStyle = this.color;
|
|
ctx.globalAlpha = GRID_GAP_ALPHA;
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
|
|
let pattern = ctx.createPattern(canvas, "repeat");
|
|
|
|
gridPatternMap.set(dimension, pattern);
|
|
gCachedGridPattern.set(devicePixelRatio, gridPatternMap);
|
|
|
|
return pattern;
|
|
}
|
|
|
|
onPageHide({ target }) {
|
|
// If a page hide event is triggered for current window's highlighter, hide the
|
|
// highlighter.
|
|
if (target.defaultView === this.win) {
|
|
this.hide();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when the page will-navigate. Used to hide the grid highlighter and clear
|
|
* the cached gap patterns and avoid using DeadWrapper obejcts as gap patterns the
|
|
* next time.
|
|
*/
|
|
onWillNavigate({ isTopLevel }) {
|
|
this._clearCache();
|
|
|
|
if (isTopLevel) {
|
|
this.hide();
|
|
}
|
|
}
|
|
|
|
_show() {
|
|
if (Services.prefs.getBoolPref(CSS_GRID_ENABLED_PREF) && !this.isGrid()) {
|
|
this.hide();
|
|
return false;
|
|
}
|
|
|
|
// The grid pattern cache should be cleared in case the color changed.
|
|
this._clearCache();
|
|
|
|
// Hide the canvas, grid element highlights and infobar.
|
|
this._hide();
|
|
|
|
return this._update();
|
|
}
|
|
|
|
_clearCache() {
|
|
gCachedGridPattern.clear();
|
|
}
|
|
|
|
/**
|
|
* Shows the grid area highlight for the given area name.
|
|
*
|
|
* @param {String} areaName
|
|
* Grid area name.
|
|
*/
|
|
showGridArea(areaName) {
|
|
this.renderGridArea(areaName);
|
|
}
|
|
|
|
/**
|
|
* Shows all the grid area highlights for the current grid.
|
|
*/
|
|
showAllGridAreas() {
|
|
this.renderGridArea();
|
|
}
|
|
|
|
/**
|
|
* Clear the grid area highlights.
|
|
*/
|
|
clearGridAreas() {
|
|
let areas = this.getElement("areas");
|
|
areas.setAttribute("d", "");
|
|
}
|
|
|
|
/**
|
|
* Shows the grid cell highlight for the given grid cell options.
|
|
*
|
|
* @param {Number} options.gridFragmentIndex
|
|
* Index of the grid fragment to render the grid cell highlight.
|
|
* @param {Number} options.rowNumber
|
|
* Row number of the grid cell to highlight.
|
|
* @param {Number} options.columnNumber
|
|
* Column number of the grid cell to highlight.
|
|
*/
|
|
showGridCell({ gridFragmentIndex, rowNumber, columnNumber }) {
|
|
this.renderGridCell(gridFragmentIndex, rowNumber, columnNumber);
|
|
}
|
|
|
|
/**
|
|
* Shows the grid line highlight for the given grid line options.
|
|
*
|
|
* @param {Number} options.gridFragmentIndex
|
|
* Index of the grid fragment to render the grid line highlight.
|
|
* @param {Number} options.lineNumber
|
|
* Line number of the grid line to highlight.
|
|
* @param {String} options.type
|
|
* The dimension type of the grid line.
|
|
*/
|
|
showGridLineNames({ gridFragmentIndex, lineNumber, type }) {
|
|
this.renderGridLineNames(gridFragmentIndex, lineNumber, type);
|
|
}
|
|
|
|
/**
|
|
* Clear the grid cell highlights.
|
|
*/
|
|
clearGridCell() {
|
|
let cells = this.getElement("cells");
|
|
cells.setAttribute("d", "");
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* Is a given grid fragment valid? i.e. does it actually have tracks? In some cases, we
|
|
* may have a fragment that defines column tracks but doesn't have any rows (or vice
|
|
* versa). In which case we do not want to draw anything for that fragment.
|
|
*
|
|
* @param {Object} fragment
|
|
* @return {Boolean}
|
|
*/
|
|
isValidFragment(fragment) {
|
|
return fragment.cols.tracks.length && fragment.rows.tracks.length;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
|
|
let root = this.getElement("root");
|
|
let cells = this.getElement("cells");
|
|
let areas = this.getElement("areas");
|
|
|
|
// Hide the root element and force the reflow in order to get the proper window's
|
|
// dimensions without increasing them.
|
|
root.setAttribute("style", "display: none");
|
|
this.win.document.documentElement.offsetWidth;
|
|
|
|
// Set the grid cells and areas fill to the current grid colour.
|
|
cells.setAttribute("style", `fill: ${this.color}`);
|
|
areas.setAttribute("style", `fill: ${this.color}`);
|
|
|
|
let { width, height } = this._winDimensions;
|
|
|
|
// 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++) {
|
|
this.renderFragment(this.gridData[i]);
|
|
}
|
|
|
|
// Display the grid area highlights if needed.
|
|
if (this.options.showAllGridAreas) {
|
|
this.showAllGridAreas();
|
|
} else if (this.options.showGridArea) {
|
|
this.showGridArea(this.options.showGridArea);
|
|
}
|
|
|
|
// Display the grid cell highlights if needed.
|
|
if (this.options.showGridCell) {
|
|
this.showGridCell(this.options.showGridCell);
|
|
}
|
|
|
|
// Display the grid line names if needed.
|
|
if (this.options.showGridLineNames) {
|
|
this.showGridLineNames(this.options.showGridLineNames);
|
|
}
|
|
|
|
this._showGrid();
|
|
this._showGridElements();
|
|
|
|
root.setAttribute("style",
|
|
`position:absolute; width:${width}px;height:${height}px; overflow:hidden`);
|
|
|
|
setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Update the grid information displayed in the grid area info bar.
|
|
*
|
|
* @param {GridArea} area
|
|
* The grid area object.
|
|
* @param {Object} bounds
|
|
* A DOMRect-like object represent the grid area rectangle.
|
|
*/
|
|
_updateGridAreaInfobar(area, bounds) {
|
|
let { width, height } = bounds;
|
|
let dim = parseFloat(width.toPrecision(6)) +
|
|
" \u00D7 " +
|
|
parseFloat(height.toPrecision(6));
|
|
|
|
this.getElement("area-infobar-name").setTextContent(area.name);
|
|
this.getElement("area-infobar-dimensions").setTextContent(dim);
|
|
|
|
let container = this.getElement("area-infobar-container");
|
|
moveInfobar(container, bounds, this.win, {
|
|
position: "bottom",
|
|
hideIfOffscreen: true
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update the grid information displayed in the grid cell info bar.
|
|
*
|
|
* @param {Number} rowNumber
|
|
* The grid cell's row number.
|
|
* @param {Number} columnNumber
|
|
* The grid cell's column number.
|
|
* @param {Object} bounds
|
|
* A DOMRect-like object represent the grid cell rectangle.
|
|
*/
|
|
_updateGridCellInfobar(rowNumber, columnNumber, bounds) {
|
|
let { width, height } = bounds;
|
|
let dim = parseFloat(width.toPrecision(6)) +
|
|
" \u00D7 " +
|
|
parseFloat(height.toPrecision(6));
|
|
let position = LAYOUT_L10N.getFormatStr("layout.rowColumnPositions",
|
|
rowNumber, columnNumber);
|
|
|
|
this.getElement("cell-infobar-position").setTextContent(position);
|
|
this.getElement("cell-infobar-dimensions").setTextContent(dim);
|
|
|
|
let container = this.getElement("cell-infobar-container");
|
|
moveInfobar(container, bounds, this.win, {
|
|
position: "top",
|
|
hideIfOffscreen: true
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update the grid information displayed in the grid line info bar.
|
|
*
|
|
* @param {String} gridLineNames
|
|
* Comma-separated string of names for the grid line.
|
|
* @param {Number} gridLineNumber
|
|
* The grid line number.
|
|
* @param {Number} x
|
|
* The x-coordinate of the grid line.
|
|
* @param {Number} y
|
|
* The y-coordinate of the grid line.
|
|
*/
|
|
_updateGridLineInfobar(gridLineNames, gridLineNumber, x, y) {
|
|
this.getElement("line-infobar-number").setTextContent(gridLineNumber);
|
|
this.getElement("line-infobar-names").setTextContent(gridLineNames);
|
|
|
|
let container = this.getElement("line-infobar-container");
|
|
moveInfobar(container,
|
|
getBoundsFromPoints([{x, y}, {x, y}, {x, y}, {x, y}]), this.win);
|
|
}
|
|
|
|
/**
|
|
* The <canvas>'s position needs to be updated if the page scrolls too much, in order
|
|
* to give the illusion that it always covers the viewport.
|
|
*/
|
|
_scrollUpdate() {
|
|
let hasPositionChanged = this.calculateCanvasPosition();
|
|
|
|
if (hasPositionChanged) {
|
|
this._update();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method is responsible to do the math that updates the <canvas>'s position,
|
|
* in accordance with the page's scroll, document's size, canvas size, and
|
|
* viewport's size.
|
|
* It's called when a page's scroll is detected.
|
|
*
|
|
* @return {Boolean} `true` if the <canvas> position was updated, `false` otherwise.
|
|
*/
|
|
calculateCanvasPosition() {
|
|
let cssCanvasSize = CANVAS_SIZE / this.win.devicePixelRatio;
|
|
let viewportSize = getViewportDimensions(this.win);
|
|
let documentSize = this._winDimensions;
|
|
let pageX = this._scroll.x;
|
|
let pageY = this._scroll.y;
|
|
let canvasWidth = cssCanvasSize;
|
|
let canvasHeight = cssCanvasSize;
|
|
let hasUpdated = false;
|
|
|
|
// Those values indicates the relative horizontal and vertical space the page can
|
|
// scroll before we have to reposition the <canvas>; they're 1/4 of the delta between
|
|
// the canvas' size and the viewport's size: that's because we want to consider both
|
|
// sides (top/bottom, left/right; so 1/2 for each side) and also we don't want to
|
|
// shown the edges of the canvas in case of fast scrolling (to avoid showing undraw
|
|
// areas, therefore another 1/2 here).
|
|
let bufferSizeX = (canvasWidth - viewportSize.width) >> 2;
|
|
let bufferSizeY = (canvasHeight - viewportSize.height) >> 2;
|
|
|
|
let { x, y } = this._canvasPosition;
|
|
|
|
// Defines the boundaries for the canvas.
|
|
let topBoundary = 0;
|
|
let bottomBoundary = documentSize.height - canvasHeight;
|
|
let leftBoundary = 0;
|
|
let rightBoundary = documentSize.width - canvasWidth;
|
|
|
|
// Defines the thresholds that triggers the canvas' position to be updated.
|
|
let topThreshold = pageY - bufferSizeY;
|
|
let bottomThreshold = pageY - canvasHeight + viewportSize.height + bufferSizeY;
|
|
let leftThreshold = pageX - bufferSizeX;
|
|
let rightThreshold = pageX - canvasWidth + viewportSize.width + bufferSizeX;
|
|
|
|
if (y < bottomBoundary && y < bottomThreshold) {
|
|
this._canvasPosition.y = Math.min(topThreshold, bottomBoundary);
|
|
hasUpdated = true;
|
|
} else if (y > topBoundary && y > topThreshold) {
|
|
this._canvasPosition.y = Math.max(bottomThreshold, topBoundary);
|
|
hasUpdated = true;
|
|
}
|
|
|
|
if (x < rightBoundary && x < rightThreshold) {
|
|
this._canvasPosition.x = Math.min(leftThreshold, rightBoundary);
|
|
hasUpdated = true;
|
|
} else if (x > leftBoundary && x > leftThreshold) {
|
|
this._canvasPosition.x = Math.max(rightThreshold, leftBoundary);
|
|
hasUpdated = true;
|
|
}
|
|
|
|
return hasUpdated;
|
|
}
|
|
|
|
/**
|
|
* Updates the <canvas> element's style in accordance with the current window's
|
|
* devicePixelRatio, and the position calculated in `calculateCanvasPosition`; it also
|
|
* clears the drawing context.
|
|
*/
|
|
updateCanvasElement() {
|
|
let size = CANVAS_SIZE / this.win.devicePixelRatio;
|
|
let { x, y } = this._canvasPosition;
|
|
|
|
// 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 matrices for both canvas drawing and SVG, 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. The translation given by the top and left padding of the element.
|
|
* 5. 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 computedStyle = this.currentNode.ownerGlobal.getComputedStyle(this.currentNode);
|
|
|
|
let paddingTop = parseFloat(computedStyle.paddingTop);
|
|
let paddingLeft = parseFloat(computedStyle.paddingLeft);
|
|
let borderTop = parseFloat(computedStyle.borderTopWidth);
|
|
let borderLeft = parseFloat(computedStyle.borderLeftWidth);
|
|
|
|
let nodeMatrix = getNodeTransformationMatrix(this.currentNode,
|
|
this.win.document.documentElement);
|
|
|
|
let m = identity();
|
|
|
|
// First, we scale based on the device pixel ratio.
|
|
m = multiply(m, scale(this.win.devicePixelRatio));
|
|
// Then, we apply the current node's transformation matrix, relative to the
|
|
// inspected window's root element, but only if it's not a identity matrix.
|
|
if (isIdentity(nodeMatrix)) {
|
|
this.hasNodeTransformations = false;
|
|
} else {
|
|
m = multiply(m, nodeMatrix);
|
|
this.hasNodeTransformations = true;
|
|
}
|
|
|
|
// Finally, we translate the origin based on the node's padding and border values.
|
|
m = multiply(m, translate(paddingLeft + borderLeft, paddingTop + borderTop));
|
|
|
|
this.currentMatrix = m;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Get the GridLine index of the last edge of the explicit grid for a grid dimension.
|
|
*
|
|
* @param {GridTracks} tracks
|
|
* The grid track of a given grid dimension.
|
|
* @return {Number} index of the last edge of the explicit grid for a grid dimension.
|
|
*/
|
|
getLastEdgeLineIndex(tracks) {
|
|
let trackIndex = tracks.length - 1;
|
|
|
|
// Traverse the grid track backwards until we find an explicit track.
|
|
while (trackIndex >= 0 && tracks[trackIndex].type != "explicit") {
|
|
trackIndex--;
|
|
}
|
|
|
|
// The grid line index is the grid track index + 1.
|
|
return trackIndex + 1;
|
|
}
|
|
|
|
renderFragment(fragment) {
|
|
if (!this.isValidFragment(fragment)) {
|
|
return;
|
|
}
|
|
|
|
this.renderLines(fragment.cols, COLUMNS, "left", "top", "height",
|
|
this.getFirstRowLinePos(fragment),
|
|
this.getLastRowLinePos(fragment));
|
|
this.renderLines(fragment.rows, ROWS, "top", "left", "width",
|
|
this.getFirstColLinePos(fragment),
|
|
this.getLastColLinePos(fragment));
|
|
|
|
if (this.options.showGridAreasOverlay) {
|
|
this.renderGridAreaOverlay();
|
|
}
|
|
|
|
// Line numbers are rendered in a 2nd step to avoid overlapping with existing lines.
|
|
if (this.options.showGridLineNumbers) {
|
|
this.renderLineNumbers(fragment.cols, COLUMNS, "left", "top",
|
|
this.getFirstRowLinePos(fragment));
|
|
this.renderLineNumbers(fragment.rows, ROWS, "top", "left",
|
|
this.getFirstColLinePos(fragment));
|
|
|
|
if (Services.prefs.getBoolPref(NEGATIVE_LINE_NUMBERS_PREF)) {
|
|
this.renderNegativeLineNumbers(fragment.cols, COLUMNS, "left", "top",
|
|
this.getLastRowLinePos(fragment));
|
|
this.renderNegativeLineNumbers(fragment.rows, ROWS, "top", "left",
|
|
this.getLastColLinePos(fragment));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render the negative grid lines given the grid dimension information of the
|
|
* column or row lines.
|
|
*
|
|
* See @param for renderLines.
|
|
*/
|
|
renderNegativeLineNumbers(gridDimension, dimensionType, mainSide, crossSide,
|
|
startPos) {
|
|
let lineStartPos = startPos;
|
|
|
|
const { lines } = gridDimension;
|
|
|
|
for (let i = 0, line = lines[i]; i < lines.length; line = lines[++i]) {
|
|
let linePos = line.start;
|
|
|
|
const negativeLineNumber = i - lines.length;
|
|
|
|
this.renderGridLineNumber(negativeLineNumber, linePos, lineStartPos, line.breadth,
|
|
dimensionType);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renders the grid area overlay on the css grid highlighter canvas.
|
|
*/
|
|
renderGridAreaOverlay() {
|
|
let padding = 1;
|
|
|
|
for (let i = 0; i < this.gridData.length; i++) {
|
|
let fragment = this.gridData[i];
|
|
|
|
for (let area of fragment.areas) {
|
|
let { rowStart, rowEnd, columnStart, columnEnd, type } = area;
|
|
|
|
if (type === "implicit") {
|
|
continue;
|
|
}
|
|
|
|
// Draw the line edges for the grid area
|
|
const areaColStart = fragment.cols.lines[columnStart - 1];
|
|
const areaColEnd = fragment.cols.lines[columnEnd - 1];
|
|
|
|
const areaRowStart = fragment.rows.lines[rowStart - 1];
|
|
const areaRowEnd = fragment.rows.lines[rowEnd - 1];
|
|
|
|
const areaColStartLinePos = areaColStart.start + areaColStart.breadth;
|
|
const areaRowStartLinePos = areaRowStart.start + areaRowStart.breadth;
|
|
|
|
this.renderLine(areaColStartLinePos + padding,
|
|
areaRowStartLinePos, areaRowEnd.start,
|
|
COLUMNS, "areaEdge");
|
|
this.renderLine(areaColEnd.start - padding,
|
|
areaRowStartLinePos, areaRowEnd.start,
|
|
COLUMNS, "areaEdge");
|
|
|
|
this.renderLine(areaRowStartLinePos + padding,
|
|
areaColStartLinePos, areaColEnd.start,
|
|
ROWS, "areaEdge");
|
|
this.renderLine(areaRowEnd.start - padding,
|
|
areaColStartLinePos, areaColEnd.start,
|
|
ROWS, "areaEdge");
|
|
|
|
this.renderGridAreaName(fragment, area);
|
|
}
|
|
}
|
|
|
|
this.ctx.restore();
|
|
}
|
|
|
|
/**
|
|
* Render grid area name on the containing grid area cell.
|
|
*
|
|
* @param {Object} fragment
|
|
* The grid fragment of the grid container.
|
|
* @param {Object} area
|
|
* The area overlay to render on the CSS highlighter canvas.
|
|
*/
|
|
renderGridAreaName(fragment, area) {
|
|
let { rowStart, rowEnd, columnStart, columnEnd } = area;
|
|
let { devicePixelRatio } = this.win;
|
|
let displayPixelRatio = getDisplayPixelRatio(this.win);
|
|
let offset = (displayPixelRatio / 2) % 1;
|
|
let fontSize = (GRID_AREA_NAME_FONT_SIZE * displayPixelRatio);
|
|
|
|
this.ctx.save();
|
|
|
|
let canvasX = Math.round(this._canvasPosition.x * devicePixelRatio);
|
|
let canvasY = Math.round(this._canvasPosition.y * devicePixelRatio);
|
|
this.ctx.translate(offset - canvasX, offset - canvasY);
|
|
|
|
this.ctx.font = fontSize + "px " + GRID_FONT_FAMILY;
|
|
this.ctx.strokeStyle = this.color;
|
|
this.ctx.textAlign = "center";
|
|
this.ctx.textBaseline = "middle";
|
|
|
|
// Draw the text for the grid area name.
|
|
for (let rowNumber = rowStart; rowNumber < rowEnd; rowNumber++) {
|
|
for (let columnNumber = columnStart; columnNumber < columnEnd; columnNumber++) {
|
|
let row = fragment.rows.tracks[rowNumber - 1];
|
|
let column = fragment.cols.tracks[columnNumber - 1];
|
|
|
|
// Check if the font size is exceeds the bounds of the containing grid cell.
|
|
if (fontSize > (column.breadth * displayPixelRatio) ||
|
|
fontSize > (row.breadth * displayPixelRatio)) {
|
|
fontSize = (column.breadth + row.breadth) / 2;
|
|
this.ctx.font = fontSize + "px " + GRID_FONT_FAMILY;
|
|
}
|
|
|
|
let textWidth = this.ctx.measureText(area.name).width;
|
|
|
|
// The width of the character 'm' approximates the height of the text.
|
|
let textHeight = this.ctx.measureText("m").width;
|
|
|
|
// Padding in pixels for the line number text inside of the line number container.
|
|
let padding = 3 * displayPixelRatio;
|
|
|
|
let boxWidth = textWidth + 2 * padding;
|
|
let boxHeight = textHeight + 2 * padding;
|
|
|
|
let x = column.start + column.breadth / 2;
|
|
let y = row.start + row.breadth / 2;
|
|
|
|
[x, y] = apply(this.currentMatrix, [x, y]);
|
|
|
|
let rectXPos = x - boxWidth / 2;
|
|
let rectYPos = y - boxHeight / 2;
|
|
|
|
// Draw a rounded rectangle with a border width of 1 pixel,
|
|
// a border color matching the grid color, and a white background
|
|
this.ctx.lineWidth = 1 * displayPixelRatio;
|
|
this.ctx.strokeStyle = this.color;
|
|
this.ctx.fillStyle = "white";
|
|
let radius = 2 * displayPixelRatio;
|
|
drawRoundedRect(this.ctx, rectXPos, rectYPos, boxWidth, boxHeight, radius);
|
|
|
|
this.ctx.fillStyle = this.color;
|
|
this.ctx.fillText(area.name, x, y + padding);
|
|
}
|
|
}
|
|
|
|
this.ctx.restore();
|
|
}
|
|
|
|
/**
|
|
* Render the grid lines given the grid dimension information of the
|
|
* column or row lines.
|
|
*
|
|
* @param {GridDimension} gridDimension
|
|
* Column or row grid dimension object.
|
|
* @param {Object} quad.bounds
|
|
* The content bounds of the box model region quads.
|
|
* @param {String} dimensionType
|
|
* The grid dimension type which is either the constant COLUMNS or ROWS.
|
|
* @param {String} mainSide
|
|
* The main side of the given grid dimension - "top" for rows and
|
|
* "left" for columns.
|
|
* @param {String} crossSide
|
|
* The cross side of the given grid dimension - "left" for rows and
|
|
* "top" for columns.
|
|
* @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, dimensionType, mainSide, crossSide,
|
|
mainSize, startPos, endPos) {
|
|
let lineStartPos = startPos;
|
|
let lineEndPos = endPos;
|
|
|
|
let lastEdgeLineIndex = this.getLastEdgeLineIndex(gridDimension.tracks);
|
|
|
|
for (let i = 0; i < gridDimension.lines.length; i++) {
|
|
let line = gridDimension.lines[i];
|
|
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);
|
|
}
|
|
|
|
// Render a second line to illustrate the gutter for non-zero breadth.
|
|
if (line.breadth > 0) {
|
|
this.renderGridGap(linePos, lineStartPos, lineEndPos, line.breadth,
|
|
dimensionType);
|
|
this.renderLine(linePos + line.breadth, lineStartPos, lineEndPos, dimensionType,
|
|
gridDimension.tracks[i].type);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render the grid lines given the grid dimension information of the
|
|
* column or row lines.
|
|
*
|
|
* see @param for renderLines.
|
|
*/
|
|
renderLineNumbers(gridDimension, dimensionType, mainSide, crossSide,
|
|
startPos) {
|
|
let lineStartPos = startPos;
|
|
|
|
// Keep track of the number of collapsed lines per line position
|
|
let stackedLines = [];
|
|
|
|
for (let i = 0; i < gridDimension.lines.length; i++) {
|
|
let line = gridDimension.lines[i];
|
|
let linePos = line.start;
|
|
|
|
// If you place something using negative numbers, you can trigger some implicit grid
|
|
// creation above and to the left of the explicit grid (assuming a horizontal-tb
|
|
// writing mode).
|
|
// The first explicit grid line gets the number of 1; any implicit grid lines
|
|
// before 1 get negative numbers, but do not get any positivity numbers.
|
|
// Since here we're rendering only the positive line numbers, we have to skip any
|
|
// implicit grid lines before the first tha is explicit.
|
|
// For such lines the API returns always 0 as line's number.
|
|
if (line.number === 0) {
|
|
continue;
|
|
}
|
|
|
|
// Check for overlapping lines. We render a second box beneath the last overlapping
|
|
// line number to indicate there are lines beneath it.
|
|
const gridLine = gridDimension.tracks[line.number - 1];
|
|
if (gridLine) {
|
|
const { breadth } = gridLine;
|
|
if (breadth === 0) {
|
|
stackedLines.push(gridDimension.lines[i].number);
|
|
if (stackedLines.length > 0) {
|
|
this.renderGridLineNumber(line.number, linePos, lineStartPos, line.breadth,
|
|
dimensionType, 1);
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
this.renderGridLineNumber(line.number, linePos, lineStartPos, line.breadth,
|
|
dimensionType);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render the grid line on the css grid highlighter canvas.
|
|
*
|
|
* @param {Number} linePos
|
|
* The line position along the x-axis for a column grid line and
|
|
* y-axis for a row grid line.
|
|
* @param {Number} startPos
|
|
* The start position of the cross side of the grid line.
|
|
* @param {Number} endPos
|
|
* The end position of the cross side of the grid line.
|
|
* @param {String} dimensionType
|
|
* The grid dimension type which is either the constant COLUMNS or ROWS.
|
|
* @param {String} lineType
|
|
* The grid line type - "edge", "explicit", or "implicit".
|
|
*/
|
|
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);
|
|
startPos = Math.round(startPos);
|
|
endPos = Math.round(endPos);
|
|
|
|
this.ctx.save();
|
|
this.ctx.setLineDash(GRID_LINES_PROPERTIES[lineType].lineDash);
|
|
this.ctx.beginPath();
|
|
this.ctx.translate(offset - x, offset - y);
|
|
|
|
let lineOptions = {
|
|
matrix: this.currentMatrix
|
|
};
|
|
|
|
if (this.options.showInfiniteLines) {
|
|
lineOptions.extendToBoundaries = [x, y, x + CANVAS_SIZE, y + CANVAS_SIZE];
|
|
}
|
|
|
|
if (dimensionType === COLUMNS) {
|
|
drawLine(this.ctx, linePos, startPos, linePos, endPos, lineOptions);
|
|
} else {
|
|
drawLine(this.ctx, startPos, linePos, endPos, linePos, lineOptions);
|
|
}
|
|
|
|
this.ctx.strokeStyle = this.color;
|
|
this.ctx.globalAlpha = GRID_LINES_PROPERTIES[lineType].alpha;
|
|
|
|
if (GRID_LINES_PROPERTIES[lineType].lineWidth) {
|
|
this.ctx.lineWidth = GRID_LINES_PROPERTIES[lineType].lineWidth * devicePixelRatio;
|
|
} else {
|
|
this.ctx.lineWidth = lineWidth;
|
|
}
|
|
|
|
this.ctx.stroke();
|
|
this.ctx.restore();
|
|
}
|
|
|
|
/**
|
|
* Render the grid line number on the css grid highlighter canvas.
|
|
*
|
|
* @param {Number} lineNumber
|
|
* The grid line number.
|
|
* @param {Number} linePos
|
|
* The line position along the x-axis for a column grid line and
|
|
* y-axis for a row grid line.
|
|
* @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.
|
|
* @param {Number||undefined} stackedLineIndex
|
|
* The line index position of the stacked line.
|
|
*/
|
|
renderGridLineNumber(lineNumber, linePos, startPos, breadth, dimensionType,
|
|
stackedLineIndex) {
|
|
let displayPixelRatio = getDisplayPixelRatio(this.win);
|
|
let { devicePixelRatio } = this.win;
|
|
let offset = (displayPixelRatio / 2) % 1;
|
|
|
|
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(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;
|
|
|
|
// Padding in pixels for the line number text inside of the line number container.
|
|
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;
|
|
|
|
let startOffset = (boxHeight + 2) / devicePixelRatio;
|
|
|
|
if (Services.prefs.getBoolPref(NEGATIVE_LINE_NUMBERS_PREF)) {
|
|
// If the line number is negative, offset it from the grid container edge,
|
|
// (downwards if its a column, rightwards if its a row).
|
|
if (lineNumber < 0) {
|
|
startPos += startOffset;
|
|
} else {
|
|
startPos -= startOffset;
|
|
}
|
|
}
|
|
|
|
if (dimensionType === COLUMNS) {
|
|
x = linePos + breadth / 2;
|
|
y = startPos;
|
|
} else {
|
|
x = startPos;
|
|
y = linePos + breadth / 2;
|
|
}
|
|
|
|
[x, y] = apply(this.currentMatrix, [x, y]);
|
|
|
|
x -= boxWidth / 2;
|
|
y -= boxHeight / 2;
|
|
|
|
if (stackedLineIndex) {
|
|
// Offset the stacked line number by half of the box's width/height
|
|
const xOffset = boxWidth / 4;
|
|
const yOffset = boxHeight / 4;
|
|
|
|
x += xOffset;
|
|
y += yOffset;
|
|
}
|
|
|
|
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;
|
|
drawRoundedRect(this.ctx, x, y, boxWidth, boxHeight, radius);
|
|
|
|
// Write the line number inside of the rectangle.
|
|
this.ctx.fillStyle = "black";
|
|
const numberText = stackedLineIndex ? "" : lineNumber;
|
|
this.ctx.fillText(numberText, x + padding, y + textHeight + padding);
|
|
|
|
this.ctx.restore();
|
|
}
|
|
|
|
/**
|
|
* Render the grid gap area on the css grid highlighter canvas.
|
|
*
|
|
* @param {Number} linePos
|
|
* The line position along the x-axis for a column grid line and
|
|
* y-axis for a row grid line.
|
|
* @param {Number} startPos
|
|
* The start position of the cross side of the grid line.
|
|
* @param {Number} endPos
|
|
* 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 displayPixelRatio = getDisplayPixelRatio(this.win);
|
|
let offset = (displayPixelRatio / 2) % 1;
|
|
|
|
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(offset - canvasX, offset - canvasY);
|
|
|
|
if (dimensionType === COLUMNS) {
|
|
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 {
|
|
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
|
|
* the grid areas should be highlighted.
|
|
*/
|
|
renderGridArea(areaName) {
|
|
let paths = [];
|
|
let { devicePixelRatio } = this.win;
|
|
let displayPixelRatio = getDisplayPixelRatio(this.win);
|
|
|
|
for (let i = 0; i < this.gridData.length; i++) {
|
|
let fragment = this.gridData[i];
|
|
|
|
for (let area of fragment.areas) {
|
|
if (areaName && areaName != area.name) {
|
|
continue;
|
|
}
|
|
|
|
let rowStart = fragment.rows.lines[area.rowStart - 1];
|
|
let rowEnd = fragment.rows.lines[area.rowEnd - 1];
|
|
let columnStart = fragment.cols.lines[area.columnStart - 1];
|
|
let columnEnd = fragment.cols.lines[area.columnEnd - 1];
|
|
|
|
let x1 = columnStart.start + columnStart.breadth;
|
|
let y1 = rowStart.start + rowStart.breadth;
|
|
let x2 = columnEnd.start;
|
|
let y2 = rowEnd.start;
|
|
|
|
let points = getPointsFromDiagonal(x1, y1, x2, y2, this.currentMatrix);
|
|
|
|
// Scale down by `devicePixelRatio` since SVG element already take them into
|
|
// account.
|
|
let svgPoints = points.map(point => ({
|
|
x: Math.round(point.x / devicePixelRatio),
|
|
y: Math.round(point.y / devicePixelRatio)
|
|
}));
|
|
|
|
// Scale down by `displayPixelRatio` since infobar's HTML elements already take it
|
|
// into account; and the zoom scaling is handled by `moveInfobar`.
|
|
let bounds = getBoundsFromPoints(points.map(point => ({
|
|
x: Math.round(point.x / displayPixelRatio),
|
|
y: Math.round(point.y / displayPixelRatio)
|
|
})));
|
|
|
|
paths.push(getPathDescriptionFromPoints(svgPoints));
|
|
|
|
// Update and show the info bar when only displaying a single grid area.
|
|
if (areaName) {
|
|
this._showGridAreaInfoBar();
|
|
this._updateGridAreaInfobar(area, bounds);
|
|
}
|
|
}
|
|
}
|
|
|
|
let areas = this.getElement("areas");
|
|
areas.setAttribute("d", paths.join(" "));
|
|
}
|
|
|
|
/**
|
|
* Render the grid cell highlight for the given grid fragment index, row and column
|
|
* number.
|
|
*
|
|
* @param {Number} gridFragmentIndex
|
|
* Index of the grid fragment to render the grid cell highlight.
|
|
* @param {Number} rowNumber
|
|
* Row number of the grid cell to highlight.
|
|
* @param {Number} columnNumber
|
|
* Column number of the grid cell to highlight.
|
|
*/
|
|
renderGridCell(gridFragmentIndex, rowNumber, columnNumber) {
|
|
let fragment = this.gridData[gridFragmentIndex];
|
|
|
|
if (!fragment) {
|
|
return;
|
|
}
|
|
|
|
let row = fragment.rows.tracks[rowNumber - 1];
|
|
let column = fragment.cols.tracks[columnNumber - 1];
|
|
|
|
if (!row || !column) {
|
|
return;
|
|
}
|
|
|
|
let x1 = column.start;
|
|
let y1 = row.start;
|
|
let x2 = column.start + column.breadth;
|
|
let y2 = row.start + row.breadth;
|
|
|
|
let { devicePixelRatio } = this.win;
|
|
let displayPixelRatio = getDisplayPixelRatio(this.win);
|
|
|
|
let points = getPointsFromDiagonal(x1, y1, x2, y2, this.currentMatrix);
|
|
|
|
// Scale down by `devicePixelRatio` since SVG element already take them into account.
|
|
let svgPoints = points.map(point => ({
|
|
x: Math.round(point.x / devicePixelRatio),
|
|
y: Math.round(point.y / devicePixelRatio)
|
|
}));
|
|
|
|
// Scale down by `displayPixelRatio` since infobar's HTML elements already take it
|
|
// into account, and the zoom scaling is handled by `moveInfobar`.
|
|
let bounds = getBoundsFromPoints(points.map(point => ({
|
|
x: Math.round(point.x / displayPixelRatio),
|
|
y: Math.round(point.y / displayPixelRatio)
|
|
})));
|
|
|
|
let cells = this.getElement("cells");
|
|
cells.setAttribute("d", getPathDescriptionFromPoints(svgPoints));
|
|
|
|
this._showGridCellInfoBar();
|
|
this._updateGridCellInfobar(rowNumber, columnNumber, bounds);
|
|
}
|
|
|
|
/**
|
|
* Render the grid line name highlight for the given grid fragment index, lineNumber,
|
|
* and dimensionType.
|
|
*
|
|
* @param {Number} gridFragmentIndex
|
|
* Index of the grid fragment to render the grid line highlight.
|
|
* @param {Number} lineNumber
|
|
* Line number of the grid line to highlight.
|
|
* @param {String} dimensionType
|
|
* The dimension type of the grid line.
|
|
*/
|
|
renderGridLineNames(gridFragmentIndex, lineNumber, dimensionType) {
|
|
let fragment = this.gridData[gridFragmentIndex];
|
|
|
|
if (!fragment || !lineNumber || !dimensionType) {
|
|
return;
|
|
}
|
|
|
|
const { names } = fragment[dimensionType].lines[lineNumber - 1];
|
|
let linePos;
|
|
|
|
if (dimensionType === ROWS) {
|
|
linePos = fragment.rows.lines[lineNumber - 1];
|
|
} else if (dimensionType === COLUMNS) {
|
|
linePos = fragment.cols.lines[lineNumber - 1];
|
|
}
|
|
|
|
if (!linePos) {
|
|
return;
|
|
}
|
|
|
|
let currentZoom = getCurrentZoom(this.win);
|
|
let { bounds } = this.currentQuads.content[gridFragmentIndex];
|
|
|
|
const rowYPosition = fragment.rows.lines[0];
|
|
const colXPosition = fragment.rows.lines[0];
|
|
|
|
let x = dimensionType === COLUMNS
|
|
? linePos.start + (bounds.left / currentZoom)
|
|
: colXPosition.start + (bounds.left / currentZoom);
|
|
|
|
let y = dimensionType === ROWS
|
|
? linePos.start + (bounds.top / currentZoom)
|
|
: rowYPosition.start + (bounds.top / currentZoom);
|
|
|
|
this._showGridLineInfoBar();
|
|
this._updateGridLineInfobar(names.join(", "), lineNumber, x, y);
|
|
}
|
|
|
|
/**
|
|
* Hide the highlighter, the canvas and the infobars.
|
|
*/
|
|
_hide() {
|
|
setIgnoreLayoutChanges(true);
|
|
this._hideGrid();
|
|
this._hideGridElements();
|
|
this._hideGridAreaInfoBar();
|
|
this._hideGridCellInfoBar();
|
|
this._hideGridLineInfoBar();
|
|
setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
|
|
}
|
|
|
|
_hideGrid() {
|
|
this.getElement("canvas").setAttribute("hidden", "true");
|
|
}
|
|
|
|
_showGrid() {
|
|
this.getElement("canvas").removeAttribute("hidden");
|
|
}
|
|
|
|
_hideGridElements() {
|
|
this.getElement("elements").setAttribute("hidden", "true");
|
|
}
|
|
|
|
_showGridElements() {
|
|
this.getElement("elements").removeAttribute("hidden");
|
|
}
|
|
|
|
_hideGridAreaInfoBar() {
|
|
this.getElement("area-infobar-container").setAttribute("hidden", "true");
|
|
}
|
|
|
|
_showGridAreaInfoBar() {
|
|
this.getElement("area-infobar-container").removeAttribute("hidden");
|
|
}
|
|
|
|
_hideGridCellInfoBar() {
|
|
this.getElement("cell-infobar-container").setAttribute("hidden", "true");
|
|
}
|
|
|
|
_showGridCellInfoBar() {
|
|
this.getElement("cell-infobar-container").removeAttribute("hidden");
|
|
}
|
|
|
|
_hideGridLineInfoBar() {
|
|
this.getElement("line-infobar-container").setAttribute("hidden", "true");
|
|
}
|
|
|
|
_showGridLineInfoBar() {
|
|
this.getElement("line-infobar-container").removeAttribute("hidden");
|
|
}
|
|
}
|
|
|
|
exports.CssGridHighlighter = CssGridHighlighter;
|