--- a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_01.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_01.js
@@ -3,58 +3,75 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Test the creation of the CSS shapes highlighter.
const TEST_URL = URL_ROOT + "doc_inspector_highlighter_cssshapes.html";
const HIGHLIGHTER_TYPE = "ShapesHighlighter";
+const SHAPE_IDS = ["polygon", "ellipse", "rect"];
+const SHAPE_TYPES = [
+ {
+ shapeName: "polygon",
+ highlighter: "polygon"
+ },
+ {
+ shapeName: "circle",
+ highlighter: "ellipse"
+ },
+ {
+ shapeName: "ellipse",
+ highlighter: "ellipse"
+ },
+ {
+ shapeName: "inset",
+ highlighter: "rect"
+ }
+];
add_task(function* () {
let {inspector, testActor} = yield openInspectorForURL(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* getShapeHidden(testActor, highlighterFront) {
+ let hidden = {};
+ for (let shape of SHAPE_IDS) {
+ hidden[shape] = yield testActor.getHighlighterNodeAttribute(
+ "shapes-" + shape, "hidden", highlighterFront);
+ }
+ return hidden;
+}
+
function* isHiddenByDefault(testActor, highlighterFront) {
info("Checking that highlighter is hidden by default");
let polygonHidden = yield testActor.getHighlighterNodeAttribute(
"shapes-polygon", "hidden", highlighterFront);
let ellipseHidden = yield testActor.getHighlighterNodeAttribute(
"shapes-ellipse", "hidden", highlighterFront);
ok(polygonHidden && ellipseHidden, "The highlighter is hidden by default");
}
function* isVisibleWhenShown(testActor, inspector, highlighterFront) {
- info("Asking to show the highlighter on the polygon node");
-
- let polygonNode = yield getNodeFront("#polygon", inspector);
- yield highlighterFront.show(polygonNode, {mode: "cssClipPath"});
-
- let polygonHidden = yield testActor.getHighlighterNodeAttribute(
- "shapes-polygon", "hidden", highlighterFront);
- ok(!polygonHidden, "The polygon highlighter is visible");
+ for (let { shapeName, highlighter } of SHAPE_TYPES) {
+ info(`Asking to show the highlighter on the ${shapeName} node`);
- info("Asking to show the highlighter on the circle node");
- let circleNode = yield getNodeFront("#circle", inspector);
- yield highlighterFront.show(circleNode, {mode: "cssClipPath"});
+ let node = yield getNodeFront(`#${shapeName}`, inspector);
+ yield highlighterFront.show(node, {mode: "cssClipPath"});
- let ellipseHidden = yield testActor.getHighlighterNodeAttribute(
- "shapes-ellipse", "hidden", highlighterFront);
- polygonHidden = yield testActor.getHighlighterNodeAttribute(
- "shapes-polygon", "hidden", highlighterFront);
- ok(!ellipseHidden, "The circle highlighter is visible");
- ok(polygonHidden, "The polygon highlighter is no longer visible");
+ let hidden = yield getShapeHidden(testActor, highlighterFront);
+ ok(!hidden[highlighter], `The ${shapeName} highlighter is visible`);
+ }
info("Hiding the highlighter");
yield highlighterFront.hide();
- polygonHidden = yield testActor.getHighlighterNodeAttribute(
- "shapes-polygon", "hidden", highlighterFront);
- ok(polygonHidden, "The highlighter is hidden");
+ let hidden = yield getShapeHidden(testActor, highlighterFront);
+ ok(hidden.polygon && hidden.ellipse && hidden.rect, "The highlighter is hidden");
}
--- a/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_02.js
+++ b/devtools/client/inspector/test/browser_inspector_highlighter-cssshape_02.js
@@ -11,16 +11,18 @@ const HIGHLIGHTER_TYPE = "ShapesHighligh
add_task(function* () {
let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
let front = inspector.inspector;
let highlighter = yield front.getHighlighterByType(HIGHLIGHTER_TYPE);
yield polygonHasCorrectAttrs(testActor, inspector, highlighter);
yield circleHasCorrectAttrs(testActor, inspector, highlighter);
+ yield ellipseHasCorrectAttrs(testActor, inspector, highlighter);
+ yield insetHasCorrectAttrs(testActor, inspector, highlighter);
yield highlighter.finalize();
});
function* polygonHasCorrectAttrs(testActor, inspector, highlighterFront) {
info("Checking polygon highlighter has correct points");
let polygonNode = yield getNodeFront("#polygon", inspector);
@@ -48,8 +50,50 @@ function* circleHasCorrectAttrs(testActo
let cy = yield testActor.getHighlighterNodeAttribute(
"shapes-ellipse", "cy", highlighterFront);
is(rx, 25, "Circle highlighter has correct rx");
is(ry, 25, "Circle highlighter has correct ry");
is(cx, 30, "Circle highlighter has correct cx");
is(cy, 40, "Circle highlighter has correct cy");
}
+
+function* ellipseHasCorrectAttrs(testActor, inspector, highlighterFront) {
+ info("Checking ellipse highlighter has correct attributes");
+
+ let ellipseNode = yield getNodeFront("#ellipse", inspector);
+ yield highlighterFront.show(ellipseNode, {mode: "cssClipPath"});
+
+ let rx = yield testActor.getHighlighterNodeAttribute(
+ "shapes-ellipse", "rx", highlighterFront);
+ let ry = yield testActor.getHighlighterNodeAttribute(
+ "shapes-ellipse", "ry", highlighterFront);
+ let cx = yield testActor.getHighlighterNodeAttribute(
+ "shapes-ellipse", "cx", highlighterFront);
+ let cy = yield testActor.getHighlighterNodeAttribute(
+ "shapes-ellipse", "cy", highlighterFront);
+
+ is(rx, 40, "Ellipse highlighter has correct rx");
+ is(ry, 30, "Ellipse highlighter has correct ry");
+ is(cx, 25, "Ellipse highlighter has correct cx");
+ is(cy, 75, "Ellipse highlighter has correct cy");
+}
+
+function* insetHasCorrectAttrs(testActor, inspector, highlighterFront) {
+ info("Checking rect highlighter has correct attributes");
+
+ let insetNode = yield getNodeFront("#inset", inspector);
+ yield highlighterFront.show(insetNode, {mode: "cssClipPath"});
+
+ let x = yield testActor.getHighlighterNodeAttribute(
+ "shapes-rect", "x", highlighterFront);
+ let y = yield testActor.getHighlighterNodeAttribute(
+ "shapes-rect", "y", highlighterFront);
+ let width = yield testActor.getHighlighterNodeAttribute(
+ "shapes-rect", "width", highlighterFront);
+ let height = yield testActor.getHighlighterNodeAttribute(
+ "shapes-rect", "height", highlighterFront);
+
+ is(x, 15, "Rect highlighter has correct x");
+ is(y, 25, "Rect highlighter has correct y");
+ is(width, 72.5, "Rect highlighter has correct width");
+ is(height, 45, "Rect highlighter has correct height");
+}
--- a/devtools/server/actors/highlighters.css
+++ b/devtools/server/actors/highlighters.css
@@ -605,14 +605,15 @@
height: 10px;
transform: translate(-5px, -5px);
background: transparent;
border-radius: 50%;
color: var(--highlighter-bubble-background-color);
}
:-moz-native-anonymous .shapes-polygon,
-:-moz-native-anonymous .shapes-ellipse {
+:-moz-native-anonymous .shapes-ellipse,
+:-moz-native-anonymous .shapes-rect {
fill: transparent;
stroke: var(--highlighter-guide-color);
shape-rendering: crispEdges;
vector-effect: non-scaling-stroke;
}
--- a/devtools/server/actors/highlighters/shapes.js
+++ b/devtools/server/actors/highlighters/shapes.js
@@ -87,21 +87,38 @@ class ShapesHighlighter extends AutoRefr
attributes: {
"id": "ellipse",
"class": "ellipse",
"hidden": true
},
prefix: this.ID_CLASS_PREFIX
});
- // TODO: Append different SVG objects for different shapes.
+ // Append a rect for inset().
+ createSVGNode(this.win, {
+ nodeType: "rect",
+ parent: mainSvg,
+ attributes: {
+ "id": "rect",
+ "class": "rect",
+ "hidden": true
+ },
+ prefix: this.ID_CLASS_PREFIX
+ });
return container;
}
+ get currentDimensions() {
+ return {
+ width: this.currentQuads.border[0].bounds.width,
+ height: this.currentQuads.border[0].bounds.height
+ };
+ }
+
/**
* Parses the CSS definition given and returns the shape type associated
* with the definition and the coordinates necessary to draw the shape.
* @param {String} definition the input CSS definition
* @returns {Object} null if the definition is not of a known shape type,
* or an object of the type { shapeType, coordinates }, where
* shapeType is the name of the shape and coordinates are an array
* or object of the coordinates needed to draw the shape.
@@ -110,16 +127,24 @@ class ShapesHighlighter extends AutoRefr
const types = [{
name: "polygon",
prefix: "polygon(",
coordParser: this.polygonPoints.bind(this)
}, {
name: "circle",
prefix: "circle(",
coordParser: this.circlePoints.bind(this)
+ }, {
+ name: "ellipse",
+ prefix: "ellipse(",
+ coordParser: this.ellipsePoints.bind(this)
+ }, {
+ name: "inset",
+ prefix: "inset(",
+ coordParser: this.insetPoints.bind(this)
}];
for (let { name, prefix, coordParser } of types) {
if (definition.includes(prefix)) {
definition = definition.substring(prefix.length, definition.length - 1);
return {
shapeType: name,
coordinates: coordParser(definition)
@@ -134,48 +159,35 @@ class ShapesHighlighter extends AutoRefr
* Parses the definition of the CSS polygon() function and returns its points,
* converted to percentages.
* @param {String} definition the arguments of the polygon() function
* @returns {Array} an array of the points of the polygon, with all values
* evaluated and converted to percentages
*/
polygonPoints(definition) {
return definition.split(",").map(coords => {
- return splitCoords(coords).map((coord, i) => {
- let size = i % 2 === 0 ? this.currentQuads.border[0].bounds.width
- : this.currentQuads.border[0].bounds.height;
- if (coord.includes("calc(")) {
- return evalCalcExpression(coord.substring(5, coord.length - 1), size);
- }
- return coordToPercent(coord, size);
- });
+ return splitCoords(coords).map(this.convertCoordsToPercent.bind(this));
});
}
/**
* Parses the definition of the CSS circle() function and returns the x/y radiuses and
* center coordinates, converted to percentages.
* @param {String} definition the arguments of the circle() function
* @returns {Object} an object of the form { rx, ry, cx, cy }, where rx and ry are the
* radiuses for the x and y axes, and cx and cy are the x/y coordinates for the
* center of the circle. All values are evaluated and converted to percentages.
*/
circlePoints(definition) {
// The computed value of circle() always has the keyword "at".
let values = definition.split(" at ");
let radius = values[0];
- let elemWidth = this.currentQuads.border[0].bounds.width;
- let elemHeight = this.currentQuads.border[0].bounds.height;
- let center = splitCoords(values[1]).map((coord, i) => {
- let size = i % 2 === 0 ? elemWidth : elemHeight;
- if (coord.includes("calc(")) {
- return evalCalcExpression(coord.substring(5, coord.length - 1), size);
- }
- return coordToPercent(coord, size);
- });
+ let elemWidth = this.currentDimensions.width;
+ let elemHeight = this.currentDimensions.height;
+ let center = splitCoords(values[1]).map(this.convertCoordsToPercent.bind(this));
if (radius === "closest-side") {
// radius is the distance from center to closest side of reference box
radius = Math.min(center[0], center[1], 100 - center[0], 100 - center[1]);
} else if (radius === "farthest-side") {
// radius is the distance from center to farthest side of reference box
radius = Math.max(center[0], center[1], 100 - center[0], 100 - center[1]);
} else {
@@ -193,16 +205,96 @@ class ShapesHighlighter extends AutoRefr
let radiusX = radius / ratioX;
let radiusY = radius / ratioY;
// rx, ry, cx, ry
return { rx: radiusX, ry: radiusY, cx: center[0], cy: center[1] };
}
/**
+ * Parses the definition of the CSS ellipse() function and returns the x/y radiuses and
+ * center coordinates, converted to percentages.
+ * @param {String} definition the arguments of the ellipse() function
+ * @returns {Object} an object of the form { rx, ry, cx, cy }, where rx and ry are the
+ * radiuses for the x and y axes, and cx and cy are the x/y coordinates for the
+ * center of the ellipse. All values are evaluated and converted to percentages
+ */
+ ellipsePoints(definition) {
+ let values = definition.split(" at ");
+ let elemWidth = this.currentDimensions.width;
+ let elemHeight = this.currentDimensions.height;
+ let center = splitCoords(values[1]).map(this.convertCoordsToPercent.bind(this));
+
+ let radii = values[0].trim().split(" ").map((radius, i) => {
+ let size = i % 2 === 0 ? elemWidth : elemHeight;
+ if (radius === "closest-side") {
+ // radius is the distance from center to closest x/y side of reference box
+ return i % 2 === 0 ? Math.min(center[0], 100 - center[0])
+ : Math.min(center[1], 100 - center[1]);
+ } else if (radius === "farthest-side") {
+ // radius is the distance from center to farthest x/y side of reference box
+ return i % 2 === 0 ? Math.max(center[0], 100 - center[0])
+ : Math.max(center[1], 100 - center[1]);
+ }
+ return coordToPercent(radius, size);
+ });
+
+ return { rx: radii[0], ry: radii[1], cx: center[0], cy: center[1] };
+ }
+
+ /**
+ * Parses the definition of the CSS inset() function and returns the x/y offsets and
+ * width/height of the shape, converted to percentages. Border radiuses (given after
+ * "round" in the definition) are currently ignored.
+ * @param {String} definition the arguments of the inset() function
+ * @returns {Object} an object of the form { x, y, width, height }, which are the top/
+ * left positions and width/height of the shape.
+ */
+ insetPoints(definition) {
+ let values = definition.split(" round ");
+ let offsets = splitCoords(values[0]).map(this.convertCoordsToPercent.bind(this));
+
+ let x, y = 0;
+ let width = this.currentDimensions.width;
+ let height = this.currentDimensions.height;
+ // The offsets, like margin/padding/border, are in order: top, right, bottom, left.
+ if (offsets.length === 1) {
+ x = y = offsets[0];
+ width = height = 100 - 2 * x;
+ } else if (offsets.length === 2) {
+ y = offsets[0];
+ x = offsets[1];
+ height = 100 - 2 * y;
+ width = 100 - 2 * x;
+ } else if (offsets.length === 3) {
+ y = offsets[0];
+ x = offsets[1];
+ height = 100 - y - offsets[2];
+ width = 100 - 2 * x;
+ } else if (offsets.length === 4) {
+ y = offsets[0];
+ x = offsets[3];
+ height = 100 - y - offsets[2];
+ width = 100 - x - offsets[1];
+ }
+
+ return { x, y, width, height };
+ }
+
+ convertCoordsToPercent(coord, i) {
+ let elemWidth = this.currentDimensions.width;
+ let elemHeight = this.currentDimensions.height;
+ let size = i % 2 === 0 ? elemWidth : elemHeight;
+ if (coord.includes("calc(")) {
+ return evalCalcExpression(coord.substring(5, coord.length - 1), size);
+ }
+ return coordToPercent(coord, size);
+ }
+
+ /**
* Destroy the nodes. Remove listeners.
*/
destroy() {
AutoRefreshHighlighter.prototype.destroy.call(this);
this.markup.destroy();
}
/**
@@ -229,34 +321,40 @@ class ShapesHighlighter extends AutoRefr
_hasMoved() {
let hasMoved = AutoRefreshHighlighter.prototype._hasMoved.call(this);
let oldShapeCoordinates = JSON.stringify(this.coordinates);
// TODO: need other modes too.
if (this.options.mode.startsWith("css")) {
let property = shapeModeToCssPropertyName(this.options.mode);
+ let style = getComputedStyle(this.currentNode)[property];
- let { coordinates, shapeType } =
- this._parseCSSShapeValue(getComputedStyle(this.currentNode)[property]);
- this.coordinates = coordinates;
- this.shapeType = shapeType;
+ if (!style || style === "none") {
+ this.coordinates = [];
+ this.shapeType = "none";
+ } else {
+ let { coordinates, shapeType } = this._parseCSSShapeValue(style);
+ this.coordinates = coordinates;
+ this.shapeType = shapeType;
+ }
}
let newShapeCoordinates = JSON.stringify(this.coordinates);
return hasMoved || oldShapeCoordinates !== newShapeCoordinates;
}
/**
* Hide all elements used to highlight CSS different shapes.
*/
_hideShapes() {
this.getElement("ellipse").setAttribute("hidden", true);
this.getElement("polygon").setAttribute("hidden", true);
+ this.getElement("rect").setAttribute("hidden", true);
}
/**
* Update the highlighter for the current node. Called whenever the element's quads
* or CSS shape has changed.
* @returns {Boolean} whether the highlighter was successfully updated
*/
_update() {
@@ -270,16 +368,20 @@ class ShapesHighlighter extends AutoRefr
this._hideShapes();
this.getElement("markers-container").setAttribute("style", "");
if (this.shapeType === "polygon") {
this._updatePolygonShape(top, left, width, height);
} else if (this.shapeType === "circle") {
this._updateCircleShape(top, left, width, height);
+ } else if (this.shapeType === "ellipse") {
+ this._updateEllipseShape(top, left, width, height);
+ } else if (this.shapeType === "inset") {
+ this._updateInsetShape(top, left, width, height);
}
setIgnoreLayoutChanges(false, this.highlighterEnv.window.document.documentElement);
return true;
}
/**
@@ -318,25 +420,69 @@ class ShapesHighlighter extends AutoRefr
let ellipseEl = this.getElement("ellipse");
ellipseEl.setAttribute("rx", rx);
ellipseEl.setAttribute("ry", ry);
ellipseEl.setAttribute("cx", cx);
ellipseEl.setAttribute("cy", cy);
ellipseEl.removeAttribute("hidden");
let shadows = `${MARKER_SIZE + cx * width / 100}px
- ${MARKER_SIZE + cy * height / 100}px 0 0,
- ${MARKER_SIZE + (cx + rx) * width / 100}px
${MARKER_SIZE + cy * height / 100}px 0 0`;
this.getElement("markers-container").setAttribute("style",
`top:${top - MARKER_SIZE}px;left:${left - MARKER_SIZE}px;box-shadow:${shadows};`);
}
/**
+ * Update the SVG ellipse to fit the CSS ellipse.
+ * @param {Number} top the top bound of the element quads
+ * @param {Number} left the left bound of the element quads
+ * @param {Number} width the width of the element quads
+ * @param {Number} height the height of the element quads
+ */
+ _updateEllipseShape(top, left, width, height) {
+ let { rx, ry, cx, cy } = this.coordinates;
+ let ellipseEl = this.getElement("ellipse");
+ ellipseEl.setAttribute("rx", rx);
+ ellipseEl.setAttribute("ry", ry);
+ ellipseEl.setAttribute("cx", cx);
+ ellipseEl.setAttribute("cy", cy);
+ ellipseEl.removeAttribute("hidden");
+
+ let shadows = `${MARKER_SIZE + cx * width / 100}px
+ ${MARKER_SIZE + cy * height / 100}px 0 0,
+ ${MARKER_SIZE + (cx + rx) * height / 100}px
+ ${MARKER_SIZE + cy * height / 100}px 0 0,
+ ${MARKER_SIZE + cx * height / 100}px
+ ${MARKER_SIZE + (cy + ry) * height / 100}px 0 0`;
+
+ this.getElement("markers-container").setAttribute("style",
+ `top:${top - MARKER_SIZE}px;left:${left - MARKER_SIZE}px;box-shadow:${shadows};`);
+ }
+
+ /**
+ * Update the SVG rect to fit the CSS inset.
+ * @param {Number} top the top bound of the element quads
+ * @param {Number} left the left bound of the element quads
+ * @param {Number} width the width of the element quads
+ * @param {Number} height the height of the element quads
+ */
+ _updateInsetShape(top, left, width, height) {
+ let rectEl = this.getElement("rect");
+ rectEl.setAttribute("x", this.coordinates.x);
+ rectEl.setAttribute("y", this.coordinates.y);
+ rectEl.setAttribute("width", this.coordinates.width);
+ rectEl.setAttribute("height", this.coordinates.height);
+ rectEl.removeAttribute("hidden");
+
+ this.getElement("markers-container").setAttribute("style",
+ `top:${top - MARKER_SIZE}px;left:${left - MARKER_SIZE}px;box-shadow:none;`);
+ }
+
+ /**
* Hide the highlighter, the outline and the infobar.
*/
_hide() {
setIgnoreLayoutChanges(true);
this._hideShapes();
this.getElement("markers-container").setAttribute("style", "");