tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

measuring-tool.js (22087B)


      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  getWindowDimensions,
     10  setIgnoreLayoutChanges,
     11 } = require("resource://devtools/shared/layout/utils.js");
     12 const {
     13  CanvasFrameAnonymousContentHelper,
     14 } = require("resource://devtools/server/actors/highlighters/utils/markup.js");
     15 
     16 // Hard coded value about the size of measuring tool label, in order to
     17 // position and flip it when is needed.
     18 const LABEL_SIZE_MARGIN = 8;
     19 const LABEL_SIZE_WIDTH = 80;
     20 const LABEL_SIZE_HEIGHT = 52;
     21 const LABEL_POS_MARGIN = 4;
     22 const LABEL_POS_WIDTH = 40;
     23 const LABEL_POS_HEIGHT = 34;
     24 const LABEL_TYPE_SIZE = "size";
     25 const LABEL_TYPE_POSITION = "position";
     26 
     27 // List of all DOM Events subscribed directly to the document from the
     28 // Measuring Tool highlighter
     29 const DOM_EVENTS = [
     30  "mousedown",
     31  "mousemove",
     32  "mouseup",
     33  "mouseleave",
     34  "scroll",
     35  "pagehide",
     36  "keydown",
     37  "keyup",
     38 ];
     39 
     40 const SIDES = ["top", "right", "bottom", "left"];
     41 const HANDLERS = [...SIDES, "topleft", "topright", "bottomleft", "bottomright"];
     42 const HANDLER_SIZE = 6;
     43 const HIGHLIGHTED_HANDLER_CLASSNAME = "highlight";
     44 
     45 const IS_OSX = Services.appinfo.OS === "Darwin";
     46 
     47 /**
     48 * The MeasuringToolHighlighter is used to measure distances in a content page.
     49 * It allows users to click and drag with their mouse to draw an area whose
     50 * dimensions will be displayed in a tooltip next to it.
     51 * This allows users to measure distances between elements on a page.
     52 */
     53 class MeasuringToolHighlighter {
     54  constructor(highlighterEnv) {
     55    this.env = highlighterEnv;
     56    this.markup = new CanvasFrameAnonymousContentHelper(
     57      highlighterEnv,
     58      this._buildMarkup.bind(this),
     59      {
     60        contentRootHostClassName: "devtools-highlighter-measuring-tool",
     61      }
     62    );
     63    this.isReady = this.markup.initialize();
     64 
     65    this.rect = { x: 0, y: 0, w: 0, h: 0 };
     66    this.mouseCoords = { x: 0, y: 0 };
     67 
     68    const { pageListenerTarget } = highlighterEnv;
     69 
     70    // Register the measuring tool instance to all events we're interested in.
     71    DOM_EVENTS.forEach(type => pageListenerTarget.addEventListener(type, this));
     72  }
     73 
     74  _buildMarkup() {
     75    const container = this.markup.createNode({
     76      attributes: { class: "highlighter-container" },
     77    });
     78 
     79    const root = this.markup.createNode({
     80      parent: container,
     81      attributes: {
     82        id: "measuring-tool-root",
     83        class: "measuring-tool-root",
     84        hidden: "true",
     85      },
     86    });
     87 
     88    const svg = this.markup.createSVGNode({
     89      nodeType: "svg",
     90      parent: root,
     91      attributes: {
     92        id: "measuring-tool-elements",
     93        class: "measuring-tool-elements",
     94        width: "100%",
     95        height: "100%",
     96      },
     97    });
     98 
     99    for (const side of SIDES) {
    100      this.markup.createSVGNode({
    101        nodeType: "line",
    102        parent: svg,
    103        attributes: {
    104          class: `measuring-tool-guide-${side}`,
    105          id: `measuring-tool-guide-${side}`,
    106          hidden: "true",
    107        },
    108      });
    109    }
    110 
    111    this.markup.createNode({
    112      nodeType: "label",
    113      attributes: {
    114        id: "measuring-tool-label-size",
    115        class: "measuring-tool-label-size",
    116        hidden: "true",
    117      },
    118      parent: root,
    119    });
    120 
    121    this.markup.createNode({
    122      nodeType: "label",
    123      attributes: {
    124        id: "measuring-tool-label-position",
    125        class: "measuring-tool-label-position",
    126        hidden: "true",
    127      },
    128      parent: root,
    129    });
    130 
    131    // Creating a <g> element in order to group all the paths below, that
    132    // together represent the measuring tool; so that would be easier move them
    133    // around
    134    const g = this.markup.createSVGNode({
    135      nodeType: "g",
    136      attributes: {
    137        id: "measuring-tool-tool",
    138      },
    139      parent: svg,
    140    });
    141 
    142    this.markup.createSVGNode({
    143      nodeType: "path",
    144      attributes: {
    145        id: "measuring-tool-box-path",
    146        class: "measuring-tool-box-path",
    147      },
    148      parent: g,
    149    });
    150 
    151    this.markup.createSVGNode({
    152      nodeType: "path",
    153      attributes: {
    154        id: "measuring-tool-diagonal-path",
    155        class: "measuring-tool-diagonal-path",
    156      },
    157      parent: g,
    158    });
    159 
    160    for (const handler of HANDLERS) {
    161      this.markup.createSVGNode({
    162        nodeType: "circle",
    163        parent: g,
    164        attributes: {
    165          class: `measuring-tool-handler-${handler}`,
    166          id: `measuring-tool-handler-${handler}`,
    167          r: HANDLER_SIZE,
    168          hidden: "true",
    169        },
    170      });
    171    }
    172 
    173    return container;
    174  }
    175 
    176  _update() {
    177    const { window } = this.env;
    178 
    179    setIgnoreLayoutChanges(true);
    180 
    181    const zoom = getCurrentZoom(window);
    182 
    183    const { width, height } = getWindowDimensions(window);
    184 
    185    const { rect } = this;
    186 
    187    const isZoomChanged = zoom !== rect.zoom;
    188 
    189    if (isZoomChanged) {
    190      rect.zoom = zoom;
    191      this.updateLabel();
    192    }
    193 
    194    const isDocumentSizeChanged =
    195      width !== rect.documentWidth || height !== rect.documentHeight;
    196 
    197    if (isDocumentSizeChanged) {
    198      rect.documentWidth = width;
    199      rect.documentHeight = height;
    200    }
    201 
    202    // If either the document's size or the zoom is changed since the last
    203    // repaint, we update the tool's size as well.
    204    if (isZoomChanged || isDocumentSizeChanged) {
    205      this.updateViewport();
    206    }
    207 
    208    setIgnoreLayoutChanges(false, window.document.documentElement);
    209 
    210    this._rafID = window.requestAnimationFrame(() => this._update());
    211  }
    212 
    213  _cancelUpdate() {
    214    if (this._rafID) {
    215      this.env.window.cancelAnimationFrame(this._rafID);
    216      this._rafID = 0;
    217    }
    218  }
    219 
    220  destroy() {
    221    this.hide();
    222 
    223    this._cancelUpdate();
    224 
    225    const { pageListenerTarget } = this.env;
    226 
    227    if (pageListenerTarget) {
    228      DOM_EVENTS.forEach(type =>
    229        pageListenerTarget.removeEventListener(type, this)
    230      );
    231    }
    232 
    233    this.markup.destroy();
    234  }
    235 
    236  show() {
    237    setIgnoreLayoutChanges(true);
    238 
    239    this.getElement("measuring-tool-root").removeAttribute("hidden");
    240 
    241    this._update();
    242 
    243    setIgnoreLayoutChanges(false, this.env.window.document.documentElement);
    244  }
    245 
    246  hide() {
    247    setIgnoreLayoutChanges(true);
    248 
    249    this.hideLabel(LABEL_TYPE_SIZE);
    250    this.hideLabel(LABEL_TYPE_POSITION);
    251 
    252    this.getElement("measuring-tool-root").setAttribute("hidden", "true");
    253 
    254    this._cancelUpdate();
    255 
    256    setIgnoreLayoutChanges(false, this.env.window.document.documentElement);
    257  }
    258 
    259  getElement(id) {
    260    return this.markup.getElement(id);
    261  }
    262 
    263  setSize(w, h) {
    264    this.setRect(undefined, undefined, w, h);
    265  }
    266 
    267  setRect(x, y, w, h) {
    268    const { rect } = this;
    269 
    270    if (typeof x !== "undefined") {
    271      rect.x = x;
    272    }
    273 
    274    if (typeof y !== "undefined") {
    275      rect.y = y;
    276    }
    277 
    278    if (typeof w !== "undefined") {
    279      rect.w = w;
    280    }
    281 
    282    if (typeof h !== "undefined") {
    283      rect.h = h;
    284    }
    285 
    286    setIgnoreLayoutChanges(true);
    287 
    288    if (this._dragging) {
    289      this.updatePaths();
    290      this.updateHandlers();
    291    }
    292 
    293    this.updateLabel();
    294 
    295    setIgnoreLayoutChanges(false, this.env.window.document.documentElement);
    296  }
    297 
    298  updatePaths() {
    299    const { x, y, w, h } = this.rect;
    300    const dir = `M0 0 L${w} 0 L${w} ${h} L0 ${h}z`;
    301 
    302    // Adding correction to the line path, otherwise some pixels are drawn
    303    // outside the main rectangle area.
    304    const x1 = w > 0 ? 0.5 : 0;
    305    const y1 = w < 0 && h < 0 ? -0.5 : 0;
    306    const w1 = w + (h < 0 && w < 0 ? 0.5 : 0);
    307    const h1 = h + (h > 0 && w > 0 ? -0.5 : 0);
    308 
    309    const linedir = `M${x1} ${y1} L${w1} ${h1}`;
    310 
    311    this.getElement("measuring-tool-box-path").setAttribute("d", dir);
    312    this.getElement("measuring-tool-diagonal-path").setAttribute("d", linedir);
    313    this.getElement("measuring-tool-tool").setAttribute(
    314      "transform",
    315      `translate(${x},${y})`
    316    );
    317  }
    318 
    319  updateLabel(type) {
    320    type = type || (this._dragging ? LABEL_TYPE_SIZE : LABEL_TYPE_POSITION);
    321 
    322    const isSizeLabel = type === LABEL_TYPE_SIZE;
    323 
    324    const label = this.getElement(`measuring-tool-label-${type}`);
    325 
    326    let origin = "top left";
    327 
    328    const { innerWidth, innerHeight, scrollX, scrollY } = this.env.window;
    329    const { x: mouseX, y: mouseY } = this.mouseCoords;
    330    let { x, y, w, h, zoom } = this.rect;
    331    const scale = 1 / zoom;
    332 
    333    w = w || 0;
    334    h = h || 0;
    335    x = x || 0;
    336    y = y || 0;
    337    if (type === LABEL_TYPE_SIZE) {
    338      x += w;
    339      y += h;
    340    } else {
    341      x = mouseX;
    342      y = mouseY;
    343    }
    344 
    345    let labelMargin, labelHeight, labelWidth;
    346 
    347    if (isSizeLabel) {
    348      labelMargin = LABEL_SIZE_MARGIN;
    349      labelWidth = LABEL_SIZE_WIDTH;
    350      labelHeight = LABEL_SIZE_HEIGHT;
    351 
    352      const d = Math.hypot(w, h).toFixed(2);
    353 
    354      label.setTextContent(`W: ${Math.abs(w)} px
    355                            H: ${Math.abs(h)} px
    356                            ↘: ${d}px`);
    357    } else {
    358      labelMargin = LABEL_POS_MARGIN;
    359      labelWidth = LABEL_POS_WIDTH;
    360      labelHeight = LABEL_POS_HEIGHT;
    361 
    362      label.setTextContent(`${mouseX}
    363                            ${mouseY}`);
    364    }
    365 
    366    // Size used to position properly the label
    367    const labelBoxWidth = (labelWidth + labelMargin) * scale;
    368    const labelBoxHeight = (labelHeight + labelMargin) * scale;
    369 
    370    const isGoingLeft = w < scrollX;
    371    const isSizeGoingLeft = isSizeLabel && isGoingLeft;
    372    const isExceedingLeftMargin = x - labelBoxWidth < scrollX;
    373    const isExceedingRightMargin = x + labelBoxWidth > innerWidth + scrollX;
    374    const isExceedingTopMargin = y - labelBoxHeight < scrollY;
    375    const isExceedingBottomMargin = y + labelBoxHeight > innerHeight + scrollY;
    376 
    377    if ((isSizeGoingLeft && !isExceedingLeftMargin) || isExceedingRightMargin) {
    378      x -= labelBoxWidth;
    379      origin = "top right";
    380    } else {
    381      x += labelMargin * scale;
    382    }
    383 
    384    if (isSizeLabel) {
    385      y += isExceedingTopMargin ? labelMargin * scale : -labelBoxHeight;
    386    } else {
    387      y += isExceedingBottomMargin ? -labelBoxHeight : labelMargin * scale;
    388    }
    389 
    390    label.setAttribute(
    391      "style",
    392      `
    393      width: ${labelWidth}px;
    394      height: ${labelHeight}px;
    395      transform-origin: ${origin};
    396      transform: translate(${x}px,${y}px) scale(${scale})
    397    `
    398    );
    399 
    400    if (!isSizeLabel) {
    401      const labelSize = this.getElement("measuring-tool-label-size");
    402      const style = labelSize.getAttribute("style");
    403 
    404      if (style) {
    405        labelSize.setAttribute(
    406          "style",
    407          style.replace(/scale[^)]+\)/, `scale(${scale})`)
    408        );
    409      }
    410    }
    411  }
    412 
    413  updateViewport() {
    414    const { devicePixelRatio } = this.env.window;
    415    const { documentWidth, documentHeight, zoom } = this.rect;
    416 
    417    // Because `devicePixelRatio` is affected by zoom (see bug 809788),
    418    // in order to get the "real" device pixel ratio, we need divide by `zoom`
    419    const pixelRatio = devicePixelRatio / zoom;
    420 
    421    // The "real" device pixel ratio is used to calculate the max stroke
    422    // width we can actually assign: on retina, for instance, it would be 0.5,
    423    // where on non high dpi monitor would be 1.
    424    const minWidth = 1 / pixelRatio;
    425    const strokeWidth = minWidth / zoom;
    426 
    427    this.getElement("measuring-tool-root").setAttribute(
    428      "style",
    429      `stroke-width:${strokeWidth};
    430       width:${documentWidth}px;
    431       height:${documentHeight}px;`
    432    );
    433  }
    434 
    435  updateGuides() {
    436    const { x, y, w, h } = this.rect;
    437 
    438    let guide = this.getElement("measuring-tool-guide-top");
    439 
    440    guide.setAttribute("x1", "0");
    441    guide.setAttribute("y1", y);
    442    guide.setAttribute("x2", "100%");
    443    guide.setAttribute("y2", y);
    444 
    445    guide = this.getElement("measuring-tool-guide-right");
    446 
    447    guide.setAttribute("x1", x + w);
    448    guide.setAttribute("y1", 0);
    449    guide.setAttribute("x2", x + w);
    450    guide.setAttribute("y2", "100%");
    451 
    452    guide = this.getElement("measuring-tool-guide-bottom");
    453 
    454    guide.setAttribute("x1", "0");
    455    guide.setAttribute("y1", y + h);
    456    guide.setAttribute("x2", "100%");
    457    guide.setAttribute("y2", y + h);
    458 
    459    guide = this.getElement("measuring-tool-guide-left");
    460 
    461    guide.setAttribute("x1", x);
    462    guide.setAttribute("y1", 0);
    463    guide.setAttribute("x2", x);
    464    guide.setAttribute("y2", "100%");
    465  }
    466 
    467  setHandlerPosition(handler, x, y) {
    468    const handlerElement = this.getElement(`measuring-tool-handler-${handler}`);
    469    handlerElement.setAttribute("cx", x);
    470    handlerElement.setAttribute("cy", y);
    471  }
    472 
    473  updateHandlers() {
    474    const { w, h } = this.rect;
    475 
    476    this.setHandlerPosition("top", w / 2, 0);
    477    this.setHandlerPosition("topright", w, 0);
    478    this.setHandlerPosition("right", w, h / 2);
    479    this.setHandlerPosition("bottomright", w, h);
    480    this.setHandlerPosition("bottom", w / 2, h);
    481    this.setHandlerPosition("bottomleft", 0, h);
    482    this.setHandlerPosition("left", 0, h / 2);
    483    this.setHandlerPosition("topleft", 0, 0);
    484  }
    485 
    486  showLabel(type) {
    487    setIgnoreLayoutChanges(true);
    488 
    489    this.getElement(`measuring-tool-label-${type}`).removeAttribute("hidden");
    490 
    491    setIgnoreLayoutChanges(false, this.env.window.document.documentElement);
    492  }
    493 
    494  hideLabel(type) {
    495    setIgnoreLayoutChanges(true);
    496 
    497    this.getElement(`measuring-tool-label-${type}`).setAttribute(
    498      "hidden",
    499      "true"
    500    );
    501 
    502    setIgnoreLayoutChanges(false, this.env.window.document.documentElement);
    503  }
    504 
    505  showGuides() {
    506    const prefix = "measuring-tool-guide-";
    507 
    508    for (const side of SIDES) {
    509      this.markup.removeAttributeForElement(`${prefix + side}`, "hidden");
    510    }
    511  }
    512 
    513  hideGuides() {
    514    const prefix = "measuring-tool-guide-";
    515 
    516    for (const side of SIDES) {
    517      this.markup.setAttributeForElement(`${prefix + side}`, "hidden", "true");
    518    }
    519  }
    520 
    521  showHandler(id) {
    522    const prefix = "measuring-tool-handler-";
    523    this.markup.removeAttributeForElement(prefix + id, "hidden");
    524  }
    525 
    526  showHandlers() {
    527    const prefix = "measuring-tool-handler-";
    528 
    529    for (const handler of HANDLERS) {
    530      this.markup.removeAttributeForElement(prefix + handler, "hidden");
    531    }
    532  }
    533 
    534  hideAll() {
    535    this.hideLabel(LABEL_TYPE_POSITION);
    536    this.hideLabel(LABEL_TYPE_SIZE);
    537    this.hideGuides();
    538    this.hideHandlers();
    539  }
    540 
    541  showGuidesAndHandlers() {
    542    // Shows the guides and handlers only if an actual area is selected
    543    if (this.rect.w !== 0 && this.rect.h !== 0) {
    544      this.updateGuides();
    545      this.showGuides();
    546      this.updateHandlers();
    547      this.showHandlers();
    548    }
    549  }
    550 
    551  hideHandlers() {
    552    const prefix = "measuring-tool-handler-";
    553 
    554    for (const handler of HANDLERS) {
    555      this.markup.setAttributeForElement(prefix + handler, "hidden", "true");
    556    }
    557  }
    558 
    559  handleEvent(event) {
    560    const { target, type } = event;
    561 
    562    switch (type) {
    563      case "mousedown": {
    564        if (event.button || this._dragging) {
    565          return;
    566        }
    567 
    568        const isHandler = event.originalTarget.id.includes("handler");
    569        if (isHandler) {
    570          this.handleResizingMouseDownEvent(event);
    571        } else {
    572          this.handleMouseDownEvent(event);
    573        }
    574        break;
    575      }
    576      case "mousemove":
    577        if (this._dragging && this._dragging.handler) {
    578          this.handleResizingMouseMoveEvent(event);
    579        } else {
    580          this.handleMouseMoveEvent(event);
    581        }
    582        break;
    583      case "mouseup":
    584        if (this._dragging) {
    585          if (this._dragging.handler) {
    586            this.handleResizingMouseUpEvent();
    587          } else {
    588            this.handleMouseUpEvent();
    589          }
    590        }
    591        break;
    592      case "mouseleave": {
    593        if (!this._dragging) {
    594          this.hideLabel(LABEL_TYPE_POSITION);
    595        }
    596        break;
    597      }
    598      case "scroll": {
    599        this.hideLabel(LABEL_TYPE_POSITION);
    600        break;
    601      }
    602      case "pagehide": {
    603        // If a page hide event is triggered for current window's highlighter, hide the
    604        // highlighter.
    605        if (target.defaultView === this.env.window) {
    606          this.destroy();
    607        }
    608        break;
    609      }
    610      case "keydown": {
    611        this.handleKeyDown(event);
    612        break;
    613      }
    614      case "keyup": {
    615        if (MeasuringToolHighlighter.#isResizeModifierPressed(event)) {
    616          this.getElement("measuring-tool-handler-topleft").classList?.remove(
    617            HIGHLIGHTED_HANDLER_CLASSNAME
    618          );
    619        }
    620        break;
    621      }
    622    }
    623  }
    624 
    625  handleMouseDownEvent(event) {
    626    const { pageX, pageY } = event;
    627    const { window } = this.env;
    628    const elementId = `measuring-tool-tool`;
    629 
    630    setIgnoreLayoutChanges(true);
    631 
    632    this.markup.getElement(elementId)?.classList.add("dragging");
    633 
    634    this.hideAll();
    635 
    636    setIgnoreLayoutChanges(false, window.document.documentElement);
    637 
    638    // Store all the initial values needed for drag & drop
    639    this._dragging = {
    640      handler: null,
    641      x: pageX,
    642      y: pageY,
    643    };
    644 
    645    this.setRect(pageX, pageY, 0, 0);
    646  }
    647 
    648  handleMouseMoveEvent(event) {
    649    const { pageX, pageY } = event;
    650    const { mouseCoords } = this;
    651    let { x, y, w, h } = this.rect;
    652    let labelType;
    653 
    654    if (this._dragging) {
    655      w = pageX - x;
    656      h = pageY - y;
    657 
    658      this.setRect(x, y, w, h);
    659 
    660      labelType = LABEL_TYPE_SIZE;
    661    } else {
    662      mouseCoords.x = pageX;
    663      mouseCoords.y = pageY;
    664      this.updateLabel(LABEL_TYPE_POSITION);
    665 
    666      labelType = LABEL_TYPE_POSITION;
    667    }
    668 
    669    this.showLabel(labelType);
    670  }
    671 
    672  handleMouseUpEvent() {
    673    setIgnoreLayoutChanges(true);
    674 
    675    this.getElement("measuring-tool-tool").classList?.remove("dragging");
    676 
    677    this.showGuidesAndHandlers();
    678 
    679    setIgnoreLayoutChanges(false, this.env.window.document.documentElement);
    680    this._dragging = null;
    681  }
    682 
    683  handleResizingMouseDownEvent(event) {
    684    const { originalTarget, pageX, pageY } = event;
    685    const { window } = this.env;
    686    const prefix = "measuring-tool-handler-";
    687    const handler = originalTarget.id.replace(prefix, "");
    688 
    689    setIgnoreLayoutChanges(true);
    690 
    691    this.markup.getElement(originalTarget.id)?.classList.add("dragging");
    692 
    693    this.hideAll();
    694    this.showHandler(handler);
    695 
    696    // Set coordinates to the current measurement area's position
    697    const [, x, y] = this.getElement("measuring-tool-tool")
    698      .getAttribute("transform")
    699      .match(/(\d+),(\d+)/);
    700    this.setRect(Number(x), Number(y));
    701 
    702    setIgnoreLayoutChanges(false, window.document.documentElement);
    703 
    704    // Store all the initial values needed for drag & drop
    705    this._dragging = {
    706      handler,
    707      x: pageX,
    708      y: pageY,
    709    };
    710  }
    711 
    712  handleResizingMouseMoveEvent(event) {
    713    const { pageX, pageY } = event;
    714    const { rect } = this;
    715    let { x, y, w, h } = rect;
    716 
    717    const { handler } = this._dragging;
    718 
    719    switch (handler) {
    720      case "top":
    721        y = pageY;
    722        h = rect.y + rect.h - pageY;
    723        break;
    724      case "topright":
    725        y = pageY;
    726        w = pageX - rect.x;
    727        h = rect.y + rect.h - pageY;
    728        break;
    729      case "right":
    730        w = pageX - rect.x;
    731        break;
    732      case "bottomright":
    733        w = pageX - rect.x;
    734        h = pageY - rect.y;
    735        break;
    736      case "bottom":
    737        h = pageY - rect.y;
    738        break;
    739      case "bottomleft":
    740        x = pageX;
    741        w = rect.x + rect.w - pageX;
    742        h = pageY - rect.y;
    743        break;
    744      case "left":
    745        x = pageX;
    746        w = rect.x + rect.w - pageX;
    747        break;
    748      case "topleft":
    749        x = pageX;
    750        y = pageY;
    751        w = rect.x + rect.w - pageX;
    752        h = rect.y + rect.h - pageY;
    753        break;
    754    }
    755 
    756    this.setRect(x, y, w, h);
    757 
    758    // Changes the resizing cursors in case the measuring box is mirrored
    759    const isMirrored =
    760      (rect.w < 0 || rect.h < 0) && !(rect.w < 0 && rect.h < 0);
    761    this.getElement("measuring-tool-tool").classList.toggle(
    762      "mirrored",
    763      isMirrored
    764    );
    765 
    766    this.showLabel("size");
    767  }
    768 
    769  handleResizingMouseUpEvent() {
    770    const { handler } = this._dragging;
    771 
    772    setIgnoreLayoutChanges(true);
    773 
    774    this.getElement(`measuring-tool-handler-${handler}`).classList?.remove(
    775      "dragging"
    776    );
    777    this.showHandlers();
    778 
    779    this.showGuidesAndHandlers();
    780 
    781    setIgnoreLayoutChanges(false, this.env.window.document.documentElement);
    782    this._dragging = null;
    783  }
    784 
    785  handleKeyDown(event) {
    786    if (MeasuringToolHighlighter.#isResizeModifierPressed(event)) {
    787      this.getElement("measuring-tool-handler-topleft").classList?.add(
    788        HIGHLIGHTED_HANDLER_CLASSNAME
    789      );
    790    }
    791 
    792    if (
    793      !["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(event.key)
    794    ) {
    795      return;
    796    }
    797 
    798    const { x, y, w, h } = this.rect;
    799    const modifier = event.shiftKey ? 10 : 1;
    800 
    801    event.preventDefault();
    802    if (MeasuringToolHighlighter.#isResizeModifierHeld(event)) {
    803      // If Ctrl (or Command on OS X) is held, resize the tool
    804      switch (event.key) {
    805        case "ArrowUp":
    806          this.setSize(undefined, h - modifier);
    807          break;
    808        case "ArrowDown":
    809          this.setSize(undefined, h + modifier);
    810          break;
    811        case "ArrowLeft":
    812          this.setSize(w - modifier, undefined);
    813          break;
    814        case "ArrowRight":
    815          this.setSize(w + modifier, undefined);
    816          break;
    817      }
    818    } else {
    819      // Arrow keys with no modifier move the tool
    820      switch (event.key) {
    821        case "ArrowUp":
    822          this.setRect(undefined, y - modifier);
    823          break;
    824        case "ArrowDown":
    825          this.setRect(undefined, y + modifier);
    826          break;
    827        case "ArrowLeft":
    828          this.setRect(x - modifier, undefined);
    829          break;
    830        case "ArrowRight":
    831          this.setRect(x + modifier, undefined);
    832          break;
    833      }
    834    }
    835 
    836    this.updatePaths();
    837    this.updateGuides();
    838    this.updateHandlers();
    839    this.updateLabel(LABEL_TYPE_SIZE);
    840  }
    841 
    842  static #isResizeModifierPressed(event) {
    843    return (
    844      (!IS_OSX && event.key === "Control") || (IS_OSX && event.key === "Meta")
    845    );
    846  }
    847 
    848  static #isResizeModifierHeld(event) {
    849    return (!IS_OSX && event.ctrlKey) || (IS_OSX && event.metaKey);
    850  }
    851 }
    852 exports.MeasuringToolHighlighter = MeasuringToolHighlighter;