Bug 1572651 - (Part 2) Split BoxModelHighlighter into observer and renderer parts. r=pbro,jdescottes

NOTE: To use the new box model highlighter, flip this pref to true: `devtools.inspector.use-new-box-model-highlighter`

Adding Julian as reviewer to check the sanity of the communication system (see `BoxModelHiglighterObserver` constructor and `BoxModelHighlighterRenderer.setMessageManager()`, `BoxModelHighlighterRenderer.onMessage()`,  `BoxModelHighlighterRenderer.postMessage()`) and Patrick for the overall highlighter behavior which is mostly a clean split of the existing [`BoxModelHighlighter`](https://searchfox.org/mozilla-central/rev/f43ae7e1c43a4a940b658381157a6ea6c5a185c1/devtools/server/actors/highlighters/box-model.js)).

---

Depends on D47091

## Preamble

This patch looks more frightening than it actually is. Let me explain:

The vast majority of the code in `box-model-highlighter-observer.js` and `box-model-highlighter-renderer.js` is a clean split of the code existing in `box-model-highlighter.js` into distinct parts which handle the node measurement (observer) and the drawing the highlighter (renderer). I kept the method names identical to help in matching them up with their original sources.

There was no simple way chunk this without confusing the daylight out of you so I decided to co-locate all changes so it's easier to track and reference methods.

I will detail below the important differences.


## Overview:

The box model highlighter is split into two distinct parts:
- an observer which monitors the node's position
- a renderer which draws the highlighter on top of the node

The renderer always lives in the parent process (browser window) and overlays an iframe with the highlighter markup:
- either over the content if highlighting in the context of the content toolbox
- or over the whole browser UI if highlighting in the context of the browser toolbox

When in the context of the browser toolbox (i.e. highlighting the browser UI), both observer and renderer live in the parent process. Communication is done by direct calls.

When in the context of the content toolbox (i.e. highlighting the page content), the observer lives in content process (so it can measure the node) while the renderer lives in the parent process. Communication is done by message passing via `MessageManager` (soon to be deprecated and replaced with JSWindowActor API)

## Notable differences after the split

- the observer checks whether it is in the content process (aka child process) and sets up the highlighter in the parent process by using [`setupInParent()`](https://docs.firefox-dev.tools/backend/actor-e10s-handling.html) and establishes a communication system to it via message manager. If the observer is in the parent process (browser toolbox scenario), the renderer is setup directly via its constructor and no additional communication system is required.

- whenever the node quads change (as determined by the untouched existing base class `auto-refresh.js`), the observer gathers the data about the node position and sends it over to the renderer. This happens in the `BoxModelHighlighterObserver._update()` (corresponding to the [`_update()` from the existing highlighter](https://searchfox.org/mozilla-central/rev/45f30e1d19bde27bf07e47a0a5dd0962dd27ba18/devtools/server/actors/highlighters/box-model.js#361-383)).

- the renderer expects its `render()` method to be called with the necessary node position information whenever it should update the highlighter. It is the entry point which then calls all the DOM manipulation methods copied over from the existing box model highlighter.

- the only notable change in DOM manipulation methods is in `BoxModelHighlighterRenderer._updateBoxModel()` (corresponding to [`updateBoxModel()` from the existing highlighter](https://searchfox.org/mozilla-central/rev/45f30e1d19bde27bf07e47a0a5dd0962dd27ba18/devtools/server/actors/highlighters/box-model.js#504-560)) where the `_nodeNeedsHighlighting()` is kept on the observer part and the canvas zoom adjustment is removed  (`this.markup.scaleRootElement(this.currentNode, rootId)`) because the canvas is no longer influenced by the page zoom (the canvas lives in the browser window, not the content window)

Differential Revision: https://phabricator.services.mozilla.com/D47092

--HG--
rename : devtools/server/actors/highlighters/box-model.js => devtools/server/actors/highlighters/box-model-renderer.js
extra : moz-landing-system : lando
This commit is contained in:
Razvan Caliman 2019-10-10 14:15:22 +00:00
parent c34e4a24a6
commit 780c3bc0b5
9 changed files with 1102 additions and 7 deletions

View File

@ -2010,6 +2010,8 @@ pref("devtools.inspector.showUserAgentShadowRoots", false);
pref("devtools.inspector.new-rulesview.enabled", false);
// Enable the compatibility tool in the inspector.
pref("devtools.inspector.compatibility.enabled", false);
// Enable the new Box Model Highlighter with renderer in parent process
pref("devtools.inspector.use-new-box-model-highlighter", false);
// Grid highlighter preferences
pref("devtools.gridinspector.gridOutlineMaxColumns", 50);

View File

@ -46,6 +46,12 @@ loader.lazyRequireGetter(
"devtools/server/actors/highlighters/box-model",
true
);
loader.lazyRequireGetter(
this,
"BoxModelHighlighterObserver",
"devtools/server/actors/highlighters/box-model-observer",
true
);
const HIGHLIGHTER_PICKED_TIMER = 1000;
const IS_OSX = Services.appinfo.OS === "Darwin";
@ -110,7 +116,7 @@ exports.register = register;
* The HighlighterActor class
*/
exports.HighlighterActor = protocol.ActorClassWithSpec(highlighterSpec, {
initialize: function(inspector, autohide) {
initialize: function(inspector, autohide, useNewBoxModelHighlighter = false) {
protocol.Actor.prototype.initialize.call(this, null);
this._autohide = autohide;
@ -119,6 +125,7 @@ exports.HighlighterActor = protocol.ActorClassWithSpec(highlighterSpec, {
this._targetActor = this._inspector.targetActor;
this._highlighterEnv = new HighlighterEnvironment();
this._highlighterEnv.initFromTargetActor(this._targetActor);
this._useNewBoxModelHighlighter = useNewBoxModelHighlighter;
this._onNavigate = this._onNavigate.bind(this);
@ -147,6 +154,15 @@ exports.HighlighterActor = protocol.ActorClassWithSpec(highlighterSpec, {
_createHighlighter: function() {
this._isPreviousWindowXUL = isXUL(this._targetActor.window);
if (this._useNewBoxModelHighlighter) {
this._highlighter = new BoxModelHighlighterObserver(
this._highlighterEnv,
this.conn
);
return;
}
if (!this._isPreviousWindowXUL) {
this._highlighter = new BoxModelHighlighter(
this._highlighterEnv,

View File

@ -0,0 +1,312 @@
/* 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 { DebuggerServer } = require("devtools/server/debugger-server");
const { AutoRefreshHighlighter } = require("./auto-refresh");
const {
getBindingElementAndPseudo,
hasPseudoClassLock,
isNodeValid,
} = require("./utils/markup");
const { PSEUDO_CLASSES } = require("devtools/shared/css/constants");
const { getCurrentZoom } = require("devtools/shared/layout/utils");
const {
getNodeDisplayName,
getNodeGridFlexType,
} = require("devtools/server/actors/inspector/utils");
const nodeConstants = require("devtools/shared/dom-node-constants");
const { LocalizationHelper } = require("devtools/shared/l10n");
const STRINGS_URI = "devtools/shared/locales/highlighters.properties";
const L10N = new LocalizationHelper(STRINGS_URI);
const {
BOX_MODEL_REGIONS,
BoxModelHighlighterRenderer,
} = require("devtools/server/actors/highlighters/box-model-renderer");
/**
* The BoxModelHighlighterObserver observes the coordinates of a node and communicates
* with the BoxModelHighlighterRenderer which draws the box model regions on top the a
* node.
*
* When in the context of the content toolbox, the observer lives in
* the child process (aka content process) and the renderer is set up in the parent
* process. They communicate via messages.
*
* When in the context of the browser toolbox, both observer and renderer live in the
* parent process. They communicate by direct reference.
*/
class BoxModelHighlighterObserver extends AutoRefreshHighlighter {
constructor(highlighterEnv, conn) {
super(highlighterEnv);
this.conn = conn;
this._ignoreScroll = true;
this.typeName = this.constructor.name.replace("Observer", "");
if (DebuggerServer.isInChildProcess) {
// eslint-disable-next-line no-restricted-properties
this.conn.setupInParent({
module: "devtools/server/actors/highlighters/box-model-renderer",
setupParent: "setupParentProcess",
});
} else {
this.renderer = new BoxModelHighlighterRenderer();
}
/**
* Optionally customize each region's fill color by adding an entry to the
* regionFill property: `highlighter.regionFill.margin = "red";
*/
this.regionFill = {};
this.onPageHide = this.onPageHide.bind(this);
this.onWillNavigate = this.onWillNavigate.bind(this);
this.highlighterEnv.on("will-navigate", this.onWillNavigate);
const { pageListenerTarget } = highlighterEnv;
pageListenerTarget.addEventListener("pagehide", this.onPageHide);
}
/**
* Destroy the nodes. Remove listeners.
*/
destroy() {
this.highlighterEnv.off("will-navigate", this.onWillNavigate);
const { pageListenerTarget } = this.highlighterEnv;
if (pageListenerTarget) {
pageListenerTarget.removeEventListener("pagehide", this.onPageHide);
}
if (DebuggerServer.isInChildProcess) {
this.postMessage("destroy");
} else {
this.renderer.destroy();
this.renderer = null;
}
AutoRefreshHighlighter.prototype.destroy.call(this);
}
get messageManager() {
if (!DebuggerServer.isInChildProcess) {
throw new Error(
"Message manager should only be used when actor is in child process."
);
}
return this.conn.parentMessageManager;
}
postMessage(topic, data = {}) {
this._msgName = `debug:${this.conn.prefix}${this.typeName}`;
this.messageManager.sendAsyncMessage(this._msgName, { topic, data });
}
/**
* Tell the renderer to update the markup of the box model highlighter.
*
* @param {Object} data
* Object with data about the node position, type and its attributes.
* @see BoxModelHighlighterRenderer.render()
*/
render(data) {
if (DebuggerServer.isInChildProcess) {
this.postMessage("render", data);
} else {
this.renderer.render(data);
}
}
/**
* Override the AutoRefreshHighlighter's _isNodeValid method to also return true for
* text nodes since these can also be highlighted.
* @param {DOMNode} node
* @return {Boolean}
*/
_isNodeValid(node) {
return (
node && (isNodeValid(node) || isNodeValid(node, nodeConstants.TEXT_NODE))
);
}
/**
* Show the highlighter on a given node
*/
_show() {
if (!BOX_MODEL_REGIONS.includes(this.options.region)) {
this.options.region = "content";
}
const shown = this._update();
this._trackMutations();
return shown;
}
/**
* Track the current node markup mutations so that the node info bar can be
* updated to reflects the node's attributes
*/
_trackMutations() {
if (isNodeValid(this.currentNode)) {
const win = this.currentNode.ownerGlobal;
this.currentNodeObserver = new win.MutationObserver(this.update);
this.currentNodeObserver.observe(this.currentNode, { attributes: true });
}
}
_untrackMutations() {
if (isNodeValid(this.currentNode) && this.currentNodeObserver) {
this.currentNodeObserver.disconnect();
this.currentNodeObserver = null;
}
}
/**
* Update the highlighter on the current highlighted node (the one that was
* passed as an argument to show(node)).
* Should be called whenever node size or attributes change
*/
_update() {
const node = this.currentNode;
let shown = false;
if (this._nodeNeedsHighlighting()) {
// Tell the renderer to update the highlighter markup and provide it
// with options, metadata and coordinates of the target node.
const data = {
...this.options,
currentQuads: { ...this.currentQuads },
regionFill: { ...this.regionFill },
nodeData: this._getNodeData(),
showBoxModel: true,
showInfoBar:
!this.options.hideInfoBar &&
(node.nodeType === node.ELEMENT_NODE ||
node.nodeType === node.TEXT_NODE),
};
this.render(data);
shown = true;
} else {
// Nothing to highlight (0px rectangle like a <script> tag for instance)
this._hide();
}
return shown;
}
/**
* Hide the highlighter, the outline and the infobar.
*/
_hide() {
this._untrackMutations();
// Tell the renderer to hide the highlighter markup.
this.render({
showBoxModel: false,
showInfoBar: false,
});
}
/**
* Can the current node be highlighted? Does it have quads.
* @return {Boolean}
*/
_nodeNeedsHighlighting() {
return (
this.currentQuads.margin.length ||
this.currentQuads.border.length ||
this.currentQuads.padding.length ||
this.currentQuads.content.length
);
}
/**
* Get data from the highlighted node to populate the infobar tooltip with
* information such as the node's id, class names, grid or flex item type, etc.
*
* @return {Object|null} Information about the highlighted node
*/
_getNodeData() {
if (!this.currentNode) {
return null;
}
const { bindingElement: node, pseudo } = getBindingElementAndPseudo(
this.currentNode
);
// Update the tag, id, classes, pseudo-classes and dimensions
const displayName = getNodeDisplayName(node);
const id = node.id ? "#" + node.id : "";
const classList = (node.classList || []).length
? "." + [...node.classList].join(".")
: "";
let pseudos = this._getPseudoClasses(node).join("");
if (pseudo) {
// Display :after as ::after
pseudos += ":" + pseudo;
}
const zoom = getCurrentZoom(this.win);
const { grid: gridType, flex: flexType } = getNodeGridFlexType(node);
const gridLayoutTextType = this._getLayoutTextType("gridType", gridType);
const flexLayoutTextType = this._getLayoutTextType("flexType", flexType);
return {
classList,
displayName,
flexLayoutTextType,
gridLayoutTextType,
id,
pseudos,
zoom,
};
}
_getLayoutTextType(layoutTypeKey, { isContainer, isItem }) {
if (!isContainer && !isItem) {
return "";
}
if (isContainer && !isItem) {
return L10N.getStr(`${layoutTypeKey}.container`);
}
if (!isContainer && isItem) {
return L10N.getStr(`${layoutTypeKey}.item`);
}
return L10N.getStr(`${layoutTypeKey}.dual`);
}
_getPseudoClasses(node) {
if (node.nodeType !== nodeConstants.ELEMENT_NODE) {
// hasPseudoClassLock can only be used on Elements.
return [];
}
return PSEUDO_CLASSES.filter(pseudo => hasPseudoClassLock(node, pseudo));
}
onPageHide({ target }) {
// If a pagehide event is triggered for current window's highlighter, hide the
// highlighter.
if (target.defaultView === this.win) {
this.hide();
}
}
onWillNavigate({ isTopLevel }) {
if (isTopLevel) {
this.hide();
}
}
}
exports.BoxModelHighlighterObserver = BoxModelHighlighterObserver;

View File

@ -0,0 +1,752 @@
/* 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 {
createNode,
createSVGNode,
moveInfobar,
} = require("devtools/server/actors/highlighters/utils/markup");
const {
HighlighterRenderer,
} = require("devtools/server/actors/highlighters/highlighter-renderer");
// Note that the order of items in this array is important because it is used
// for drawing the BoxModelHighlighter's path elements correctly.
const BOX_MODEL_REGIONS = ["margin", "border", "padding", "content"];
exports.BOX_MODEL_REGIONS = BOX_MODEL_REGIONS;
const BOX_MODEL_SIDES = ["top", "right", "bottom", "left"];
// Width of BoxModelHighlighter guides
const GUIDE_STROKE_WIDTH = 1;
/**
* The BoxModelHighlighterRenderer receives node coordinates from the
* BoxModelHighlighterObserver and draws the box model regions on top of a node.
* If the node is a block box, then each region will be displayed as 1 polygon.
* If the node is an inline box though, each region may be represented by 1 or
* more polygons, depending on how many line boxes the inline element has.
*
* Structure:
* <div class="highlighter-container">
* <div class="box-model-root">
* <svg class="box-model-elements" hidden="true">
* <g class="box-model-regions">
* <path class="box-model-margin" points="..." />
* <path class="box-model-border" points="..." />
* <path class="box-model-padding" points="..." />
* <path class="box-model-content" points="..." />
* </g>
* <line class="box-model-guide-top" x1="..." y1="..." x2="..." y2="..." />
* <line class="box-model-guide-right" x1="..." y1="..." x2="..." y2="..." />
* <line class="box-model-guide-bottom" x1="..." y1="..." x2="..." y2="..." />
* <line class="box-model-guide-left" x1="..." y1="..." x2="..." y2="..." />
* </svg>
* <div class="box-model-infobar-container">
* <div class="box-model-infobar-arrow highlighter-infobar-arrow-top" />
* <div class="box-model-infobar">
* <div class="box-model-infobar-text" align="center">
* <span class="box-model-infobar-tagname">Node name</span>
* <span class="box-model-infobar-id">Node id</span>
* <span class="box-model-infobar-classes">.someClass</span>
* <span class="box-model-infobar-pseudo-classes">:hover</span>
* <span class="box-model-infobar-grid-type">Grid Type</span>
* <span class="box-model-infobar-flex-type">Flex Type</span>
* </div>
* </div>
* <div class="box-model-infobar-arrow box-model-infobar-arrow-bottom"/>
* </div>
* </div>
* </div>
*/
class BoxModelHighlighterRenderer extends HighlighterRenderer {
constructor(mm, prefix) {
super();
// @override Highlighter type name.
this.typeName = this.constructor.name.replace("Renderer", "");
// String used to prefix ids and classnames of highlighter nodes.
this.ID_CLASS_PREFIX = "box-model-";
// If there is a message manager and connection prefix, it means the observer lives
// in the content process so we need to setup a communication system with it.
// Otherwise, both renderer and observer live in the parent process and there is no
// need for a message-based communication system.
if (mm && prefix) {
this.setMessageManager(mm, prefix);
this.init(false);
} else {
this.init(true);
}
}
/**
* Update the rendering of the box model highlighter, inforbar and guides for a node
* using quad coordinates, node information and options from the BoxModelHighlighterObserver.
*
* @override
*
* @param {Object} data
* Information used for rendering the box model highlighter, infobar and guides.
*
* @param {Boolean} data.showBoxModel
* Whether to show the box model highlighter.
* Defaults to false
* @param {Boolean} data.showInfoBar
* Whether to show the tooltip with the node's dimensions, class names, id, etc.
* Defaults to false
* @param {Object} data.currentQuads
* Collection of quad coordinates for a node's box model regions keyed by region
* @param {Object} data.regionFill
* Optional collection of custom fill colors for box model regions keyed by
* region. Ex: data.regionFill.margin = "red"
*
* @param {Object} data.nodeData
* Collection of information about a node used for the infobar tooltip.
* @param {String} data.nodeData.classList
* String with list of class names separated by a dot (.) insead of space.
* @param {String} data.nodeData.displayName
* Node tag name.
* @param {String} data.nodeData.flexLayoutTextType
* Flex item or flex container.
* @param {String} data.nodeData.gridLayoutTextType
* Grid idem or flex container.
* @param {String} data.nodeData.id
* Node id attribute.
* @param {String} data.nodeData.pseudos
* Pseudo-element type if the node is a ::before/::after pseudo-element
* @param {Number} data.nodeData.zoom
* Zoom level of the content page where the node exists.
*
* @param {Object} data.options
* Collection of optional overrides in the highlighter rendering.
* @param {String} data.options.region
* Specifies the region that the guides should outline:
* "content" (default), "padding", "border" or "margin".
* @param {Boolean} data.options.hideGuides
* Defaults to false
* @param {Boolean} data.options.hideInfoBar
* Defaults to false
* @param {String} data.options.showOnly
* If set, only this region will be highlighted. Use with onlyRegionArea
* to only highlight the area of the region:
* "content", "padding", "border" or "margin"
* @param {Boolean} data.options.onlyRegionArea
* This can be set to true to make each region's box only highlight the
* area of the corresponding region rather than the area of nested
* regions too. This is useful when used with showOnly.
*/
render(data = {}) {
this.currentQuads = data.currentQuads || {};
this.nodeData = data.nodeData || null;
this.options = data.options || {};
this.regionFill = data.regionFill || {};
const { showBoxModel = false, showInfoBar = false } = data;
if (!showBoxModel) {
this._hideBoxModel();
this._hideInfobar();
return;
}
this._updateBoxModel();
this._showBoxModel();
if (!showInfoBar) {
this._hideInfobar();
} else {
this._updateInfobar();
this._showInfobar();
}
}
getElement(id) {
return this.markup.getElement(this.ID_CLASS_PREFIX + id);
}
/**
* Hide the infobar
*/
_hideInfobar() {
this.getElement("infobar-container").setAttribute("hidden", "true");
}
/**
* Show the infobar
*/
_showInfobar() {
this.getElement("infobar-container").removeAttribute("hidden");
}
/**
* Hide the box model
*/
_hideBoxModel() {
this.getElement("elements").setAttribute("hidden", "true");
}
/**
* Show the box model
*/
_showBoxModel() {
this.getElement("elements").removeAttribute("hidden");
}
/**
* Update the box model
*/
_updateBoxModel() {
const options = this.options;
options.region = options.region || "content";
for (let i = 0; i < BOX_MODEL_REGIONS.length; i++) {
const boxType = BOX_MODEL_REGIONS[i];
const nextBoxType = BOX_MODEL_REGIONS[i + 1];
const box = this.getElement(boxType);
if (this.regionFill[boxType]) {
box.setAttribute("style", "fill:" + this.regionFill[boxType]);
} else {
box.setAttribute("style", "");
}
// Highlight all quads for this region by setting the "d" attribute of the
// corresponding <path>.
const path = [];
for (let j = 0; j < this.currentQuads[boxType].length; j++) {
const boxQuad = this.currentQuads[boxType][j];
const nextBoxQuad = this.currentQuads[nextBoxType]
? this.currentQuads[nextBoxType][j]
: null;
path.push(this._getBoxPathCoordinates(boxQuad, nextBoxQuad));
}
box.setAttribute("d", path.join(" "));
box.removeAttribute("faded");
// If showOnly is defined, either hide the other regions, or fade them out
// if onlyRegionArea is set too.
if (options.showOnly && options.showOnly !== boxType) {
if (options.onlyRegionArea) {
box.setAttribute("faded", "true");
} else {
box.removeAttribute("d");
}
}
if (boxType === options.region && !options.hideGuides) {
this._showGuides(boxType);
} else if (options.hideGuides) {
this._hideGuides();
}
}
}
/**
* Calculate an outer quad based on the quads returned by getAdjustedQuads.
* The BoxModelHighlighter may highlight more than one boxes, so in this case
* create a new quad that "contains" all of these quads.
* This is useful to position the guides and infobar.
* This may happen if the BoxModelHighlighter is used to highlight an inline
* element that spans line breaks.
* @param {String} region The box-model region to get the outer quad for.
* @return {Object} A quad-like object {p1,p2,p3,p4,bounds}
*/
_getOuterQuad(region) {
const quads = this.currentQuads[region];
if (!quads || !quads.length) {
return null;
}
const quad = {
p1: { x: Infinity, y: Infinity },
p2: { x: -Infinity, y: Infinity },
p3: { x: -Infinity, y: -Infinity },
p4: { x: Infinity, y: -Infinity },
bounds: {
bottom: -Infinity,
height: 0,
left: Infinity,
right: -Infinity,
top: Infinity,
width: 0,
x: 0,
y: 0,
},
};
for (const q of quads) {
quad.p1.x = Math.min(quad.p1.x, q.p1.x);
quad.p1.y = Math.min(quad.p1.y, q.p1.y);
quad.p2.x = Math.max(quad.p2.x, q.p2.x);
quad.p2.y = Math.min(quad.p2.y, q.p2.y);
quad.p3.x = Math.max(quad.p3.x, q.p3.x);
quad.p3.y = Math.max(quad.p3.y, q.p3.y);
quad.p4.x = Math.min(quad.p4.x, q.p4.x);
quad.p4.y = Math.max(quad.p4.y, q.p4.y);
quad.bounds.bottom = Math.max(quad.bounds.bottom, q.bounds.bottom);
quad.bounds.top = Math.min(quad.bounds.top, q.bounds.top);
quad.bounds.left = Math.min(quad.bounds.left, q.bounds.left);
quad.bounds.right = Math.max(quad.bounds.right, q.bounds.right);
}
quad.bounds.x = quad.bounds.left;
quad.bounds.y = quad.bounds.top;
quad.bounds.width = quad.bounds.right - quad.bounds.left;
quad.bounds.height = quad.bounds.bottom - quad.bounds.top;
return quad;
}
_getBoxPathCoordinates(boxQuad, nextBoxQuad) {
const { p1, p2, p3, p4 } = boxQuad;
let path;
if (!nextBoxQuad || !this.options.onlyRegionArea) {
// If this is the content box (inner-most box) or if we're not being asked
// to highlight only region areas, then draw a simple rectangle.
path =
"M" +
p1.x +
"," +
p1.y +
" " +
"L" +
p2.x +
"," +
p2.y +
" " +
"L" +
p3.x +
"," +
p3.y +
" " +
"L" +
p4.x +
"," +
p4.y;
} else {
// Otherwise, just draw the region itself, not a filled rectangle.
const { p1: np1, p2: np2, p3: np3, p4: np4 } = nextBoxQuad;
path =
"M" +
p1.x +
"," +
p1.y +
" " +
"L" +
p2.x +
"," +
p2.y +
" " +
"L" +
p3.x +
"," +
p3.y +
" " +
"L" +
p4.x +
"," +
p4.y +
" " +
"L" +
p1.x +
"," +
p1.y +
" " +
"L" +
np1.x +
"," +
np1.y +
" " +
"L" +
np4.x +
"," +
np4.y +
" " +
"L" +
np3.x +
"," +
np3.y +
" " +
"L" +
np2.x +
"," +
np2.y +
" " +
"L" +
np1.x +
"," +
np1.y;
}
return path;
}
_getOuterBounds() {
for (const region of BOX_MODEL_REGIONS) {
const quad = this._getOuterQuad(region);
if (!quad) {
// Invisible element such as a script tag.
break;
}
const { bottom, height, left, right, top, width, x, y } = quad.bounds;
if (width > 0 || height > 0) {
return { bottom, height, left, right, top, width, x, y };
}
}
return {
bottom: 0,
height: 0,
left: 0,
right: 0,
top: 0,
width: 0,
x: 0,
y: 0,
};
}
/**
* We only want to show guides for horizontal and vertical edges as this helps
* to line them up. This method finds these edges and displays a guide there.
* @param {String} region The region around which the guides should be shown.
*/
_showGuides(region) {
const quad = this._getOuterQuad(region);
if (!quad) {
// Invisible element such as a script tag.
return;
}
const { p1, p2, p3, p4 } = quad;
const allX = [p1.x, p2.x, p3.x, p4.x].sort((a, b) => a - b);
const allY = [p1.y, p2.y, p3.y, p4.y].sort((a, b) => a - b);
const toShowX = [];
const toShowY = [];
for (const arr of [allX, allY]) {
for (let i = 0; i < arr.length; i++) {
const val = arr[i];
if (i !== arr.lastIndexOf(val)) {
if (arr === allX) {
toShowX.push(val);
} else {
toShowY.push(val);
}
arr.splice(arr.lastIndexOf(val), 1);
}
}
}
// Move guide into place or hide it if no valid co-ordinate was found.
this._updateGuide("top", Math.round(toShowY[0]));
this._updateGuide("right", Math.round(toShowX[1]) - 1);
this._updateGuide("bottom", Math.round(toShowY[1] - 1));
this._updateGuide("left", Math.round(toShowX[0]));
}
_hideGuides() {
for (const side of BOX_MODEL_SIDES) {
this.getElement("guide-" + side).setAttribute("hidden", "true");
}
}
/**
* Move a guide to the appropriate position and display it. If no point is
* passed then the guide is hidden.
*
* @param {String} side
* The guide to update
* @param {Integer} point
* x or y co-ordinate. If this is undefined we hide the guide.
*/
_updateGuide(side, point = -1) {
const guide = this.getElement("guide-" + side);
if (point <= 0) {
guide.setAttribute("hidden", "true");
return false;
}
if (side === "top" || side === "bottom") {
guide.setAttribute("x1", "0");
guide.setAttribute("y1", point + "");
guide.setAttribute("x2", "100%");
guide.setAttribute("y2", point + "");
} else {
guide.setAttribute("x1", point + "");
guide.setAttribute("y1", "0");
guide.setAttribute("x2", point + "");
guide.setAttribute("y2", "100%");
}
guide.removeAttribute("hidden");
return true;
}
/**
* Move the Infobar to the right place in the highlighter.
*/
_moveInfobar() {
const bounds = this._getOuterBounds();
const container = this.getElement("infobar-container");
moveInfobar(container, bounds, this.iframe.contentWindow);
}
/**
* Update node information (displayName#id.class)
*/
_updateInfobar() {
if (!this.nodeData) {
return;
}
const {
classList,
displayName,
flexLayoutTextType,
gridLayoutTextType,
id,
pseudos,
zoom,
} = this.nodeData;
// We want to display the original `width` and `height`, instead of the ones affected
// by any zoom. Since the infobar can be displayed also for text nodes, we can't
// access the computed style for that, and this is why we recalculate them here.
const quad = this._getOuterQuad("border");
if (!quad) {
return;
}
const { width, height } = quad.bounds;
const dim =
parseFloat((width / zoom).toPrecision(6)) +
" \u00D7 " +
parseFloat((height / zoom).toPrecision(6));
this.getElement("infobar-tagname").setTextContent(displayName);
this.getElement("infobar-id").setTextContent(id);
this.getElement("infobar-classes").setTextContent(classList);
this.getElement("infobar-pseudo-classes").setTextContent(pseudos);
this.getElement("infobar-dimensions").setTextContent(dim);
this.getElement("infobar-grid-type").setTextContent(gridLayoutTextType);
this.getElement("infobar-flex-type").setTextContent(flexLayoutTextType);
this._moveInfobar();
}
/**
* @override
*/
_buildMarkup() {
const doc = this.win.document;
const highlighterContainer = doc.createElement("div");
highlighterContainer.setAttribute("role", "presentation");
highlighterContainer.className = "highlighter-container box-model";
// Build the root wrapper, used to adapt to the page zoom.
const rootWrapper = createNode(this.win, {
parent: highlighterContainer,
attributes: {
id: "root",
class: "root",
role: "presentation",
},
prefix: this.ID_CLASS_PREFIX,
});
// Building the nodeinfo bar markup
const infobarContainer = createNode(this.win, {
parent: rootWrapper,
attributes: {
class: "infobar-container",
id: "infobar-container",
position: "top",
hidden: "true",
},
prefix: this.ID_CLASS_PREFIX,
});
const infobar = createNode(this.win, {
parent: infobarContainer,
attributes: {
class: "infobar",
},
prefix: this.ID_CLASS_PREFIX,
});
const texthbox = createNode(this.win, {
parent: infobar,
attributes: {
class: "infobar-text",
},
prefix: this.ID_CLASS_PREFIX,
});
createNode(this.win, {
nodeType: "span",
parent: texthbox,
attributes: {
class: "infobar-tagname",
id: "infobar-tagname",
},
prefix: this.ID_CLASS_PREFIX,
});
createNode(this.win, {
nodeType: "span",
parent: texthbox,
attributes: {
class: "infobar-id",
id: "infobar-id",
},
prefix: this.ID_CLASS_PREFIX,
});
createNode(this.win, {
nodeType: "span",
parent: texthbox,
attributes: {
class: "infobar-classes",
id: "infobar-classes",
},
prefix: this.ID_CLASS_PREFIX,
});
createNode(this.win, {
nodeType: "span",
parent: texthbox,
attributes: {
class: "infobar-pseudo-classes",
id: "infobar-pseudo-classes",
},
prefix: this.ID_CLASS_PREFIX,
});
createNode(this.win, {
nodeType: "span",
parent: texthbox,
attributes: {
class: "infobar-dimensions",
id: "infobar-dimensions",
},
prefix: this.ID_CLASS_PREFIX,
});
createNode(this.win, {
nodeType: "span",
parent: texthbox,
attributes: {
class: "infobar-grid-type",
id: "infobar-grid-type",
},
prefix: this.ID_CLASS_PREFIX,
});
createNode(this.win, {
nodeType: "span",
parent: texthbox,
attributes: {
class: "infobar-flex-type",
id: "infobar-flex-type",
},
prefix: this.ID_CLASS_PREFIX,
});
// Building the SVG element with its polygons and lines
const svg = createSVGNode(this.win, {
nodeType: "svg",
parent: rootWrapper,
attributes: {
id: "elements",
width: "100%",
height: "100%",
hidden: "true",
role: "presentation",
},
prefix: this.ID_CLASS_PREFIX,
});
const regions = createSVGNode(this.win, {
nodeType: "g",
parent: svg,
attributes: {
class: "regions",
role: "presentation",
},
prefix: this.ID_CLASS_PREFIX,
});
for (const region of BOX_MODEL_REGIONS) {
createSVGNode(this.win, {
nodeType: "path",
parent: regions,
attributes: {
class: region,
id: region,
role: "presentation",
},
prefix: this.ID_CLASS_PREFIX,
});
}
for (const side of BOX_MODEL_SIDES) {
createSVGNode(this.win, {
nodeType: "line",
parent: svg,
attributes: {
class: "guide-" + side,
id: "guide-" + side,
"stroke-width": GUIDE_STROKE_WIDTH,
role: "presentation",
},
prefix: this.ID_CLASS_PREFIX,
});
}
return highlighterContainer;
}
}
exports.BoxModelHighlighterRenderer = BoxModelHighlighterRenderer;
/**
* Setup function that runs in parent process and sets up the rendering part of the
* box model highlighter and the communication channel with the observer part
* of the box model highlighter which lives in the content process.
*
*
* @param {Object} options.mm
* Message manager that corresponds to the current content tab.
* @param {String} options.prefix
* Unique prefix for message manager messages.
* This is the debugger-server-connection prefix.
* @return {Object}
* Defines event listeners for when client disconnects or browser gets
* swapped.
*/
function setupParentProcess({ mm, prefix }) {
let renderer = new BoxModelHighlighterRenderer(mm, prefix);
return {
onBrowserSwap: newMM => renderer.setMessageManager(newMM, prefix),
onDisconnected: () => {
renderer.destroy();
renderer = null;
},
};
}
exports.setupParentProcess = setupParentProcess;

View File

@ -101,7 +101,6 @@ class BoxModelHighlighter extends AutoRefreshHighlighter {
super(highlighterEnv);
this.ID_CLASS_PREFIX = "box-model-";
this.markup = new CanvasFrameAnonymousContentHelper(
this.highlighterEnv,
this._buildMarkup.bind(this)

View File

@ -11,6 +11,8 @@ DIRS += [
DevToolsModules(
'accessible.js',
'auto-refresh.js',
'box-model-observer.js',
'box-model-renderer.js',
'box-model.js',
'css-grid.js',
'css-transform.js',

View File

@ -196,15 +196,21 @@ exports.InspectorActor = protocol.ActorClassWithSpec(inspectorSpec, {
*
* @param {Boolean} autohide Optionally autohide the highlighter after an
* element has been picked
* @param {Boolean} useNewBoxModelHighlighter Whether to use the new box model
* highlighter that has split renderer and observer parts.
* @return {HighlighterActor}
*/
getHighlighter: function(autohide) {
getHighlighter: function(autohide, useNewBoxModelHighlighter) {
if (this._highlighterPromise) {
return this._highlighterPromise;
}
this._highlighterPromise = this.getWalker().then(walker => {
const highlighter = HighlighterActor(this, autohide);
const highlighter = HighlighterActor(
this,
autohide,
useNewBoxModelHighlighter
);
this.manage(highlighter);
return highlighter;
});

View File

@ -30,7 +30,9 @@ const TELEMETRY_EYEDROPPER_OPENED_MENU =
const SHOW_ALL_ANONYMOUS_CONTENT_PREF =
"devtools.inspector.showAllAnonymousContent";
const SHOW_UA_SHADOW_ROOTS_PREF = "devtools.inspector.showUserAgentShadowRoots";
const FISSION_ENABLED = "devtools.browsertoolbox.fission";
const FISSION_ENABLED_PREF = "devtools.browsertoolbox.fission";
const USE_NEW_BOX_MODEL_HIGHLIGHTER_PREF =
"devtools.inspector.use-new-box-model-highlighter";
const telemetry = new Telemetry();
@ -531,7 +533,10 @@ class InspectorFront extends FrontClassWithSpec(inspectorSpec) {
async _getHighlighter() {
const autohide = !flags.testing;
this.highlighter = await this.getHighlighter(autohide);
this.highlighter = await this.getHighlighter(
autohide,
Services.prefs.getBoolPref(USE_NEW_BOX_MODEL_HIGHLIGHTER_PREF)
);
}
hasHighlighter(type) {
@ -604,7 +609,7 @@ class InspectorFront extends FrontClassWithSpec(inspectorSpec) {
* @return {Array} The list of InspectorFront instances.
*/
async getChildInspectors() {
const fissionEnabled = Services.prefs.getBoolPref(FISSION_ENABLED);
const fissionEnabled = Services.prefs.getBoolPref(FISSION_ENABLED_PREF);
const childInspectors = [];
const target = this.targetFront;
// this line can be removed when we are ready for fission frames

View File

@ -406,6 +406,7 @@ const inspectorSpec = generateActorSpec({
getHighlighter: {
request: {
autohide: Arg(0, "boolean"),
useNewBoxModelHighlighter: Arg(1, "boolean"),
},
response: {
highligter: RetVal("highlighter"),