mirror of
https://github.com/zen-browser/desktop.git
synced 2026-09-18 10:44:46 +00:00
gh-15384: Sync upstream Firefox to version 156.0 (gh-15409)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
931c2f71dc0b1e11668a5ca165dd75e2e4297174
|
||||
07c99209a6e4c794752e9a242215199ad3ea10f6
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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<Element, string>} 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",
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(", ");
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -629,6 +629,8 @@
|
||||
--tab-icon-end-margin: 8.5px;
|
||||
|
||||
& .tabbrowser-tab {
|
||||
overflow: visible;
|
||||
|
||||
& .tab-label-container {
|
||||
opacity: 1;
|
||||
display: flex;
|
||||
|
||||
@@ -16,4 +16,6 @@ support-files = [
|
||||
|
||||
["browser_normal_tabs_apply.js"]
|
||||
|
||||
["browser_normal_tabs_order.js"]
|
||||
|
||||
["browser_normal_tabs_projection.js"]
|
||||
|
||||
101
src/zen/tests/device_sync/browser_normal_tabs_order.js
Normal file
101
src/zen/tests/device_sync/browser_normal_tabs_order.js
Normal file
@@ -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();
|
||||
});
|
||||
@@ -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"]
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user