feat: Leave a screenshot of the page behind when switching windows or tabs, b=no-bug, c=common

This commit is contained in:
Mr. M
2025-11-28 13:55:23 +01:00
parent 722ffa5e97
commit 7dd3cf5817
2 changed files with 130 additions and 6 deletions

View File

@@ -60,3 +60,11 @@
}
}
}
.zen-pseudo-browser-image {
position: absolute;
inset: 0;
opacity: 0.4;
pointer-events: none;
z-index: 2;
}

View File

@@ -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() {}
}