summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/highlighters/rulers.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/highlighters/rulers.js')
-rw-r--r--devtools/server/actors/highlighters/rulers.js294
1 files changed, 294 insertions, 0 deletions
diff --git a/devtools/server/actors/highlighters/rulers.js b/devtools/server/actors/highlighters/rulers.js
new file mode 100644
index 000000000..01e082e67
--- /dev/null
+++ b/devtools/server/actors/highlighters/rulers.js
@@ -0,0 +1,294 @@
+/* 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 events = require("sdk/event/core");
+const { getCurrentZoom,
+ setIgnoreLayoutChanges } = require("devtools/shared/layout/utils");
+const {
+ CanvasFrameAnonymousContentHelper,
+ createSVGNode, createNode } = require("./utils/markup");
+
+// Maximum size, in pixel, for the horizontal ruler and vertical ruler
+// used by RulersHighlighter
+const RULERS_MAX_X_AXIS = 10000;
+const RULERS_MAX_Y_AXIS = 15000;
+// Number of steps after we add a graduation, marker and text in
+// RulersHighliter; currently the unit is in pixel.
+const RULERS_GRADUATION_STEP = 5;
+const RULERS_MARKER_STEP = 50;
+const RULERS_TEXT_STEP = 100;
+
+/**
+ * The RulersHighlighter is a class that displays both horizontal and
+ * vertical rules on the page, along the top and left edges, with pixel
+ * graduations, useful for users to quickly check distances
+ */
+function RulersHighlighter(highlighterEnv) {
+ this.env = highlighterEnv;
+ this.markup = new CanvasFrameAnonymousContentHelper(highlighterEnv,
+ this._buildMarkup.bind(this));
+
+ let { pageListenerTarget } = highlighterEnv;
+ pageListenerTarget.addEventListener("scroll", this);
+ pageListenerTarget.addEventListener("pagehide", this);
+}
+
+RulersHighlighter.prototype = {
+ typeName: "RulersHighlighter",
+
+ ID_CLASS_PREFIX: "rulers-highlighter-",
+
+ _buildMarkup: function () {
+ let { window } = this.env;
+ let prefix = this.ID_CLASS_PREFIX;
+
+ function createRuler(axis, size) {
+ let width, height;
+ let isHorizontal = true;
+
+ if (axis === "x") {
+ width = size;
+ height = 16;
+ } else if (axis === "y") {
+ width = 16;
+ height = size;
+ isHorizontal = false;
+ } else {
+ throw new Error(
+ `Invalid type of axis given; expected "x" or "y" but got "${axis}"`);
+ }
+
+ let g = createSVGNode(window, {
+ nodeType: "g",
+ attributes: {
+ id: `${axis}-axis`
+ },
+ parent: svg,
+ prefix
+ });
+
+ createSVGNode(window, {
+ nodeType: "rect",
+ attributes: {
+ y: isHorizontal ? 0 : 16,
+ width,
+ height
+ },
+ parent: g
+ });
+
+ let gRule = createSVGNode(window, {
+ nodeType: "g",
+ attributes: {
+ id: `${axis}-axis-ruler`
+ },
+ parent: g,
+ prefix
+ });
+
+ let pathGraduations = createSVGNode(window, {
+ nodeType: "path",
+ attributes: {
+ "class": "ruler-graduations",
+ width,
+ height
+ },
+ parent: gRule,
+ prefix
+ });
+
+ let pathMarkers = createSVGNode(window, {
+ nodeType: "path",
+ attributes: {
+ "class": "ruler-markers",
+ width,
+ height
+ },
+ parent: gRule,
+ prefix
+ });
+
+ let gText = createSVGNode(window, {
+ nodeType: "g",
+ attributes: {
+ id: `${axis}-axis-text`,
+ "class": (isHorizontal ? "horizontal" : "vertical") + "-labels"
+ },
+ parent: g,
+ prefix
+ });
+
+ let dGraduations = "";
+ let dMarkers = "";
+ let graduationLength;
+
+ for (let i = 0; i < size; i += RULERS_GRADUATION_STEP) {
+ if (i === 0) {
+ continue;
+ }
+
+ graduationLength = (i % 2 === 0) ? 6 : 4;
+
+ if (i % RULERS_TEXT_STEP === 0) {
+ graduationLength = 8;
+ createSVGNode(window, {
+ nodeType: "text",
+ parent: gText,
+ attributes: {
+ x: isHorizontal ? 2 + i : -i - 1,
+ y: 5
+ }
+ }).textContent = i;
+ }
+
+ if (isHorizontal) {
+ if (i % RULERS_MARKER_STEP === 0) {
+ dMarkers += `M${i} 0 L${i} ${graduationLength}`;
+ } else {
+ dGraduations += `M${i} 0 L${i} ${graduationLength} `;
+ }
+ } else {
+ if (i % 50 === 0) {
+ dMarkers += `M0 ${i} L${graduationLength} ${i}`;
+ } else {
+ dGraduations += `M0 ${i} L${graduationLength} ${i}`;
+ }
+ }
+ }
+
+ pathGraduations.setAttribute("d", dGraduations);
+ pathMarkers.setAttribute("d", dMarkers);
+
+ return g;
+ }
+
+ let container = createNode(window, {
+ attributes: {"class": "highlighter-container"}
+ });
+
+ let root = createNode(window, {
+ parent: container,
+ attributes: {
+ "id": "root",
+ "class": "root"
+ },
+ prefix
+ });
+
+ let svg = createSVGNode(window, {
+ nodeType: "svg",
+ parent: root,
+ attributes: {
+ id: "elements",
+ "class": "elements",
+ width: "100%",
+ height: "100%",
+ hidden: "true"
+ },
+ prefix
+ });
+
+ createRuler("x", RULERS_MAX_X_AXIS);
+ createRuler("y", RULERS_MAX_Y_AXIS);
+
+ return container;
+ },
+
+ handleEvent: function (event) {
+ switch (event.type) {
+ case "scroll":
+ this._onScroll(event);
+ break;
+ case "pagehide":
+ this.destroy();
+ break;
+ }
+ },
+
+ _onScroll: function (event) {
+ let prefix = this.ID_CLASS_PREFIX;
+ let { scrollX, scrollY } = event.view;
+
+ this.markup.getElement(`${prefix}x-axis-ruler`)
+ .setAttribute("transform", `translate(${-scrollX})`);
+ this.markup.getElement(`${prefix}x-axis-text`)
+ .setAttribute("transform", `translate(${-scrollX})`);
+ this.markup.getElement(`${prefix}y-axis-ruler`)
+ .setAttribute("transform", `translate(0, ${-scrollY})`);
+ this.markup.getElement(`${prefix}y-axis-text`)
+ .setAttribute("transform", `translate(0, ${-scrollY})`);
+ },
+
+ _update: function () {
+ let { window } = this.env;
+
+ setIgnoreLayoutChanges(true);
+
+ let zoom = getCurrentZoom(window);
+ let isZoomChanged = zoom !== this._zoom;
+
+ if (isZoomChanged) {
+ this._zoom = zoom;
+ this.updateViewport();
+ }
+
+ setIgnoreLayoutChanges(false, window.document.documentElement);
+
+ this._rafID = window.requestAnimationFrame(() => this._update());
+ },
+
+ _cancelUpdate: function () {
+ if (this._rafID) {
+ this.env.window.cancelAnimationFrame(this._rafID);
+ this._rafID = 0;
+ }
+ },
+ updateViewport: function () {
+ let { devicePixelRatio } = this.env.window;
+
+ // Because `devicePixelRatio` is affected by zoom (see bug 809788),
+ // in order to get the "real" device pixel ratio, we need divide by `zoom`
+ let pixelRatio = devicePixelRatio / this._zoom;
+
+ // The "real" device pixel ratio is used to calculate the max stroke
+ // width we can actually assign: on retina, for instance, it would be 0.5,
+ // where on non high dpi monitor would be 1.
+ let minWidth = 1 / pixelRatio;
+ let strokeWidth = Math.min(minWidth, minWidth / this._zoom);
+
+ this.markup.getElement(this.ID_CLASS_PREFIX + "root").setAttribute("style",
+ `stroke-width:${strokeWidth};`);
+ },
+
+ destroy: function () {
+ this.hide();
+
+ let { pageListenerTarget } = this.env;
+ pageListenerTarget.removeEventListener("scroll", this);
+ pageListenerTarget.removeEventListener("pagehide", this);
+
+ this.markup.destroy();
+
+ events.emit(this, "destroy");
+ },
+
+ show: function () {
+ this.markup.removeAttributeForElement(this.ID_CLASS_PREFIX + "elements",
+ "hidden");
+
+ this._update();
+
+ return true;
+ },
+
+ hide: function () {
+ this.markup.setAttributeForElement(this.ID_CLASS_PREFIX + "elements",
+ "hidden", "true");
+
+ this._cancelUpdate();
+ }
+};
+exports.RulersHighlighter = RulersHighlighter;