diff --git a/src/zen/common/styles/zen-browser-container.css b/src/zen/common/styles/zen-browser-container.css index a1715df59..7763b65d6 100644 --- a/src/zen/common/styles/zen-browser-container.css +++ b/src/zen/common/styles/zen-browser-container.css @@ -60,3 +60,11 @@ } } } + +.zen-pseudo-browser-image { + position: absolute; + inset: 0; + opacity: 0.4; + pointer-events: none; + z-index: 2; +} diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index 6d3b89118..cfa4ddb52 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -24,6 +24,8 @@ const EVENTS = [ 'TabAddedToEssentials', 'TabRemovedFromEssentials', + 'TabSelect', + 'focus', 'unload', ]; @@ -51,6 +53,19 @@ class nsZenWindowSync { lastHandlerPromise: Promise.resolve(), }; + /** + * Last focused window. + * Used to determine which window to sync tab contents visibility from. + */ + #lastFocusedWindow = null; + + /** + * Last selected tab. + * Used to determine if we should run another sync operation + * when switching browser views. + */ + #lastSelectedTab = null; + /** * Iterator that yields all currently opened browser windows. * (Might miss the most recent one.) @@ -330,8 +345,9 @@ class nsZenWindowSync { * @param {Object} aOurTab - The tab in the current window. * @param {Object} aOtherTab - The tab in the other window. */ - #swapBrowserDocShells(aOurTab, aOtherTab) { + async #swapBrowserDocShells(aOurTab, aOtherTab) { try { + await this.#styleSwapedBrowsers(aOurTab, aOtherTab); aOurTab.ownerGlobal.gBrowser.swapBrowsersAndCloseOther(aOurTab, aOtherTab, false); const kAttributesToRemove = ['muted', 'soundplaying', 'sharing', 'pictureinpicture']; // swapBrowsersAndCloseOther already takes care of transferring attributes like 'muted', @@ -339,27 +355,99 @@ class nsZenWindowSync { for (let attr of kAttributesToRemove) { aOtherTab.removeAttribute(attr); } - aOtherTab.linkedBrowser.style.opacity = 0; - aOurTab.linkedBrowser.style.opacity = ''; } catch (e) { // Handle any errors that may occur during the swapBrowsers operation. console.error('Error swapping browsers:', e); } } + /** + * Styles the swapped browsers to ensure proper visibility and layout. + * + * @param {Object} aOurTab - The tab in the current window. + * @param {Object} aOtherTab - The tab in the other window. + */ + async #styleSwapedBrowsers(aOurTab, aOtherTab) { + const ourBrowser = aOurTab.linkedBrowser; + const otherBrowser = aOtherTab.linkedBrowser; + + const browserBlob = await aOtherTab.ownerGlobal.PageThumbs.captureToBlob( + aOtherTab.linkedBrowser, + { + fullScale: true, + fullViewport: true, + } + ); + + let mySrc = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(browserBlob); + reader.onloadend = function () { + // result includes identifier 'data:image/png;base64,' plus the base64 data + resolve(reader.result); + }; + reader.onerror = function () { + reject(new Error('Failed to read blob as data URL')); + }; + }); + + const [img, loadPromise] = this.#createPseudoImageForBrowser(otherBrowser, mySrc); + // Run a reflow to ensure the image is rendered before hiding the browser. + void img.getBoundingClientRect(); + await loadPromise; + otherBrowser.style.opacity = 0; + otherBrowser.style.pointerEvents = 'none'; + + this.#maybeRemovePseudoImageForBrowser(ourBrowser); + ourBrowser.style.opacity = ''; + ourBrowser.style.pointerEvents = ''; + } + + /** + * Create and insert a new pseudo image for a browser element. + * + * @param {Object} aBrowser - The browser element to create the pseudo image for. + * @param {string} aSrc - The source URL of the image. + * @returns {Object} The created pseudo image element. + */ + #createPseudoImageForBrowser(aBrowser, aSrc) { + const doc = aBrowser.ownerDocument; + const img = doc.createElement('img'); + img.className = 'zen-pseudo-browser-image'; + aBrowser.after(img); + const loadPromise = new Promise((resolve) => { + img.onload = () => resolve(); + img.src = aSrc; + }); + return [img, loadPromise]; + } + + /** + * Removes the pseudo image element for a browser if it exists. + * + * @param {Object} aBrowser - The browser element to remove the pseudo image for. + */ + #maybeRemovePseudoImageForBrowser(aBrowser) { + const elements = aBrowser.parentNode?.querySelectorAll('.zen-pseudo-browser-image'); + if (elements) { + elements.forEach((element) => element.remove()); + } + } + /** * Retrieves the active tab, where the web contents are being viewed * from other windows by its ID. * * @param {Window} aWindow - The window to exclude. * @param {string} aTabId - The ID of the tab to retrieve. + * @param {Function} filter - A function to filter the tabs. * @returns {Object|null} The active tab from other windows if found, otherwise null. */ - #getActiveTabFromOtherWindows(aWindow, aTabId) { + #getActiveTabFromOtherWindows(aWindow, aTabId, filter = (tab) => tab?._zenContentsVisible) { for (let window of this.#browserWindows) { if (window !== aWindow) { const tab = this.#getTabFromWindow(window, aTabId); - if (tab?._zenContentsVisible) { + if (filter(tab)) { return tab; } } @@ -371,9 +459,22 @@ class nsZenWindowSync { * Handles tab switch or window focus events to synchronize tab contents visibility. * * @param {Window} aWindow - The window that triggered the event. + * @param {Object} aPreviousTab - The previously selected tab. */ - onTabSwitchOrWindowFocus(aWindow) { + onTabSwitchOrWindowFocus(aWindow, aPreviousTab = null) { const selectedTab = aWindow.gBrowser.selectedTab; + if (aPreviousTab?._zenContentsVisible) { + const otherTabToShow = this.#getActiveTabFromOtherWindows( + aWindow, + aPreviousTab.id, + (tab) => tab?.selected + ); + if (otherTabToShow) { + otherTabToShow._zenContentsVisible = true; + delete aPreviousTab._zenContentsVisible; + this.#swapBrowserDocShells(otherTabToShow, aPreviousTab); + } + } if (selectedTab._zenContentsVisible) { return; } @@ -463,9 +564,24 @@ class nsZenWindowSync { on_focus(aEvent) { const { ownerGlobal: window } = aEvent.target; + if (this.#lastFocusedWindow?.deref() === window) { + return; + } + this.#lastFocusedWindow = new WeakRef(window); + this.#lastSelectedTab = new WeakRef(window.gBrowser.selectedTab); this.onTabSwitchOrWindowFocus(window); } + on_TabSelect(aEvent) { + const tab = aEvent.target; + if (this.#lastSelectedTab?.deref() === tab) { + return; + } + this.#lastSelectedTab = new WeakRef(tab); + const previousTab = aEvent.detail.previousTab; + this.onTabSwitchOrWindowFocus(aEvent.target.ownerGlobal, previousTab); + } + on_unload() {} }