feat: Make sure to run view swaps before rendering a new frame, b=no-bug, c=split-view, tabs

This commit is contained in:
mr. m
2026-01-19 13:40:27 +01:00
parent cad9f01722
commit 745d0edc82
4 changed files with 68 additions and 64 deletions

View File

@@ -571,10 +571,15 @@ class nsZenWindowSync {
* @param {object} aOtherTab - The tab in the other window.
*/
async #swapBrowserDocShellsAsync(aOurTab, aOtherTab) {
this.#maybeFlushTabState(aOtherTab);
await this.#styleSwapedBrowsers(aOurTab, aOtherTab, () => {
this.#swapBrowserDocShellsInner(aOurTab, aOtherTab);
});
let promise = this.#maybeFlushTabState(aOurTab);
await this.#styleSwapedBrowsers(
aOurTab,
aOtherTab,
() => {
this.#swapBrowserDocShellsInner(aOurTab, aOtherTab);
},
promise
);
}
/**
@@ -623,16 +628,17 @@ class nsZenWindowSync {
* @param {object} options - Options object.
* @param {boolean} options.focus - Indicates if the tab should be focused after the swap.
* @param {boolean} options.onClose - Indicates if the swap is done during a tab close operation.
* @returns {Promise|null} A promise that resolves when the tab state is flushed, or null if the swap cannot be performed.
*/
#swapBrowserDocShellsInner(aOurTab, aOtherTab, { focus = true, onClose = false } = {}) {
// Can't swap between chrome and content processes.
if (aOurTab.linkedBrowser.isRemoteBrowser != aOtherTab.linkedBrowser.isRemoteBrowser) {
return false;
return null;
}
// See https://github.com/zen-browser/desktop/issues/11851, swapping the browsers
// don't seem to update the state's cache properly, leading to issues when restoring
// the session later on.
let tabStateEntries = this.#getTabEntriesFromCache(aOtherTab);
let tabStateEntries = this.#getTabEntriesFromCache(aOurTab);
// Running `swapBrowsersAndCloseOther` doesn't expect us to use the tab after
// the operation, so it doesn't really care about cleaning up the other tab.
// We need to make a new tab progress listener for the other tab after the swap.
@@ -681,13 +687,14 @@ class nsZenWindowSync {
// inside the web content area without having to click outside and back in.
aOurTab.linkedBrowser.blur();
aOurTab.ownerGlobal.gBrowser._adjustFocusAfterTabSwitch(aOurTab);
aOurTab.linkedBrowser.docShellIsActive = true;
}
// Ensure the tab's state is flushed after the swap. By doing this,
// we can re-schedule another session store delayed process to fire.
// It's also important to note that if we don't flush the state here,
// we would start receiving invalid history changes from the the incorrect
// browser view that was just swapped out.
this.#maybeFlushTabState(aOurTab).finally(() => {
return this.#maybeFlushTabState(aOurTab).finally(() => {
if (!tabStateEntries?.entries.length) {
this.log(`Error: No tab state entries found for tab ${aOtherTab.id} during swap`);
return;
@@ -696,7 +703,6 @@ class nsZenWindowSync {
history: tabStateEntries,
});
});
return true;
}
/**
@@ -705,42 +711,45 @@ class nsZenWindowSync {
* @param {object} aOurTab - The tab in the current window.
* @param {object} aOtherTab - The tab in the other window.
* @param {Function|undefined} callback - The callback function to execute after styling.
* @param {Promise|null} promiseToWait - A promise to wait for before executing the callback.
*/
async #styleSwapedBrowsers(aOurTab, aOtherTab, callback = undefined) {
#styleSwapedBrowsers(aOurTab, aOtherTab, callback = undefined, promiseToWait = null) {
const ourBrowser = aOurTab.linkedBrowser;
const otherBrowser = aOtherTab.linkedBrowser;
return new Promise((resolve) => {
aOurTab.ownerGlobal.requestAnimationFrame(async () => {
if (callback) {
const browserBlob = await aOtherTab.ownerGlobal.PageThumbs.captureToBlob(
aOtherTab.linkedBrowser,
{
fullScale: true,
fullViewport: true,
}
);
if (callback) {
const browserBlob = await aOtherTab.ownerGlobal.PageThumbs.captureToBlob(
aOtherTab.linkedBrowser,
{
fullScale: true,
fullViewport: true,
let mySrc = await new Promise((r, re) => {
const reader = new FileReader();
reader.readAsDataURL(browserBlob);
reader.onloadend = function () {
// result includes identifier 'data:image/png;base64,' plus the base64 data
r(reader.result);
};
reader.onerror = function () {
re(new Error("Failed to read blob as data URL"));
};
});
this.#createPseudoImageForBrowser(otherBrowser, mySrc);
otherBrowser.setAttribute("zen-pseudo-hidden", "true");
await promiseToWait;
callback();
}
);
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"));
};
this.#maybeRemovePseudoImageForBrowser(ourBrowser);
ourBrowser.removeAttribute("zen-pseudo-hidden");
resolve();
});
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.setAttribute("zen-pseudo-hidden", "true");
callback();
}
this.#maybeRemovePseudoImageForBrowser(ourBrowser);
ourBrowser.removeAttribute("zen-pseudo-hidden");
});
}
/**
@@ -748,18 +757,13 @@ class nsZenWindowSync {
*
* @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";
img.src = aSrc;
aBrowser.after(img);
const loadPromise = new Promise((resolve) => {
img.onload = () => resolve();
img.src = aSrc;
});
return [img, loadPromise];
}
/**
@@ -846,15 +850,16 @@ class nsZenWindowSync {
// Ignore previous tabs that are still "active". These scenarios could happen for example,
// when selecting on a split view tab that was already active.
if (aPreviousTab?._zenContentsVisible && !activeTabs.includes(aPreviousTab)) {
const otherTabToShow = this.#getActiveTabFromOtherWindows(
aWindow,
aPreviousTab.id,
(tab) => tab?.selected
);
if (otherTabToShow) {
otherTabToShow._zenContentsVisible = true;
delete aPreviousTab._zenContentsVisible;
await this.#swapBrowserDocShellsAsync(otherTabToShow, aPreviousTab);
let tabsToSwap = aPreviousTab.splitView ? aPreviousTab.group.tabs : [aPreviousTab];
for (const tab of tabsToSwap) {
const otherTabToShow = this.#getActiveTabFromOtherWindows(aWindow, tab.id, (t) =>
t?.splitView ? t.group.tabs.some((st) => st.selected) : t?.selected
);
if (otherTabToShow) {
otherTabToShow._zenContentsVisible = true;
delete tab._zenContentsVisible;
await this.#swapBrowserDocShellsAsync(otherTabToShow, tab);
}
}
}
let promises = [];
@@ -1238,7 +1243,7 @@ class nsZenWindowSync {
.map((tab) => this.getItemFromWindow(win, tab.id))
.filter(Boolean);
if (otherWindowTabs.length && win.gZenViewSplitter) {
const group = win.gZenViewSplitter.splitTabs(otherWindowTabs, "grid", -1);
const group = win.gZenViewSplitter.splitTabs(otherWindowTabs, undefined, -1);
if (group) {
let otherTabGroup = group.tabs[0].group;
otherTabGroup.id = tabGroup.id;

View File

@@ -181,6 +181,9 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
*/
onTabSelect(event) {
const previousTab = event.detail.previousTab;
if (previousTab === gBrowser.selectedTab && this._canDrop) {
return;
}
if (previousTab && !previousTab.hasAttribute("zen-empty-tab")) {
this._lastOpenedTab = previousTab;
}
@@ -1257,9 +1260,6 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
this._data.push(splitData);
if (!this._sessionRestoring && initialIndex >= 0) {
window.gBrowser.selectedTab = tabs[tabIndexToUse] ?? tabs[0];
}
if (!this._sessionRestoring) {
this.activateSplitView(splitData);
}
@@ -1777,6 +1777,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
delete this._dndElement;
}
if (this.fakeBrowser) {
delete this._canDrop;
delete this._hasAnimated;
this.fakeBrowser.remove();
delete this.fakeBrowser;
@@ -2075,11 +2076,12 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
for (const groupData of data) {
const group = document.getElementById(groupData.groupId);
if (!group) {
if (!gBrowser.isTabGroup(group)) {
continue;
}
// Backwards compatibility
group.setAttribute("split-view-group", "true");
if (!groupData?.layoutTree) {
this.splitTabs(group.tabs, group.gridType);
delete this._sessionRestoring;

View File

@@ -81,15 +81,12 @@
}
#browser {
--zen-min-toolbox-padding: 0.4rem;
--zen-min-toolbox-padding: 5px;
@media (-moz-platform: macos) {
--zen-min-toolbox-padding: 0.5rem;
}
@media (-moz-platform: linux) {
--zen-min-toolbox-padding: 0.35rem;
--zen-min-toolbox-padding: 6px;
}
--zen-toolbox-padding: max(var(--zen-min-toolbox-padding), calc(var(--zen-element-separation) / 1.5));
--zen-toolbox-padding: var(--zen-min-toolbox-padding);
}
/* ==========================================================================

View File

@@ -20,7 +20,7 @@
"brandShortName": "Zen",
"brandFullName": "Zen Browser",
"release": {
"displayVersion": "1.17.15b",
"displayVersion": "1.18b",
"github": {
"repo": "zen-browser/desktop"
},