From 04e7db5d3a84ec754c14d6fc6930fa3ccd00c836 Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Tue, 15 Sep 2026 23:06:35 +0200 Subject: [PATCH] gh-15384: Sync upstream Firefox to version `156.0` (gh-15409) --- README.md | 2 +- build/firefox-cache/l10n-last-commit-hash | 2 +- .../firefox/issue_15123.patch | 109 +++++------- src/zen/common/styles/zen-browser-ui.css | 4 +- src/zen/common/styles/zen-omnibox.css | 6 +- src/zen/common/zenThemeModifier.js | 2 +- src/zen/compact-mode/ZenCompactMode.mjs | 4 +- src/zen/drag-and-drop/ZenDragAndDrop.js | 56 +++++-- src/zen/folders/ZenFolders.mjs | 157 ++++++++++++++++-- src/zen/folders/zen-folders.css | 11 +- .../ZenLiveFoldersManager.sys.mjs | 74 +++++---- src/zen/sessionstore/ZenWindowSync.sys.mjs | 2 +- src/zen/spaces/ZenGradientGenerator.mjs | 6 +- src/zen/sync/ZenSpacesSyncApplier.sys.mjs | 2 +- src/zen/tabs/ZenPinnedTabManager.mjs | 8 + src/zen/tabs/zen-tabs/vertical-tabs.css | 2 + src/zen/tests/device_sync/browser.toml | 2 + .../device_sync/browser_normal_tabs_order.js | 101 +++++++++++ .../mochitests/tabMediaIndicator/browser.toml | 4 +- surfer.json | 4 +- 20 files changed, 410 insertions(+), 148 deletions(-) create mode 100644 src/zen/tests/device_sync/browser_normal_tabs_order.js diff --git a/README.md b/README.md index c2355ea0b..da0257811 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Zen is a firefox-based browser with the aim of pushing your productivity to a ne ### Firefox Versions -- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `155.0.1`! +- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `156.0`! - [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 156.0`! ### Contributing diff --git a/build/firefox-cache/l10n-last-commit-hash b/build/firefox-cache/l10n-last-commit-hash index a8635172d..8c18941a1 100644 --- a/build/firefox-cache/l10n-last-commit-hash +++ b/build/firefox-cache/l10n-last-commit-hash @@ -1 +1 @@ -931c2f71dc0b1e11668a5ca165dd75e2e4297174 \ No newline at end of file +07c99209a6e4c794752e9a242215199ad3ea10f6 \ No newline at end of file diff --git a/src/external-patches/firefox/issue_15123.patch b/src/external-patches/firefox/issue_15123.patch index 75a19cc6b..07a6720de 100644 --- a/src/external-patches/firefox/issue_15123.patch +++ b/src/external-patches/firefox/issue_15123.patch @@ -1,13 +1,7 @@ diff --git a/toolkit/modules/subprocess/subprocess_shared_unix.js b/toolkit/modules/subprocess/subprocess_shared_unix.js --- a/toolkit/modules/subprocess/subprocess_shared_unix.js +++ b/toolkit/modules/subprocess/subprocess_shared_unix.js -@@ -52,16 +52,17 @@ - - close: [ctypes.default_abi, ctypes.int, ctypes.int /* fildes */], - - dup: [ctypes.default_abi, ctypes.int, ctypes.int], - -+ // Variadic arguments use a different calling convention on Apple silicon. +@@ -57,11 +57,11 @@ fcntl: [ ctypes.default_abi, ctypes.int, @@ -64,79 +58,62 @@ diff --git a/toolkit/modules/subprocess/test/xpcshell/test_subprocess_pipe_flags new file mode 100644 --- /dev/null +++ b/toolkit/modules/subprocess/test/xpcshell/test_subprocess_pipe_flags.js -@@ -0,0 +1,73 @@ +@@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + ++// fcntl commands for reading the flags set by the subprocess modules. The ++// values are the same on Linux and macOS but are not exposed by ++// ChromeUtils.getLibcConstants(). ++const F_GETFD = 1; ++const F_GETFL = 3; ++ +add_task(async function test_subprocess_pipe_flags() { -+ const { Subprocess, getSubprocessImplForTest } = ChromeUtils.importESModule( ++ const { getSubprocessImplForTest } = ChromeUtils.importESModule( + "resource://gre/modules/Subprocess.sys.mjs" + ); -+ const { ctypes } = ChromeUtils.importESModule( -+ "resource://gre/modules/ctypes.sys.mjs" ++ const { libc } = ChromeUtils.importESModule( ++ "resource://gre/modules/subprocess/subprocess_unix.sys.mjs" + ); -+ const constants = ChromeUtils.getLibcConstants(); -+ const library = ctypes.open("a.out"); -+ const fcntl = library.declare( -+ "fcntl", -+ ctypes.default_abi, -+ ctypes.int, -+ ctypes.int, -+ ctypes.int, -+ "..." ++ const { FD_CLOEXEC, O_NONBLOCK } = ChromeUtils.getLibcConstants(); ++ ++ const worker = getSubprocessImplForTest().Process.getWorker(); ++ equal( ++ libc.fcntl(worker.signalFd, F_GETFD), ++ FD_CLOEXEC, ++ "The main-thread signal pipe has exactly FD_CLOEXEC set" + ); -+ // Darwin's fcntl commands for reading descriptor and file status flags. -+ const F_GETFD = 1; -+ const F_GETFL = 3; -+ let process; + -+ try { -+ const worker = getSubprocessImplForTest().Process.getWorker(); -+ Assert.equal( -+ fcntl(worker.signalFd, F_GETFD), -+ constants.FD_CLOEXEC, -+ "The main-thread signal pipe has exactly FD_CLOEXEC set" ++ const proc = await Subprocess.call({ ++ command: await Subprocess.pathSearch("cat"), ++ stderr: "pipe", ++ }); ++ const fds = await worker.call("getFds", [proc.id]); ++ ok( ++ fds.every(Number.isInteger), ++ "The worker owns the stdin, stdout and stderr pipes" ++ ); ++ for (const fd of fds) { ++ equal( ++ libc.fcntl(fd, F_GETFD), ++ FD_CLOEXEC, ++ "Worker pipes have exactly FD_CLOEXEC set" + ); -+ -+ process = await Subprocess.call({ -+ command: "/bin/cat", -+ stderr: "pipe", -+ disclaim: true, -+ }); -+ const fds = await worker.call("getFds", [process.id]); -+ // Only inspect the descriptors while the worker owns them and cat is alive. -+ // Exact flags also rule out an unintended FD_CLOFORK on macOS. -+ for (const fd of fds) { -+ Assert.equal( -+ fcntl(fd, F_GETFD), -+ constants.FD_CLOEXEC, -+ "Worker pipes have exactly FD_CLOEXEC set" -+ ); -+ Assert.equal( -+ fcntl(fd, F_GETFL) & constants.O_NONBLOCK, -+ constants.O_NONBLOCK, -+ "Worker pipes are nonblocking" -+ ); -+ } -+ -+ const output = process.stdout.readString(5); -+ await process.stdin.write("hello"); -+ Assert.equal(await output, "hello", "The subprocess pipes transfer data"); -+ await process.stdin.close(); -+ Assert.equal( -+ (await process.wait()).exitCode, -+ 0, -+ "The subprocess exits cleanly" ++ equal( ++ libc.fcntl(fd, F_GETFL) & O_NONBLOCK, ++ O_NONBLOCK, ++ "Worker pipes are nonblocking" + ); -+ } finally { -+ if (process && process.exitCode === null) { -+ await process.kill(); -+ } -+ library.close(); + } ++ ++ const output = proc.stdout.readString(5); ++ await proc.stdin.write("hello"); ++ equal(await output, "hello", "The subprocess pipes transfer data"); ++ await proc.stdin.close(); ++ equal((await proc.wait()).exitCode, 0, "The subprocess exits cleanly"); +}); diff --git a/toolkit/modules/subprocess/test/xpcshell/xpcshell.toml b/toolkit/modules/subprocess/test/xpcshell/xpcshell.toml --- a/toolkit/modules/subprocess/test/xpcshell/xpcshell.toml @@ -148,7 +125,7 @@ diff --git a/toolkit/modules/subprocess/test/xpcshell/xpcshell.toml b/toolkit/mo requesttimeoutfactor = 2 # Slow on Windows +["test_subprocess_pipe_flags.js"] -+run-if = ["os == 'mac'"] ++skip-if = ["os == 'win'"] + ["test_subprocess_polling.js"] diff --git a/src/zen/common/styles/zen-browser-ui.css b/src/zen/common/styles/zen-browser-ui.css index 9d3201960..82d34f7a5 100644 --- a/src/zen/common/styles/zen-browser-ui.css +++ b/src/zen/common/styles/zen-browser-ui.css @@ -61,7 +61,7 @@ body, background: var(--zen-main-browser-background); opacity: var(--zen-background-opacity); transition: 0s; - background-blend-mode: screen; + background-blend-mode: lighten; } &:is(.zen-toolbar-background) { @@ -74,7 +74,7 @@ body, background: var(--zen-main-browser-background-old); opacity: calc(1 - var(--zen-background-opacity)); transition: 0s; - background-blend-mode: screen; + background-blend-mode: lighten; } &:is(.zen-toolbar-background) { diff --git a/src/zen/common/styles/zen-omnibox.css b/src/zen/common/styles/zen-omnibox.css index 77ec73bf3..fa328167c 100644 --- a/src/zen/common/styles/zen-omnibox.css +++ b/src/zen/common/styles/zen-omnibox.css @@ -17,7 +17,7 @@ } .urlbar { - --urlbarview-separator-color: light-dark(hsl(0, 0%, 80%), hsl(0, 0%, 20%)); + --urlbarview-separator-color: light-dark(hsl(0, 0%, 85%), hsl(0, 0%, 15%)); --urlbarview-background-color-hover: var(--toolbarbutton-background-color-hover); border-radius: calc(var(--toolbarbutton-border-radius) - 2px); --urlbarView-results-padding: 10px !important; @@ -284,7 +284,7 @@ background-color: var(--zen-urlbar-background-transparent, var(--zen-urlbar-background-base)) !important; box-shadow: 0 30px 140px -15px light-dark(rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.6)) !important; backdrop-filter: none !important; - outline: 0.5px solid light-dark(rgba(0, 0, 0, 0.2), rgba(255, 255, 255, 0.2)) !important; + outline: 0.5px solid light-dark(rgba(0, 0, 0, 0.2), rgba(255, 255, 255, 0.1)) !important; outline-offset: var(--zen-urlbar-outline-offset) !important; /* stylelint-disable-next-line media-query-no-invalid */ @@ -295,7 +295,7 @@ &, .urlbar-background { - border-radius: 12px !important; + border-radius: 14px !important; } &[breakout-extend][animate-searchmode="true"]::before { diff --git a/src/zen/common/zenThemeModifier.js b/src/zen/common/zenThemeModifier.js index 15622734a..c7e01aaaa 100644 --- a/src/zen/common/zenThemeModifier.js +++ b/src/zen/common/zenThemeModifier.js @@ -109,7 +109,7 @@ if (AppConstants.platform == "macosx") { const targetRadius = window.matchMedia("(-moz-mac-tahoe-theme)") .matches - ? 12 + ? 11 : 10; document.documentElement.style.setProperty( "--zen-border-radius", diff --git a/src/zen/compact-mode/ZenCompactMode.mjs b/src/zen/compact-mode/ZenCompactMode.mjs index 56ffcc956..4ba7ad996 100644 --- a/src/zen/compact-mode/ZenCompactMode.mjs +++ b/src/zen/compact-mode/ZenCompactMode.mjs @@ -541,7 +541,7 @@ window.gZenCompactModeManager = { ease: "easeIn", type: "spring", bounce: 0, - duration: 0.12, + duration: 0.1, } ) .then(() => { @@ -597,7 +597,7 @@ window.gZenCompactModeManager = { ease: "easeOut", type: "spring", bounce: 0, - duration: 0.12, + duration: 0.1, } ) .then(() => { diff --git a/src/zen/drag-and-drop/ZenDragAndDrop.js b/src/zen/drag-and-drop/ZenDragAndDrop.js index b73499014..7097732b2 100644 --- a/src/zen/drag-and-drop/ZenDragAndDrop.js +++ b/src/zen/drag-and-drop/ZenDragAndDrop.js @@ -158,9 +158,9 @@ if (tab.hasAttribute("zen-essential")) { tab.style.visibility = "hidden"; } - this.ZenDragAndDropService.armDropLanding( - !args[0]?.fromTabList && this.getDropEffectForTabDrag(event) == "move" - ); + this._dropLandingArmed = + !args[0]?.fromTabList && this.getDropEffectForTabDrag(event) == "move"; + this.ZenDragAndDropService.armDropLanding(this._dropLandingArmed); } #createDragImageForTabs(movingTabs) { @@ -907,6 +907,8 @@ this.#dragOverSplit.fakeTab = element; this.#dragOverSplit.canDrop = true; + // A drop into the split takes the drag image away at once. + this.ZenDragAndDropService.armDropLanding(false); } _clearDragOverSplit() { @@ -914,6 +916,9 @@ clearTimeout(this.#dragOverSplit.timer); } this.#dragOverSplit.fakeTab?.remove(); + if (this.#dragOverSplit.canDrop && this._dropLandingArmed) { + this.ZenDragAndDropService.armDropLanding(true); + } this.#dragOverSplit.timer = null; this.#dragOverSplit.fakeTab = null; @@ -1040,34 +1045,57 @@ } this.clearSpaceSwitchTimer(); gZenFolders.highlightGroupOnDragOver(null); - // A drop into a split merges the tab away; nothing lands on it. const toSplit = !!this.#dragOverSplit.canDrop; + const placesBefore = new Map( + this.#draggedElements(event).map(element => [ + element, + this.#placeOf(element), + ]) + ); super.handle_drop(event); this.#maybeClearVerticalPinnedGridDragOver(); this.#handle_dropSwitchSpace(event); this.#handle_dropCreateSplit(event); this._clearDragOverSplit(); if (!toSplit) { - this.#landDragImage(event); + this.#landDragImage(event, placesBefore); } } + /** + * @param {DragEvent} event + * @returns {Element[]} The elements of the dragged tabs, in the order of + * their drag images, when they are this window's + */ + #draggedElements(event) { + const draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0); + if (draggedTab?.documentGlobal !== window) { + return []; + } + return (this._dragImageTabs ?? [draggedTab]).map(elementToMove); + } + + #placeOf(element) { + return `${element.screenX},${element.screenY}`; + } + /** * Has the drag image land on the dropped tab rather than vanish. The OS * slides it onto the tab's new place and ends the drag once it is there, * which is when the tab shows again. Only macOS does so. * * @param {DragEvent} event - The drop + * @param {Map} placesBefore - Where each dragged element + * was before the drop. */ - #landDragImage(event) { + #landDragImage(event, placesBefore) { // The drop is done, so its indicator goes before the image lands. this.clearDragOverVisuals(); - const draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0); - if (draggedTab?.documentGlobal !== window || gReduceMotion) { + if (gReduceMotion) { return; } - const elements = (this._dragImageTabs ?? [draggedTab]).map(elementToMove); - for (const element of elements) { + const landing = []; + for (const element of this.#draggedElements(event)) { const { width, height } = element.getBoundingClientRect(); this.ZenDragAndDropService.addDropLandingRect( Math.round(element.screenX), @@ -1075,9 +1103,12 @@ Math.round(width), Math.round(height) ); - element.style.visibility = "hidden"; + if (placesBefore.get(element) !== this.#placeOf(element)) { + element.style.visibility = "hidden"; + landing.push(element); + } } - this._landingElements = elements; + this._landingElements = landing; } #handle_dropSwitchSpace(event) { @@ -1303,6 +1334,7 @@ thisFromGlobal._clearDragOverSplit(); this.#maybeClearVerticalPinnedGridDragOver(); thisFromGlobal.originalDragImageArgs = []; + thisFromGlobal._dropLandingArmed = false; this.#firstHapticFeedbackPlayed = false; window.removeEventListener( "dragenter", diff --git a/src/zen/folders/ZenFolders.mjs b/src/zen/folders/ZenFolders.mjs index 96df8ff93..22119f054 100644 --- a/src/zen/folders/ZenFolders.mjs +++ b/src/zen/folders/ZenFolders.mjs @@ -1505,10 +1505,78 @@ class nsZenFolders extends nsZenDOMOperatedFeature { #createAnimation(items, targetState, opts, callback = () => {}) { items = Array.isArray(items) ? items : [items]; return items.map(item => - gZenUIManager.motion.animate(item, targetState, opts).then(callback) + this.#animateItem(item, targetState, opts).then(callback) ); } + /** + * Animates an element to the given target state. An array value is a + * [from, to] pair, "auto" endpoints are resolved by measuring and an + * empty string animates back to the element's natural value. The final + * values are left applied as inline styles. + * + * @param {Element} item - The element to animate. + * @param {object} targetState - Property to value (or [from, to]) map. + * @param {object} opts - The animation options. + * @param {number} opts.duration - The duration in seconds. + * @param {string} opts.ease - The easing name. + * @returns {Promise} Resolves when the animation has finished. + */ + async #animateItem(item, targetState, { duration = 0.18, ease } = {}) { + const computed = window.getComputedStyle(item); + const toCssValue = (prop, value) => + typeof value === "number" && prop !== "opacity" + ? `${value}px` + : String(value); + const measure = prop => { + const value = computed[prop]; + if (value === "auto") { + return `${item.getBoundingClientRect()[prop] ?? 0}px`; + } + return value; + }; + const from = {}; + const to = {}; + const finalStyles = new Map(); + for (const [prop, value] of Object.entries(targetState)) { + let [start, end] = Array.isArray(value) ? value : [undefined, value]; + const cssProp = prop.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`); + if (start === undefined || start === "auto") { + start = measure(prop); + } else { + start = toCssValue(prop, start); + } + if (end === "" || end === "auto") { + // Resolve the natural value by clearing any inline override. + item.style.removeProperty(cssProp); + end = measure(prop); + finalStyles.set(cssProp, null); + } else { + end = toCssValue(prop, end); + finalStyles.set(cssProp, end); + } + from[prop] = start; + to[prop] = end; + } + const animation = item.animate([from, to], { + duration: duration * 1000, + easing: ease === "easeInOut" ? "ease-in-out" : "ease", + }); + try { + await animation.finished; + } catch (e) { + // The animation was cancelled, leave the element as-is. + return; + } + for (const [cssProp, value] of finalStyles) { + if (value === null) { + item.style.removeProperty(cssProp); + } else { + item.style.setProperty(cssProp, value); + } + } + } + #calculateHeightShift(tabsContainer, selectedTabs) { let heightShift = 0; if (selectedTabs.length) { @@ -1526,7 +1594,11 @@ class nsZenFolders extends nsZenDOMOperatedFeature { } get #folderAnimationDuration() { - return this._dontAnimateFolder ? 0 : 0.12; + return this._dontAnimateFolder ? 0 : 0.18; + } + + get #folderRevealDuration() { + return this._dontAnimateFolder ? 0 : 0.22; } async animateCollapse(group) { @@ -1546,6 +1618,9 @@ class nsZenFolders extends nsZenDOMOperatedFeature { splitViewIds, activeFoldersIds, }); + if (selectedTabs.length) { + tabsContainer.removeAttribute("hidden"); + } const collapsedHeight = this.#calculateHeightShift( tabsContainer, selectedTabs @@ -1764,6 +1839,9 @@ class nsZenFolders extends nsZenDOMOperatedFeature { // Set correct margin-top after animation const afterAnimate = () => { + if (folder.hasAttribute("has-active")) { + return; + } groupStart.style.removeProperty("margin-top"); this.styleCleanup(groupItems); // Trigger the recalculation so that zen returns @@ -1785,7 +1863,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature { { marginTop: -(collapsedHeight + 4), }, - { duration: 0.12, ease: "easeInOut" }, + { duration: this.#folderAnimationDuration, ease: "easeInOut" }, afterAnimate ) ); @@ -1821,6 +1899,9 @@ class nsZenFolders extends nsZenDOMOperatedFeature { // Set correct margin-top after animation const afterAnimate = () => { + if (folder.hasAttribute("has-active")) { + return; + } groupStart.style.removeProperty("margin-top"); this.styleCleanup(groupItems); // Trigger the recalculation so that zen returns @@ -1845,7 +1926,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature { { marginTop: -(collapsedHeight + 4), }, - { duration: 0.12, ease: "easeInOut" }, + { duration: this.#folderAnimationDuration, ease: "easeInOut" }, afterAnimate ), ]; @@ -1870,7 +1951,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature { height: 0, }, { - duration: 0.12, + duration: this.#folderAnimationDuration, ease: "easeInOut", } ); @@ -1899,12 +1980,31 @@ class nsZenFolders extends nsZenDOMOperatedFeature { const selectedTabs = []; const splitViewIds = new Set(); const itemsToHide = []; + const newlyActive = new Set(); const groupItems = this.#collectGroupItems(group, { selectedTabs, splitViewIds, }); + if (group.collapsed && selectedTabs.length) { + const active = new Set([...(group.activeTabs ?? []), ...selectedTabs]); + const tabs = group.tabs.filter(tab => !tab.hasAttribute("zen-empty-tab")); + if ( + tabs.length && + tabs.every( + tab => + active.has(tab) || + tab.selected || + tab.multiselected || + tab.hasAttribute("folder-active") + ) + ) { + group.collapsed = false; + return; + } + } + for (const tab of selectedTabs) { let currentGroup = tab?.group?.hasAttribute("split-view-group") ? tab.group.group @@ -1916,6 +2016,11 @@ class nsZenFolders extends nsZenDOMOperatedFeature { if (activeTabs.length) { if (currentGroup.collapsed) { if (currentGroup.hasAttribute("has-active")) { + for (const activeTab of activeTabs) { + if (!currentGroup.activeTabs.includes(activeTab)) { + newlyActive.add(activeTab); + } + } // It is important to keep the sequence of elements as in the DOM currentGroup.activeTabs = [ ...new Set([...currentGroup.activeTabs, ...activeTabs]), @@ -1944,7 +2049,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature { { marginTop: 0, }, - { duration: 0.12, ease: "easeInOut" }, + { duration: this.#folderAnimationDuration, ease: "easeInOut" }, afterMarginTop ) ); @@ -1963,13 +2068,14 @@ class nsZenFolders extends nsZenDOMOperatedFeature { } const itemsToShow = []; + const itemsToReveal = []; if (selectedTabs.length) { for (let i = 0; i < groupItems.length; i++) { const { item, splitViewId } = groupItems[i]; let itemVisible = item.visible; if (itemVisible) { - itemsToShow.push(item); + (newlyActive.has(item) ? itemsToReveal : itemsToShow).push(item); } // Skip selected items @@ -2003,18 +2109,48 @@ class nsZenFolders extends nsZenDOMOperatedFeature { height: "", }, { - duration: 0.12, + duration: this.#folderAnimationDuration, ease: "easeInOut", } ), + ...itemsToReveal.flatMap(item => { + const state = { + opacity: [0, ""], + height: [0, ""], + minHeight: [0, ""], + }; + const folder = item.group?.hasAttribute("split-view-group") + ? item.group.group + : item.group; + const slidesFromFolder = !(folder?.activeTabs ?? []).some( + active => + !newlyActive.has(active) && + active.compareDocumentPosition(item) & + Node.DOCUMENT_POSITION_FOLLOWING + ); + if (slidesFromFolder) { + const height = + item.getBoundingClientRect().height || + (folder?.activeTabs ?? []).reduce( + (found, active) => found || active.getBoundingClientRect().height, + 0 + ); + state.transform = [`translateY(-${height}px)`, ""]; + } + return this.#createAnimation(item, state, { + duration: this.#folderRevealDuration, + ease: "easeInOut", + }); + }), ...this.#createAnimation( itemsToHide, { opacity: 0, height: 0, + minHeight: 0, }, { - duration: 0.12, + duration: this.#folderAnimationDuration, ease: "easeInOut", } ) @@ -2047,7 +2183,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature { { marginTop: expand ? 0 : -(heightContainer + 4), }, - { duration: 0.12, ease: "easeInOut" } + { duration: this.#folderAnimationDuration, ease: "easeInOut" } ); } @@ -2055,6 +2191,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature { items.forEach(item => { item.style.removeProperty("opacity"); item.style.removeProperty("height"); + item.style.removeProperty("min-height"); }); } } diff --git a/src/zen/folders/zen-folders.css b/src/zen/folders/zen-folders.css index 5cb7f2b1e..cc9afea01 100644 --- a/src/zen/folders/zen-folders.css +++ b/src/zen/folders/zen-folders.css @@ -193,15 +193,8 @@ zen-folder { } } - :root[zen-sidebar-expanded] &[has-active] > .tab-group-label-container { - & .tab-reset-button { - display: flex; - opacity: 0; - } - - &:hover .tab-reset-button { - opacity: 1; - } + :root[zen-sidebar-expanded] &[has-active] > .tab-group-label-container:hover .tab-reset-button { + display: flex; } } diff --git a/src/zen/live-folders/ZenLiveFoldersManager.sys.mjs b/src/zen/live-folders/ZenLiveFoldersManager.sys.mjs index 450b35d55..95eee90aa 100644 --- a/src/zen/live-folders/ZenLiveFoldersManager.sys.mjs +++ b/src/zen/live-folders/ZenLiveFoldersManager.sys.mjs @@ -498,41 +498,51 @@ class nsZenLiveFoldersManager { ); }) .map(item => { - const tab = this.window.gBrowser.addTrustedTab(item.url, { - createLazyBrowser: true, - inBackground: true, - skipAnimation: true, - noInitialLabel: true, - lazyTabTitle: item.title, - userContextId, - }); - // createLazyBrowser can't be pinned by default - this.window.gBrowser.pinTab(tab); - if (userContextId) { - tab.setAttribute("zenDefaultUserContextId", "true"); - } - if (item.icon) { - this.window.gBrowser.setIcon(tab, item.icon); - if (tab.linkedBrowser) { - lazy.TabStateCache.update(tab.linkedBrowser.permanentKey, { - image: null, + try { + const tab = this.window.gBrowser.addTrustedTab(item.url, { + createLazyBrowser: true, + inBackground: true, + skipAnimation: true, + noInitialLabel: true, + lazyTabTitle: item.title, + userContextId, + }); + // createLazyBrowser can't be pinned by default + this.window.gBrowser.pinTab(tab); + if (userContextId) { + tab.setAttribute("zenDefaultUserContextId", "true"); + } + if (item.icon) { + this.window.gBrowser.setIcon(tab, item.icon); + if (tab.linkedBrowser) { + lazy.TabStateCache.update(tab.linkedBrowser.permanentKey, { + image: null, + }); + } + } + tab.setAttribute( + "zen-live-folder-item-id", + this.#makeCompositeId(liveFolder.id, item.id) + ); + if (item.subtitle) { + tab.setAttribute("zen-show-sublabel", item.subtitle); + const tabLabel = tab.querySelector(".zen-tab-sublabel"); + this.window.document.l10n.setArgs(tabLabel, { + tabSubtitle: item.subtitle, }); } - } - tab.setAttribute( - "zen-live-folder-item-id", - this.#makeCompositeId(liveFolder.id, item.id) - ); - if (item.subtitle) { - tab.setAttribute("zen-show-sublabel", item.subtitle); - const tabLabel = tab.querySelector(".zen-tab-sublabel"); - this.window.document.l10n.setArgs(tabLabel, { - tabSubtitle: item.subtitle, - }); - } - return tab; - }); + return tab; + } catch (e) { + console.error( + "ZenLiveFoldersManager: Failed to add tab for item", + item.url, + e + ); + return null; + } + }) + .filter(tab => tab); // Wait for tabs to (hopefully) be initialized on all windows lazy.setTimeout(() => { diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index 832f07daa..84e3b0eb4 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -330,7 +330,7 @@ class nsZenWindowSync { // browser/components/extensions/parent/ext-browser.js. // See: Bug 1960104 - Improve tab group ID generation in addTabGroup // This is implemented from gBrowser.addTabGroup. - return `${Date.now()}-${Math.round(Math.random() * 100)}`; + return `${Date.now()}-${Services.uuid.generateUUID().toString().slice(1, -1)}`; } /** diff --git a/src/zen/spaces/ZenGradientGenerator.mjs b/src/zen/spaces/ZenGradientGenerator.mjs index 83f2f194a..ce42663b2 100644 --- a/src/zen/spaces/ZenGradientGenerator.mjs +++ b/src/zen/spaces/ZenGradientGenerator.mjs @@ -1337,7 +1337,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature { this.useAlgo = themedColors[0]?.algorithm ?? ""; this.#currentLightness = themedColors[0]?.lightness ?? 50; - const rotation = -45; // TODO: Detect rotation based on the accent color + const rotation = -30; // TODO: Detect rotation based on the accent color if (themedColors.length === 0) { const getBrowserBg = () => { if (this.canBeTransparent) { @@ -1367,8 +1367,8 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature { if (themedColors.length === 2) { if (!forToolbar) { return [ - `linear-gradient(${rotation}deg, ${this.#getSingleRGBColor(themedColors[1], forToolbar)} 0%, transparent 100%)`, - `linear-gradient(${rotation + 180}deg, ${this.#getSingleRGBColor(themedColors[0], forToolbar)} 0%, transparent 100%)`, + `linear-gradient(${rotation}deg, ${this.#getSingleRGBColor(themedColors[1], forToolbar)} 20%, transparent 100%)`, + `linear-gradient(${rotation + 180}deg, ${this.#getSingleRGBColor(themedColors[0], forToolbar)} 20%, transparent 100%)`, ] .reverse() .join(", "); diff --git a/src/zen/sync/ZenSpacesSyncApplier.sys.mjs b/src/zen/sync/ZenSpacesSyncApplier.sys.mjs index 118fc7f81..6179a2b89 100644 --- a/src/zen/sync/ZenSpacesSyncApplier.sys.mjs +++ b/src/zen/sync/ZenSpacesSyncApplier.sys.mjs @@ -880,7 +880,7 @@ class nsZenSpacesSyncApplier { prev && prev.parentNode && prev.parentNode === el.parentNode && - prev.nextElementSibling !== el + prev.compareDocumentPosition(el) & win.Node.DOCUMENT_POSITION_PRECEDING ) { win.gBrowser.zenHandleTabMove(el, () => prev.after(el)); } diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index 99823367d..6ba4270cc 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -374,6 +374,14 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { case "reset-switch": case "switch": if (behavior.includes("unload")) { + if (pinnedTabs.some(tab => tab.selected)) { + const selectedTabs = pinnedTabs.filter(tab => tab.selected); + const tabToBlurTo = gBrowser._findTabToBlurTo( + selectedTabs[0], + pinnedTabs + ); + gBrowser.selectedTab = tabToBlurTo; + } for (const tab of pinnedTabs) { if (tab.hasAttribute("glance-id")) { // We have a glance tab inside the tab we are trying to unload, diff --git a/src/zen/tabs/zen-tabs/vertical-tabs.css b/src/zen/tabs/zen-tabs/vertical-tabs.css index 9b571af60..71a056922 100644 --- a/src/zen/tabs/zen-tabs/vertical-tabs.css +++ b/src/zen/tabs/zen-tabs/vertical-tabs.css @@ -629,6 +629,8 @@ --tab-icon-end-margin: 8.5px; & .tabbrowser-tab { + overflow: visible; + & .tab-label-container { opacity: 1; display: flex; diff --git a/src/zen/tests/device_sync/browser.toml b/src/zen/tests/device_sync/browser.toml index 65b727ebe..ba0d3b24b 100644 --- a/src/zen/tests/device_sync/browser.toml +++ b/src/zen/tests/device_sync/browser.toml @@ -16,4 +16,6 @@ support-files = [ ["browser_normal_tabs_apply.js"] +["browser_normal_tabs_order.js"] + ["browser_normal_tabs_projection.js"] diff --git a/src/zen/tests/device_sync/browser_normal_tabs_order.js b/src/zen/tests/device_sync/browser_normal_tabs_order.js new file mode 100644 index 000000000..1f9e1a210 --- /dev/null +++ b/src/zen/tests/device_sync/browser_normal_tabs_order.js @@ -0,0 +1,101 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_RapidTabsGetUniqueSyncIds() { + await gZenWorkspaces.promiseInitialized; + const tabs = []; + for (let i = 0; i < 60; i++) { + tabs.push( + gBrowser.addTrustedTab(`https://example.com/?rapid-${i}`, { + inBackground: true, + skipAnimation: true, + }) + ); + } + await TestUtils.waitForCondition( + () => tabs.every(tab => tab.id), + "every rapidly-opened tab is assigned a sync id" + ); + const ids = tabs.map(tab => tab.id); + Assert.equal( + new Set(ids).size, + ids.length, + "every rapidly-opened tab has a unique sync id" + ); + for (const tab of tabs) { + BrowserTestUtils.removeTab(tab); + } +}); + +add_task(async function test_IncomingOrderKeepsUnlistedTabInPlace() { + await gZenWorkspaces.promiseInitialized; + await SpecialPowers.pushPrefEnv({ set: [[NORMAL_TABS_PREF, true]] }); + + const uuid = gZenWorkspaces.activeWorkspace; + const opened = [ + await openSyncableTab("https://example.com/?order-a"), + await openSyncableTab("https://example.com/?order-b"), + await openSyncableTab("https://example.com/?order-c"), + ]; + const container = opened[0].parentNode; + Assert.ok( + opened.every(tab => tab.parentNode === container), + "the three tabs share a container" + ); + + const domOrder = () => + [...container.children].filter(node => opened.includes(node)); + const [first, middle, last] = domOrder(); + + const spaceData = (await collectProjections()).get(uuid)?.data; + Assert.ok(spaceData, "the active space projects"); + const record = { + id: uuid, + deleted: false, + cleartext: { + kind: "space", + data: { ...spaceData, children: [first.id, last.id] }, + }, + }; + const failed = await ZenSpacesSyncApplier.applyBatch([record]); + Assert.deepEqual(failed, [], "the space order applies cleanly"); + + Assert.deepEqual( + domOrder(), + [first, middle, last], + "the unlisted tab stays between its neighbours, not pushed to the bottom" + ); + + ZenSpacesSyncModel.noteApplied(uuid, null); + for (const tab of opened) { + BrowserTestUtils.removeTab(tab); + } + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_AppliedNormalTabReprojectsFaithfully() { + await gZenWorkspaces.promiseInitialized; + await SpecialPowers.pushPrefEnv({ set: [[NORMAL_TABS_PREF, true]] }); + + const id = "test-sync-normal-faithful"; + const failed = await ZenSpacesSyncApplier.applyBatch([ + tabRecord(id, { pinned: false, url: "https://example.com/?faithful" }), + ]); + Assert.deepEqual(failed, [], "the normal-tab record applies cleanly"); + const tab = document.getElementById(id); + Assert.ok(gBrowser.isTab(tab), "the tab materializes"); + + const projections = await collectProjections(); + const changes = ZenSpacesSyncModel.computeChangedIDs(); + Assert.ok( + projections.get(id), + "the applied normal tab re-projects (is not dropped from the sidebar)" + ); + Assert.ok(!(id in changes) || projections.get(id), "and is not tombstoned"); + + ZenSpacesSyncModel.noteApplied(id, null); + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); diff --git a/src/zen/tests/mochitests/tabMediaIndicator/browser.toml b/src/zen/tests/mochitests/tabMediaIndicator/browser.toml index 4b221ad11..41ec23ef4 100644 --- a/src/zen/tests/mochitests/tabMediaIndicator/browser.toml +++ b/src/zen/tests/mochitests/tabMediaIndicator/browser.toml @@ -38,12 +38,12 @@ https_first_disabled = true ["browser_mute.js"] -["browser_mute2.js"] - ["browser_mute_persist_navigation.js"] ["browser_mute_restore_closed_audible_tab.js"] +["browser_mute2.js"] + ["browser_mute_webAudio.js"] ["browser_sound_indicator_silent_video.js"] diff --git a/surfer.json b/surfer.json index 75b5d5971..5e946b570 100644 --- a/surfer.json +++ b/surfer.json @@ -5,7 +5,7 @@ "binaryName": "zen", "version": { "product": "firefox", - "version": "155.0.1", + "version": "156.0", "candidate": "156.0", "candidateBuild": 1 }, @@ -20,7 +20,7 @@ "brandShortName": "Zen", "brandFullName": "Zen Browser", "release": { - "displayVersion": "1.22.1b", + "displayVersion": "1.22.2b", "github": { "repo": "zen-browser/desktop" },