rulers.js (8009B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { 8 getCurrentZoom, 9 setIgnoreLayoutChanges, 10 } = require("resource://devtools/shared/layout/utils.js"); 11 const { 12 CanvasFrameAnonymousContentHelper, 13 } = require("resource://devtools/server/actors/highlighters/utils/markup.js"); 14 15 // Maximum size, in pixel, for the horizontal ruler and vertical ruler 16 // used by RulersHighlighter 17 const RULERS_MAX_X_AXIS = 10000; 18 const RULERS_MAX_Y_AXIS = 15000; 19 // Number of steps after we add a graduation, marker and text in 20 // RulersHighliter; currently the unit is in pixel. 21 const RULERS_GRADUATION_STEP = 5; 22 const RULERS_MARKER_STEP = 50; 23 const RULERS_TEXT_STEP = 100; 24 25 /** 26 * The RulersHighlighter is a class that displays both horizontal and 27 * vertical rules on the page, along the top and left edges, with pixel 28 * graduations, useful for users to quickly check distances 29 */ 30 class RulersHighlighter { 31 constructor(highlighterEnv) { 32 this.env = highlighterEnv; 33 this.markup = new CanvasFrameAnonymousContentHelper( 34 highlighterEnv, 35 this._buildMarkup.bind(this), 36 { 37 contentRootHostClassName: "devtools-highlighter-rulers", 38 } 39 ); 40 this.isReady = this.markup.initialize(); 41 42 const { pageListenerTarget } = highlighterEnv; 43 pageListenerTarget.addEventListener("scroll", this); 44 pageListenerTarget.addEventListener("pagehide", this); 45 } 46 47 _buildMarkup() { 48 const createRuler = (axis, size) => { 49 let width, height; 50 let isHorizontal = true; 51 52 if (axis === "x") { 53 width = size; 54 height = 16; 55 } else if (axis === "y") { 56 width = 16; 57 height = size; 58 isHorizontal = false; 59 } else { 60 throw new Error( 61 `Invalid type of axis given; expected "x" or "y" but got "${axis}"` 62 ); 63 } 64 65 const g = this.markup.createSVGNode({ 66 nodeType: "g", 67 attributes: { 68 id: `rulers-highlighter-${axis}-axis`, 69 }, 70 parent: svg, 71 }); 72 73 this.markup.createSVGNode({ 74 nodeType: "rect", 75 attributes: { 76 y: isHorizontal ? 0 : 16, 77 width, 78 height, 79 }, 80 parent: g, 81 }); 82 83 const gRule = this.markup.createSVGNode({ 84 nodeType: "g", 85 attributes: { 86 id: `rulers-highlighter-${axis}-axis-ruler`, 87 }, 88 parent: g, 89 }); 90 91 const pathGraduations = this.markup.createSVGNode({ 92 nodeType: "path", 93 attributes: { 94 class: "rulers-highlighter-ruler-graduations", 95 width, 96 height, 97 }, 98 parent: gRule, 99 }); 100 101 const pathMarkers = this.markup.createSVGNode({ 102 nodeType: "path", 103 attributes: { 104 class: "rulers-highlighter-ruler-markers", 105 width, 106 height, 107 }, 108 parent: gRule, 109 }); 110 111 const gText = this.markup.createSVGNode({ 112 nodeType: "g", 113 attributes: { 114 id: `rulers-highlighter-${axis}-axis-text`, 115 class: isHorizontal 116 ? "rulers-highlighter-horizontal-labels" 117 : "rulers-highlighter-vertical-labels", 118 }, 119 parent: g, 120 }); 121 122 let dGraduations = ""; 123 let dMarkers = ""; 124 let graduationLength; 125 126 for (let i = 0; i < size; i += RULERS_GRADUATION_STEP) { 127 if (i === 0) { 128 continue; 129 } 130 131 graduationLength = i % 2 === 0 ? 6 : 4; 132 133 if (i % RULERS_TEXT_STEP === 0) { 134 graduationLength = 8; 135 this.markup.createSVGNode({ 136 nodeType: "text", 137 parent: gText, 138 attributes: { 139 x: isHorizontal ? 2 + i : -i - 1, 140 y: 5, 141 }, 142 }).textContent = i; 143 } 144 145 if (isHorizontal) { 146 if (i % RULERS_MARKER_STEP === 0) { 147 dMarkers += `M${i} 0 L${i} ${graduationLength}`; 148 } else { 149 dGraduations += `M${i} 0 L${i} ${graduationLength} `; 150 } 151 } else if (i % 50 === 0) { 152 dMarkers += `M0 ${i} L${graduationLength} ${i}`; 153 } else { 154 dGraduations += `M0 ${i} L${graduationLength} ${i}`; 155 } 156 } 157 158 pathGraduations.setAttribute("d", dGraduations); 159 pathMarkers.setAttribute("d", dMarkers); 160 161 return g; 162 }; 163 164 const container = this.markup.createNode({ 165 attributes: { class: "highlighter-container" }, 166 }); 167 168 const root = this.markup.createNode({ 169 parent: container, 170 attributes: { 171 id: "rulers-highlighter-root", 172 class: "rulers-highlighter-root", 173 }, 174 }); 175 176 const svg = this.markup.createSVGNode({ 177 nodeType: "svg", 178 parent: root, 179 attributes: { 180 id: "rulers-highlighter-elements", 181 class: "rulers-highlighter-elements", 182 width: "100%", 183 height: "100%", 184 hidden: "true", 185 }, 186 }); 187 188 createRuler("x", RULERS_MAX_X_AXIS); 189 createRuler("y", RULERS_MAX_Y_AXIS); 190 191 return container; 192 } 193 194 handleEvent(event) { 195 switch (event.type) { 196 case "scroll": 197 this._onScroll(event); 198 break; 199 case "pagehide": 200 // If a page hide event is triggered for current window's highlighter, hide the 201 // highlighter. 202 if (event.target.defaultView === this.env.window) { 203 this.destroy(); 204 } 205 break; 206 } 207 } 208 209 _onScroll(event) { 210 const { scrollX, scrollY } = event.view; 211 212 this.markup 213 .getElement(`rulers-highlighter-x-axis-ruler`) 214 .setAttribute("transform", `translate(${-scrollX})`); 215 this.markup 216 .getElement(`rulers-highlighter-x-axis-text`) 217 .setAttribute("transform", `translate(${-scrollX})`); 218 this.markup 219 .getElement(`rulers-highlighter-y-axis-ruler`) 220 .setAttribute("transform", `translate(0, ${-scrollY})`); 221 this.markup 222 .getElement(`rulers-highlighter-y-axis-text`) 223 .setAttribute("transform", `translate(0, ${-scrollY})`); 224 } 225 226 _update() { 227 const { window } = this.env; 228 229 setIgnoreLayoutChanges(true); 230 231 const zoom = getCurrentZoom(window); 232 const isZoomChanged = zoom !== this._zoom; 233 234 if (isZoomChanged) { 235 this._zoom = zoom; 236 this.updateViewport(); 237 } 238 239 setIgnoreLayoutChanges(false, window.document.documentElement); 240 241 this._rafID = window.requestAnimationFrame(() => this._update()); 242 } 243 244 _cancelUpdate() { 245 if (this._rafID) { 246 this.env.window.cancelAnimationFrame(this._rafID); 247 this._rafID = 0; 248 } 249 } 250 updateViewport() { 251 const { devicePixelRatio } = this.env.window; 252 253 // Because `devicePixelRatio` is affected by zoom (see bug 809788), 254 // in order to get the "real" device pixel ratio, we need divide by `zoom` 255 const pixelRatio = devicePixelRatio / this._zoom; 256 257 // The "real" device pixel ratio is used to calculate the max stroke 258 // width we can actually assign: on retina, for instance, it would be 0.5, 259 // where on non high dpi monitor would be 1. 260 const minWidth = 1 / pixelRatio; 261 const strokeWidth = Math.min(minWidth, minWidth / this._zoom); 262 263 this.markup 264 .getElement("rulers-highlighter-root") 265 .setAttribute("style", `stroke-width:${strokeWidth};`); 266 } 267 268 destroy() { 269 this.hide(); 270 271 const { pageListenerTarget } = this.env; 272 273 if (pageListenerTarget) { 274 pageListenerTarget.removeEventListener("scroll", this); 275 pageListenerTarget.removeEventListener("pagehide", this); 276 } 277 278 this.markup.destroy(); 279 } 280 281 show() { 282 this.markup.removeAttributeForElement( 283 "rulers-highlighter-elements", 284 "hidden" 285 ); 286 287 this._update(); 288 289 return true; 290 } 291 292 hide() { 293 this.markup.setAttributeForElement( 294 "rulers-highlighter-elements", 295 "hidden", 296 "true" 297 ); 298 299 this._cancelUpdate(); 300 } 301 } 302 exports.RulersHighlighter = RulersHighlighter;