Bug 1933479 - Add tab close button on hover to vertical tabs when sidebar is collapse...
[gecko.git] / toolkit / modules / HiddenFrame.sys.mjs
blob3edbca1314475ec725cb007958a6680f1c825727
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 file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /**
6  * This module contains `HiddenFrame`, a class which creates a windowless browser,
7  * and `HiddenBrowserManager` which is a singleton that can be used to manage
8  * creating and using multiple hidden frames.
9  */
11 const XUL_PAGE = Services.io.newURI("chrome://global/content/win.xhtml");
13 const gAllHiddenFrames = new Set();
15 // The screen sizes to use for the background browser created by
16 // `HiddenBrowserManager`.
17 const BACKGROUND_WIDTH = 1024;
18 const BACKGROUND_HEIGHT = 768;
20 let cleanupRegistered = false;
21 function ensureCleanupRegistered() {
22   if (!cleanupRegistered) {
23     cleanupRegistered = true;
24     Services.obs.addObserver(function () {
25       for (let hiddenFrame of gAllHiddenFrames) {
26         hiddenFrame.destroy();
27       }
28     }, "xpcom-shutdown");
29   }
32 /**
33  * A hidden frame class. It takes care of creating a windowless browser and
34  * passing the window containing a blank XUL <window> back.
35  */
36 export class HiddenFrame {
37   #frame = null;
38   #browser = null;
39   #listener = null;
40   #webProgress = null;
41   #deferred = null;
43   /**
44    * Gets the |contentWindow| of the hidden frame. Creates the frame if needed.
45    *
46    * @returns {Promise} Returns a promise which is resolved when the hidden frame has finished
47    *          loading.
48    */
49   get() {
50     if (!this.#deferred) {
51       this.#deferred = Promise.withResolvers();
52       this.#create();
53     }
55     return this.#deferred.promise;
56   }
58   /**
59    * Fetch a sync ref to the window inside the frame (needed for the add-on SDK).
60    *
61    * @returns {DOMWindow}
62    */
63   getWindow() {
64     this.get();
65     return this.#browser.document.ownerGlobal;
66   }
68   /**
69    * Destroys the browser, freeing resources.
70    */
71   destroy() {
72     if (this.#browser) {
73       if (this.#listener) {
74         this.#webProgress.removeProgressListener(this.#listener);
75         this.#listener = null;
76         this.#webProgress = null;
77       }
78       this.#frame = null;
79       this.#deferred = null;
81       gAllHiddenFrames.delete(this);
82       this.#browser.close();
83       this.#browser = null;
84     }
85   }
87   #create() {
88     ensureCleanupRegistered();
89     let chromeFlags = Ci.nsIWebBrowserChrome.CHROME_REMOTE_WINDOW;
90     if (Services.appinfo.fissionAutostart) {
91       chromeFlags |= Ci.nsIWebBrowserChrome.CHROME_FISSION_WINDOW;
92     }
93     this.#browser = Services.appShell.createWindowlessBrowser(
94       true,
95       chromeFlags
96     );
97     this.#browser.QueryInterface(Ci.nsIInterfaceRequestor);
98     gAllHiddenFrames.add(this);
99     this.#webProgress = this.#browser.getInterface(Ci.nsIWebProgress);
100     this.#listener = {
101       QueryInterface: ChromeUtils.generateQI([
102         "nsIWebProgressListener",
103         "nsIWebProgressListener2",
104         "nsISupportsWeakReference",
105       ]),
106     };
107     this.#listener.onStateChange = (wbp, request, stateFlags) => {
108       if (!request) {
109         return;
110       }
111       if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
112         this.#webProgress.removeProgressListener(this.#listener);
113         this.#listener = null;
114         this.#webProgress = null;
115         // Get the window reference via the document.
116         this.#frame = this.#browser.document.ownerGlobal;
117         this.#deferred.resolve(this.#frame);
118       }
119     };
120     this.#webProgress.addProgressListener(
121       this.#listener,
122       Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT
123     );
124     let docShell = this.#browser.docShell;
125     let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
126     docShell.createAboutBlankDocumentViewer(systemPrincipal, systemPrincipal);
127     let browsingContext = this.#browser.browsingContext;
128     browsingContext.useGlobalHistory = false;
129     let loadURIOptions = {
130       triggeringPrincipal: systemPrincipal,
131     };
132     this.#browser.loadURI(XUL_PAGE, loadURIOptions);
133   }
137  * A manager for hidden browsers. Responsible for creating and destroying a
138  * hidden frame to hold them.
139  */
140 export const HiddenBrowserManager = new (class HiddenBrowserManager {
141   /**
142    * The hidden frame if one has been created.
143    *
144    * @type {HiddenFrame | null}
145    */
146   #frame = null;
147   /**
148    * The number of hidden browser elements currently in use.
149    *
150    * @type {number}
151    */
152   #browsers = 0;
154   /**
155    * Creates and returns a new hidden browser.
156    *
157    * @returns {Browser}
158    */
159   async #acquireBrowser() {
160     this.#browsers++;
161     if (!this.#frame) {
162       this.#frame = new HiddenFrame();
163     }
165     let frame = await this.#frame.get();
166     let doc = frame.document;
167     let browser = doc.createXULElement("browser");
168     browser.setAttribute("remote", "true");
169     browser.setAttribute("type", "content");
170     browser.setAttribute(
171       "style",
172       `
173         width: ${BACKGROUND_WIDTH}px;
174         min-width: ${BACKGROUND_WIDTH}px;
175         height: ${BACKGROUND_HEIGHT}px;
176         min-height: ${BACKGROUND_HEIGHT}px;
177       `
178     );
179     browser.setAttribute("maychangeremoteness", "true");
180     doc.documentElement.appendChild(browser);
182     return browser;
183   }
185   /**
186    * Releases the given hidden browser.
187    *
188    * @param {Browser} browser
189    *   The hidden browser element.
190    */
191   #releaseBrowser(browser) {
192     browser.remove();
194     this.#browsers--;
195     if (this.#browsers == 0) {
196       this.#frame.destroy();
197       this.#frame = null;
198     }
199   }
201   /**
202    * Calls a callback function with a new hidden browser.
203    * This function will return whatever the callback function returns.
204    *
205    * @param {Callback} callback
206    *   The callback function will be called with the browser element and may
207    *   be asynchronous.
208    * @returns {T}
209    */
210   async withHiddenBrowser(callback) {
211     let browser = await this.#acquireBrowser();
212     try {
213       return await callback(browser);
214     } finally {
215       this.#releaseBrowser(browser);
216     }
217   }
218 })();