Backed out changeset 9df2731a0530 (bug 1867644) for causing mochitests failures in...
[gecko.git] / browser / components / screenshots / ScreenshotsOverlayChild.sys.mjs
blobe03ce5e41a798c4e06fd5e139462cb088cdb9ac5
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/. */
5 /**
6  * The Screenshots overlay is inserted into the document's
7  * anonymous content container (see dom/webidl/Document.webidl).
8  *
9  * This container gets cleared automatically when the document navigates.
10  *
11  * To retrieve the AnonymousContent instance, use the `content` getter.
12  */
15  * Below are the states of the screenshots overlay
16  * States:
17  *  "crosshairs":
18  *    Nothing has happened, and the crosshairs will follow the movement of the mouse
19  *  "draggingReady":
20  *    The user has pressed the mouse button, but hasn't moved enough to create a selection
21  *  "dragging":
22  *    The user has pressed down a mouse button, and is dragging out an area far enough to show a selection
23  *  "selected":
24  *    The user has selected an area
25  *  "resizing":
26  *    The user is resizing the selection
27  */
29 import {
30   setMaxDetectHeight,
31   setMaxDetectWidth,
32   getBestRectForElement,
33   Region,
34   WindowDimensions,
35 } from "chrome://browser/content/screenshots/overlayHelpers.mjs";
37 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
39 const STATES = {
40   CROSSHAIRS: "crosshairs",
41   DRAGGING_READY: "draggingReady",
42   DRAGGING: "dragging",
43   SELECTED: "selected",
44   RESIZING: "resizing",
47 const lazy = {};
49 ChromeUtils.defineLazyGetter(lazy, "overlayLocalization", () => {
50   return new Localization(["browser/screenshotsOverlay.ftl"], true);
51 });
53 const REGION_CHANGE_THRESHOLD = 5;
54 const SCROLL_BY_EDGE = 20;
56 export class ScreenshotsOverlay {
57   #content;
58   #initialized = false;
59   #state = "";
60   #moverId;
61   #cachedEle;
62   #lastPageX;
63   #lastPageY;
64   #lastClientX;
65   #lastClientY;
66   #previousDimensions;
67   #methodsUsed;
69   get markup() {
70     let [cancel, instructions, download, copy] =
71       lazy.overlayLocalization.formatMessagesSync([
72         { id: "screenshots-overlay-cancel-button" },
73         { id: "screenshots-overlay-instructions" },
74         { id: "screenshots-overlay-download-button" },
75         { id: "screenshots-overlay-copy-button" },
76       ]);
78     return `
79       <template>
80         <link rel="stylesheet" href="chrome://browser/content/screenshots/overlay/overlay.css" />
81         <div id="screenshots-component">
82           <div id="preview-container" hidden>
83             <div class="face-container">
84               <div class="eye left"><div id="left-eye" class="eyeball"></div></div>
85               <div class="eye right"><div id="right-eye" class="eyeball"></div></div>
86               <div class="face"></div>
87             </div>
88             <div class="preview-instructions">${instructions.value}</div>
89             <button class="screenshots-button" id="screenshots-cancel-button">${cancel.value}</button>
90           </div>
91           <div id="hover-highlight" hidden></div>
92           <div id="selection-container" hidden>
93             <div id="top-background" class="bghighlight"></div>
94             <div id="bottom-background" class="bghighlight"></div>
95             <div id="left-background" class="bghighlight"></div>
96             <div id="right-background" class="bghighlight"></div>
97             <div id="highlight" class="highlight" tabindex="0">
98               <div id="mover-topLeft" class="mover-target direction-topLeft" tabindex="0">
99                 <div class="mover"></div>
100               </div>
101               <div id="mover-top" class="mover-target direction-top">
102                 <div class="mover"></div>
103               </div>
104               <div id="mover-topRight" class="mover-target direction-topRight" tabindex="0">
105                 <div class="mover"></div>
106               </div>
107               <div id="mover-left" class="mover-target direction-left">
108                 <div class="mover"></div>
109               </div>
110               <div id="mover-right" class="mover-target direction-right">
111                 <div class="mover"></div>
112               </div>
113               <div id="mover-bottomLeft" class="mover-target direction-bottomLeft" tabindex="0">
114                 <div class="mover"></div>
115               </div>
116               <div id="mover-bottom" class="mover-target direction-bottom">
117                 <div class="mover"></div>
118               </div>
119               <div id="mover-bottomRight" class="mover-target direction-bottomRight" tabindex="0">
120                 <div class="mover"></div>
121               </div>
122               <div id="selection-size-container">
123                 <span id="selection-size"></span>
124               </div>
125             </div>
126           </div>
127           <div id="buttons-container" hidden>
128             <div class="buttons-wrapper">
129               <button id="cancel" class="screenshots-button" title="${cancel.value}" aria-label="${cancel.value}" tabindex="0"><img/></button>
130               <button id="copy" class="screenshots-button" title="${copy.value}" aria-label="${copy.value}" tabindex="0"><img/>${copy.value}</button>
131               <button id="download" class="screenshots-button primary" title="${download.value}" aria-label="${download.value}" tabindex="0"><img/>${download.value}</button>
132             </div>
133           </div>
134         </div>
135       </template>`;
136   }
138   get fragment() {
139     if (!this.overlayTemplate) {
140       let parser = new DOMParser();
141       let doc = parser.parseFromString(this.markup, "text/html");
142       this.overlayTemplate = this.document.importNode(
143         doc.querySelector("template"),
144         true
145       );
146     }
147     let fragment = this.overlayTemplate.content.cloneNode(true);
148     return fragment;
149   }
151   get initialized() {
152     return this.#initialized;
153   }
155   get state() {
156     return this.#state;
157   }
159   get methodsUsed() {
160     return this.#methodsUsed;
161   }
163   constructor(contentDocument) {
164     this.document = contentDocument;
165     this.window = contentDocument.ownerGlobal;
167     this.windowDimensions = new WindowDimensions();
168     this.selectionRegion = new Region(this.windowDimensions);
169     this.hoverElementRegion = new Region(this.windowDimensions);
170     this.resetMethodsUsed();
171   }
173   get content() {
174     if (!this.#content || Cu.isDeadWrapper(this.#content)) {
175       return null;
176     }
177     return this.#content;
178   }
180   getElementById(id) {
181     return this.content.root.getElementById(id);
182   }
184   async initialize() {
185     if (this.initialized) {
186       return;
187     }
189     this.windowDimensions.reset();
191     this.#content = this.document.insertAnonymousContent();
192     this.#content.root.appendChild(this.fragment);
194     this.initializeElements();
195     this.updateWindowDimensions();
197     this.#setState(STATES.CROSSHAIRS);
199     this.#initialized = true;
200   }
202   /**
203    * Get all the elements that will be used.
204    */
205   initializeElements() {
206     this.previewCancelButton = this.getElementById("screenshots-cancel-button");
207     this.cancelButton = this.getElementById("cancel");
208     this.copyButton = this.getElementById("copy");
209     this.downloadButton = this.getElementById("download");
211     this.previewContainer = this.getElementById("preview-container");
212     this.hoverElementContainer = this.getElementById("hover-highlight");
213     this.selectionContainer = this.getElementById("selection-container");
214     this.buttonsContainer = this.getElementById("buttons-container");
215     this.screenshotsContainer = this.getElementById("screenshots-component");
217     this.leftEye = this.getElementById("left-eye");
218     this.rightEye = this.getElementById("right-eye");
220     this.leftBackgroundEl = this.getElementById("left-background");
221     this.topBackgroundEl = this.getElementById("top-background");
222     this.rightBackgroundEl = this.getElementById("right-background");
223     this.bottomBackgroundEl = this.getElementById("bottom-background");
224     this.highlightEl = this.getElementById("highlight");
226     this.topLeftMover = this.getElementById("mover-topLeft");
227     this.topRightMover = this.getElementById("mover-topRight");
228     this.bottomLeftMover = this.getElementById("mover-bottomLeft");
229     this.bottomRightMover = this.getElementById("mover-bottomRight");
231     this.selectionSize = this.getElementById("selection-size");
232   }
234   /**
235    * Removes all event listeners and removes the overlay from the Anonymous Content
236    */
237   tearDown(options = {}) {
238     if (this.#content) {
239       if (!(options.doNotResetMethods === true)) {
240         this.resetMethodsUsed();
241       }
242       try {
243         this.document.removeAnonymousContent(this.#content);
244       } catch (e) {
245         // If the current window isn't the one the content was inserted into, this
246         // will fail, but that's fine.
247       }
248     }
249     this.#initialized = false;
250     this.#setState("");
251   }
253   resetMethodsUsed() {
254     this.#methodsUsed = {
255       element: 0,
256       region: 0,
257       move: 0,
258       resize: 0,
259     };
260   }
262   /**
263    * Returns the x and y coordinates of the event relative to both the
264    * viewport and the page.
265    * @param {Event} event The event
266    * @returns
267    *  {
268    *    clientX: The x position relative to the viewport
269    *    clientY: The y position relative to the viewport
270    *    pageX: The x position relative to the entire page
271    *    pageY: The y position relative to the entire page
272    *  }
273    */
274   getCoordinatesFromEvent(event) {
275     const { clientX, clientY, pageX, pageY } = event;
277     return { clientX, clientY, pageX, pageY };
278   }
280   handleEvent(event) {
281     if (event.button > 0) {
282       return;
283     }
285     switch (event.type) {
286       case "click":
287         this.handleClick(event);
288         break;
289       case "pointerdown":
290         this.handlePointerDown(event);
291         break;
292       case "pointermove":
293         this.handlePointerMove(event);
294         break;
295       case "pointerup":
296         this.handlePointerUp(event);
297         break;
298       case "keydown":
299         this.handleKeyDown(event);
300         break;
301       case "keyup":
302         this.handleKeyUp(event);
303         break;
304     }
305   }
307   handleClick(event) {
308     switch (event.originalTarget.id) {
309       case "screenshots-cancel-button":
310       case "cancel":
311         this.maybeCancelScreenshots();
312         break;
313       case "copy":
314         this.#dispatchEvent("Screenshots:Copy", {
315           region: this.selectionRegion.dimensions,
316         });
317         break;
318       case "download":
319         this.#dispatchEvent("Screenshots:Download", {
320           region: this.selectionRegion.dimensions,
321         });
322         break;
323     }
324   }
326   maybeCancelScreenshots() {
327     if (this.#state === STATES.CROSSHAIRS) {
328       this.#dispatchEvent("Screenshots:Close", {
329         reason: "overlay_cancel",
330       });
331     } else {
332       this.#setState(STATES.CROSSHAIRS);
333     }
334   }
336   /**
337    * Handles the pointerdown event depending on the state.
338    * Early return when a pointer down happens on a button.
339    * @param {Event} event The pointerown event
340    */
341   handlePointerDown(event) {
342     if (
343       event.originalTarget.id === "screenshots-cancel-button" ||
344       event.originalTarget.closest("#buttons-container") ===
345         this.buttonsContainer
346     ) {
347       event.stopPropagation();
348       return;
349     }
351     const { pageX, pageY } = this.getCoordinatesFromEvent(event);
353     switch (this.#state) {
354       case STATES.CROSSHAIRS: {
355         this.crosshairsDragStart(pageX, pageY);
356         break;
357       }
358       case STATES.SELECTED: {
359         this.selectedDragStart(pageX, pageY, event.originalTarget.id);
360         break;
361       }
362     }
363   }
365   /**
366    * Handles the pointermove event depending on the state
367    * @param {Event} event The pointermove event
368    */
369   handlePointerMove(event) {
370     const { pageX, pageY, clientX, clientY } =
371       this.getCoordinatesFromEvent(event);
373     switch (this.#state) {
374       case STATES.CROSSHAIRS: {
375         this.crosshairsMove(clientX, clientY);
376         break;
377       }
378       case STATES.DRAGGING_READY: {
379         this.draggingReadyDrag(pageX, pageY);
380         break;
381       }
382       case STATES.DRAGGING: {
383         this.draggingDrag(pageX, pageY);
384         break;
385       }
386       case STATES.RESIZING: {
387         this.resizingDrag(pageX, pageY);
388         break;
389       }
390     }
391   }
393   /**
394    * Handles the pointerup event depending on the state
395    * @param {Event} event The pointerup event
396    */
397   handlePointerUp(event) {
398     const { pageX, pageY, clientX, clientY } =
399       this.getCoordinatesFromEvent(event);
401     switch (this.#state) {
402       case STATES.DRAGGING_READY: {
403         this.draggingReadyDragEnd(pageX - clientX, pageY - clientY);
404         break;
405       }
406       case STATES.DRAGGING: {
407         this.draggingDragEnd(pageX, pageY, event.originalTarget.id);
408         break;
409       }
410       case STATES.RESIZING: {
411         this.resizingDragEnd(pageX, pageY);
412         break;
413       }
414     }
415   }
417   /**
418    * Handles when a keydown occurs in the screenshots component.
419    * @param {Event} event The keydown event
420    */
421   handleKeyDown(event) {
422     switch (event.key) {
423       case "ArrowLeft":
424         this.handleArrowLeftKeyDown(event);
425         break;
426       case "ArrowUp":
427         this.handleArrowUpKeyDown(event);
428         break;
429       case "ArrowRight":
430         this.handleArrowRightKeyDown(event);
431         break;
432       case "ArrowDown":
433         this.handleArrowDownKeyDown(event);
434         break;
435       case "Tab":
436         this.maybeLockFocus(event);
437         break;
438       case "Escape":
439         this.maybeCancelScreenshots();
440         break;
441     }
442   }
444   /**
445    * Gets the accel key depending on the platform.
446    * metaKey for macOS. ctrlKey for Windows and Linux.
447    * @param {Event} event The keydown event
448    * @returns {Boolean} True if the accel key is pressed, false otherwise.
449    */
450   getAccelKey(event) {
451     if (AppConstants.platform === "macosx") {
452       return event.metaKey;
453     }
454     return event.ctrlKey;
455   }
457   /**
458    * Move the region or its left or right side to the left.
459    * Just the arrow key will move the region by 1px.
460    * Arrow key + shift will move the region by 10px.
461    * Arrow key + control/meta will move to the edge of the window.
462    * @param {Event} event The keydown event
463    */
464   handleArrowLeftKeyDown(event) {
465     switch (event.originalTarget.id) {
466       case "highlight":
467         if (this.getAccelKey(event)) {
468           let width = this.selectionRegion.width;
469           this.selectionRegion.left = this.windowDimensions.scrollX;
470           this.selectionRegion.right = this.windowDimensions.scrollX + width;
471           break;
472         }
474         this.selectionRegion.right -= 10 ** event.shiftKey;
475       // eslint-disable-next-line no-fallthrough
476       case "mover-topLeft":
477       case "mover-bottomLeft":
478         if (this.getAccelKey(event)) {
479           this.selectionRegion.left = this.windowDimensions.scrollX;
480           break;
481         }
483         this.selectionRegion.left -= 10 ** event.shiftKey;
484         this.scrollIfByEdge(
485           this.selectionRegion.left,
486           this.windowDimensions.scrollY + this.windowDimensions.clientHeight / 2
487         );
488         break;
489       case "mover-topRight":
490       case "mover-bottomRight":
491         if (this.getAccelKey(event)) {
492           let left = this.selectionRegion.left;
493           this.selectionRegion.left = this.windowDimensions.scrollX;
494           this.selectionRegion.right = left;
495           if (event.originalTarget.id === "mover-topRight") {
496             this.topLeftMover.focus();
497           } else if (event.originalTarget.id === "mover-bottomRight") {
498             this.bottomLeftMover.focus();
499           }
500           break;
501         }
503         this.selectionRegion.right -= 10 ** event.shiftKey;
504         if (this.selectionRegion.x1 >= this.selectionRegion.x2) {
505           this.selectionRegion.sortCoords();
506           if (event.originalTarget.id === "mover-topRight") {
507             this.topLeftMover.focus();
508           } else if (event.originalTarget.id === "mover-bottomRight") {
509             this.bottomLeftMover.focus();
510           }
511         }
512         break;
513       default:
514         return;
515     }
517     if (this.#state !== STATES.RESIZING) {
518       this.#setState(STATES.RESIZING);
519     }
521     event.preventDefault();
522     this.drawSelectionContainer();
523   }
525   /**
526    * Move the region or its top or bottom side upward.
527    * Just the arrow key will move the region by 1px.
528    * Arrow key + shift will move the region by 10px.
529    * Arrow key + control/meta will move to the edge of the window.
530    * @param {Event} event The keydown event
531    */
532   handleArrowUpKeyDown(event) {
533     switch (event.originalTarget.id) {
534       case "highlight":
535         if (this.getAccelKey(event)) {
536           let height = this.selectionRegion.height;
537           this.selectionRegion.top = this.windowDimensions.scrollY;
538           this.selectionRegion.bottom = this.windowDimensions.scrollY + height;
539           break;
540         }
542         this.selectionRegion.bottom -= 10 ** event.shiftKey;
543       // eslint-disable-next-line no-fallthrough
544       case "mover-topLeft":
545       case "mover-topRight":
546         if (this.getAccelKey(event)) {
547           this.selectionRegion.top = this.windowDimensions.scrollY;
548           break;
549         }
551         this.selectionRegion.top -= 10 ** event.shiftKey;
552         this.scrollIfByEdge(
553           this.windowDimensions.scrollX + this.windowDimensions.clientWidth / 2,
554           this.selectionRegion.top
555         );
556         break;
557       case "mover-bottomLeft":
558       case "mover-bottomRight":
559         if (this.getAccelKey(event)) {
560           let top = this.selectionRegion.top;
561           this.selectionRegion.top = this.windowDimensions.scrollY;
562           this.selectionRegion.bottom = top;
563           if (event.originalTarget.id === "mover-bottomLeft") {
564             this.topLeftMover.focus();
565           } else if (event.originalTarget.id === "mover-bottomRight") {
566             this.topRightMover.focus();
567           }
568           break;
569         }
571         this.selectionRegion.bottom -= 10 ** event.shiftKey;
572         if (this.selectionRegion.y1 >= this.selectionRegion.y2) {
573           this.selectionRegion.sortCoords();
574           if (event.originalTarget.id === "mover-bottomLeft") {
575             this.topLeftMover.focus();
576           } else if (event.originalTarget.id === "mover-bottomRight") {
577             this.topRightMover.focus();
578           }
579         }
580         break;
581       default:
582         return;
583     }
585     if (this.#state !== STATES.RESIZING) {
586       this.#setState(STATES.RESIZING);
587     }
589     event.preventDefault();
590     this.drawSelectionContainer();
591   }
593   /**
594    * Move the region or its left or right side to the right.
595    * Just the arrow key will move the region by 1px.
596    * Arrow key + shift will move the region by 10px.
597    * Arrow key + control/meta will move to the edge of the window.
598    * @param {Event} event The keydown event
599    */
600   handleArrowRightKeyDown(event) {
601     switch (event.originalTarget.id) {
602       case "highlight":
603         if (this.getAccelKey(event)) {
604           let width = this.selectionRegion.width;
605           let { scrollX, clientWidth } = this.windowDimensions.dimensions;
606           this.selectionRegion.right = scrollX + clientWidth;
607           this.selectionRegion.left = this.selectionRegion.right - width;
608           break;
609         }
611         this.selectionRegion.left += 10 ** event.shiftKey;
612       // eslint-disable-next-line no-fallthrough
613       case "mover-topRight":
614       case "mover-bottomRight":
615         if (this.getAccelKey(event)) {
616           this.selectionRegion.right =
617             this.windowDimensions.scrollX + this.windowDimensions.clientWidth;
618           break;
619         }
621         this.selectionRegion.right += 10 ** event.shiftKey;
622         this.scrollIfByEdge(
623           this.selectionRegion.right,
624           this.windowDimensions.scrollY + this.windowDimensions.clientHeight / 2
625         );
626         break;
627       case "mover-topLeft":
628       case "mover-bottomLeft":
629         if (this.getAccelKey(event)) {
630           let right = this.selectionRegion.right;
631           this.selectionRegion.right =
632             this.windowDimensions.scrollX + this.windowDimensions.clientWidth;
633           this.selectionRegion.left = right;
634           if (event.originalTarget.id === "mover-topLeft") {
635             this.topRightMover.focus();
636           } else if (event.originalTarget.id === "mover-bottomLeft") {
637             this.bottomRightMover.focus();
638           }
639           break;
640         }
642         this.selectionRegion.left += 10 ** event.shiftKey;
643         if (this.selectionRegion.x1 >= this.selectionRegion.x2) {
644           this.selectionRegion.sortCoords();
645           if (event.originalTarget.id === "mover-topLeft") {
646             this.topRightMover.focus();
647           } else if (event.originalTarget.id === "mover-bottomLeft") {
648             this.bottomRightMover.focus();
649           }
650         }
651         break;
652       default:
653         return;
654     }
656     if (this.#state !== STATES.RESIZING) {
657       this.#setState(STATES.RESIZING);
658     }
660     event.preventDefault();
661     this.drawSelectionContainer();
662   }
664   /**
665    * Move the region or its top or bottom side downward.
666    * Just the arrow key will move the region by 1px.
667    * Arrow key + shift will move the region by 10px.
668    * Arrow key + control/meta will move to the edge of the window.
669    * @param {Event} event The keydown event
670    */
671   handleArrowDownKeyDown(event) {
672     switch (event.originalTarget.id) {
673       case "highlight":
674         if (this.getAccelKey(event)) {
675           let height = this.selectionRegion.height;
676           let { scrollY, clientHeight } = this.windowDimensions.dimensions;
677           this.selectionRegion.bottom = scrollY + clientHeight;
678           this.selectionRegion.top = this.selectionRegion.bottom - height;
679           break;
680         }
682         this.selectionRegion.top += 10 ** event.shiftKey;
683       // eslint-disable-next-line no-fallthrough
684       case "mover-bottomLeft":
685       case "mover-bottomRight":
686         if (this.getAccelKey(event)) {
687           this.selectionRegion.bottom =
688             this.windowDimensions.scrollY + this.windowDimensions.clientHeight;
689           break;
690         }
692         this.selectionRegion.bottom += 10 ** event.shiftKey;
693         this.scrollIfByEdge(
694           this.windowDimensions.scrollX + this.windowDimensions.clientWidth / 2,
695           this.selectionRegion.bottom
696         );
697         break;
698       case "mover-topLeft":
699       case "mover-topRight":
700         if (this.getAccelKey(event)) {
701           let bottom = this.selectionRegion.bottom;
702           this.selectionRegion.bottom =
703             this.windowDimensions.scrollY + this.windowDimensions.clientHeight;
704           this.selectionRegion.top = bottom;
705           if (event.originalTarget.id === "mover-topLeft") {
706             this.bottomLeftMover.focus();
707           } else if (event.originalTarget.id === "mover-topRight") {
708             this.bottomRightMover.focus();
709           }
710           break;
711         }
713         this.selectionRegion.top += 10 ** event.shiftKey;
714         if (this.selectionRegion.y1 >= this.selectionRegion.y2) {
715           this.selectionRegion.sortCoords();
716           if (event.originalTarget.id === "mover-topLeft") {
717             this.bottomLeftMover.focus();
718           } else if (event.originalTarget.id === "mover-topRight") {
719             this.bottomRightMover.focus();
720           }
721         }
722         break;
723       default:
724         return;
725     }
727     if (this.#state !== STATES.RESIZING) {
728       this.#setState(STATES.RESIZING);
729     }
731     event.preventDefault();
732     this.drawSelectionContainer();
733   }
735   /**
736    * We lock focus to the overlay when a region is selected.
737    * Can still escape with shift + F6.
738    * @param {Event} event The keydown event
739    */
740   maybeLockFocus(event) {
741     if (this.#state !== STATES.SELECTED) {
742       return;
743     }
745     event.preventDefault();
746     if (event.originalTarget.id === "highlight" && event.shiftKey) {
747       this.downloadButton.focus();
748     } else if (event.originalTarget.id === "download" && !event.shiftKey) {
749       this.highlightEl.focus();
750     } else {
751       // The content document can listen for keydown events and prevent moving
752       // focus so we manually move focus to the next element here.
753       let direction = event.shiftKey
754         ? Services.focus.MOVEFOCUS_BACKWARD
755         : Services.focus.MOVEFOCUS_FORWARD;
756       Services.focus.moveFocus(this.window, null, direction, 0);
757     }
758   }
760   /**
761    * Handles when a keydown occurs in the screenshots component.
762    * All we need to do on keyup is set the state to selected.
763    * @param {Event} event The keydown event
764    */
765   handleKeyUp(event) {
766     switch (event.key) {
767       case "ArrowLeft":
768       case "ArrowUp":
769       case "ArrowRight":
770       case "ArrowDown":
771         switch (event.originalTarget.id) {
772           case "highlight":
773           case "mover-bottomLeft":
774           case "mover-bottomRight":
775           case "mover-topLeft":
776           case "mover-topRight":
777             event.preventDefault();
778             this.#setState(STATES.SELECTED);
779             break;
780         }
781         break;
782     }
783   }
785   /**
786    * Dispatch a custom event to the ScreenshotsComponentChild actor
787    * @param {String} eventType The name of the event
788    * @param {object} detail Extra details to send to the child actor
789    */
790   #dispatchEvent(eventType, detail) {
791     this.window.dispatchEvent(
792       new CustomEvent(eventType, {
793         bubbles: true,
794         detail,
795       })
796     );
797   }
799   /**
800    * Set a new state for the overlay
801    * @param {String} newState
802    */
803   #setState(newState) {
804     if (this.#state === STATES.SELECTED && newState === STATES.CROSSHAIRS) {
805       this.#dispatchEvent("Screenshots:RecordEvent", {
806         eventName: "started",
807         reason: "overlay_retry",
808       });
809     }
810     if (newState !== this.#state) {
811       this.#dispatchEvent("Screenshots:OverlaySelection", {
812         hasSelection: newState == STATES.SELECTED,
813       });
814     }
815     this.#state = newState;
817     switch (this.#state) {
818       case STATES.CROSSHAIRS: {
819         this.crosshairsStart();
820         break;
821       }
822       case STATES.DRAGGING_READY: {
823         this.draggingReadyStart();
824         break;
825       }
826       case STATES.DRAGGING: {
827         this.draggingStart();
828         break;
829       }
830       case STATES.SELECTED: {
831         this.selectedStart();
832         break;
833       }
834       case STATES.RESIZING: {
835         this.resizingStart();
836         break;
837       }
838     }
839   }
841   /**
842    * Hide hover element, selection and buttons containers.
843    * Show the preview container and the panel.
844    * This is the initial state of the overlay.
845    */
846   crosshairsStart() {
847     this.hideHoverElementContainer();
848     this.hideSelectionContainer();
849     this.hideButtonsContainer();
850     this.showPreviewContainer();
851     this.#dispatchEvent("Screenshots:ShowPanel");
852     this.#previousDimensions = null;
853     this.#cachedEle = null;
854     this.hoverElementRegion.resetDimensions();
855   }
857   /**
858    * Hide the panel because we have started dragging.
859    */
860   draggingReadyStart() {
861     this.#dispatchEvent("Screenshots:HidePanel");
862   }
864   /**
865    * Hide the preview, hover element and buttons containers.
866    * Show the selection container.
867    */
868   draggingStart() {
869     this.hidePreviewContainer();
870     this.hideButtonsContainer();
871     this.hideHoverElementContainer();
872     this.drawSelectionContainer();
873   }
875   /**
876    * Hide the preview and hover element containers.
877    * Draw the selection and buttons containers.
878    */
879   selectedStart() {
880     this.hidePreviewContainer();
881     this.hideHoverElementContainer();
882     this.drawSelectionContainer();
883     this.drawButtonsContainer();
884   }
886   /**
887    * Hide the buttons container.
888    * Store the width and height of the current selected region.
889    * The dimensions will be used when moving the region along the edge of the
890    * page and for recording telemetry.
891    */
892   resizingStart() {
893     this.hideButtonsContainer();
894     let { width, height } = this.selectionRegion.dimensions;
895     this.#previousDimensions = { width, height };
896   }
898   /**
899    * Dragging has started so we set the initial selection region and set the
900    * state to draggingReady.
901    * @param {Number} pageX The x position relative to the page
902    * @param {Number} pageY The y position relative to the page
903    */
904   crosshairsDragStart(pageX, pageY) {
905     this.selectionRegion.dimensions = {
906       left: pageX,
907       top: pageY,
908       right: pageX,
909       bottom: pageY,
910     };
912     this.#setState(STATES.DRAGGING_READY);
913   }
915   /**
916    * If the background is clicked we set the state to crosshairs
917    * otherwise set the state to resizing
918    * @param {Number} pageX The x position relative to the page
919    * @param {Number} pageY The y position relative to the page
920    * @param {String} targetId The id of the event target
921    */
922   selectedDragStart(pageX, pageY, targetId) {
923     if (targetId === this.screenshotsContainer.id) {
924       this.#setState(STATES.CROSSHAIRS);
925       return;
926     }
927     this.#moverId = targetId;
928     this.#lastPageX = pageX;
929     this.#lastPageY = pageY;
931     this.#setState(STATES.RESIZING);
932   }
934   /**
935    * Draw the eyes in the preview container and find the element currently
936    * being hovered.
937    * @param {Number} clientX The x position relative to the viewport
938    * @param {Number} clientY The y position relative to the viewport
939    */
940   crosshairsMove(clientX, clientY) {
941     this.drawPreviewEyes(clientX, clientY);
943     this.handleElementHover(clientX, clientY);
944   }
946   /**
947    * Set the selection region dimensions and if the region is at least 40
948    * pixels diagnally in distance, set the state to dragging.
949    * @param {Number} pageX The x position relative to the page
950    * @param {Number} pageY The y position relative to the page
951    */
952   draggingReadyDrag(pageX, pageY) {
953     this.selectionRegion.dimensions = {
954       right: pageX,
955       bottom: pageY,
956     };
958     if (this.selectionRegion.distance > 40) {
959       this.#setState(STATES.DRAGGING);
960     }
961   }
963   /**
964    * Scroll if along the edge of the viewport, update the selection region
965    * dimensions and draw the selection container.
966    * @param {Number} pageX The x position relative to the page
967    * @param {Number} pageY The y position relative to the page
968    */
969   draggingDrag(pageX, pageY) {
970     this.scrollIfByEdge(pageX, pageY);
971     this.selectionRegion.dimensions = {
972       right: pageX,
973       bottom: pageY,
974     };
976     this.drawSelectionContainer();
977   }
979   /**
980    * Resize the selection region depending on the mover that started the resize.
981    * @param {Number} pageX The x position relative to the page
982    * @param {Number} pageY The y position relative to the page
983    */
984   resizingDrag(pageX, pageY) {
985     this.scrollIfByEdge(pageX, pageY);
986     switch (this.#moverId) {
987       case "mover-topLeft": {
988         this.selectionRegion.dimensions = {
989           left: pageX,
990           top: pageY,
991         };
992         break;
993       }
994       case "mover-top": {
995         this.selectionRegion.dimensions = { top: pageY };
996         break;
997       }
998       case "mover-topRight": {
999         this.selectionRegion.dimensions = {
1000           top: pageY,
1001           right: pageX,
1002         };
1003         break;
1004       }
1005       case "mover-right": {
1006         this.selectionRegion.dimensions = {
1007           right: pageX,
1008         };
1009         break;
1010       }
1011       case "mover-bottomRight": {
1012         this.selectionRegion.dimensions = {
1013           right: pageX,
1014           bottom: pageY,
1015         };
1016         break;
1017       }
1018       case "mover-bottom": {
1019         this.selectionRegion.dimensions = {
1020           bottom: pageY,
1021         };
1022         break;
1023       }
1024       case "mover-bottomLeft": {
1025         this.selectionRegion.dimensions = {
1026           left: pageX,
1027           bottom: pageY,
1028         };
1029         break;
1030       }
1031       case "mover-left": {
1032         this.selectionRegion.dimensions = { left: pageX };
1033         break;
1034       }
1035       case "highlight": {
1036         let diffX = this.#lastPageX - pageX;
1037         let diffY = this.#lastPageY - pageY;
1039         let newLeft;
1040         let newRight;
1041         let newTop;
1042         let newBottom;
1044         // Unpack dimensions to use here
1045         let {
1046           left: boxLeft,
1047           top: boxTop,
1048           right: boxRight,
1049           bottom: boxBottom,
1050           width: boxWidth,
1051           height: boxHeight,
1052         } = this.selectionRegion.dimensions;
1053         let { scrollWidth, scrollHeight } = this.windowDimensions.dimensions;
1055         // wait until all 4 if elses have completed before setting box dimensions
1056         if (boxWidth <= this.#previousDimensions.width && boxLeft === 0) {
1057           newLeft = boxRight - this.#previousDimensions.width;
1058         } else {
1059           newLeft = boxLeft;
1060         }
1062         if (
1063           boxWidth <= this.#previousDimensions.width &&
1064           boxRight === scrollWidth
1065         ) {
1066           newRight = boxLeft + this.#previousDimensions.width;
1067         } else {
1068           newRight = boxRight;
1069         }
1071         if (boxHeight <= this.#previousDimensions.height && boxTop === 0) {
1072           newTop = boxBottom - this.#previousDimensions.height;
1073         } else {
1074           newTop = boxTop;
1075         }
1077         if (
1078           boxHeight <= this.#previousDimensions.height &&
1079           boxBottom === scrollHeight
1080         ) {
1081           newBottom = boxTop + this.#previousDimensions.height;
1082         } else {
1083           newBottom = boxBottom;
1084         }
1086         this.selectionRegion.dimensions = {
1087           left: newLeft - diffX,
1088           top: newTop - diffY,
1089           right: newRight - diffX,
1090           bottom: newBottom - diffY,
1091         };
1093         this.#lastPageX = pageX;
1094         this.#lastPageY = pageY;
1095         break;
1096       }
1097     }
1098     this.drawSelectionContainer();
1099   }
1101   /**
1102    * If there is a valid element region, update and draw the selection
1103    * container and set the state to selected.
1104    * Otherwise set the state to crosshairs.
1105    */
1106   draggingReadyDragEnd() {
1107     if (this.hoverElementRegion.isRegionValid) {
1108       this.selectionRegion.dimensions = this.hoverElementRegion.dimensions;
1109       this.#setState(STATES.SELECTED);
1110       this.downloadButton.focus();
1111       this.#dispatchEvent("Screenshots:RecordEvent", {
1112         eventName: "selected",
1113         reason: "element",
1114       });
1115       this.#methodsUsed.element += 1;
1116     } else {
1117       this.#setState(STATES.CROSSHAIRS);
1118     }
1119   }
1121   /**
1122    * Update the selection region dimensions and set the state to selected.
1123    * @param {Number} pageX The x position relative to the page
1124    * @param {Number} pageY The y position relative to the page
1125    */
1126   draggingDragEnd(pageX, pageY) {
1127     this.selectionRegion.dimensions = {
1128       right: pageX,
1129       bottom: pageY,
1130     };
1131     this.selectionRegion.sortCoords();
1132     this.#setState(STATES.SELECTED);
1133     this.maybeRecordRegionSelected();
1134     this.#methodsUsed.region += 1;
1135     this.downloadButton.focus();
1136   }
1138   /**
1139    * Update the selection region dimensions by calling `resizingDrag` and set
1140    * the state to selected.
1141    * @param {Number} pageX The x position relative to the page
1142    * @param {Number} pageY The y position relative to the page
1143    */
1144   resizingDragEnd(pageX, pageY) {
1145     this.resizingDrag(pageX, pageY);
1146     this.selectionRegion.sortCoords();
1147     this.#setState(STATES.SELECTED);
1148     this.maybeRecordRegionSelected();
1149     if (this.#moverId === "highlight") {
1150       this.#methodsUsed.move += 1;
1151     } else {
1152       this.#methodsUsed.resize += 1;
1153     }
1154   }
1156   maybeRecordRegionSelected() {
1157     let { width, height } = this.selectionRegion.dimensions;
1159     if (
1160       !this.#previousDimensions ||
1161       (Math.abs(this.#previousDimensions.width - width) >
1162         REGION_CHANGE_THRESHOLD &&
1163         Math.abs(this.#previousDimensions.height - height) >
1164           REGION_CHANGE_THRESHOLD)
1165     ) {
1166       this.#dispatchEvent("Screenshots:RecordEvent", {
1167         eventName: "selected",
1168         reason: "region_selection",
1169       });
1170     }
1171     this.#previousDimensions = { width, height };
1172   }
1174   /**
1175    * Draw the preview eyes pointer towards the mouse.
1176    * @param {Number} clientX The x position relative to the viewport
1177    * @param {Number} clientY The y position relative to the viewport
1178    */
1179   drawPreviewEyes(clientX, clientY) {
1180     let { clientWidth, clientHeight } = this.windowDimensions.dimensions;
1181     const xpos = Math.floor((10 * (clientX - clientWidth / 2)) / clientWidth);
1182     const ypos = Math.floor((10 * (clientY - clientHeight / 2)) / clientHeight);
1183     const move = `transform:translate(${xpos}px, ${ypos}px);`;
1184     this.leftEye.style = move;
1185     this.rightEye.style = move;
1186   }
1188   showPreviewContainer() {
1189     this.previewContainer.hidden = false;
1190   }
1192   hidePreviewContainer() {
1193     this.previewContainer.hidden = true;
1194   }
1196   updatePreviewContainer() {
1197     let { clientWidth, clientHeight } = this.windowDimensions.dimensions;
1198     this.previewContainer.style.width = `${clientWidth}px`;
1199     this.previewContainer.style.height = `${clientHeight}px`;
1200   }
1202   /**
1203    * Update the screenshots overlay container based on the window dimensions.
1204    */
1205   updateScreenshotsOverlayContainer() {
1206     let { scrollWidth, scrollHeight } = this.windowDimensions.dimensions;
1207     this.screenshotsContainer.style = `width:${scrollWidth}px;height:${scrollHeight}px;`;
1208   }
1210   showScreenshotsOverlayContainer() {
1211     this.screenshotsContainer.hidden = false;
1212   }
1214   hideScreenshotsOverlayContainer() {
1215     this.screenshotsContainer.hidden = true;
1216   }
1218   /**
1219    * Draw the hover element container based on the hover element region.
1220    */
1221   drawHoverElementRegion() {
1222     this.showHoverElementContainer();
1224     let { top, left, width, height } = this.hoverElementRegion.dimensions;
1226     this.hoverElementContainer.style = `top:${top}px;left:${left}px;width:${width}px;height:${height}px;`;
1227   }
1229   showHoverElementContainer() {
1230     this.hoverElementContainer.hidden = false;
1231   }
1233   hideHoverElementContainer() {
1234     this.hoverElementContainer.hidden = true;
1235   }
1237   /**
1238    * Draw each background element and the highlight element base on the
1239    * selection region.
1240    */
1241   drawSelectionContainer() {
1242     this.showSelectionContainer();
1244     let { top, left, right, bottom, width, height } =
1245       this.selectionRegion.dimensions;
1247     this.highlightEl.style = `top:${top}px;left:${left}px;width:${width}px;height:${height}px;`;
1249     this.leftBackgroundEl.style = `top:${top}px;width:${left}px;height:${height}px;`;
1250     this.topBackgroundEl.style.height = `${top}px`;
1251     this.rightBackgroundEl.style = `top:${top}px;left:${right}px;width:calc(100% - ${right}px);height:${height}px;`;
1252     this.bottomBackgroundEl.style = `top:${bottom}px;height:calc(100% - ${bottom}px);`;
1254     this.updateSelectionSizeText();
1255   }
1257   updateSelectionSizeText() {
1258     let dpr = this.windowDimensions.devicePixelRatio;
1259     let { width, height } = this.selectionRegion.dimensions;
1261     let [selectionSizeTranslation] =
1262       lazy.overlayLocalization.formatMessagesSync([
1263         {
1264           id: "screenshots-overlay-selection-region-size",
1265           args: {
1266             width: Math.floor(width * dpr),
1267             height: Math.floor(height * dpr),
1268           },
1269         },
1270       ]);
1271     this.selectionSize.textContent = selectionSizeTranslation.value;
1272   }
1274   showSelectionContainer() {
1275     this.selectionContainer.hidden = false;
1276   }
1278   hideSelectionContainer() {
1279     this.selectionContainer.hidden = true;
1280   }
1282   /**
1283    * Draw the buttons container in the bottom right corner of the selection
1284    * container if possible.
1285    * The buttons will be visible in the viewport if the selection container
1286    * is within the viewport, otherwise skip drawing the buttons.
1287    */
1288   drawButtonsContainer() {
1289     this.showButtonsContainer();
1291     let {
1292       left: boxLeft,
1293       top: boxTop,
1294       right: boxRight,
1295       bottom: boxBottom,
1296     } = this.selectionRegion.dimensions;
1297     let { clientWidth, clientHeight, scrollX, scrollY } =
1298       this.windowDimensions.dimensions;
1300     if (
1301       boxTop > scrollY + clientHeight ||
1302       boxBottom < scrollY ||
1303       boxLeft > scrollX + clientWidth ||
1304       boxRight < scrollX
1305     ) {
1306       // The box is offscreen so need to draw the buttons
1307       return;
1308     }
1310     let top = boxBottom;
1312     if (scrollY + clientHeight - boxBottom < 70) {
1313       if (boxBottom < scrollY + clientHeight) {
1314         top = boxBottom - 60;
1315       } else if (scrollY + clientHeight - boxTop < 70) {
1316         top = boxTop - 60;
1317       } else {
1318         top = scrollY + clientHeight - 60;
1319       }
1320     }
1322     if (boxRight < 300) {
1323       this.buttonsContainer.style.left = `${boxLeft}px`;
1324       this.buttonsContainer.style.right = "";
1325     } else {
1326       this.buttonsContainer.style.right = `calc(100% - ${boxRight}px)`;
1327       this.buttonsContainer.style.left = "";
1328     }
1330     this.buttonsContainer.style.top = `${top}px`;
1331   }
1333   showButtonsContainer() {
1334     this.buttonsContainer.hidden = false;
1335   }
1337   hideButtonsContainer() {
1338     this.buttonsContainer.hidden = true;
1339   }
1341   /**
1342    * Set the pointer events to none on the screenshots elements so
1343    * elementFromPoint can find the real element at the given point.
1344    */
1345   setPointerEventsNone() {
1346     this.screenshotsContainer.style.pointerEvents = "none";
1347   }
1349   resetPointerEvents() {
1350     this.screenshotsContainer.style.pointerEvents = "";
1351   }
1353   /**
1354    * Try to find a reasonable element for a given point.
1355    * If a reasonable element is found, draw the hover element container for
1356    * that element region.
1357    * @param {Number} clientX The x position relative to the viewport
1358    * @param {Number} clientY The y position relative to the viewport
1359    */
1360   handleElementHover(clientX, clientY) {
1361     this.setPointerEventsNone();
1362     let ele = this.document.elementFromPoint(clientX, clientY);
1363     this.resetPointerEvents();
1365     if (this.#cachedEle && this.#cachedEle === ele) {
1366       // Still hovering over the same element
1367       return;
1368     }
1369     this.#cachedEle = ele;
1371     let rect = getBestRectForElement(ele, this.document);
1372     if (rect) {
1373       let { scrollX, scrollY } = this.windowDimensions.dimensions;
1374       let { left, top, right, bottom } = rect;
1375       let newRect = {
1376         left: left + scrollX,
1377         top: top + scrollY,
1378         right: right + scrollX,
1379         bottom: bottom + scrollY,
1380       };
1381       this.hoverElementRegion.dimensions = newRect;
1382       this.drawHoverElementRegion();
1383     } else {
1384       this.hoverElementRegion.resetDimensions();
1385       this.hideHoverElementContainer();
1386     }
1387   }
1389   /**
1390    * Scroll the viewport if near one or both of the edges.
1391    * @param {Number} pageX The x position relative to the page
1392    * @param {Number} pageY The y position relative to the page
1393    */
1394   scrollIfByEdge(pageX, pageY) {
1395     let { scrollX, scrollY, clientWidth, clientHeight } =
1396       this.windowDimensions.dimensions;
1398     if (pageY - scrollY < SCROLL_BY_EDGE) {
1399       // Scroll up
1400       this.scrollWindow(0, -(SCROLL_BY_EDGE - (pageY - scrollY)));
1401     } else if (scrollY + clientHeight - pageY < SCROLL_BY_EDGE) {
1402       // Scroll down
1403       this.scrollWindow(0, SCROLL_BY_EDGE - (scrollY + clientHeight - pageY));
1404     }
1406     if (pageX - scrollX <= SCROLL_BY_EDGE) {
1407       // Scroll left
1408       this.scrollWindow(-(SCROLL_BY_EDGE - (pageX - scrollX)), 0);
1409     } else if (scrollX + clientWidth - pageX <= SCROLL_BY_EDGE) {
1410       // Scroll right
1411       this.scrollWindow(SCROLL_BY_EDGE - (scrollX + clientWidth - pageX), 0);
1412     }
1413   }
1415   /**
1416    * Scroll the window by the given amount.
1417    * @param {Number} x The x amount to scroll
1418    * @param {Number} y The y amount to scroll
1419    */
1420   scrollWindow(x, y) {
1421     this.window.scrollBy(x, y);
1422     this.updateScreenshotsOverlayDimensions("scroll");
1423   }
1425   /**
1426    * The page was resized or scrolled. We need to update the screenshots
1427    * container size so we don't draw outside the page bounds.
1428    * @param {String} eventType will be "scroll" or "resize"
1429    */
1430   updateScreenshotsOverlayDimensions(eventType) {
1431     this.updateWindowDimensions();
1433     if (this.#state === STATES.CROSSHAIRS) {
1434       if (eventType === "resize") {
1435         this.hideHoverElementContainer();
1436         this.updatePreviewContainer();
1437       } else if (eventType === "scroll") {
1438         if (this.#lastClientX && this.#lastClientY) {
1439           this.#cachedEle = null;
1440           this.handleElementHover(this.#lastClientX, this.#lastClientY);
1441         }
1442       }
1443     } else if (this.#state === STATES.SELECTED) {
1444       this.drawButtonsContainer();
1445       this.updateSelectionSizeText();
1446     }
1447   }
1449   /**
1450    * Returns the window's dimensions for the current window.
1451    *
1452    * @return {Object} An object containing window dimensions
1453    *   {
1454    *     clientWidth: The width of the viewport
1455    *     clientHeight: The height of the viewport
1456    *     scrollWidth: The width of the enitre page
1457    *     scrollHeight: The height of the entire page
1458    *     scrollX: The X scroll offset of the viewport
1459    *     scrollY: The Y scroll offest of the viewport
1460    *     scrollMinX: The X mininmun the viewport can scroll to
1461    *     scrollMinY: The Y mininmun the viewport can scroll to
1462    *   }
1463    */
1464   getDimensionsFromWindow() {
1465     let {
1466       innerHeight,
1467       innerWidth,
1468       scrollMaxY,
1469       scrollMaxX,
1470       scrollMinY,
1471       scrollMinX,
1472       scrollY,
1473       scrollX,
1474     } = this.window;
1476     let scrollWidth = innerWidth + scrollMaxX - scrollMinX;
1477     let scrollHeight = innerHeight + scrollMaxY - scrollMinY;
1478     let clientHeight = innerHeight;
1479     let clientWidth = innerWidth;
1481     const scrollbarHeight = {};
1482     const scrollbarWidth = {};
1483     this.window.windowUtils.getScrollbarSize(
1484       false,
1485       scrollbarWidth,
1486       scrollbarHeight
1487     );
1488     scrollWidth -= scrollbarWidth.value;
1489     scrollHeight -= scrollbarHeight.value;
1490     clientWidth -= scrollbarWidth.value;
1491     clientHeight -= scrollbarHeight.value;
1493     return {
1494       clientWidth,
1495       clientHeight,
1496       scrollWidth,
1497       scrollHeight,
1498       scrollX,
1499       scrollY,
1500       scrollMinX,
1501       scrollMinY,
1502     };
1503   }
1505   /**
1506    * Update the screenshots container because the window has changed size of
1507    * scrolled. The screenshots-overlay-container doesn't shrink with the page
1508    * when the window is resized so we have to manually find the width and
1509    * height of the page and make sure we aren't drawing outside the actual page
1510    * dimensions.
1511    */
1512   updateWindowDimensions() {
1513     let {
1514       clientWidth,
1515       clientHeight,
1516       scrollWidth,
1517       scrollHeight,
1518       scrollX,
1519       scrollY,
1520       scrollMinX,
1521       scrollMinY,
1522     } = this.getDimensionsFromWindow();
1524     let shouldUpdate = true;
1526     if (
1527       clientHeight < this.windowDimensions.clientHeight ||
1528       clientWidth < this.windowDimensions.clientWidth
1529     ) {
1530       let widthDiff = this.windowDimensions.clientWidth - clientWidth;
1531       let heightDiff = this.windowDimensions.clientHeight - clientHeight;
1533       this.windowDimensions.dimensions = {
1534         scrollWidth: scrollWidth - Math.max(widthDiff, 0),
1535         scrollHeight: scrollHeight - Math.max(heightDiff, 0),
1536         clientWidth,
1537         clientHeight,
1538       };
1540       if (this.#state === STATES.SELECTED) {
1541         let didShift = this.selectionRegion.shift();
1542         if (didShift) {
1543           this.drawSelectionContainer();
1544           this.drawButtonsContainer();
1545         }
1546       } else if (this.#state === STATES.CROSSHAIRS) {
1547         this.updatePreviewContainer();
1548       }
1549       this.updateScreenshotsOverlayContainer();
1550       // We just updated the screenshots container so we check if the window
1551       // dimensions are still accurate
1552       let { scrollWidth: updatedWidth, scrollHeight: updatedHeight } =
1553         this.getDimensionsFromWindow();
1555       // If the width and height are the same then we don't need to draw the overlay again
1556       if (updatedWidth === scrollWidth && updatedHeight === scrollHeight) {
1557         shouldUpdate = false;
1558       }
1560       scrollWidth = updatedWidth;
1561       scrollHeight = updatedHeight;
1562     }
1564     setMaxDetectHeight(Math.max(clientHeight + 100, 700));
1565     setMaxDetectWidth(Math.max(clientWidth + 100, 1000));
1567     this.windowDimensions.dimensions = {
1568       clientWidth,
1569       clientHeight,
1570       scrollWidth,
1571       scrollHeight,
1572       scrollX,
1573       scrollY,
1574       scrollMinX,
1575       scrollMinY,
1576       devicePixelRatio: this.window.devicePixelRatio,
1577     };
1579     if (shouldUpdate) {
1580       this.updatePreviewContainer();
1581       this.updateScreenshotsOverlayContainer();
1582     }
1583   }