From b66b05dcf673b01a3c68d4283088e93ae9a61400 Mon Sep 17 00:00:00 2001
From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com>
Date: Sun, 28 Dec 2025 17:58:18 +0100
Subject: [PATCH 1/2] feat: Allow pinned tabs to be collapsible, p=#11753,
c=tabs, folders, workspaces
* fix: Fixed restoring the previous pinned state clearing up the custom icon, b=no-bug, c=tabs
* feat: Allow pinned tabs to be collapsible, b=no-bug, c=tabs, folders, workspaces
* fix: Fixed new folder context menu item not working, b=no-bug, c=common, folders
---
.../sessionstore/SessionStore-sys-mjs.patch | 4 +-
.../tabbrowser/content/tab-js.patch | 18 ++-
.../tabbrowser/content/tabgroup-js.patch | 38 +++---
.../tabbrowser/content/tabs-js.patch | 22 ++--
src/browser/themes/shared/zen-icons/icons.css | 3 +-
.../shared/zen-icons/lin/arrow-right.svg | 2 +-
src/zen/common/modules/ZenUIManager.mjs | 2 -
src/zen/folders/ZenFolder.mjs | 18 +--
src/zen/folders/ZenFolders.mjs | 39 ++++---
src/zen/folders/zen-folders.css | 15 +--
.../sessionstore/ZenSessionManager.sys.mjs | 3 +-
src/zen/sessionstore/ZenWindowSync.sys.mjs | 4 +-
src/zen/tabs/ZenPinnedTabManager.mjs | 5 +-
src/zen/workspaces/ZenGradientGenerator.mjs | 6 +-
src/zen/workspaces/ZenWorkspace.mjs | 110 ++++++++++++++++--
src/zen/workspaces/ZenWorkspaceCreation.mjs | 6 +-
src/zen/workspaces/ZenWorkspaceIcons.mjs | 6 +-
src/zen/workspaces/ZenWorkspaces.mjs | 39 ++++++-
src/zen/workspaces/ZenWorkspacesStorage.mjs | 6 +-
src/zen/workspaces/zen-workspaces.css | 52 ++++++++-
20 files changed, 299 insertions(+), 99 deletions(-)
diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch
index fe61001fd..7eaf8210e 100644
--- a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch
+++ b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs
-index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..4439fe5fb3c7002b173415b615892ef356b22959 100644
+index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..697dde4378c43ae6db46a6b7eb2997982201ec27 100644
--- a/browser/components/sessionstore/SessionStore.sys.mjs
+++ b/browser/components/sessionstore/SessionStore.sys.mjs
@@ -127,6 +127,8 @@ const TAB_EVENTS = [
@@ -177,7 +177,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..4439fe5fb3c7002b173415b615892ef3
+ winData.folders = aWindow.gZenFolders?.storeDataForSessionStore() || [];
+ winData.activeZenSpace = aWindow.gZenWorkspaces?.activeWorkspace || null;
-+ winData.spaces = aWindow.gZenWorkspaces?.getWorkspaces();
++ winData.spaces = aWindow.gZenWorkspaces?.getWorkspacesForSessionStore();
// update tab group state for this window
winData.groups = [];
for (let tabGroup of aWindow.gBrowser.tabGroups) {
diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch
index dd0edd05f..c7be0f3eb 100644
--- a/src/browser/components/tabbrowser/content/tab-js.patch
+++ b/src/browser/components/tabbrowser/content/tab-js.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tab.js b/browser/components/tabbrowser/content/tab.js
-index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7600f564a 100644
+index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..160e277d64eaac8408aed90eaf62606479424001 100644
--- a/browser/components/tabbrowser/content/tab.js
+++ b/browser/components/tabbrowser/content/tab.js
@@ -21,6 +21,7 @@
@@ -87,7 +87,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7
}
get lastAccessed() {
-@@ -382,7 +395,12 @@
+@@ -382,7 +395,18 @@
}
get group() {
@@ -97,11 +97,17 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7
+ }
+ if (gBrowser.isTabGroup(this.parentElement?.parentElement)) {
+ return this.parentElement.parentElement;
++ }
++ if (this.pinned) {
++ let collapsiblePins = gZenWorkspaces.workspaceElement(this.getAttribute('zen-workspace-id'))?.collapsiblePins;
++ if (collapsiblePins?.collapsed) {
++ return collapsiblePins;
++ }
+ }
}
get splitview() {
-@@ -473,6 +491,8 @@
+@@ -473,6 +497,8 @@
this.style.MozUserFocus = "ignore";
} else if (
event.target.classList.contains("tab-close-button") ||
@@ -110,7 +116,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7
event.target.classList.contains("tab-icon-overlay") ||
event.target.classList.contains("tab-audio-button")
) {
-@@ -527,6 +547,10 @@
+@@ -527,6 +553,10 @@
this.style.MozUserFocus = "";
}
@@ -121,7 +127,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7
on_click(event) {
if (event.button != 0) {
return;
-@@ -587,6 +611,14 @@
+@@ -587,6 +617,14 @@
// (see tabbrowser-tabs 'click' handler).
gBrowser.tabContainer._blockDblClick = true;
}
@@ -136,7 +142,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7
}
on_dblclick(event) {
-@@ -610,6 +642,8 @@
+@@ -610,6 +648,8 @@
animate: true,
triggeringEvent: event,
});
diff --git a/src/browser/components/tabbrowser/content/tabgroup-js.patch b/src/browser/components/tabbrowser/content/tabgroup-js.patch
index 3842442c7..1fb4ef153 100644
--- a/src/browser/components/tabbrowser/content/tabgroup-js.patch
+++ b/src/browser/components/tabbrowser/content/tabgroup-js.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tabgroup.js b/browser/components/tabbrowser/content/tabgroup.js
-index 394b2af2e187b82bb3e98ebcdc6e66b63036e20d..4757555c7654a14578a5d057323057ebc71c2f5f 100644
+index 394b2af2e187b82bb3e98ebcdc6e66b63036e20d..e92927612abf12631c384a8f54b6a607fb699424 100644
--- a/browser/components/tabbrowser/content/tabgroup.js
+++ b/browser/components/tabbrowser/content/tabgroup.js
@@ -14,11 +14,11 @@
@@ -68,10 +68,10 @@ index 394b2af2e187b82bb3e98ebcdc6e66b63036e20d..4757555c7654a14578a5d057323057eb
- return false;
- });
+ this.appendChild = function (child) {
-+ this.querySelector(".tab-group-container").appendChild(child);
++ this.groupContainer.appendChild(child);
+ for (let tab of this.tabs) {
+ if (tab.hasAttribute("zen-empty-tab") && tab.group === this) {
-+ this.querySelector(".zen-tab-group-start").after(tab);
++ this.groupStartElement.after(tab);
+ }
+ }
+ };
@@ -92,7 +92,7 @@ index 394b2af2e187b82bb3e98ebcdc6e66b63036e20d..4757555c7654a14578a5d057323057eb
});
}
- this.#tabChangeObserver.observe(this, { childList: true });
-+ const container = this.querySelector(".tab-group-container");
++ const container = this.groupContainer;
+ if (container) {
+ this.#tabChangeObserver.observe(container, { childList: true });
+ }
@@ -117,7 +117,7 @@ index 394b2af2e187b82bb3e98ebcdc6e66b63036e20d..4757555c7654a14578a5d057323057eb
});
}
-@@ -466,13 +492,57 @@
+@@ -466,13 +492,65 @@
* @returns {MozTabbrowserTab[]}
*/
get tabs() {
@@ -126,20 +126,29 @@ index 394b2af2e187b82bb3e98ebcdc6e66b63036e20d..4757555c7654a14578a5d057323057eb
- if (childrenArray[i].tagName == "tab-split-view-wrapper") {
- childrenArray.splice(i, 1, ...childrenArray[i].tabs);
+ // add other group tabs if they are under this group
-+ let childs = Array.from(this.querySelector(".tab-group-container")?.children ?? []);
++ let childs = Array.from(this.groupContainer?.children ?? []);
+ const tabsCollect = [];
+ for (let item of childs) {
+ tabsCollect.push(item);
+ if (gBrowser.isTabGroup(item)) {
+ tabsCollect.push(...item.tabs);
-+ }
-+ }
+ }
+ }
+- return childrenArray.filter(node => node.matches("tab"));
+ return tabsCollect.filter(node => node.matches("tab"));
+ }
+
++ get groupContainer() {
++ return this.querySelector(".tab-group-container");
++ }
++
++ get groupStartElement() {
++ return this.querySelector(".zen-tab-group-start");
++ }
++
+ get childGroupsAndTabs() {
+ const result = [];
-+ const container = this.querySelector(".tab-group-container");
++ const container = this.groupContainer;
+
+ for (const item of Array.from(container.children)) {
+ if (gBrowser.isTab(item)) {
@@ -169,9 +178,8 @@ index 394b2af2e187b82bb3e98ebcdc6e66b63036e20d..4757555c7654a14578a5d057323057eb
+ currentGroup = currentGroup?.group;
+ if (currentGroup.collapsed) {
+ return false;
- }
- }
-- return childrenArray.filter(node => node.matches("tab"));
++ }
++ }
+ return true;
+ }
+
@@ -180,7 +188,7 @@ index 394b2af2e187b82bb3e98ebcdc6e66b63036e20d..4757555c7654a14578a5d057323057eb
}
/**
-@@ -553,7 +623,6 @@
+@@ -553,7 +631,6 @@
addTabs(tabs, metricsContext) {
for (let tab of tabs) {
if (tab.pinned) {
@@ -188,7 +196,7 @@ index 394b2af2e187b82bb3e98ebcdc6e66b63036e20d..4757555c7654a14578a5d057323057eb
}
let tabToMove =
this.ownerGlobal === tab.ownerGlobal
-@@ -616,7 +685,7 @@
+@@ -616,7 +693,7 @@
*/
on_click(event) {
let isToggleElement =
@@ -197,7 +205,7 @@ index 394b2af2e187b82bb3e98ebcdc6e66b63036e20d..4757555c7654a14578a5d057323057eb
event.target === this.#overflowCountLabel;
if (isToggleElement && event.button === 0) {
event.preventDefault();
-@@ -687,5 +756,6 @@
+@@ -687,5 +764,6 @@
}
}
diff --git a/src/browser/components/tabbrowser/content/tabs-js.patch b/src/browser/components/tabbrowser/content/tabs-js.patch
index ef5bb94e3..9c2dcf099 100644
--- a/src/browser/components/tabbrowser/content/tabs-js.patch
+++ b/src/browser/components/tabbrowser/content/tabs-js.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tabs.js b/browser/components/tabbrowser/content/tabs.js
-index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac8793422474052f476573 100644
+index 6b6c04599fe80983d13d2069ca62b99d8ad70271..04144081560f1678dc9673736ef2bd9d9ca3f478 100644
--- a/browser/components/tabbrowser/content/tabs.js
+++ b/browser/components/tabbrowser/content/tabs.js
@@ -436,7 +436,7 @@
@@ -54,7 +54,7 @@ index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac879342247405
return this.hasAttribute("overflow");
}
-@@ -837,29 +839,54 @@
+@@ -837,29 +839,56 @@
if (pinnedChildren?.at(-1)?.id == "pinned-tabs-container-periphery") {
pinnedChildren.pop();
}
@@ -81,6 +81,8 @@ index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac879342247405
+ tabs.splice(i, 1);
+ // add the tabs in the group to the list
+ tabs.splice(i, 0, ...tab.tabs);
++ } else if (tab.classList.contains("zen-tab-group-start")) {
++ tabs.splice(i, 1);
+ }
+ }
+ };
@@ -119,7 +121,7 @@ index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac879342247405
}
/**
-@@ -926,17 +953,10 @@
+@@ -926,17 +955,10 @@
let elementIndex = 0;
@@ -139,7 +141,7 @@ index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac879342247405
if (isTab(child) && child.visible) {
child.elementIndex = elementIndex++;
focusableItems.push(child);
-@@ -944,11 +964,13 @@
+@@ -944,11 +966,13 @@
child.labelElement.elementIndex = elementIndex++;
focusableItems.push(child.labelElement);
@@ -154,7 +156,7 @@ index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac879342247405
} else if (child.tagName == "tab-split-view-wrapper") {
let visibleTabsInSplitView = child.tabs.filter(tab => tab.visible);
visibleTabsInSplitView.forEach(tab => {
-@@ -992,6 +1014,7 @@
+@@ -992,6 +1016,7 @@
_invalidateCachedTabs() {
this.#allTabs = null;
this._invalidateCachedVisibleTabs();
@@ -162,7 +164,7 @@ index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac879342247405
}
_invalidateCachedVisibleTabs() {
-@@ -1095,7 +1118,7 @@
+@@ -1095,7 +1120,7 @@
if (node == null) {
// We have a container for non-tab elements at the end of the scrollbox.
@@ -171,7 +173,7 @@ index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac879342247405
}
node.before(tab);
-@@ -1193,7 +1216,7 @@
+@@ -1193,7 +1218,7 @@
// There are separate "new tab" buttons for horizontal tabs toolbar, vertical tabs and
// for when the tab strip is overflowed (which is shared by vertical and horizontal tabs);
// Attach the long click popup to all of them.
@@ -180,7 +182,7 @@ index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac879342247405
const newTab2 = this.newTabButton;
const newTabVertical = document.getElementById(
"vertical-tabs-newtab-button"
-@@ -1294,8 +1317,10 @@
+@@ -1294,8 +1319,10 @@
*/
_handleTabSelect(aInstant) {
let selectedTab = this.selectedItem;
@@ -191,7 +193,7 @@ index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac879342247405
selectedTab._notselectedsinceload = false;
}
-@@ -1304,7 +1329,7 @@
+@@ -1304,7 +1331,7 @@
* @param {boolean} [shouldScrollInstantly=false]
*/
#ensureTabIsVisible(tab, shouldScrollInstantly = false) {
@@ -200,7 +202,7 @@ index 6b6c04599fe80983d13d2069ca62b99d8ad70271..a765f2decc3a565226ac879342247405
if (arrowScrollbox?.overflowing) {
arrowScrollbox.ensureElementIsVisible(tab, shouldScrollInstantly);
}
-@@ -1437,7 +1462,7 @@
+@@ -1437,7 +1464,7 @@
}
_notifyBackgroundTab(aTab) {
diff --git a/src/browser/themes/shared/zen-icons/icons.css b/src/browser/themes/shared/zen-icons/icons.css
index 80db0d71d..37443d6bc 100644
--- a/src/browser/themes/shared/zen-icons/icons.css
+++ b/src/browser/themes/shared/zen-icons/icons.css
@@ -7,6 +7,7 @@
.subviewbutton,
#zen-welcome-start-button,
.zen-toast button,
+.zen-current-workspace-indicator-chevron,
.pinned-tabs-container-separator toolbarbutton {
-moz-context-properties: fill, fill-opacity !important;
fill: currentColor !important;
@@ -116,7 +117,7 @@
}
}
-#zen-rice-share-options .options-header,
+.zen-current-workspace-indicator-chevron,
#PanelUI-zen-gradient-generator-color-page-right {
list-style-image: url('arrow-right.svg');
}
diff --git a/src/browser/themes/shared/zen-icons/lin/arrow-right.svg b/src/browser/themes/shared/zen-icons/lin/arrow-right.svg
index 6f6854344..88847cd96 100644
--- a/src/browser/themes/shared/zen-icons/lin/arrow-right.svg
+++ b/src/browser/themes/shared/zen-icons/lin/arrow-right.svg
@@ -2,4 +2,4 @@
# 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/.
-
+
diff --git a/src/zen/common/modules/ZenUIManager.mjs b/src/zen/common/modules/ZenUIManager.mjs
index ae63ace1e..70c9d10ba 100644
--- a/src/zen/common/modules/ZenUIManager.mjs
+++ b/src/zen/common/modules/ZenUIManager.mjs
@@ -125,8 +125,6 @@ window.gZenUIManager = {
}
menu.setAttribute('hidden', 'true');
}
- // The first separator in the tab context menu is now useless.
- document.getElementById('tabContextMenu').querySelector('menuseparator').remove();
},
_initCreateNewPopup() {
diff --git a/src/zen/folders/ZenFolder.mjs b/src/zen/folders/ZenFolder.mjs
index 6c1d2dbc7..3c3413e6a 100644
--- a/src/zen/folders/ZenFolder.mjs
+++ b/src/zen/folders/ZenFolder.mjs
@@ -2,7 +2,7 @@
// 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/.
-class ZenFolder extends MozTabbrowserTabGroup {
+export class nsZenFolder extends MozTabbrowserTabGroup {
#initialized = false;
static markup = `
@@ -68,7 +68,7 @@ class ZenFolder extends MozTabbrowserTabGroup {
}
this.#initialized = true;
this._activeTabs = [];
- this.icon.appendChild(ZenFolder.rawIcon.cloneNode(true));
+ this.icon.appendChild(nsZenFolder.rawIcon.cloneNode(true));
this.labelElement.parentElement.setAttribute('context', 'zenFolderActions');
@@ -81,7 +81,7 @@ class ZenFolder extends MozTabbrowserTabGroup {
};
if (this.collapsed) {
- this.querySelector('.tab-group-container').setAttribute('hidden', true);
+ this.groupContainer.setAttribute('hidden', true);
}
}
@@ -141,7 +141,7 @@ class ZenFolder extends MozTabbrowserTabGroup {
gZenFolders.createFolder([], {
renameFolder: !gZenUIManager.testingEnabled,
label: 'Subfolder',
- insertAfter: this.querySelector('.tab-group-container').lastElementChild,
+ insertAfter: this.groupContainer.lastElementChild,
});
}
@@ -181,8 +181,12 @@ class ZenFolder extends MozTabbrowserTabGroup {
}
get allItems() {
- return [...this.querySelector('.tab-group-container').children].filter(
- (child) => !child.classList.contains('zen-tab-group-start')
+ return [...this.groupContainer.children].filter(
+ (child) =>
+ !(
+ child.classList.contains('zen-tab-group-start') ||
+ child.classList.contains('pinned-tabs-container-separator')
+ )
);
}
@@ -274,4 +278,4 @@ class ZenFolder extends MozTabbrowserTabGroup {
}
}
-customElements.define('zen-folder', ZenFolder);
+customElements.define('zen-folder', nsZenFolder);
diff --git a/src/zen/folders/ZenFolders.mjs b/src/zen/folders/ZenFolders.mjs
index 4cf42f469..e1fcd7c97 100644
--- a/src/zen/folders/ZenFolders.mjs
+++ b/src/zen/folders/ZenFolders.mjs
@@ -189,6 +189,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
window.addEventListener('TabSelect', this);
window.addEventListener('TabOpen', this);
const onNewFolder = this.#onNewFolder.bind(this);
+ document.getElementById('zen-context-menu-new-folder').addEventListener('command', onNewFolder);
document
.getElementById('zen-context-menu-new-folder-toolbar')
.addEventListener('command', onNewFolder);
@@ -803,6 +804,9 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
if (!isTab && !groupElem?.hasAttribute('selected') && !forCollapse) {
groupElem = null; // Don't indent if the group is not selected
}
+ if (groupElem?.tagName.toLowerCase() === 'zen-workspace-collapsible-pins') {
+ groupElem = null; // Don't indent if it's inside the collapsible pinned tabs
+ }
let level = groupElem?.level + 1 || 0;
if (gBrowser.isTabGroupLabel(groupElem)) {
// If it is a group label, we should not increase its level by one.
@@ -1036,8 +1040,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
}
default: {
// Should insert after zen-empty-tab
- const start =
- parentWorkingData.node.querySelector('.zen-tab-group-start').nextElementSibling;
+ const start = parentWorkingData.node.groupStartElement.nextElementSibling;
start.after(node);
}
}
@@ -1128,8 +1131,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
const dropElementGroup = dropElement?.isZenFolder ? dropElement : dropElement?.group;
const isSplitGroup = dropElement?.group?.hasAttribute('split-view-group');
- let firstGroupElem =
- dropElementGroup?.querySelector('.zen-tab-group-start')?.nextElementSibling;
+ let firstGroupElem = dropElementGroup?.groupStartElement.nextElementSibling;
if (gBrowser.isTabGroup(firstGroupElem)) firstGroupElem = firstGroupElem.labelElement;
const isInMiddleZone =
@@ -1212,6 +1214,11 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
return heightShift;
} else {
heightShift += window.windowUtils.getBoundsWithoutFlushing(tabsContainer).height;
+ if (tabsContainer.separatorElement) {
+ heightShift -= window.windowUtils.getBoundsWithoutFlushing(
+ tabsContainer.separatorElement
+ ).height;
+ }
}
return heightShift;
}
@@ -1225,8 +1232,8 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
const activeFoldersIds = new Set();
const itemsToHide = [];
- const tabsContainer = group.querySelector('.tab-group-container');
- const groupStart = group.querySelector('.zen-tab-group-start');
+ const tabsContainer = group.groupContainer;
+ const groupStart = group.groupStartElement;
const groupItems = this.#collectGroupItems(group, {
selectedTabs,
@@ -1304,11 +1311,11 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
const animations = [];
const itemsToHide = [];
- const tabsContainer = group.querySelector('.tab-group-container');
+ const tabsContainer = group.groupContainer;
tabsContainer.removeAttribute('hidden');
tabsContainer.style.overflow = 'hidden';
- const groupStart = group.querySelector('.zen-tab-group-start');
+ const groupStart = group.groupStartElement;
const itemsToShow = this.#normalizeGroupItems(group.childGroupsAndTabs);
const activeFolders = group.childActiveGroups;
@@ -1422,7 +1429,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
folder.removeAttribute('has-active');
folder.activeTabs = [];
const groupItems = this.#normalizeGroupItems(folder.allItems);
- const tabsContainer = folder.querySelector('.tab-group-container');
+ const tabsContainer = folder.groupContainer;
// Set correct margin-top after animation
const afterAnimate = () => {
@@ -1436,7 +1443,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
groupStart.style.marginTop = `${-(collapsedHeight + 4)}px`;
};
- const groupStart = folder.querySelector('.zen-tab-group-start');
+ const groupStart = folder.groupStartElement;
const collapsedHeight = this.#calculateHeightShift(tabsContainer, []);
// Collect animations for this specific folder becoming inactive
@@ -1474,7 +1481,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
animations.push(async () => {
folder.removeAttribute('has-active');
const groupItems = this.#normalizeGroupItems(folder.allItems);
- const tabsContainer = folder.querySelector('.tab-group-container');
+ const tabsContainer = folder.groupContainer;
// Set correct margin-top after animation
const afterAnimate = () => {
@@ -1488,7 +1495,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
groupStart.style.marginTop = `${-(collapsedHeight + 4)}px`;
};
- const groupStart = folder.querySelector('.zen-tab-group-start');
+ const groupStart = folder.groupStartElement;
const collapsedHeight = this.#calculateHeightShift(tabsContainer, []);
// Collect animations for this specific folder becoming inactive
@@ -1573,8 +1580,8 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
currentGroup.activeTabs = activeTabs;
}
- const tabsContainer = currentGroup.querySelector('.tab-group-container');
- const groupStart = currentGroup.querySelector('.zen-tab-group-start');
+ const tabsContainer = currentGroup.groupContainer;
+ const groupStart = currentGroup.groupStartElement;
tabsContainer.style.overflow = 'clip';
if (tabsContainer.hasAttribute('hidden')) tabsContainer.removeAttribute('hidden');
@@ -1673,8 +1680,8 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
animateGroupMove(group, expand = false) {
if (!group?.isZenFolder) return;
- const groupStart = group.querySelector('.zen-tab-group-start');
- const tabsContainer = group.querySelector('.tab-group-container');
+ const groupStart = group.groupStartElement;
+ const tabsContainer = group.groupContainer;
const heightContainer = expand ? 0 : this.#calculateHeightShift(tabsContainer, []);
tabsContainer.style.overflow = 'clip';
diff --git a/src/zen/folders/zen-folders.css b/src/zen/folders/zen-folders.css
index f1be64211..1cd1cb43d 100644
--- a/src/zen/folders/zen-folders.css
+++ b/src/zen/folders/zen-folders.css
@@ -202,16 +202,6 @@ zen-folder {
}
}
- &[collapsed] {
- & > .tabbrowser-tab:not([hidden]) {
- display: flex;
- }
-
- &:not([has-active]) > .tab-group-container {
- overflow-y: clip;
- }
- }
-
:root[zen-sidebar-expanded] &[has-active] > .tab-group-label-container {
& .tab-reset-button {
display: flex;
@@ -224,6 +214,11 @@ zen-folder {
}
}
+zen-workspace[collapsedpinnedtabs] .zen-workspace-pinned-tabs-section,
+zen-folder[collapsed]:not([has-active]) > .tab-group-container {
+ overflow-y: clip;
+}
+
/* Tabs popup */
#zen-folder-tabs-popup {
--arrowpanel-padding: 0;
diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs
index 254d1e379..9b82ae703 100644
--- a/src/zen/sessionstore/ZenSessionManager.sys.mjs
+++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs
@@ -224,6 +224,7 @@ export class nsZenSessionManager {
// If there's no initial state, nothing to restore. This would
// happen if the file is empty or corrupted.
if (!initialState) {
+ this.log('No initial state to restore!');
return;
}
// If there are no windows, we create an empty one. By default,
@@ -249,7 +250,7 @@ export class nsZenSessionManager {
// guarantee that all tabs, groups, folders and split view data
// are properly synced across all windows.
this.log(`Restoring Zen session data into ${initialState.windows?.length || 0} windows`);
- for (const winData of initialState.windows || []) {
+ for (const winData of initialState.windows) {
this.#restoreWindowData(winData);
}
}
diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs
index 2d8878751..c8f2dfd17 100644
--- a/src/zen/sessionstore/ZenWindowSync.sys.mjs
+++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs
@@ -176,8 +176,8 @@ class nsZenWindowSync {
// This should only happen really when updating from an older version
// that didn't have this feature.
this.#runOnAllWindows(null, (aWindow) => {
- const { gBrowser } = aWindow;
- for (let tab of gBrowser.tabs) {
+ const { gZenWorkspaces } = aWindow;
+ for (let tab of gZenWorkspaces.allStoredTabs) {
if (!tab.id) {
tab.id = this.#newTabSyncId;
lazy.TabStateFlusher.flush(tab.linkedBrowser);
diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs
index fda9a0ebd..1ab19ee08 100644
--- a/src/zen/tabs/ZenPinnedTabManager.mjs
+++ b/src/zen/tabs/ZenPinnedTabManager.mjs
@@ -322,11 +322,14 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
const state = this.#getTabState(tab);
const initialState = tab._zenPinnedInitialState;
+ if (!initialState?.entry) {
+ return;
+ }
// Remove everything except the entry we want to keep
state.entries = [initialState.entry];
- state.image = initialState.image;
+ state.image = tab.zenStaticIcon || initialState.image;
state.index = 0;
SessionStore.setTabState(tab, state);
diff --git a/src/zen/workspaces/ZenGradientGenerator.mjs b/src/zen/workspaces/ZenGradientGenerator.mjs
index a1495d4ed..ad0ee2497 100644
--- a/src/zen/workspaces/ZenGradientGenerator.mjs
+++ b/src/zen/workspaces/ZenGradientGenerator.mjs
@@ -1,6 +1,6 @@
-// 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/.
+/* 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/. */
import { nsZenMultiWindowFeature } from 'chrome://browser/content/zen-components/ZenCommonUtils.mjs';
diff --git a/src/zen/workspaces/ZenWorkspace.mjs b/src/zen/workspaces/ZenWorkspace.mjs
index 7cb1af16d..a888b6732 100644
--- a/src/zen/workspaces/ZenWorkspace.mjs
+++ b/src/zen/workspaces/ZenWorkspace.mjs
@@ -1,17 +1,59 @@
-// 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/.
+/* 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/. */
+
+import { nsZenFolder } from 'chrome://browser/content/zen-components/ZenFolder.mjs';
+
+// A helper class to manage collapsible pinned tabs in a workspace.
+class nsZenCollapsiblePins extends nsZenFolder {
+ #spaceElement;
+
+ connectedCallback() {
+ this.setAttribute('hidden', 'true');
+ this.#spaceElement = this.parentElement;
+ super.connectedCallback();
+ }
+
+ get groupContainer() {
+ return this.#spaceElement.pinnedTabsContainer;
+ }
+
+ get groupStartElement() {
+ // Fetch this instead of the tab-group-start since it is not guaranteed this
+ // element will be the first child of the pinned tabs container.
+ return this.#spaceElement.pinnedTabsContainer.querySelector('.space-fake-collapsible-start');
+ }
+
+ get collapsed() {
+ return super.collapsed;
+ }
+
+ set collapsed(value) {
+ if (value) {
+ this.#spaceElement.setAttribute('collapsedpinnedtabs', 'true');
+ } else {
+ this.#spaceElement.removeAttribute('collapsedpinnedtabs');
+ }
+ super.collapsed = value;
+ }
+}
+
+export class nsZenWorkspace extends MozXULElement {
+ #initialPinnedElementChildrenCount;
-class nsZenWorkspace extends MozXULElement {
static get markup() {
return `
-
+
+
+
+
+
{
+ if (this.hasPinnedTabs) {
+ // Prevent renaming when there are pinned tabs
+ event.stopPropagation();
+ }
+ });
this.pinnedTabsContainer.scrollbox = this.scrollbox;
+ this.#initialPinnedElementChildrenCount = this.pinnedTabsContainer.children.length;
this.indicator
.querySelector('.zen-workspaces-actions')
@@ -92,10 +144,20 @@ class nsZenWorkspace extends MozXULElement {
this.indicator
.querySelector('.zen-current-workspace-indicator-icon')
.addEventListener('dblclick', (event) => {
+ if (this.hasPinnedTabs) {
+ return;
+ }
event.stopPropagation();
gZenWorkspaces.changeWorkspaceIcon();
});
+ this.indicator.addEventListener('click', (event) => {
+ if (this.hasPinnedTabs) {
+ event.stopPropagation();
+ this.collapsiblePins.collapsed = !this.collapsiblePins.collapsed;
+ }
+ });
+
if (!gZenWorkspaces.currentWindowIsSyncing) {
let actionsButton = this.indicator.querySelector('.zen-workspaces-actions');
const moveTabToFragment = window.MozXULElement.parseXULToFragment(
@@ -169,11 +231,26 @@ class nsZenWorkspace extends MozXULElement {
this.tabsContainer.setAttribute('zen-workspace-id', this.id);
this.pinnedTabsContainer.setAttribute('zen-workspace-id', this.id);
+ this.collapsiblePins = document.createXULElement('zen-workspace-collapsible-pins');
+ this.prepend(this.collapsiblePins);
+
this.#updateOverflow();
this.onGradientCacheChanged = this.#onGradientCacheChanged.bind(this);
window.addEventListener('ZenGradientCacheChanged', this.onGradientCacheChanged);
+ const tabPinCallback = () => {
+ this.checkPinsExistence();
+ };
+
+ this.addEventListener('TabPinned', tabPinCallback);
+ this.addEventListener('TabUnpinned', tabPinCallback);
+ this.addEventListener('TabClose', (event) => {
+ if (event.target.pinned) {
+ tabPinCallback();
+ }
+ });
+
this.dispatchEvent(
new CustomEvent('ZenWorkspaceAttached', {
bubbles: true,
@@ -185,6 +262,7 @@ class nsZenWorkspace extends MozXULElement {
disconnectedCallback() {
window.removeEventListener('ZenGradientCacheChanged', this.onGradientCacheChanged);
+ super.disconnectedCallback();
}
get active() {
@@ -200,6 +278,14 @@ class nsZenWorkspace extends MozXULElement {
this.#updateOverflow();
}
+ get hasPinnedTabs() {
+ return this.hasAttribute('haspinnedtabs');
+ }
+
+ get hasCollapsedPinnedTabs() {
+ return this.hasAttribute('collapsedpinnedtabs');
+ }
+
#updateOverflow() {
if (!this.scrollbox) return;
if (this.overflows) {
@@ -273,6 +359,15 @@ class nsZenWorkspace extends MozXULElement {
this.style.setProperty('--zen-primary-color', primaryColor);
}
+ checkPinsExistence() {
+ if (this.pinnedTabsContainer.children.length > this.#initialPinnedElementChildrenCount) {
+ this.setAttribute('haspinnedtabs', 'true');
+ } else {
+ this.removeAttribute('haspinnedtabs');
+ this.collapsiblePins.collapsed = false;
+ }
+ }
+
clearThemeStyles() {
this.style.colorScheme = '';
this.style.removeProperty('--toolbox-textcolor');
@@ -311,3 +406,4 @@ class nsZenWorkspace extends MozXULElement {
}
customElements.define('zen-workspace', nsZenWorkspace);
+customElements.define('zen-workspace-collapsible-pins', nsZenCollapsiblePins);
diff --git a/src/zen/workspaces/ZenWorkspaceCreation.mjs b/src/zen/workspaces/ZenWorkspaceCreation.mjs
index d037bade7..23b74e073 100644
--- a/src/zen/workspaces/ZenWorkspaceCreation.mjs
+++ b/src/zen/workspaces/ZenWorkspaceCreation.mjs
@@ -1,6 +1,6 @@
-// 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/.
+/* 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/. */
class nsZenWorkspaceCreation extends MozXULElement {
#wasInCollapsedMode = false;
diff --git a/src/zen/workspaces/ZenWorkspaceIcons.mjs b/src/zen/workspaces/ZenWorkspaceIcons.mjs
index 9672c4a27..a9b8b99a3 100644
--- a/src/zen/workspaces/ZenWorkspaceIcons.mjs
+++ b/src/zen/workspaces/ZenWorkspaceIcons.mjs
@@ -1,6 +1,6 @@
-// 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/.
+/* 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/. */
class nsZenWorkspaceIcons extends MozXULElement {
constructor() {
diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs
index 872b6af93..d70e70dc7 100644
--- a/src/zen/workspaces/ZenWorkspaces.mjs
+++ b/src/zen/workspaces/ZenWorkspaces.mjs
@@ -1,6 +1,6 @@
-// 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/.
+/* 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/. */
import { nsZenThemePicker } from 'chrome://browser/content/zen-components/ZenGradientGenerator.mjs';
@@ -465,6 +465,7 @@ class nsZenWorkspaces {
workspaceWrapper.pinnedTabsContainer,
tabs
);
+ workspaceWrapper.checkPinsExistence();
resolve();
},
{ once: true }
@@ -827,6 +828,20 @@ class nsZenWorkspaces {
return [...this._workspaceCache];
}
+ getWorkspacesForSessionStore() {
+ const spaces = this.getWorkspaces();
+ let spacesForSS = [];
+ for (const space of spaces) {
+ let newSpace = { ...space };
+ const element = this.workspaceElement(space.uuid);
+ if (element) {
+ newSpace.hasCollapsedPinnedTabs = element.hasCollapsedPinnedTabs;
+ }
+ spacesForSS.push(newSpace);
+ }
+ return spacesForSS;
+ }
+
async workspaceBookmarks() {
if (this.privateWindowOrDisabled) {
this._workspaceBookmarksCache = {
@@ -854,11 +869,22 @@ class nsZenWorkspaces {
if (this.#hasInitialized) {
return;
}
- this._workspaceCache = aWinData.spaces?.length
- ? aWinData.spaces
+ const spacesFromStore = aWinData.spaces || [];
+ this._workspaceCache = spacesFromStore.length
+ ? [...spacesFromStore]
: [await this.createAndSaveWorkspace('Space', undefined, true)];
+ for (const workspace of this._workspaceCache) {
+ // We don't want to depend on this by mistake
+ delete workspace.hasCollapsedPinnedTabs;
+ }
this.activeWorkspace = aWinData.activeZenSpace || this._workspaceCache[0].uuid;
await this.initializeWorkspaces();
+ for (const workspace of spacesFromStore) {
+ const element = this.workspaceElement(workspace.uuid);
+ if (element) {
+ element.collapsiblePins.collapsed = workspace.hasCollapsedPinnedTabs || false;
+ }
+ }
this.#hasInitialized = true;
}
@@ -1788,11 +1814,14 @@ class nsZenWorkspaces {
}
const indicatorName = workspaceIndicator.querySelector('.zen-current-workspace-indicator-name');
const indicatorIcon = workspaceIndicator.querySelector('.zen-current-workspace-indicator-icon');
+ const iconStack = workspaceIndicator.querySelector('.zen-current-workspace-indicator-stack');
if (this.workspaceHasIcon(currentWorkspace)) {
indicatorIcon.removeAttribute('no-icon');
+ iconStack.removeAttribute('no-icon');
} else {
indicatorIcon.setAttribute('no-icon', 'true');
+ iconStack.setAttribute('no-icon', 'true');
}
const icon = this.getWorkspaceIcon(currentWorkspace);
indicatorIcon.innerHTML = '';
diff --git a/src/zen/workspaces/ZenWorkspacesStorage.mjs b/src/zen/workspaces/ZenWorkspacesStorage.mjs
index 80c7a82e5..f7df8a31e 100644
--- a/src/zen/workspaces/ZenWorkspacesStorage.mjs
+++ b/src/zen/workspaces/ZenWorkspacesStorage.mjs
@@ -1,6 +1,6 @@
-// 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/.
+/* 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/. */
// Integration of workspace-specific bookmarks into Places
window.ZenWorkspaceBookmarksStorage = {
diff --git a/src/zen/workspaces/zen-workspaces.css b/src/zen/workspaces/zen-workspaces.css
index 584c366b1..7c258c712 100644
--- a/src/zen/workspaces/zen-workspaces.css
+++ b/src/zen/workspaces/zen-workspaces.css
@@ -155,13 +155,14 @@
/* Mark workspaces indicator */
.zen-current-workspace-indicator {
+ --indicator-gap: 10px;
margin-top: 1px;
padding: calc(2px + var(--tab-inline-padding) + var(--zen-toolbox-padding));
font-weight: 500;
position: relative;
max-height: var(--zen-workspace-indicator-height);
min-height: var(--zen-workspace-indicator-height);
- gap: 10px;
+ gap: var(--indicator-gap);
align-items: center;
flex-direction: row !important;
max-width: 100%;
@@ -368,3 +369,52 @@ zen-workspace {
}
%include create-workspace-form.css
+
+/* Pinned tabs collapse styles */
+
+.zen-current-workspace-indicator-chevron {
+ display: none;
+}
+
+:root[zen-sidebar-expanded] {
+ .zen-current-workspace-indicator-stack {
+ transition: margin-inline-end 0.1s;
+
+ &[no-icon='true'] {
+ margin-inline-end: calc(-1 * (var(--indicator-gap) + 16px));
+ }
+ }
+
+ .zen-current-workspace-indicator-chevron {
+ width: 16px;
+ height: 16px;
+ transition: transform 0.2s, opacity 0.2s;
+ transform: rotate(90deg);
+ padding: 2px;
+
+ .zen-current-workspace-indicator-stack[no-icon='true'] & {
+ display: flex;
+ opacity: 0;
+ }
+ }
+
+ & zen-workspace[haspinnedtabs] .zen-current-workspace-indicator:hover,
+ & zen-workspace[collapsedpinnedtabs] .zen-current-workspace-indicator {
+ .zen-current-workspace-indicator-chevron {
+ display: flex;
+ opacity: 1;
+ }
+
+ .zen-current-workspace-indicator-stack {
+ margin-inline-end: 0;
+ }
+
+ .zen-current-workspace-indicator-icon {
+ display: none;
+ }
+ }
+
+ zen-workspace[collapsedpinnedtabs] .zen-current-workspace-indicator-chevron {
+ transform: rotate(0deg);
+ }
+}
From 335c024e6da8dfa705193a01c88616704ec48d21 Mon Sep 17 00:00:00 2001
From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com>
Date: Sun, 28 Dec 2025 18:26:52 +0100
Subject: [PATCH 2/2] chore: Drag and drop refactor, p=#11723
* feat: Full cross-window workspace syncing, b=no-bug, c=workspaces
* feat: Also change icons and labels if the tab is pending, b=no-bug, c=tabs, workspaces
* feat: Dont session duplicate the tabs, b=no-bug, c=workspaces
* feat: Properly handle tab moves, b=no-bug, c=workspaces
* feat: Start on new session restore, b=no-bug, c=no-component
* Discard changes to prefs/browser.yaml
* feat: Start doing out own session restore, b=no-bug, c=folders, tabs
* feat: Stop using pinned manager and use zen session sidebar, b=no-bug, c=common, folders, tabs, workspaces
* feat: Dont restore windows that are already initialized, b=no-bug, c=no-component
* chore: Update patches to ff 145, b=no-bug, c=no-component
* Discard changes to src/browser/components/sessionstore/SessionStore-sys-mjs.patch
* Discard changes to src/browser/components/tabbrowser/content/tab-js.patch
* Discard changes to src/browser/components/tabbrowser/content/tabbrowser-js.patch
* Discard changes to src/zen/tabs/ZenPinnedTabsStorage.mjs
* feat: Run session saver before opening a new winodw, b=no-bug, c=tabs
* feat: Clone the previous state, b=no-bug, c=no-component
* feat: Move window sync to its own JS module, b=no-bug, c=workspaces
* feat: Run session saver before opening a new window, b=no-bug, c=no-component
* feat: Full cross-window workspace syncing, b=no-bug, c=workspaces
* feat: Also change icons and labels if the tab is pending, b=no-bug, c=tabs, workspaces
* feat: Dont session duplicate the tabs, b=no-bug, c=workspaces
* feat: Start on new session restore, b=no-bug, c=no-component
* feat: Properly handle tab moves, b=no-bug, c=workspaces
* Discard changes to prefs/browser.yaml
* feat: Start doing out own session restore, b=no-bug, c=folders, tabs
* feat: Stop using pinned manager and use zen session sidebar, b=no-bug, c=common, folders, tabs, workspaces
* feat: Dont restore windows that are already initialized, b=no-bug, c=no-component
* chore: Update patches to ff 145, b=no-bug, c=no-component
* Discard changes to src/browser/components/sessionstore/SessionStore-sys-mjs.patch
* Discard changes to src/browser/components/tabbrowser/content/tab-js.patch
* Discard changes to src/browser/components/tabbrowser/content/tabbrowser-js.patch
* Discard changes to src/zen/tabs/ZenPinnedTabsStorage.mjs
* feat: Run session saver before opening a new winodw, b=no-bug, c=tabs
* feat: Clone the previous state, b=no-bug, c=no-component
* feat: Move window sync to its own JS module, b=no-bug, c=workspaces
* feat: Run session saver before opening a new window, b=no-bug, c=no-component
* feat: Start making use of IDs instead of sync identifiers, b=no-bug, c=folders
* feat: Listen to new tab opens for new sync system, b=no-bug, c=common, folders, tabs
* feat: Listen for more tab events and properly sync them, b=no-bug, c=common, folders, tabs
* feat: Start moving browser views to the selected windows, b=no-bug, c=no-component
* chore: Remove extra patch, b=no-bug, c=no-component
* feat: Leave a screenshot of the page behind when switching windows or tabs, b=no-bug, c=common
* feat: Run session saves right before writing and quiting, b=no-bug, c=common
* fix: Fixed going back to a different window not allowing to type on inputs, b=no-bug, c=no-component
* feat: Start syncing folders as well, b=no-bug, c=folders
* Discard changes to src/browser/components/tabbrowser/content/tab-js.patch
* chore: Update patches to ff 146, b=no-bug, c=no-component
* feat: Early support for unsynced windoiws, b=no-bug, c=workspaces
* fix: Move back active views when closing a window, b=no-bug, c=no-component
* feat: Stop rendering sub-layers when swaping browsers, b=no-bug, c=common
* feat: Improved support for unsynced windows support, b=no-bug, c=workspaces, folders
* feat: Implemented 'Move To...' Button for unsynced windows, b=no-bug, c=workspaces, common
* feat: Make sure to properly flush all windows when making a new one and fix removing progress listeners, b=no-bug, c=workspaces
* feat: Make sure to not lose any tabs when opening from a private window, b=no-bug, c=workspaces
* feat: Allow unload to run instantly and fix closing windows on mac, b=no-bug, c=no-component
* feat: Make sure to always initialize an empty state with the sidebar object, b=no-bug, c=workspaces
* chore: Small fixes and QA checks, b=no-bug, c=tabs, workspaces
* fix: Fixed tab labels not changing on unfocused windows, b=no-bug, c=no-component
* feat: Fixed closing windows on macos not returning to the original views, b=no-bug, c=no-component
* chore: Turn off debug flags by default, b=no-bug, c=no-component
* feat: Start implementing old pinned tab behaviour we used to have, b=no-bug, c=common, tabs
* feat: Unsynced windows should always be allowed to change labels, b=no-bug, c=welcome
* feat: Make sure we wait long enough before we initialize workspaces, b=no-bug, c=workspaces
* feat: Dont mix remoteness when changing browser views and restore window spaces, b=no-bug, c=common, folders, workspaces
* test: Fixed tests for the pinned tabs manager, b=no-bug, c=tabs, folders, tests, welcome
* feat: Added partial support for split views, b=no-bug, c=split-view
* chore: Finished basic support for split views, b=no-bug, c=folders, split-view
* feat: Always make sure to save the last closed window to the sidebar object, b=no-bug, c=no-component
* feat: Implement workspace sync store into the session file, b=closes #10857, c=common, tabs, tests, workspaces
* feat: New drag and drop system, b=no-bug, c=tabs, common, folders
* feat: Add support for drag-and-dropping tabs into groups, b=no-bug, c=common, folders, tabs, workspaces
* feat: Add a default value for the workspace cache, b=no-bug, c=workspaces
* fix: Default assign an array instead of an object to the workspaces cache, b=no-bug, c=workspaces
* feat: Take into consideration win data may not have allocated spaces, b=no-bug, c=workspaces
* feat: Always make sure we are assigning the correct space ID, b=no-bug, c=workspaces
* feat: Make sure to initialize windows even if there are no tabs, b=no-bug, c=no-component
* feat: Improved drag and drop simulation, b=no-bug, c=common, tabs
* feat: Implement opacity changes to the drag image, b=no-bug, c=tabs, common, split-view
* feat: Support for drag and dropping outside the window, b=no-bug, c=split-view
* feat: Added transitions when reordering, b=no-bug, c=split-view, tabs
* feat: Started working on drag and dropping to essentials, b=no-bug, c=no-component
* Discard changes to locales/en-US/browser/browser/zen-workspaces.ftl
* Discard changes to prefs/zen/view.yaml
* Discard changes to prefs/zen/zen.yaml
* Discard changes to src/browser/base/content/zen-assets.inc.xhtml
* Discard changes to src/browser/base/content/zen-assets.jar.inc.mn
* Discard changes to src/browser/base/content/zen-panels/popups.inc
* Discard changes to src/browser/base/content/zen-preloaded.inc.xhtml
* Discard changes to src/browser/components/places/content/editBookmark-js.patch
* Discard changes to src/browser/components/sessionstore/SessionStore-sys-mjs.patch
* Discard changes to src/browser/components/sessionstore/TabState-sys-mjs.patch
* Discard changes to src/browser/components/tabbrowser/content/tab-js.patch
* Discard changes to src/browser/components/urlbar/UrlbarProviderPlaces-sys-mjs.patch
* Discard changes to src/zen/ZenComponents.manifest
* Discard changes to src/browser/components/tabbrowser/content/tabbrowser-js.patch
* feat: Finish migration, b=no-bug, c=no-component
* feat: Add support for multi tabs dragging, b=no-bug, c=tabs
* feat: Added support for essential tabs, b=no-bug, c=split-view, tabs, workspaces
* feat: Added support to switch space when holding on the side of the sidebar, b=no-bug, c=common, split-view, workspaces
* Discard changes to src/browser/components/tabbrowser/content/tabbrowser-js.patch
* chore: Fixed merge conflicts, b=no-bug, c=no-component
* feat: Added support for split views, b=no-bug, c=split-view
---------
Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
---
prefs/zen/view.yaml | 6 -
prefs/zen/zen.yaml | 9 +
.../base/content/zen-assets.jar.inc.mn | 1 +
.../tabbrowser/content/drag-and-drop-js.patch | 292 +---
.../tabbrowser/content/tabbrowser-js.patch | 159 +--
.../tabbrowser/content/tabs-js.patch | 11 +-
src/widget/cocoa/nsDragService-mm.patch | 47 +
src/zen/common/ZenPreloadedScripts.js | 2 +
src/zen/common/modules/ZenUIManager.mjs | 1 +
src/zen/drag-and-drop/ZenDragAndDrop.js | 1223 +++++++++++++++++
src/zen/drag-and-drop/components.conf | 14 +
src/zen/drag-and-drop/jar.inc.mn | 5 +
src/zen/drag-and-drop/moz.build | 31 +
src/zen/drag-and-drop/nsIZenDragAndDrop.idl | 25 +
src/zen/drag-and-drop/nsZenDragAndDrop.cpp | 48 +
src/zen/drag-and-drop/nsZenDragAndDrop.h | 45 +
src/zen/folders/ZenFolder.mjs | 2 +-
src/zen/folders/ZenFolders.mjs | 60 +-
src/zen/moz.build | 1 +
src/zen/sessionstore/ZenWindowSync.sys.mjs | 6 +
src/zen/split-view/ZenViewSplitter.mjs | 179 +--
src/zen/split-view/zen-decks.css | 41 +-
src/zen/tabs/ZenPinnedTabManager.mjs | 109 +-
src/zen/tabs/zen-tabs/vertical-tabs.css | 24 +-
src/zen/workspaces/ZenWorkspace.mjs | 2 +-
src/zen/workspaces/ZenWorkspaces.mjs | 20 +-
26 files changed, 1755 insertions(+), 608 deletions(-)
create mode 100644 src/widget/cocoa/nsDragService-mm.patch
create mode 100644 src/zen/drag-and-drop/ZenDragAndDrop.js
create mode 100644 src/zen/drag-and-drop/components.conf
create mode 100644 src/zen/drag-and-drop/jar.inc.mn
create mode 100644 src/zen/drag-and-drop/moz.build
create mode 100644 src/zen/drag-and-drop/nsIZenDragAndDrop.idl
create mode 100644 src/zen/drag-and-drop/nsZenDragAndDrop.cpp
create mode 100644 src/zen/drag-and-drop/nsZenDragAndDrop.h
diff --git a/prefs/zen/view.yaml b/prefs/zen/view.yaml
index 83d21386a..df33af7c9 100644
--- a/prefs/zen/view.yaml
+++ b/prefs/zen/view.yaml
@@ -40,12 +40,6 @@
- name: zen.view.window.scheme
value: 2
-- name: zen.view.drag-and-drop.move-over-threshold
- value: 70
-
-- name: zen.view.drag-and-drop.edge-zone-threshold
- value: 25
-
- name: zen.view.context-menu.refresh
value: '@IS_TWILIGHT@'
diff --git a/prefs/zen/zen.yaml b/prefs/zen/zen.yaml
index 9c6e9d787..4dbcbd18d 100644
--- a/prefs/zen/zen.yaml
+++ b/prefs/zen/zen.yaml
@@ -20,6 +20,15 @@
- name: zen.tabs.close-window-with-empty
value: true
+- name: zen.tabs.use-legacy-drag-and-drop
+ value: false
+
+- name: zen.tabs.folder-dragover-threshold-percent
+ value: 20 # Percentage of folder height to trigger dragover
+
+- name: zen.tabs.dnd-switch-space-delay
+ value: 1000 # milliseconds
+
- name: zen.ctrlTab.show-pending-tabs
value: false
diff --git a/src/browser/base/content/zen-assets.jar.inc.mn b/src/browser/base/content/zen-assets.jar.inc.mn
index 04f9cfb52..d97cdfaf6 100644
--- a/src/browser/base/content/zen-assets.jar.inc.mn
+++ b/src/browser/base/content/zen-assets.jar.inc.mn
@@ -4,6 +4,7 @@
#include ../../../zen/common/jar.inc.mn
#include ../../../zen/compact-mode/jar.inc.mn
+#include ../../../zen/drag-and-drop/jar.inc.mn
#include ../../../zen/split-view/jar.inc.mn
#include ../../../zen/mods/jar.inc.mn
#include ../../../zen/workspaces/jar.inc.mn
diff --git a/src/browser/components/tabbrowser/content/drag-and-drop-js.patch b/src/browser/components/tabbrowser/content/drag-and-drop-js.patch
index 3cc560c34..01d32e2f7 100644
--- a/src/browser/components/tabbrowser/content/drag-and-drop-js.patch
+++ b/src/browser/components/tabbrowser/content/drag-and-drop-js.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/drag-and-drop.js b/browser/components/tabbrowser/content/drag-and-drop.js
-index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9548dce83 100644
+index 97b931c3c7385a52d20204369fcf6d6999053687..6a136cf14d0bc081507a05f298f12ac7a7914601 100644
--- a/browser/components/tabbrowser/content/drag-and-drop.js
+++ b/browser/components/tabbrowser/content/drag-and-drop.js
@@ -32,6 +32,9 @@
@@ -12,26 +12,33 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
if (isTab(element)) {
return element;
}
-@@ -112,6 +115,10 @@
+@@ -112,6 +115,9 @@
}
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
+ if (draggedTab && dropEffect === "move") {
-+ gZenPinnedTabManager.applyDragoverClass(event, draggedTab);
+ gZenViewSplitter.onBrowserDragEndToSplit(event);
+ }
if (
(dropEffect == "move" || dropEffect == "copy") &&
document == draggedTab.ownerDocument &&
-@@ -266,6 +273,18 @@
+@@ -130,10 +136,6 @@
+
+ // Pinned tabs in expanded vertical mode are on a grid format and require
+ // different logic to drag and drop.
+- if (this._isContainerVerticalPinnedGrid(draggedTab)) {
+- this._animateExpandedPinnedTabMove(event);
+- return;
+- }
+ this._animateTabMove(event);
+ return;
+ }
+@@ -266,6 +268,15 @@
this._tabDropIndicator.hidden = true;
event.stopPropagation();
-+ if (draggedTab?.hasAttribute("zen-has-splitted")) {
-+ draggedTab.removeAttribute("zen-has-splitted");
-+ draggedTab._visuallySelected = false;
-+ }
+ if (draggedTab && dropEffect == "move") {
++ this.handle_drop_transition?.(draggedTab._dragData.dropElement, draggedTab, movingTabs, draggedTab._dragData.dropBefore);
+ let moved = gZenPinnedTabManager.moveToAnotherTabContainerIfNecessary(event, movingTabs);
+
+ if (moved) {
@@ -42,7 +49,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
if (draggedTab && dropEffect == "copy") {
let duplicatedDraggedTab;
let duplicatedTabs = [];
-@@ -291,8 +310,9 @@
+@@ -291,8 +302,9 @@
let translateOffsetY = oldTranslateY % tabHeight;
let newTranslateX = oldTranslateX - translateOffsetX;
let newTranslateY = oldTranslateY - translateOffsetY;
@@ -54,7 +61,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
if (this._isContainerVerticalPinnedGrid(draggedTab)) {
// Update both translate axis for pinned vertical expanded tabs
-@@ -308,8 +328,8 @@
+@@ -308,8 +320,8 @@
}
} else {
let tabs = this._tabbrowserTabs.ariaFocusableItems.slice(
@@ -65,7 +72,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
);
let size = this._tabbrowserTabs.verticalMode ? "height" : "width";
let screenAxis = this._tabbrowserTabs.verticalMode
-@@ -362,11 +382,13 @@
+@@ -362,11 +374,13 @@
this._dragToPinPromoCard,
];
let shouldPin =
@@ -79,7 +86,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
isTab(draggedTab) &&
draggedTab.pinned &&
this._tabbrowserTabs.arrowScrollbox.contains(event.target);
-@@ -384,6 +406,7 @@
+@@ -384,6 +398,7 @@
(oldTranslateY && oldTranslateY != newTranslateY);
} else if (this._tabbrowserTabs.verticalMode) {
shouldTranslate &&= oldTranslateY && oldTranslateY != newTranslateY;
@@ -87,7 +94,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
} else {
shouldTranslate &&= oldTranslateX && oldTranslateX != newTranslateX;
}
-@@ -440,7 +463,7 @@
+@@ -440,7 +455,7 @@
item.removeAttribute("tabdrop-samewindow");
resolve();
};
@@ -96,7 +103,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
postTransitionCleanup();
} else {
let onTransitionEnd = transitionendEvent => {
-@@ -581,6 +604,7 @@
+@@ -581,6 +596,7 @@
let nextItem = this._tabbrowserTabs.ariaFocusableItems[newIndex];
let tabGroup = isTab(nextItem) && nextItem.group;
@@ -104,7 +111,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
gBrowser.loadTabs(urls, {
inBackground,
replace,
-@@ -618,7 +642,16 @@
+@@ -618,7 +634,16 @@
this._expandGroupOnDrop(draggedTab);
}
this._resetTabsAfterDrop(draggedTab.ownerDocument);
@@ -122,7 +129,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
if (
dt.mozUserCancelled ||
dt.dropEffect != "none" ||
-@@ -822,7 +855,10 @@
+@@ -822,7 +847,10 @@
_getDragTarget(event, { ignoreSides = false } = {}) {
let { target } = event;
while (target) {
@@ -134,7 +141,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
break;
}
target = target.parentNode;
-@@ -839,14 +875,17 @@
+@@ -839,14 +867,17 @@
return null;
}
}
@@ -154,7 +161,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
!this._tabbrowserTabs.expandOnHover
);
}
-@@ -877,7 +916,8 @@
+@@ -877,7 +908,8 @@
isTabGroupLabel(draggedTab) &&
draggedTab._dragData?.expandGroupOnDrop
) {
@@ -164,19 +171,23 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
}
}
-@@ -942,10 +982,7 @@
- if (this._isContainerVerticalPinnedGrid(tab)) {
- // In expanded vertical mode, the max number of pinned tabs per row is dynamic
- // Set this before adjusting dragged tab's position
-- let pinnedTabs = this._tabbrowserTabs.visibleTabs.slice(
-- 0,
-- gBrowser.pinnedTabCount
-- );
-+ let pinnedTabs = this._tabbrowserTabs.ariaFocusableItems.slice(0, gBrowser._numZenEssentials);
- let tabsPerRow = 0;
- let position = RTL_UI
- ? window.windowUtils.getBoundsWithoutFlushing(
-@@ -1112,7 +1149,7 @@
+@@ -1055,7 +1087,6 @@
+ // using updateDragImage. On Linux, we can use a panel.
+ if (platform == "win" || platform == "macosx") {
+ captureListener = function () {
+- dt.updateDragImage(canvas, dragImageOffset, dragImageOffset);
+ };
+ } else {
+ // Create a panel to use it in setDragImage
+@@ -1093,7 +1124,6 @@
+ );
+ dragImageOffset = dragImageOffset * scale;
+ }
+- dt.setDragImage(toDrag, dragImageOffset, dragImageOffset);
+
+ // _dragData.offsetX/Y give the coordinates that the mouse should be
+ // positioned relative to the corner of the new window created upon
+@@ -1112,7 +1142,7 @@
let dropEffect = this.getDropEffectForTabDrag(event);
let isMovingInTabStrip = !fromTabList && dropEffect == "move";
let collapseTabGroupDuringDrag =
@@ -185,7 +196,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
tab._dragData = {
offsetX: this._tabbrowserTabs.verticalMode
-@@ -1122,7 +1159,7 @@
+@@ -1122,7 +1152,7 @@
? event.screenY - window.screenY - tabOffset
: event.screenY - window.screenY,
scrollPos:
@@ -194,7 +205,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
? this._tabbrowserTabs.pinnedTabsContainer.scrollPosition
: this._tabbrowserTabs.arrowScrollbox.scrollPosition,
screenX: event.screenX,
-@@ -1149,6 +1186,7 @@
+@@ -1149,6 +1179,7 @@
if (collapseTabGroupDuringDrag) {
tab.group.collapsed = true;
@@ -202,224 +213,15 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
}
}
}
-@@ -1173,6 +1211,16 @@
+@@ -1173,6 +1204,7 @@
if (tabStripItemElement.hasAttribute("dragtarget")) {
return;
}
-+ let { movingTabs: zenMovingTabs } = tab._dragData;
-+ for (let movingTab of zenMovingTabs.slice(zenMovingTabs.findIndex(t => t._tPos == tab._tPos))) {
-+ if (isTabGroupLabel(tab)) {
-+ movingTab = movingTab.parentElement;
-+ }
-+ // "dragtarget" contains the following rules which must only be set AFTER the above
-+ // elements have been adjusted. {z-index: 3 !important, position: absolute !important}
-+ movingTab.setAttribute("zen-dragtarget", "");
-+ }
+ return;
let isPinned = tab.pinned;
let numPinned = gBrowser.pinnedTabCount;
let allTabs = this._tabbrowserTabs.ariaFocusableItems;
-@@ -1624,10 +1672,7 @@
- return;
- }
-
-- let tabs = this._tabbrowserTabs.visibleTabs.slice(
-- 0,
-- gBrowser.pinnedTabCount
-- );
-+ let tabs = this._tabbrowserTabs.ariaFocusableItems.slice(0, gBrowser._numZenEssentials);
-
- let directionX = screenX > dragData.animLastScreenX;
- let directionY = screenY > dragData.animLastScreenY;
-@@ -1636,6 +1681,8 @@
-
- let { width: tabWidth, height: tabHeight } =
- draggedTab.getBoundingClientRect();
-+ tabWidth += 4; // Add 4px to account for the gap
-+ tabHeight += 4;
- let shiftSizeX = tabWidth * movingTabs.length;
- let shiftSizeY = tabHeight;
- dragData.tabWidth = tabWidth;
-@@ -1672,8 +1719,8 @@
- let lastBoundX =
- lastTabInRow.screenX +
- lastTabInRow.getBoundingClientRect().width -
-- (lastMovingTabScreenX + tabWidth);
-- let lastBoundY = periphery.screenY - (lastMovingTabScreenY + tabHeight);
-+ (lastMovingTabScreenX + tabWidth) + 4;
-+ let lastBoundY = lastTab.screenY - lastMovingTabScreenY;
- translateX = Math.min(Math.max(translateX, firstBoundX), lastBoundX);
- translateY = Math.min(Math.max(translateY, firstBoundY), lastBoundY);
-
-@@ -1833,13 +1880,18 @@
- this._clearDragOverGroupingTimer();
- this.#clearPinnedDropIndicatorTimer();
-
-- let isPinned = draggedTab.pinned;
-- let numPinned = gBrowser.pinnedTabCount;
-+ let isPinned = draggedTab?.group ? draggedTab.group.pinned : draggedTab.pinned;
-+ let numPinned = gBrowser._numVisiblePinTabsWithoutCollapsed;
-+ let essential = draggedTab.hasAttribute("zen-essential");
-+ const isDraggingFolder = isTabGroupLabel(draggedTab) && draggedTab.group?.isZenFolder;
- let allTabs = this._tabbrowserTabs.ariaFocusableItems;
- let tabs = allTabs.slice(
-- isPinned ? 0 : numPinned,
-- isPinned ? numPinned : undefined
-+ (isPinned && essential) ? 0 : gBrowser._numZenEssentials,
-+ isPinned ? (essential ? gBrowser._numZenEssentials : (isDraggingFolder ? numPinned : undefined)) : undefined
- );
-+ if (draggedTab.group?.hasAttribute("split-view-group")) {
-+ draggedTab = draggedTab.group.labelElement;
-+ }
-
- if (this._rtlMode) {
- tabs.reverse();
-@@ -1854,7 +1906,7 @@
- let translateAxis = this._tabbrowserTabs.verticalMode
- ? "translateY"
- : "translateX";
-- let { width: tabWidth, height: tabHeight } = bounds(draggedTab);
-+ let { width: tabWidth, height: tabHeight } = bounds(draggedTab.group?.hasAttribute("split-view-group") ? draggedTab.group : draggedTab);
- let tabSize = this._tabbrowserTabs.verticalMode ? tabHeight : tabWidth;
- let translateX = event.screenX - dragData.screenX;
- let translateY = event.screenY - dragData.screenY;
-@@ -1870,6 +1922,12 @@
- );
- let lastMovingTab = movingTabs.at(-1);
- let firstMovingTab = movingTabs[0];
-+ if (lastMovingTab.group?.hasAttribute("split-view-group")) {
-+ lastMovingTab = lastMovingTab.group;
-+ }
-+ if (firstMovingTab.group?.hasAttribute("split-view-group")) {
-+ firstMovingTab = firstMovingTab.group;
-+ }
- let endEdge = ele => ele[screenAxis] + bounds(ele)[size];
- let lastMovingTabScreen = endEdge(lastMovingTab);
- let firstMovingTabScreen = firstMovingTab[screenAxis];
-@@ -1884,6 +1942,13 @@
- let endBound = this._rtlMode
- ? endEdge(this._tabbrowserTabs) - lastMovingTabScreen
- : periphery[screenAxis] - 1 - lastMovingTabScreen;
-+ {
-+ let firstTab = tabs.at(this._rtlMode ? -1 : 0);
-+ let lastTab = tabs.at(this._rtlMode ? 0 : -1);
-+ startBound = firstTab[screenAxis] - firstMovingTabScreen;
-+ endBound = endEdge(lastTab) - lastMovingTabScreen;
-+ endBound = gZenPinnedTabManager.getLastTabBound(endBound, lastTab, isDraggingFolder);
-+ }
- translate = Math.min(Math.max(translate, startBound), endBound);
-
- // Center the tab under the cursor if the tab is not under the cursor while dragging
-@@ -2075,6 +2140,8 @@
- };
-
- let dropElement = getOverlappedElement();
-+ if (dropElement?.hasAttribute("split-view-group")) dropElement = dropElement.labelElement;
-+ gZenPinnedTabManager.animateSeparatorMove(movingTabs, dropElement, isPinned, event);
-
- let newDropElementIndex;
- if (dropElement) {
-@@ -2157,7 +2224,7 @@
- ? Services.prefs.getIntPref(
- "browser.tabs.dragDrop.moveOverThresholdPercent"
- ) / 100
-- : 0.5;
-+ : Services.prefs.getIntPref('zen.view.drag-and-drop.move-over-threshold') / 100;
- moveOverThreshold = Math.min(1, Math.max(0, moveOverThreshold));
- let shouldMoveOver = overlapPercent > moveOverThreshold;
- if (logicalForward && shouldMoveOver) {
-@@ -2190,6 +2257,7 @@
- // If dragging a group over another group, don't make it look like it is
- // possible to drop the dragged group inside the other group.
- if (
-+ false &&
- isTabGroupLabel(draggedTab) &&
- dropElement?.group &&
- (!dropElement.group.collapsed ||
-@@ -2216,20 +2284,13 @@
- let isOutOfBounds = isPinned
- ? dropElement.elementIndex >= numPinned
- : dropElement.elementIndex < numPinned;
-- if (isOutOfBounds) {
-- // Drop after last pinned tab
-- dropElement = this._tabbrowserTabs.ariaFocusableItems[numPinned - 1];
-- dropBefore = false;
-- }
- }
-
-- if (
-- gBrowser._tabGroupsEnabled &&
-- isTab(draggedTab) &&
-- !isPinned &&
-- (!numPinned || newDropElementIndex >= numPinned)
-- ) {
-+ if (isTab(draggedTab) || isTabGroupLabel(draggedTab)) {
- let dragOverGroupingThreshold = 1 - moveOverThreshold;
-+ if (draggedTab && !dropElement?.group) {
-+ gZenFolders.highlightGroupOnDragOver(null);
-+ }
- let groupingDelay = Services.prefs.getIntPref(
- "browser.tabs.dragDrop.createGroup.delayMS"
- );
-@@ -2237,6 +2298,7 @@
- // When dragging tab(s) over an ungrouped tab, signal to the user
- // that dropping the tab(s) will create a new tab group.
- let shouldCreateGroupOnDrop =
-+ false &&
- !movingTabsSet.has(dropElement) &&
- isTab(dropElement) &&
- !dropElement?.group &&
-@@ -2245,6 +2307,7 @@
- // When dragging tab(s) over a collapsed tab group label, signal to the
- // user that dropping the tab(s) will add them to the group.
- let shouldDropIntoCollapsedTabGroup =
-+ false &&
- isTabGroupLabel(dropElement) &&
- dropElement.group.collapsed &&
- overlapPercent > dragOverGroupingThreshold;
-@@ -2302,6 +2365,14 @@
- dropElement = dropElementGroup.tabs[0];
- dropBefore = true;
- }
-+ ({ dropElement, colorCode, dropBefore } = gZenFolders.handleDragOverTabGroupLabel(
-+ dropElement,
-+ draggedTab,
-+ overlapPercent,
-+ movingTabs,
-+ dropBefore,
-+ colorCode
-+ ));
- }
- this._setDragOverGroupColor(colorCode);
- this._tabbrowserTabs.toggleAttribute(
-@@ -2324,10 +2395,11 @@
- dragData.dropBefore = dropBefore;
- dragData.animDropElementIndex = newDropElementIndex;
-
-+ gZenFolders.setFolderIndentation(movingTabs, dropElement);
- // Shift background tabs to leave a gap where the dragged tab
- // would currently be dropped.
- for (let item of tabs) {
-- if (item == draggedTab) {
-+ if (item == draggedTab || (item.group?.hasAttribute("split-view-group") && item.group == draggedTab.group)) {
- continue;
- }
-
-@@ -2417,11 +2489,13 @@
- }
-
- finishAnimateTabMove() {
-+ gZenPinnedTabManager.onDragFinish();
- if (!this.#isMovingTab()) {
- return;
- }
-
- this.#setMovingTabMode(false);
-+ gZenFolders.highlightGroupOnDragOver(null);
-
- for (let item of this._tabbrowserTabs.ariaFocusableItems) {
- this._resetGroupTarget(item);
-@@ -2457,7 +2531,7 @@
+@@ -2457,7 +2489,7 @@
tab.style.left = "";
tab.style.top = "";
tab.style.maxWidth = "";
@@ -428,7 +230,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..bc49f4f5a90638d725eca016d00f30d9
}
for (let label of draggedTabDocument.getElementsByClassName(
"tab-group-label-container"
-@@ -2467,7 +2541,7 @@
+@@ -2467,7 +2499,7 @@
label.style.left = "";
label.style.top = "";
label.style.maxWidth = "";
diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch
index 97eb477f4..9d0784d69 100644
--- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch
+++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
-index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d797de7be 100644
+index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..00a9810cc894b6a21adb78b70a15049cc1db3edf 100644
--- a/browser/components/tabbrowser/content/tabbrowser.js
+++ b/browser/components/tabbrowser/content/tabbrowser.js
@@ -386,6 +386,7 @@
@@ -87,7 +87,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
tab.linkedPanel = uniqueId;
this._selectedTab = tab;
this._selectedBrowser = browser;
-@@ -898,13 +951,17 @@
+@@ -898,13 +951,18 @@
}
this.showTab(aTab);
@@ -100,17 +100,21 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
);
// If periphery is null, append to end
- this.pinnedTabsContainer.insertBefore(aTab, periphery);
++ this.tabContainer.tabDragAndDrop.handle_drop_transition(this.tabs[this.pinnedTabCount - 1], aTab, [aTab], false);
+ aTab.hasAttribute("zen-essential") ? gZenWorkspaces.getEssentialsSection(aTab).appendChild(aTab) : this.pinnedTabsContainer.insertBefore(aTab, this.pinnedTabsContainer.lastChild)
});
+ }
aTab.setAttribute("pinned", "true");
this._updateTabBarForPinnedTabs();
-@@ -917,11 +974,15 @@
+@@ -917,11 +975,18 @@
}
this.#handleTabMove(aTab, () => {
+ const handled = gZenFolders.handleTabUnpin(aTab);
++ if (!handled) {
++ this.tabContainer.tabDragAndDrop.handle_drop_transition(this.tabs[this.pinnedTabCount + 1 /* empty + extra */], aTab, [aTab], true);
++ }
+
// we remove this attribute first, so that allTabs represents
// the moving of a tab from the pinned tabs container
@@ -123,7 +127,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
});
aTab.style.marginInlineStart = "";
-@@ -1098,6 +1159,9 @@
+@@ -1098,6 +1163,9 @@
let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"];
@@ -133,7 +137,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
if (
aIconURL &&
!LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol))
-@@ -1107,6 +1171,9 @@
+@@ -1107,6 +1175,9 @@
);
return;
}
@@ -143,7 +147,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
let browser = this.getBrowserForTab(aTab);
browser.mIconURL = aIconURL;
-@@ -1379,7 +1446,6 @@
+@@ -1379,7 +1450,6 @@
// Preview mode should not reset the owner
if (!this._previewMode && !oldTab.selected) {
@@ -151,7 +155,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
}
let lastRelatedTab = this._lastRelatedTabMap.get(oldTab);
-@@ -1470,6 +1536,7 @@
+@@ -1470,6 +1540,7 @@
if (!this._previewMode) {
newTab.recordTimeFromUnloadToReload();
newTab.updateLastAccessed();
@@ -159,7 +163,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
oldTab.updateLastAccessed();
// if this is the foreground window, update the last-seen timestamps.
if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) {
-@@ -1622,6 +1689,9 @@
+@@ -1622,6 +1693,9 @@
}
let activeEl = document.activeElement;
@@ -169,7 +173,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
// If focus is on the old tab, move it to the new tab.
if (activeEl == oldTab) {
newTab.focus();
-@@ -1945,6 +2015,11 @@
+@@ -1945,6 +2019,11 @@
}
_setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) {
@@ -181,7 +185,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
if (!aLabel || aLabel.includes("about:reader?")) {
return false;
}
-@@ -2053,7 +2128,7 @@
+@@ -2053,7 +2132,7 @@
newIndex = this.selectedTab._tPos + 1;
}
@@ -190,7 +194,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
if (this.isTabGroupLabel(targetTab)) {
throw new Error(
"Replacing a tab group label with a tab is not supported"
-@@ -2328,6 +2403,7 @@
+@@ -2328,6 +2407,7 @@
uriIsAboutBlank,
userContextId,
skipLoad,
@@ -198,7 +202,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
} = {}) {
let b = document.createXULElement("browser");
// Use the JSM global to create the permanentKey, so that if the
-@@ -2401,8 +2477,7 @@
+@@ -2401,8 +2481,7 @@
// we use a different attribute name for this?
b.setAttribute("name", name);
}
@@ -208,7 +212,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
b.setAttribute("transparent", "true");
}
-@@ -2567,7 +2642,7 @@
+@@ -2567,7 +2646,7 @@
let panel = this.getPanel(browser);
let uniqueId = this._generateUniquePanelID();
@@ -217,7 +221,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
aTab.linkedPanel = uniqueId;
// Inject the into the DOM if necessary.
-@@ -2626,8 +2701,8 @@
+@@ -2626,8 +2705,8 @@
// If we transitioned from one browser to two browsers, we need to set
// hasSiblings=false on both the existing browser and the new browser.
if (this.tabs.length == 2) {
@@ -228,7 +232,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
} else {
aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1;
}
-@@ -2814,7 +2889,6 @@
+@@ -2814,7 +2893,6 @@
this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, {
tabIndex: tab._tPos + 1,
userContextId: tab.userContextId,
@@ -236,7 +240,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
focusUrlBar: true,
});
resolve(this.selectedBrowser);
-@@ -2923,6 +2997,9 @@
+@@ -2923,6 +3001,9 @@
schemelessInput,
hasValidUserGestureActivation = false,
textDirectiveUserActivation = false,
@@ -246,7 +250,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
} = {}
) {
// all callers of addTab that pass a params object need to pass
-@@ -2933,10 +3010,17 @@
+@@ -2933,10 +3014,17 @@
);
}
@@ -264,7 +268,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
// If we're opening a foreground tab, set the owner by default.
ownerTab ??= inBackground ? null : this.selectedTab;
-@@ -2944,6 +3028,7 @@
+@@ -2944,6 +3032,7 @@
if (this.selectedTab.owner) {
this.selectedTab.owner = null;
}
@@ -272,7 +276,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
// Find the tab that opened this one, if any. This is used for
// determining positioning, and inherited attributes such as the
-@@ -2996,6 +3081,21 @@
+@@ -2996,6 +3085,21 @@
noInitialLabel,
skipBackgroundNotify,
});
@@ -294,7 +298,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
if (insertTab) {
// Insert the tab into the tab container in the correct position.
this.#insertTabAtIndex(t, {
-@@ -3004,6 +3104,7 @@
+@@ -3004,6 +3108,7 @@
ownerTab,
openerTab,
pinned,
@@ -302,7 +306,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
bulkOrderedOpen,
tabGroup: tabGroup ?? openerTab?.group,
});
-@@ -3022,6 +3123,7 @@
+@@ -3022,6 +3127,7 @@
openWindowInfo,
skipLoad,
triggeringRemoteType,
@@ -310,7 +314,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
}));
if (focusUrlBar) {
-@@ -3146,6 +3248,12 @@
+@@ -3146,6 +3252,12 @@
}
}
@@ -323,7 +327,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
// Additionally send pinned tab events
if (pinned) {
this.#notifyPinnedStatus(t);
-@@ -3349,10 +3457,10 @@
+@@ -3349,10 +3461,10 @@
isAdoptingGroup = false,
isUserTriggered = false,
telemetryUserCreateSource = "unknown",
@@ -335,7 +339,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
}
if (!color) {
-@@ -3373,9 +3481,14 @@
+@@ -3373,9 +3485,14 @@
label,
isAdoptingGroup
);
@@ -352,7 +356,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
);
group.addTabs(tabs);
-@@ -3496,7 +3609,7 @@
+@@ -3496,7 +3613,7 @@
}
this.#handleTabMove(tab, () =>
@@ -361,7 +365,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
);
}
-@@ -3698,6 +3811,7 @@
+@@ -3698,6 +3815,7 @@
openWindowInfo,
skipLoad,
triggeringRemoteType,
@@ -369,7 +373,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
}
) {
// If we don't have a preferred remote type (or it is `NOT_REMOTE`), and
-@@ -3767,6 +3881,7 @@
+@@ -3767,6 +3885,7 @@
openWindowInfo,
name,
skipLoad,
@@ -377,7 +381,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
});
}
-@@ -3955,7 +4070,7 @@
+@@ -3955,7 +4074,7 @@
// Add a new tab if needed.
if (!tab) {
let createLazyBrowser =
@@ -386,7 +390,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
let url = "about:blank";
if (tabData.entries?.length) {
-@@ -3992,8 +4107,10 @@
+@@ -3992,8 +4111,10 @@
insertTab: false,
skipLoad: true,
preferredRemoteType,
@@ -398,7 +402,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
if (select) {
tabToSelect = tab;
}
-@@ -4005,7 +4122,8 @@
+@@ -4005,7 +4126,8 @@
this.pinTab(tab);
// Then ensure all the tab open/pinning information is sent.
this._fireTabOpen(tab, {});
@@ -408,7 +412,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
let { groupId } = tabData;
const tabGroup = tabGroupWorkingData.get(groupId);
// if a tab refers to a tab group we don't know, skip any group
-@@ -4019,7 +4137,10 @@
+@@ -4019,7 +4141,10 @@
tabGroup.stateData.id,
tabGroup.stateData.color,
tabGroup.stateData.collapsed,
@@ -420,7 +424,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
);
tabsFragment.appendChild(tabGroup.node);
}
-@@ -4064,9 +4185,23 @@
+@@ -4064,9 +4189,23 @@
// to remove the old selected tab.
if (tabToSelect) {
let leftoverTab = this.selectedTab;
@@ -436,15 +440,15 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
+ gZenWorkspaces._initialTab._shouldRemove = true;
+ }
+ }
- }
++ }
+ else {
+ gZenWorkspaces._tabToRemoveForEmpty = this.selectedTab;
-+ }
+ }
+ this._hasAlreadyInitializedZenSessionStore = true;
if (tabs.length > 1 || !tabs[0].selected) {
this._updateTabsAfterInsert();
-@@ -4257,11 +4392,14 @@
+@@ -4257,11 +4396,14 @@
if (ownerTab) {
tab.owner = ownerTab;
}
@@ -460,7 +464,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
if (
!bulkOrderedOpen &&
((openerTab &&
-@@ -4273,7 +4411,7 @@
+@@ -4273,7 +4415,7 @@
let lastRelatedTab =
openerTab && this._lastRelatedTabMap.get(openerTab);
let previousTab = lastRelatedTab || openerTab || this.selectedTab;
@@ -469,7 +473,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
tabGroup = previousTab.group;
}
if (
-@@ -4284,7 +4422,7 @@
+@@ -4284,7 +4426,7 @@
) {
elementIndex = Infinity;
} else if (previousTab.visible) {
@@ -478,7 +482,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
} else if (previousTab == FirefoxViewHandler.tab) {
elementIndex = 0;
}
-@@ -4312,14 +4450,14 @@
+@@ -4312,14 +4454,14 @@
}
// Ensure index is within bounds.
if (tab.pinned) {
@@ -497,7 +501,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
if (pinned && !itemAfter?.pinned) {
itemAfter = null;
-@@ -4330,7 +4468,7 @@
+@@ -4330,7 +4472,7 @@
this.tabContainer._invalidateCachedTabs();
@@ -506,7 +510,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
if (this.isTab(itemAfter) && itemAfter.group == tabGroup) {
// Place at the front of, or between tabs in, the same tab group
this.tabContainer.insertBefore(tab, itemAfter);
-@@ -4358,7 +4496,11 @@
+@@ -4358,7 +4500,11 @@
const tabContainer = pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
@@ -518,7 +522,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
}
this._updateTabsAfterInsert();
-@@ -4366,6 +4508,7 @@
+@@ -4366,6 +4512,7 @@
if (pinned) {
this._updateTabBarForPinnedTabs();
}
@@ -526,7 +530,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
TabBarVisibility.update();
}
-@@ -4916,6 +5059,7 @@
+@@ -4916,6 +5063,7 @@
telemetrySource,
} = {}
) {
@@ -534,7 +538,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
// When 'closeWindowWithLastTab' pref is enabled, closing all tabs
// can be considered equivalent to closing the window.
if (
-@@ -5005,6 +5149,7 @@
+@@ -5005,6 +5153,7 @@
if (lastToClose) {
this.removeTab(lastToClose, aParams);
}
@@ -542,7 +546,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
} catch (e) {
console.error(e);
}
-@@ -5043,6 +5188,12 @@
+@@ -5043,6 +5192,12 @@
aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start();
}
@@ -555,7 +559,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
// Handle requests for synchronously removing an already
// asynchronously closing tab.
if (!animate && aTab.closing) {
-@@ -5057,6 +5208,9 @@
+@@ -5057,6 +5212,9 @@
// state).
let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width;
let isLastTab = this.#isLastTabInWindow(aTab);
@@ -565,7 +569,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
if (
!this._beginRemoveTab(aTab, {
closeWindowFastpath: true,
-@@ -5105,7 +5259,13 @@
+@@ -5105,7 +5263,13 @@
// We're not animating, so we can cancel the animation stopwatch.
Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId);
aTab._closeTimeAnimTimerId = null;
@@ -580,7 +584,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
return;
}
-@@ -5239,7 +5399,7 @@
+@@ -5239,7 +5403,7 @@
closeWindowWithLastTab != null
? closeWindowWithLastTab
: !window.toolbar.visible ||
@@ -589,7 +593,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
if (closeWindow) {
// We've already called beforeunload on all the relevant tabs if we get here,
-@@ -5263,6 +5423,7 @@
+@@ -5263,6 +5427,7 @@
newTab = true;
}
@@ -597,7 +601,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
aTab._endRemoveArgs = [closeWindow, newTab];
// swapBrowsersAndCloseOther will take care of closing the window without animation.
-@@ -5303,13 +5464,7 @@
+@@ -5303,13 +5468,7 @@
aTab._mouseleave();
if (newTab) {
@@ -612,7 +616,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
} else {
TabBarVisibility.update();
}
-@@ -5442,6 +5597,7 @@
+@@ -5442,6 +5601,7 @@
this.tabs[i]._tPos = i;
}
@@ -620,7 +624,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
if (!this._windowIsClosing) {
// update tab close buttons state
this.tabContainer._updateCloseButtons();
-@@ -5663,6 +5819,7 @@
+@@ -5663,6 +5823,7 @@
}
let excludeTabs = new Set(aExcludeTabs);
@@ -628,7 +632,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
// If this tab has a successor, it should be selectable, since
// hiding or closing a tab removes that tab as a successor.
-@@ -5675,13 +5832,13 @@
+@@ -5675,13 +5836,13 @@
!excludeTabs.has(aTab.owner) &&
Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
) {
@@ -644,7 +648,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
);
let tab = this.tabContainer.findNextTab(aTab, {
-@@ -5697,7 +5854,7 @@
+@@ -5697,7 +5858,7 @@
}
if (tab) {
@@ -653,7 +657,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
}
// If no qualifying visible tab was found, see if there is a tab in
-@@ -5718,7 +5875,7 @@
+@@ -5718,7 +5879,7 @@
});
}
@@ -662,7 +666,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
}
_blurTab(aTab) {
-@@ -5729,7 +5886,7 @@
+@@ -5729,7 +5890,7 @@
* @returns {boolean}
* False if swapping isn't permitted, true otherwise.
*/
@@ -671,7 +675,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
// Do not allow transfering a private tab to a non-private window
// and vice versa.
if (
-@@ -5783,6 +5940,7 @@
+@@ -5783,6 +5944,7 @@
// fire the beforeunload event in the process. Close the other
// window if this was its last tab.
if (
@@ -679,7 +683,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
!remoteBrowser._beginRemoveTab(aOtherTab, {
adoptedByTab: aOurTab,
closeWindowWithLastTab: true,
-@@ -5794,7 +5952,7 @@
+@@ -5794,7 +5956,7 @@
// If this is the last tab of the window, hide the window
// immediately without animation before the docshell swap, to avoid
// about:blank being painted.
@@ -688,7 +692,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
if (closeWindow) {
let win = aOtherTab.ownerGlobal;
win.windowUtils.suppressAnimation(true);
-@@ -5918,11 +6076,13 @@
+@@ -5918,11 +6080,13 @@
}
// Finish tearing down the tab that's going away.
@@ -702,7 +706,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
this.setTabTitle(aOurTab);
-@@ -6124,10 +6284,10 @@
+@@ -6124,10 +6288,10 @@
SessionStore.deleteCustomTabValue(aTab, "hiddenBy");
}
@@ -715,7 +719,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
aTab.selected ||
aTab.closing ||
// Tabs that are sharing the screen, microphone or camera cannot be hidden.
-@@ -6185,7 +6345,8 @@
+@@ -6185,7 +6349,8 @@
*
* @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab
*/
@@ -725,7 +729,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
if (this.tabs.length == 1) {
return null;
}
-@@ -6209,12 +6370,14 @@
+@@ -6209,12 +6374,14 @@
}
// tell a new window to take the "dropped" tab
@@ -741,7 +745,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
}
/**
-@@ -6319,7 +6482,7 @@
+@@ -6319,7 +6486,7 @@
* `true` if element is a ``
*/
isTabGroup(element) {
@@ -750,7 +754,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
}
/**
-@@ -6404,8 +6567,8 @@
+@@ -6404,8 +6571,8 @@
}
// Don't allow mixing pinned and unpinned tabs.
@@ -761,7 +765,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
} else {
tabIndex = Math.max(tabIndex, this.pinnedTabCount);
}
-@@ -6431,10 +6594,16 @@
+@@ -6431,10 +6598,16 @@
this.#handleTabMove(
element,
() => {
@@ -780,7 +784,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
if (neighbor && this.isTab(element) && tabIndex > element._tPos) {
neighbor.after(element);
} else {
-@@ -6492,23 +6661,28 @@
+@@ -6492,23 +6665,28 @@
#moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) {
if (this.isTabGroupLabel(targetElement)) {
targetElement = targetElement.group;
@@ -815,7 +819,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
} else if (!element.pinned && targetElement && targetElement.pinned) {
// If the caller asks to move an unpinned element next to a pinned
// tab, move the unpinned element to be the first unpinned element
-@@ -6521,14 +6695,34 @@
+@@ -6521,14 +6699,34 @@
// move the tab group right before the first unpinned tab.
// 4. Moving a tab group and the first unpinned tab is grouped:
// move the tab group right before the first unpinned tab's tab group.
@@ -851,7 +855,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
element.pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
-@@ -6537,7 +6731,7 @@
+@@ -6537,7 +6735,7 @@
element,
() => {
if (moveBefore) {
@@ -860,7 +864,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
} else if (targetElement) {
targetElement.after(element);
} else {
-@@ -6607,10 +6801,10 @@
+@@ -6607,10 +6805,10 @@
* @param {TabMetricsContext} [metricsContext]
*/
moveTabToGroup(aTab, aGroup, metricsContext) {
@@ -873,7 +877,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
return;
}
if (aTab.group && aTab.group.id === aGroup.id) {
-@@ -6656,6 +6850,7 @@
+@@ -6656,6 +6854,7 @@
let state = {
tabIndex: tab._tPos,
@@ -881,7 +885,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
};
if (tab.visible) {
state.elementIndex = tab.elementIndex;
-@@ -6682,7 +6877,7 @@
+@@ -6682,7 +6881,7 @@
let changedTabGroup =
previousTabState.tabGroupId != currentTabState.tabGroupId;
@@ -890,7 +894,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
tab.dispatchEvent(
new CustomEvent("TabMove", {
bubbles: true,
-@@ -6723,6 +6918,10 @@
+@@ -6723,6 +6922,10 @@
moveActionCallback();
@@ -901,17 +905,16 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
// Clear tabs cache after moving nodes because the order of tabs may have
// changed.
this.tabContainer._invalidateCachedTabs();
-@@ -6815,6 +7014,9 @@
+@@ -6815,6 +7018,8 @@
params.userContextId = aTab.getAttribute("usercontextid");
}
let newTab = this.addWebTab("about:blank", params);
+ newTab._zenContentsVisible = true;
+ newTab.zenStaticLabel = aTab.zenStaticLabel;
-+ newTab.zenStaticIcon = aTab.zenStaticIcon;
let newBrowser = this.getBrowserForTab(newTab);
aTab.container.tabDragAndDrop.finishAnimateTabMove();
-@@ -7623,7 +7825,7 @@
+@@ -7623,7 +7828,7 @@
// preventDefault(). It will still raise the window if appropriate.
break;
}
@@ -920,7 +923,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
window.focus();
aEvent.preventDefault();
break;
-@@ -7640,7 +7842,6 @@
+@@ -7640,7 +7845,6 @@
}
case "TabGroupCollapse":
aEvent.target.tabs.forEach(tab => {
@@ -928,7 +931,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
});
break;
case "TabGroupCreateByUser":
-@@ -8589,6 +8790,7 @@
+@@ -8589,6 +8793,7 @@
aWebProgress.isTopLevel
) {
this.mTab.setAttribute("busy", "true");
@@ -936,7 +939,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
gBrowser._tabAttrModified(this.mTab, ["busy"]);
this.mTab._notselectedsinceload = !this.mTab.selected;
}
-@@ -8670,6 +8872,7 @@
+@@ -8670,6 +8875,7 @@
// known defaults. Note we use the original URL since about:newtab
// redirects to a prerendered page.
const shouldRemoveFavicon =
@@ -944,7 +947,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..b01c89f2e0d3b44dddaa01a20b4da13d
!this.mBrowser.mIconURL &&
!ignoreBlank &&
!(originalLocation.spec in FAVICON_DEFAULTS);
-@@ -9623,7 +9826,7 @@ var TabContextMenu = {
+@@ -9623,7 +9829,7 @@ var TabContextMenu = {
);
contextUnpinSelectedTabs.hidden =
!this.contextTab.pinned || !this.multiselected;
diff --git a/src/browser/components/tabbrowser/content/tabs-js.patch b/src/browser/components/tabbrowser/content/tabs-js.patch
index 9c2dcf099..e45c1b56e 100644
--- a/src/browser/components/tabbrowser/content/tabs-js.patch
+++ b/src/browser/components/tabbrowser/content/tabs-js.patch
@@ -1,7 +1,16 @@
diff --git a/browser/components/tabbrowser/content/tabs.js b/browser/components/tabbrowser/content/tabs.js
-index 6b6c04599fe80983d13d2069ca62b99d8ad70271..04144081560f1678dc9673736ef2bd9d9ca3f478 100644
+index 6b6c04599fe80983d13d2069ca62b99d8ad70271..6d5ae983446bc778f3075d79f8ff14748dd7756f 100644
--- a/browser/components/tabbrowser/content/tabs.js
+++ b/browser/components/tabbrowser/content/tabs.js
+@@ -235,7 +235,7 @@
+ true
+ )
+ ? new window.TabStacking(this)
+- : new window.TabDragAndDrop(this);
++ : Services.prefs.getBoolPref("zen.tabs.use-legacy-drag-and-drop") ? new window.TabDragAndDrop(this) : new window.ZenDragAndDrop(this);
+ this.tabDragAndDrop.init();
+ }
+
@@ -436,7 +436,7 @@
// and we're not hitting the scroll buttons.
if (
diff --git a/src/widget/cocoa/nsDragService-mm.patch b/src/widget/cocoa/nsDragService-mm.patch
new file mode 100644
index 000000000..48afb3c5c
--- /dev/null
+++ b/src/widget/cocoa/nsDragService-mm.patch
@@ -0,0 +1,47 @@
+diff --git a/widget/cocoa/nsDragService.mm b/widget/cocoa/nsDragService.mm
+index f1614b823a859ff8fbc74982f205bb1f2ef29beb..897c24846a97c132babe3ad79da12ebfcec90484 100644
+--- a/widget/cocoa/nsDragService.mm
++++ b/widget/cocoa/nsDragService.mm
+@@ -23,6 +23,7 @@
+ #include "mozilla/PresShell.h"
+ #include "mozilla/dom/Document.h"
+ #include "mozilla/dom/DocumentInlines.h"
++#include "mozilla/nsZenDragAndDrop.h"
+ #include "nsIContent.h"
+ #include "nsCocoaUtils.h"
+ #include "mozilla/gfx/2D.h"
+@@ -148,6 +149,10 @@
+ bitsPerPixel:32];
+
+ uint8_t* dest = [imageRep bitmapData];
++ auto drag_translucency = DRAG_TRANSLUCENCY;
++ if (auto zenDragAndDrop = zen::nsZenDragAndDrop::GetZenDragAndDropInstance()) {
++ drag_translucency = zenDragAndDrop->GetDragImageOpacity();
++ }
+ for (uint32_t i = 0; i < height; ++i) {
+ uint8_t* src = map.mData + i * map.mStride;
+ for (uint32_t j = 0; j < width; ++j) {
+@@ -155,15 +160,15 @@
+ // is premultipled here. Also, Quartz likes RGBA, so do that translation
+ // as well.
+ #ifdef IS_BIG_ENDIAN
+- dest[0] = uint8_t(src[1] * DRAG_TRANSLUCENCY);
+- dest[1] = uint8_t(src[2] * DRAG_TRANSLUCENCY);
+- dest[2] = uint8_t(src[3] * DRAG_TRANSLUCENCY);
+- dest[3] = uint8_t(src[0] * DRAG_TRANSLUCENCY);
++ dest[0] = uint8_t(src[1] * drag_translucency);
++ dest[1] = uint8_t(src[2] * drag_translucency);
++ dest[2] = uint8_t(src[3] * drag_translucency);
++ dest[3] = uint8_t(src[0] * drag_translucency);
+ #else
+- dest[0] = uint8_t(src[2] * DRAG_TRANSLUCENCY);
+- dest[1] = uint8_t(src[1] * DRAG_TRANSLUCENCY);
+- dest[2] = uint8_t(src[0] * DRAG_TRANSLUCENCY);
+- dest[3] = uint8_t(src[3] * DRAG_TRANSLUCENCY);
++ dest[0] = uint8_t(src[2] * drag_translucency);
++ dest[1] = uint8_t(src[1] * drag_translucency);
++ dest[2] = uint8_t(src[0] * drag_translucency);
++ dest[3] = uint8_t(src[3] * drag_translucency);
+ #endif
+ src += 4;
+ dest += 4;
diff --git a/src/zen/common/ZenPreloadedScripts.js b/src/zen/common/ZenPreloadedScripts.js
index c511febfc..ea62dadb4 100644
--- a/src/zen/common/ZenPreloadedScripts.js
+++ b/src/zen/common/ZenPreloadedScripts.js
@@ -11,4 +11,6 @@
ChromeUtils.importESModule("chrome://browser/content/zen-components/ZenMods.mjs", { global: "current" });
ChromeUtils.importESModule("chrome://browser/content/zen-components/ZenKeyboardShortcuts.mjs", { global: "current" });
ChromeUtils.importESModule("chrome://browser/content/zen-components/ZenSessionStore.mjs", { global: "current" });
+
+ Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/ZenDragAndDrop.js", this);
}
diff --git a/src/zen/common/modules/ZenUIManager.mjs b/src/zen/common/modules/ZenUIManager.mjs
index 70c9d10ba..a43e7e708 100644
--- a/src/zen/common/modules/ZenUIManager.mjs
+++ b/src/zen/common/modules/ZenUIManager.mjs
@@ -1223,6 +1223,7 @@ window.gZenVerticalTabsManager = {
// Always move the splitter next to the sidebar
const splitter = document.getElementById('zen-sidebar-splitter');
+ splitter.addEventListener('dragover', gBrowser.tabContainer);
this.navigatorToolbox.after(splitter);
window.dispatchEvent(new Event('resize'));
if (!isCompactMode) {
diff --git a/src/zen/drag-and-drop/ZenDragAndDrop.js b/src/zen/drag-and-drop/ZenDragAndDrop.js
new file mode 100644
index 000000000..eab09edd7
--- /dev/null
+++ b/src/zen/drag-and-drop/ZenDragAndDrop.js
@@ -0,0 +1,1223 @@
+/* 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';
+
+// Wrap in a block to prevent leaking to window scope.
+{
+ const isTab = (element) => gBrowser.isTab(element);
+ const isTabGroupLabel = (element) => gBrowser.isTabGroupLabel(element);
+
+ /**
+ * The elements in the tab strip from `this.ariaFocusableItems` that contain
+ * logical information are:
+ *
+ * - (.tabbrowser-tab)
+ * - label element (.tab-group-label)
+ *
+ * The elements in the tab strip that contain the space inside of the
+ * element are:
+ *
+ * - (.tabbrowser-tab)
+ * - label element wrapper (.tab-group-label-container)
+ *
+ * When working with tab strip items, if you need logical information, you
+ * can get it directly, e.g. `element.elementIndex` or `element._tPos`. If
+ * you need spatial information like position or dimensions, then you should
+ * call this function. For example, `elementToMove(element).getBoundingClientRect()`
+ * or `elementToMove(element).style.top`.
+ *
+ * @param {MozTabbrowserTab|typeof MozTabbrowserTabGroup.labelElement} element
+ * @returns {MozTabbrowserTab|vbox}
+ */
+ const elementToMove = (element) => {
+ if (
+ element.closest('.zen-current-workspace-indicator') ||
+ element.hasAttribute('split-view-group')
+ ) {
+ return element;
+ }
+ if (element.group?.hasAttribute('split-view-group')) {
+ return element.group;
+ }
+ if (isTab(element)) {
+ return element;
+ }
+ if (isTabGroupLabel(element)) {
+ return element.closest('.tab-group-label-container');
+ }
+ throw new Error(`Element "${element.tagName}" is not expected to move`);
+ };
+
+ window.ZenDragAndDrop = class extends window.TabDragAndDrop {
+ #dragOverBackground = null;
+ #lastDropTarget = null;
+ originalDragImageArgs = [];
+ #isOutOfWindow = false;
+ #maxTabsPerRow = 0;
+ #changeSpaceTimer = null;
+ #isAnimatingTabMove = false;
+
+ constructor(tabbrowserTabs) {
+ super(tabbrowserTabs);
+
+ XPCOMUtils.defineLazyServiceGetter(
+ this,
+ 'ZenDragAndDropService',
+ '@mozilla.org/zen/drag-and-drop;1',
+ Ci.nsIZenDragAndDrop
+ );
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ '_dndSwitchSpaceDelay',
+ 'zen.tabs.dnd-switch-space-delay',
+ 1000
+ );
+ }
+
+ init() {
+ super.init();
+ this.handle_windowDragEnter = this.handle_windowDragEnter.bind(this);
+ window.addEventListener('dragleave', this.handle_windowDragLeave.bind(this), true);
+ }
+
+ startTabDrag(event, tab, ...args) {
+ this.ZenDragAndDropService.onDragStart(1);
+
+ super.startTabDrag(event, tab, ...args);
+ const dt = event.dataTransfer;
+ if (isTabGroupLabel(tab)) {
+ tab = tab.group;
+ }
+ const draggingTabs = tab.multiselected ? gBrowser.selectedTabs : [tab];
+ const { offsetX, offsetY } = this.#getDragImageOffset(event, tab, draggingTabs);
+ const dragImage = this.#createDragImageForTabs(draggingTabs);
+ this.originalDragImageArgs = [dragImage, offsetX, offsetY];
+ dt.setDragImage(...this.originalDragImageArgs);
+ }
+
+ #createDragImageForTabs(movingTabs) {
+ const periphery = gZenWorkspaces.activeWorkspaceElement.querySelector(
+ '#tabbrowser-arrowscrollbox-periphery'
+ );
+ const wrapper = document.createElement('div');
+ const tabRect = window.windowUtils.getBoundsWithoutFlushing(movingTabs[0]);
+ for (let i = 0; i < movingTabs.length; i++) {
+ const tab = movingTabs[i];
+ const tabClone = tab.cloneNode(true);
+ if (tabClone.hasAttribute('zen-essential')) {
+ tabClone.style.minWidth = tab.style.maxWidth = '54px';
+ tabClone.style.minHeight = tab.style.maxHeight = '50px';
+ }
+ if (i > 0) {
+ tabClone.style.transform = `translate(${i * 4}px, -${i * (tabRect.height - 4)}px)`;
+ tabClone.style.opacity = '0.2';
+ tabClone.style.zIndex = `${-i}`;
+ }
+ wrapper.appendChild(tabClone);
+ }
+ this.#maybeCreateDragImageDot(movingTabs, wrapper);
+ wrapper.style.width = tabRect.width + 'px';
+ wrapper.style.height = tabRect.height * movingTabs.length + 'px';
+ wrapper.style.position = 'fixed';
+ wrapper.style.top = '-9999px';
+ periphery.appendChild(wrapper);
+ this._tempDragImageParent = wrapper;
+ return wrapper;
+ }
+
+ #maybeCreateDragImageDot(movingTabs, wrapper) {
+ if (movingTabs.length > 1) {
+ const dot = document.createElement('div');
+ dot.textContent = movingTabs.length;
+ dot.style.position = 'absolute';
+ dot.style.top = '-10px';
+ dot.style.left = '-16px';
+ dot.style.background = 'red';
+ dot.style.borderRadius = '50%';
+ dot.style.fontWeight = 'bold';
+ dot.style.fontSize = '10px';
+ dot.style.lineHeight = '16px';
+ dot.style.justifyContent = dot.style.alignItems = 'center';
+ dot.style.height = dot.style.minWidth = '16px';
+ dot.style.textAlign = 'center';
+ dot.style.color = 'white';
+ wrapper.appendChild(dot);
+ }
+ }
+
+ _animateTabMove(event) {
+ let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
+ if (event.target.closest('#zen-essentials')) {
+ if (!isTab(draggedTab)) {
+ this.clearDragOverVisuals();
+ return;
+ }
+ return this.#animateVerticalPinnedGridDragOver(event);
+ } else if (this._fakeEssentialTab) {
+ this.#makeDragImageNonEssential(event);
+ }
+ let dragData = draggedTab._dragData;
+ let movingTabs = dragData.movingTabs;
+ let movingTabsSet = dragData.movingTabsSet;
+
+ dragData.animLastScreenPos ??= this._tabbrowserTabs.verticalMode
+ ? dragData.screenY
+ : dragData.screenX;
+ let allTabs = this._tabbrowserTabs.ariaFocusableItems;
+ let numEssentials = gBrowser._numZenEssentials;
+ let isEssential = draggedTab.hasAttribute('zen-essential');
+ let tabs = allTabs.slice(
+ isEssential ? 0 : numEssentials,
+ isEssential ? numEssentials : undefined
+ );
+
+ let screen = this._tabbrowserTabs.verticalMode ? event.screenY : event.screenX;
+ if (screen == dragData.animLastScreenPos) {
+ return;
+ }
+ let screenForward = screen > dragData.animLastScreenPos;
+ dragData.animLastScreenPos = screen;
+
+ this._clearDragOverGroupingTimer();
+
+ if (this._rtlMode) {
+ tabs.reverse();
+ }
+
+ let bounds = (ele) => window.windowUtils.getBoundsWithoutFlushing(ele);
+ let logicalForward = screenForward != this._rtlMode;
+ let screenAxis = this._tabbrowserTabs.verticalMode ? 'screenY' : 'screenX';
+ let size = this._tabbrowserTabs.verticalMode ? 'height' : 'width';
+ let { width: tabWidth, height: tabHeight } = bounds(draggedTab);
+ let tabSize = this._tabbrowserTabs.verticalMode ? tabHeight : tabWidth;
+ let translateX = event.screenX - dragData.screenX;
+ let translateY = event.screenY - dragData.screenY;
+
+ dragData.tabWidth = tabWidth;
+ dragData.tabHeight = tabHeight;
+ dragData.translateX = translateX;
+ dragData.translateY = translateY;
+
+ // Move the dragged tab based on the mouse position.
+ let periphery = document.getElementById('tabbrowser-arrowscrollbox-periphery');
+ let lastMovingTab = movingTabs.at(-1);
+ let firstMovingTab = movingTabs[0];
+ let endEdge = (ele) => ele[screenAxis] + bounds(ele)[size];
+ let lastMovingTabScreen = endEdge(lastMovingTab);
+ let firstMovingTabScreen = firstMovingTab[screenAxis];
+ let shiftSize = lastMovingTabScreen - firstMovingTabScreen;
+ let translate = screen - dragData[screenAxis];
+
+ // Constrain the range over which the moving tabs can move between the edge of the tabstrip and periphery.
+ // Add 1 to periphery so we don't overlap it.
+ let startBound = this._rtlMode
+ ? endEdge(periphery) + 1 - firstMovingTabScreen
+ : this._tabbrowserTabs[screenAxis] - firstMovingTabScreen;
+ let endBound = this._rtlMode
+ ? endEdge(this._tabbrowserTabs) - lastMovingTabScreen
+ : periphery[screenAxis] - 1 - lastMovingTabScreen;
+ let firstTab = tabs.at(this._rtlMode ? -1 : 0);
+ let lastTab = tabs.at(this._rtlMode ? 0 : -1);
+ startBound = firstTab[screenAxis] - firstMovingTabScreen;
+ endBound = endEdge(lastTab) - lastMovingTabScreen;
+ translate = Math.min(Math.max(translate, startBound), endBound);
+
+ // Center the tab under the cursor if the tab is not under the cursor while dragging
+ let draggedTabScreenAxis = draggedTab[screenAxis] + translate;
+ if (
+ (screen < draggedTabScreenAxis || screen > draggedTabScreenAxis + tabSize) &&
+ draggedTabScreenAxis + tabSize < endBound &&
+ draggedTabScreenAxis > startBound
+ ) {
+ translate = screen - draggedTab[screenAxis] - tabSize / 2;
+ // Ensure, after the above calculation, we are still within bounds
+ translate = Math.min(Math.max(translate, startBound), endBound);
+ }
+
+ if (!gBrowser.pinnedTabCount && !this._dragToPinPromoCard.shouldRender) {
+ let pinnedDropIndicatorMargin = parseFloat(
+ window.getComputedStyle(this._pinnedDropIndicator).marginInline
+ );
+ this._checkWithinPinnedContainerBounds({
+ firstMovingTabScreen,
+ lastMovingTabScreen,
+ pinnedTabsStartEdge: this._rtlMode
+ ? endEdge(this._tabbrowserTabs.arrowScrollbox) + pinnedDropIndicatorMargin
+ : this[screenAxis],
+ pinnedTabsEndEdge: this._rtlMode
+ ? endEdge(this._tabbrowserTabs)
+ : this._tabbrowserTabs.arrowScrollbox[screenAxis] - pinnedDropIndicatorMargin,
+ translate,
+ draggedTab,
+ });
+ }
+
+ dragData.translatePos = translate;
+
+ tabs = tabs.filter((t) => !movingTabsSet.has(t) || t == draggedTab);
+
+ /**
+ * When the `draggedTab` is just starting to move, the `draggedTab` is in
+ * its original location and the `dropElementIndex == draggedTab.elementIndex`.
+ * Any tabs or tab group labels passed in as `item` will result in a 0 shift
+ * because all of those items should also continue to appear in their original
+ * locations.
+ *
+ * Once the `draggedTab` is more "backward" in the tab strip than its original
+ * position, any tabs or tab group labels between the `draggedTab`'s original
+ * `elementIndex` and the current `dropElementIndex` should shift "forward"
+ * out of the way of the dragging tabs.
+ *
+ * When the `draggedTab` is more "forward" in the tab strip than its original
+ * position, any tabs or tab group labels between the `draggedTab`'s original
+ * `elementIndex` and the current `dropElementIndex` should shift "backward"
+ * out of the way of the dragging tabs.
+ *
+ * @param {MozTabbrowserTab|MozTabbrowserTabGroup.label} item
+ * @param {number} dropElementIndex
+ * @returns {number}
+ */
+ let getTabShift = (item, dropElementIndex) => {
+ if (item.elementIndex < draggedTab.elementIndex && item.elementIndex >= dropElementIndex) {
+ return this._rtlMode ? -shiftSize : shiftSize;
+ }
+ if (item.elementIndex > draggedTab.elementIndex && item.elementIndex < dropElementIndex) {
+ return this._rtlMode ? shiftSize : -shiftSize;
+ }
+ return 0;
+ };
+
+ let oldDropElementIndex = dragData.animDropElementIndex ?? movingTabs[0].elementIndex;
+
+ /**
+ * Returns the higher % by which one element overlaps another
+ * in the tab strip.
+ *
+ * When element 1 is further forward in the tab strip:
+ *
+ * p1 p2 p1+s1 p2+s2
+ * | | | |
+ * ---------------------------------
+ * ========================
+ * s1
+ * ===================
+ * s2
+ * ==========
+ * overlap
+ *
+ * When element 2 is further forward in the tab strip:
+ *
+ * p2 p1 p2+s2 p1+s1
+ * | | | |
+ * ---------------------------------
+ * ========================
+ * s2
+ * ===================
+ * s1
+ * ==========
+ * overlap
+ *
+ * @param {number} p1
+ * Position (x or y value in screen coordinates) of element 1.
+ * @param {number} s1
+ * Size (width or height) of element 1.
+ * @param {number} p2
+ * Position (x or y value in screen coordinates) of element 2.
+ * @param {number} s2
+ * Size (width or height) of element 1.
+ * @returns {number}
+ * Percent between 0.0 and 1.0 (inclusive) of element 1 or element 2
+ * that is overlapped by the other element. If the elements have
+ * different sizes, then this returns the larger overlap percentage.
+ */
+ function greatestOverlap(p1, s1, p2, s2) {
+ let overlapSize;
+ if (p1 < p2) {
+ // element 1 starts first
+ overlapSize = p1 + s1 - p2;
+ } else {
+ // element 2 starts first
+ overlapSize = p2 + s2 - p1;
+ }
+
+ // No overlap if size is <= 0
+ if (overlapSize <= 0) {
+ return 0;
+ }
+
+ // Calculate the overlap fraction from each element's perspective.
+ let overlapPercent = Math.max(overlapSize / s1, overlapSize / s2);
+
+ return Math.min(overlapPercent, 1);
+ }
+
+ /**
+ * Determine what tab/tab group label we're dragging over.
+ *
+ * When dragging right or downwards, the reference point for overlap is
+ * the right or bottom edge of the most forward moving tab.
+ *
+ * When dragging left or upwards, the reference point for overlap is the
+ * left or top edge of the most backward moving tab.
+ *
+ * @returns {Element|null}
+ * The tab or tab group label that should be used to visually shift tab
+ * strip elements out of the way of the dragged tab(s) during a drag
+ * operation. Note: this is not used to determine where the dragged
+ * tab(s) will be dropped, it is only used for visual animation at this
+ * time.
+ */
+ let getOverlappedElement = () => {
+ let point = (screenForward ? lastMovingTabScreen : firstMovingTabScreen) + translate;
+ let low = 0;
+ let high = tabs.length - 1;
+ while (low <= high) {
+ let mid = Math.floor((low + high) / 2);
+ if (tabs[mid] == draggedTab && ++mid > high) {
+ break;
+ }
+ let element = tabs[mid];
+ let elementForSize = elementToMove(element);
+ screen = elementForSize[screenAxis] + getTabShift(element, oldDropElementIndex);
+
+ if (screen > point) {
+ high = mid - 1;
+ } else if (screen + bounds(elementForSize)[size] < point) {
+ low = mid + 1;
+ } else {
+ return element;
+ }
+ }
+ return null;
+ };
+
+ let dropElement = getOverlappedElement();
+
+ let newDropElementIndex;
+ if (dropElement) {
+ newDropElementIndex = dropElement.elementIndex;
+ } else {
+ // When the dragged element(s) moves past a tab strip item, the dragged
+ // element's leading edge starts dragging over empty space, resulting in
+ // no overlapping `dropElement`. In these cases, try to fall back to the
+ // previous animation drop element index to avoid unstable animations
+ // (tab strip items snapping back and forth to shift out of the way of
+ // the dragged element(s)).
+ newDropElementIndex = oldDropElementIndex;
+
+ // We always want to have a `dropElement` so that we can determine where to
+ // logically drop the dragged element(s).
+ //
+ // It's tempting to set `dropElement` to
+ // `this.ariaFocusableItems.at(oldDropElementIndex)`, and that is correct
+ // for most cases, but there are edge cases:
+ //
+ // 1) the drop element index range needs to be one larger than the number of
+ // items that can move in the tab strip. The simplest example is when all
+ // tabs are ungrouped and unpinned: for 5 tabs, the drop element index needs
+ // to be able to go from 0 (become the first tab) to 5 (become the last tab).
+ // `this.ariaFocusableItems.at(5)` would be `undefined` when dragging to the
+ // end of the tab strip. In this specific case, it works to fall back to
+ // setting the drop element to the last tab.
+ //
+ // 2) the `elementIndex` values of the tab strip items do not change during
+ // the drag operation. When dragging the last tab or multiple tabs at the end
+ // of the tab strip, having `dropElement` fall back to the last tab makes the
+ // drop element one of the moving tabs. This can have some unexpected behavior
+ // if not careful. Falling back to the last tab that's not moving (instead of
+ // just the last tab) helps ensure that `dropElement` is always a stable target
+ // to drop next to.
+ //
+ // 3) all of the elements in the tab strip are moving, in which case there can't
+ // be a drop element and it should stay `undefined`.
+ //
+ // 4) we just started dragging and the `oldDropElementIndex` has its default
+ // valuë of `movingTabs[0].elementIndex`. In this case, the drop element
+ // shouldn't be a moving tab, so keep it `undefined`.
+ let lastPossibleDropElement = this._rtlMode
+ ? tabs.find((t) => t != draggedTab)
+ : tabs.findLast((t) => t != draggedTab);
+ let maxElementIndexForDropElement = lastPossibleDropElement?.elementIndex;
+ if (Number.isInteger(maxElementIndexForDropElement)) {
+ let index = Math.min(oldDropElementIndex, maxElementIndexForDropElement);
+ let oldDropElementCandidate = this._tabbrowserTabs.ariaFocusableItems.at(index);
+ if (!movingTabsSet.has(oldDropElementCandidate)) {
+ dropElement = oldDropElementCandidate;
+ }
+ }
+ }
+
+ let moveOverThreshold;
+ let overlapPercent;
+ let dropBefore;
+ if (dropElement) {
+ let dropElementForOverlap = elementToMove(dropElement);
+
+ let dropElementScreen = dropElementForOverlap[screenAxis];
+ let dropElementPos = dropElementScreen + getTabShift(dropElement, oldDropElementIndex);
+ let dropElementSize = bounds(dropElementForOverlap)[size];
+ let firstMovingTabPos = firstMovingTabScreen + translate;
+ overlapPercent = greatestOverlap(
+ firstMovingTabPos,
+ shiftSize,
+ dropElementPos,
+ dropElementSize
+ );
+
+ moveOverThreshold = gBrowser._tabGroupsEnabled
+ ? Services.prefs.getIntPref('browser.tabs.dragDrop.moveOverThresholdPercent') / 100
+ : 0.5;
+ moveOverThreshold = Math.min(1, Math.max(0, moveOverThreshold));
+ let shouldMoveOver = overlapPercent > moveOverThreshold;
+ if (logicalForward && shouldMoveOver) {
+ newDropElementIndex++;
+ } else if (!logicalForward && !shouldMoveOver) {
+ newDropElementIndex++;
+ if (newDropElementIndex > oldDropElementIndex) {
+ // FIXME: Not quite sure what's going on here, but this check
+ // prevents jittery back-and-forth movement of background tabs
+ // in certain cases.
+ newDropElementIndex = oldDropElementIndex;
+ }
+ }
+
+ // Recalculate the overlap with the updated drop index for when the
+ // drop element moves over.
+ dropElementPos = dropElementScreen + getTabShift(dropElement, newDropElementIndex);
+ overlapPercent = greatestOverlap(
+ firstMovingTabPos,
+ shiftSize,
+ dropElementPos,
+ dropElementSize
+ );
+ dropBefore = firstMovingTabPos < dropElementPos;
+ if (this._rtlMode) {
+ dropBefore = !dropBefore;
+ }
+ }
+
+ this._tabbrowserTabs.removeAttribute('movingtab-group');
+ this._resetGroupTarget(document.querySelector('[dragover-groupTarget]'));
+
+ delete dragData.shouldDropIntoCollapsedTabGroup;
+
+ [dropBefore, dropElement] = this.#applyDragoverIndicator(
+ event,
+ tabs,
+ movingTabs,
+ draggedTab
+ ) ?? [dropBefore, dropElement];
+
+ // Default to dropping into `dropElement`'s tab group, if it exists.
+ let dropElementGroup = dropElement?.group;
+ let colorCode = dropElementGroup?.color;
+
+ let lastUnmovingTabInGroup = dropElementGroup?.tabs.findLast((t) => !movingTabsSet.has(t));
+ if (
+ isTab(dropElement) &&
+ dropElementGroup &&
+ dropElement == lastUnmovingTabInGroup &&
+ !dropBefore
+ ) {
+ // Dragging tab over the last tab of a tab group, but not enough
+ // for it to drop into the tab group. Drop it after the tab group instead.
+ dropElement = dropElementGroup;
+ colorCode = undefined;
+ } else if (isTabGroupLabel(dropElement)) {
+ // Dropping right before the first tab in the tab group.
+ dropElement = dropElementGroup.tabs[0];
+ dropBefore = true;
+ }
+ this._setDragOverGroupColor(colorCode);
+ this._tabbrowserTabs.toggleAttribute('movingtab-addToGroup', colorCode);
+ this._tabbrowserTabs.toggleAttribute('movingtab-ungroup', !colorCode);
+
+ if (
+ newDropElementIndex == oldDropElementIndex &&
+ dropBefore == dragData.dropBefore &&
+ dropElement == dragData.dropElement
+ ) {
+ return;
+ }
+
+ dragData.dropElement = dropElement;
+ dragData.dropBefore = dropBefore;
+ dragData.animDropElementIndex = newDropElementIndex;
+ }
+
+ #isMovingTab() {
+ return this._tabbrowserTabs.hasAttribute('movingtab');
+ }
+
+ get #dragShiftableItems() {
+ const separator = gZenWorkspaces.pinnedTabsContainer.querySelector(
+ '.pinned-tabs-container-separator'
+ );
+ // Make sure to always return the separator at the start of the array
+ return Services.prefs.getBoolPref('zen.view.show-newtab-button-top')
+ ? [separator, gZenWorkspaces.activeWorkspaceElement.newTabButton]
+ : [separator];
+ }
+
+ handle_dragover(event) {
+ super.handle_dragover(event);
+ if (!gZenVerticalTabsManager._prefsSidebarExpanded) {
+ return;
+ }
+ this.#handle_sidebarDragOver(event);
+ }
+
+ #shouldSwitchSpace(event) {
+ const padding = 10;
+ // If we are hovering over the edges of the gNavToolbox or the splitter, we
+ // can change the workspace after a short delay.
+ const splitter = document.getElementById('zen-sidebar-splitter');
+ let rect = window.windowUtils.getBoundsWithoutFlushing(gNavToolbox);
+ if (!(gZenCompactModeManager.preference && gZenCompactModeManager.canHideSidebar)) {
+ rect.width += window.windowUtils.getBoundsWithoutFlushing(splitter).width;
+ }
+ const { clientX } = event;
+ const isNearLeftEdge = clientX >= rect.left - padding && clientX <= rect.left + padding;
+ const isNearRightEdge = clientX >= rect.right - padding && clientX <= rect.right + padding;
+ return { isNearLeftEdge, isNearRightEdge };
+ }
+
+ clearSpaceSwitchTimer() {
+ if (this.#changeSpaceTimer) {
+ clearTimeout(this.#changeSpaceTimer);
+ this.#changeSpaceTimer = null;
+ }
+ }
+
+ #handle_sidebarDragOver(event) {
+ const dt = event.dataTransfer;
+ const draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
+ // TODO: Add support for switching spaces when dragging folders and split-view groups.
+ if (!isTab(draggedTab) || draggedTab.hasAttribute('zen-essential')) {
+ this.clearSpaceSwitchTimer();
+ return;
+ }
+ const { isNearLeftEdge, isNearRightEdge } = this.#shouldSwitchSpace(event);
+ if (isNearLeftEdge || isNearRightEdge) {
+ if (!this.#changeSpaceTimer) {
+ this.#changeSpaceTimer = setTimeout(() => {
+ this.clearDragOverVisuals();
+ dt.updateDragImage(...this.originalDragImageArgs);
+ gZenWorkspaces.changeWorkspaceShortcut(
+ isNearLeftEdge ? -1 : 1,
+ false,
+ /* Disable wrapping */ true
+ );
+ this.#changeSpaceTimer = null;
+ }, this._dndSwitchSpaceDelay);
+ }
+ } else if (this.#changeSpaceTimer) {
+ this.clearSpaceSwitchTimer();
+ }
+ }
+
+ handle_windowDragEnter(event) {
+ if (!this.#isMovingTab() || !this.#isOutOfWindow) {
+ return;
+ }
+ this.#isOutOfWindow = false;
+ const dt = event.dataTransfer;
+ dt.updateDragImage(...this.originalDragImageArgs);
+ }
+
+ handle_windowDragLeave(event) {
+ const canvas = this._tabbrowserTabs._dndCanvas;
+ if (!this.#isMovingTab() || !canvas) {
+ return;
+ }
+ let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
+ if (!isTab(draggedTab)) {
+ return;
+ }
+ this.clearSpaceSwitchTimer();
+ const { clientX, clientY } = event;
+ const { innerWidth, innerHeight } = window;
+ const isOutOfWindow =
+ clientX < 0 || clientX > innerWidth || clientY < 0 || clientY > innerHeight;
+ if (isOutOfWindow && !this.#isOutOfWindow) {
+ this.#isOutOfWindow = true;
+ this.clearDragOverVisuals();
+ const dt = event.dataTransfer;
+ let dragData = draggedTab._dragData;
+ let movingTabs = dragData.movingTabs;
+ if (!this._browserDragImageWrapper) {
+ const wrappingDiv = document.createXULElement('vbox');
+ canvas.style.borderRadius = '8px';
+ canvas.style.border = '2px solid white';
+ wrappingDiv.style.width = 200 + 'px';
+ wrappingDiv.style.height = 130 + 'px';
+ wrappingDiv.style.position = 'relative';
+ this.#maybeCreateDragImageDot(movingTabs, wrappingDiv);
+ wrappingDiv.appendChild(canvas);
+ this._browserDragImageWrapper = wrappingDiv;
+ document.documentElement.appendChild(wrappingDiv);
+ }
+ dt.updateDragImage(
+ this._browserDragImageWrapper,
+ this.originalDragImageArgs[1],
+ this.originalDragImageArgs[2]
+ );
+ window.addEventListener('dragover', this.handle_windowDragEnter, {
+ once: true,
+ capture: true,
+ });
+ }
+ }
+
+ handle_drop(event) {
+ this.clearSpaceSwitchTimer();
+ super.handle_drop(event);
+ const dt = event.dataTransfer;
+ let draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
+ if (
+ isTab(draggedTab) &&
+ !draggedTab.hasAttribute('zen-essential') &&
+ draggedTab.getAttribute('zen-workspace-id') != gZenWorkspaces.activeWorkspace
+ ) {
+ const movingTabs = draggedTab._dragData.movingTabs;
+ for (let tab of movingTabs) {
+ tab.setAttribute('zen-workspace-id', gZenWorkspaces.activeWorkspace);
+ }
+ gBrowser.selectedTab = draggedTab;
+ }
+ gZenWorkspaces.updateTabsContainers();
+ }
+
+ handle_drop_transition(dropElement, draggedTab, movingTabs, dropBefore) {
+ if (isTabGroupLabel(dropElement)) {
+ dropElement = dropElement.group;
+ }
+ if (isTabGroupLabel(draggedTab)) {
+ draggedTab = draggedTab.group;
+ }
+ let animations = [];
+ try {
+ if (
+ this.#isAnimatingTabMove ||
+ !gZenStartup.isReady ||
+ gReduceMotion ||
+ !dropElement ||
+ dropElement.group !== draggedTab.group ||
+ dropElement.hasAttribute('zen-essential') ||
+ draggedTab.hasAttribute('zen-essential') ||
+ draggedTab.getAttribute('zen-workspace-id') != gZenWorkspaces.activeWorkspace
+ ) {
+ return;
+ }
+ this.#isAnimatingTabMove = true;
+ const animateElement = (ele, translateY) => {
+ ele.style.transform = `translateY(${translateY}px)`;
+ setTimeout(() => {
+ setTimeout(() => {
+ animations.push(
+ gZenUIManager.motion
+ .animate(
+ ele,
+ {
+ y: [translateY, 0],
+ },
+ {
+ duration: 0.1,
+ bounce: 0,
+ }
+ )
+ .then(() => {
+ ele.style.transform = '';
+ })
+ );
+ });
+ });
+ };
+ const items = this._tabbrowserTabs.ariaFocusableItems;
+ let rect = window.windowUtils.getBoundsWithoutFlushing(draggedTab);
+ let tabsInBetween = [];
+ let startIndex = Math.min(draggedTab.elementIndex, dropElement.elementIndex + !dropBefore);
+ let endIndex = Math.max(draggedTab.elementIndex, dropElement.elementIndex - dropBefore);
+ for (let i = startIndex; i <= endIndex; i++) {
+ let tab = items[i];
+ if (!movingTabs.includes(tab) && isTab(tab)) {
+ tabsInBetween.push(tab);
+ }
+ }
+ let extraTranslate = 0;
+ let translateY =
+ draggedTab.elementIndex > dropElement.elementIndex ? -rect.height : rect.height;
+ translateY *= movingTabs.length;
+ if (draggedTab.pinned != dropElement.pinned) {
+ const shiftableItems = this.#dragShiftableItems;
+ for (let item of shiftableItems) {
+ // We also need to animate these shiftable items and add it to the extraTranslate
+ // so the dragged tab ends up in the right position.
+ let itemRect = window.windowUtils.getBoundsWithoutFlushing(item);
+ extraTranslate += itemRect.height;
+ animateElement(item, translateY);
+ }
+ }
+ // Animate tabs in between moving out of the way
+ for (let tab of tabsInBetween) {
+ animateElement(tab, translateY);
+ }
+ let draggedTabTranslateY =
+ draggedTab.elementIndex > dropElement.elementIndex
+ ? rect.height * tabsInBetween.length
+ : -rect.height * tabsInBetween.length;
+ draggedTabTranslateY +=
+ extraTranslate * (draggedTab.elementIndex > dropElement.elementIndex ? 1 : -1);
+ animateElement(draggedTab, draggedTabTranslateY);
+ } catch (e) {
+ console.error(e);
+ }
+ Promise.all(animations).finally(() => {
+ this.#isAnimatingTabMove = false;
+ });
+ }
+
+ handle_dragend(event) {
+ let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
+ this.ZenDragAndDropService.onDragEnd();
+ super.handle_dragend(event);
+ this.#removeDragOverBackground();
+ gZenPinnedTabManager.removeTabContainersDragoverClass();
+ this.#maybeClearVerticalPinnedGridDragOver(draggedTab);
+ this.originalDragImageArgs = [];
+ window.removeEventListener('dragover', this.handle_windowDragEnter, { capture: true });
+ this.#isOutOfWindow = false;
+ if (this._browserDragImageWrapper) {
+ this._browserDragImageWrapper.remove();
+ delete this._browserDragImageWrapper;
+ }
+ if (this._tempDragImageParent) {
+ this._tempDragImageParent.remove();
+ delete this._tempDragImageParent;
+ }
+ }
+
+ #applyDragOverBackground(element) {
+ if (this.#dragOverBackground && this.#lastDropTarget === element) {
+ return false;
+ }
+ const margin = 2;
+ const rect = window.windowUtils.getBoundsWithoutFlushing(element);
+ this.#dragOverBackground = document.createElement('div');
+ this.#dragOverBackground.id = 'zen-dragover-background';
+ this.#dragOverBackground.style.height = `${rect.height - margin * 2}px`;
+ this.#dragOverBackground.style.top = `${rect.top + margin}px`;
+ gNavToolbox.appendChild(this.#dragOverBackground);
+ this.#lastDropTarget = element;
+ return true;
+ }
+
+ #removeDragOverBackground() {
+ if (this.#dragOverBackground) {
+ this.#dragOverBackground.remove();
+ this.#dragOverBackground = null;
+ this.#lastDropTarget = null;
+ }
+ }
+
+ clearDragOverVisuals() {
+ this.#removeDragOverBackground();
+ gZenPinnedTabManager.removeTabContainersDragoverClass();
+ }
+
+ #applyDragoverIndicator(event, tabs, movingTabs, draggedTab) {
+ const separation = 4;
+ const dropZoneSelector =
+ ':is(.tabbrowser-tab, .zen-drop-target, .tab-group-label, tab-group[split-view-group])';
+ let shouldPlayHapticFeedback = false;
+ let showIndicatorUnderNewTabButton = false;
+ let dropElement = event.target.closest(dropZoneSelector);
+ let dropBefore;
+ if (!dropElement) {
+ if (event.target.classList.contains('zen-workspace-empty-space')) {
+ dropElement = this._tabbrowserTabs.ariaFocusableItems.at(-1);
+ // Only if there are no normal tabs to drop after
+ showIndicatorUnderNewTabButton = !tabs.some((tab) => !(tab.group || tab).pinned);
+ } else {
+ const numEssentials = gBrowser._numZenEssentials;
+ const numPinned = gBrowser.pinnedTabCount - numEssentials;
+ const tabToUse = event.target.closest(dropZoneSelector);
+ if (!tabToUse) {
+ this.clearDragOverVisuals();
+ return;
+ }
+ const isPinned = tabToUse.pinned;
+ const relativeTabs = tabs.slice(
+ isPinned ? 0 : numPinned,
+ isPinned ? numPinned : undefined
+ );
+ const draggedTabRect = elementToMove(tabToUse).getBoundingClientRect();
+ dropElement = event.clientY > draggedTabRect.top ? relativeTabs.at(-1) : relativeTabs[0];
+ }
+ }
+ dropElement = elementToMove(dropElement);
+ this.#maybeClearVerticalPinnedGridDragOver(draggedTab);
+ if (this.#lastDropTarget !== dropElement) {
+ shouldPlayHapticFeedback = this.#lastDropTarget !== null;
+ this.#removeDragOverBackground();
+ }
+ let isZenFolder = dropElement.parentElement?.isZenFolder;
+ let canHightlightGroup =
+ gZenFolders.highlightGroupOnDragOver(dropElement.parentElement, movingTabs) || !isZenFolder;
+ let rect = window.windowUtils.getBoundsWithoutFlushing(dropElement);
+ const overlapPercent = (event.clientY - rect.top) / rect.height;
+ // We wan't to leave a small threshold (20% for example) so we can drag tabs below and above
+ // a folder label without dragging into the folder.
+ let threshold = Services.prefs.getIntPref('zen.tabs.folder-dragover-threshold-percent') / 100;
+ let dropIntoFolder =
+ isZenFolder && (overlapPercent < threshold || overlapPercent > 1 - threshold);
+ if (
+ isTabGroupLabel(draggedTab) &&
+ draggedTab.group?.isZenFolder &&
+ (isTab(dropElement) || dropElement.hasAttribute('split-view-group')) &&
+ (!dropElement.pinned || dropElement.hasAttribute('zen-essential'))
+ ) {
+ this.clearDragOverVisuals();
+ return;
+ }
+ if (
+ isTab(dropElement) ||
+ dropIntoFolder ||
+ showIndicatorUnderNewTabButton ||
+ dropElement.hasAttribute('split-view-group')
+ ) {
+ if (showIndicatorUnderNewTabButton) {
+ rect = window.windowUtils.getBoundsWithoutFlushing(this.#dragShiftableItems.at(-1));
+ }
+ const indicator = gZenPinnedTabManager.dragIndicator;
+ let top = 0;
+ threshold =
+ Services.prefs.getIntPref('browser.tabs.dragDrop.moveOverThresholdPercent') / 100;
+ if (overlapPercent > threshold) {
+ top = Math.round(rect.top + rect.height) + 'px';
+ dropBefore = false;
+ } else {
+ top = Math.round(rect.top) + 'px';
+ dropBefore = true;
+ }
+ if (indicator.style.top !== top) {
+ shouldPlayHapticFeedback = true;
+ }
+ indicator.setAttribute('orientation', 'horizontal');
+ indicator.style.setProperty('--indicator-left', rect.left + separation / 2 + 'px');
+ indicator.style.setProperty('--indicator-width', rect.width - separation + 'px');
+ indicator.style.top = top;
+ indicator.style.removeProperty('left');
+ this.#removeDragOverBackground();
+ if (!isTab(dropElement) && dropElement?.parentElement?.isZenFolder) {
+ dropElement = dropElement.parentElement;
+ }
+ } else if (dropElement.classList.contains('zen-drop-target') && canHightlightGroup) {
+ shouldPlayHapticFeedback =
+ this.#applyDragOverBackground(dropElement) && !gZenPinnedTabManager._dragIndicator;
+ gZenPinnedTabManager.removeTabContainersDragoverClass();
+ dropElement = dropElement.parentElement?.labelElement || dropElement;
+ }
+
+ if (shouldPlayHapticFeedback) {
+ Services.zen.playHapticFeedback();
+ }
+ return [dropBefore, dropElement];
+ }
+
+ #getDragImageOffset(event, tab, draggingTabs) {
+ if (draggingTabs.length > 1) {
+ return {
+ offsetX: 18,
+ offsetY: 18,
+ };
+ }
+ const rect = tab.getBoundingClientRect();
+ return {
+ offsetX: event.clientX - rect.left,
+ offsetY: event.clientY - rect.top,
+ };
+ }
+
+ #animateVerticalPinnedGridDragOver(event) {
+ let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
+ let dragData = draggedTab._dragData;
+ let movingTabs = dragData.movingTabs;
+ this.clearDragOverVisuals();
+ if (
+ !draggedTab.hasAttribute('zen-essential') &&
+ gBrowser._numZenEssentials >= gZenPinnedTabManager.maxEssentialTabs
+ ) {
+ return;
+ }
+
+ if (!this._fakeEssentialTab) {
+ const numEssentials = gBrowser._numZenEssentials;
+ let pinnedTabs = this._tabbrowserTabs.ariaFocusableItems.slice(0, numEssentials);
+ this._fakeEssentialTab = document.createXULElement('vbox');
+ this._fakeEssentialTab.elementIndex = numEssentials;
+ this.#makeDragImageEssential(event);
+ delete dragData.animDropElementIndex;
+ if (draggedTab.hasAttribute('zen-essential')) {
+ draggedTab.style.visibility = 'hidden';
+ } else {
+ event.target.closest('.zen-essentials-container').appendChild(this._fakeEssentialTab);
+ gZenWorkspaces.updateTabsContainers();
+ pinnedTabs.push(this._fakeEssentialTab);
+ }
+ let tabsPerRow = 0;
+ let position = RTL_UI
+ ? window.windowUtils.getBoundsWithoutFlushing(this._tabbrowserTabs.pinnedTabsContainer)
+ .right
+ : 0;
+ for (let pinnedTab of pinnedTabs) {
+ let tabPosition;
+ let rect = window.windowUtils.getBoundsWithoutFlushing(pinnedTab);
+ if (RTL_UI) {
+ tabPosition = rect.right;
+ if (tabPosition > position) {
+ break;
+ }
+ } else {
+ tabPosition = rect.left;
+ if (tabPosition < position) {
+ break;
+ }
+ }
+ tabsPerRow++;
+ position = tabPosition;
+ }
+ this.#maxTabsPerRow = tabsPerRow;
+ }
+ let usingFakeElement = !!this._fakeEssentialTab.parentElement;
+ let elementMoving = usingFakeElement ? this._fakeEssentialTab : draggedTab;
+ if (usingFakeElement) {
+ movingTabs = [this._fakeEssentialTab];
+ }
+
+ let dragDataScreenX = usingFakeElement ? this._fakeEssentialTab.screenX : dragData.screenX;
+ let dragDataScreenY = usingFakeElement ? this._fakeEssentialTab.screenY : dragData.screenY;
+
+ dragData.animLastScreenX ??= dragDataScreenX;
+ dragData.animLastScreenY ??= dragDataScreenY;
+
+ let screenX = event.screenX;
+ let screenY = event.screenY;
+
+ if (screenY == dragData.animLastScreenY && screenX == dragData.animLastScreenX) {
+ return;
+ }
+
+ let tabs = this._tabbrowserTabs.visibleTabs.slice(0, gBrowser._numZenEssentials);
+ if (usingFakeElement) {
+ tabs.push(this._fakeEssentialTab);
+ }
+
+ let directionX = screenX > dragData.animLastScreenX;
+ let directionY = screenY > dragData.animLastScreenY;
+ dragData.animLastScreenY = screenY;
+ dragData.animLastScreenX = screenX;
+
+ let { width: tabWidth, height: tabHeight } = elementMoving.getBoundingClientRect();
+ tabWidth += 4; // Add 6px to account for the gap
+ tabHeight += 4;
+ let shiftSizeX = tabWidth;
+ let shiftSizeY = tabHeight;
+ dragData.tabWidth = tabWidth;
+ dragData.tabHeight = tabHeight;
+
+ // Move the dragged tab based on the mouse position.
+ let firstTabInRow;
+ let lastTabInRow;
+ let lastTab = tabs.at(-1);
+ if (RTL_UI) {
+ firstTabInRow =
+ tabs.length >= this.#maxTabsPerRow ? tabs[this.#maxTabsPerRow - 1] : lastTab;
+ lastTabInRow = tabs[0];
+ } else {
+ firstTabInRow = tabs[0];
+ lastTabInRow = tabs.length >= this.#maxTabsPerRow ? tabs[this.#maxTabsPerRow - 1] : lastTab;
+ }
+ let lastMovingTabScreenX = movingTabs.at(-1).screenX;
+ let lastMovingTabScreenY = movingTabs.at(-1).screenY;
+ let firstMovingTabScreenX = movingTabs[0].screenX;
+ let firstMovingTabScreenY = movingTabs[0].screenY;
+ let translateX = screenX - dragDataScreenX;
+ let translateY = screenY - dragDataScreenY;
+ let firstBoundX = firstTabInRow.screenX - firstMovingTabScreenX;
+ let firstBoundY = this._tabbrowserTabs.screenY - firstMovingTabScreenY;
+ let lastBoundX =
+ lastTabInRow.screenX +
+ lastTabInRow.getBoundingClientRect().width -
+ (lastMovingTabScreenX + tabWidth);
+ let lastBoundY = lastTab.screenY - lastMovingTabScreenY;
+ translateX = Math.min(Math.max(translateX, firstBoundX), lastBoundX);
+ translateY = Math.min(Math.max(translateY, firstBoundY), lastBoundY);
+
+ // Center the tab under the cursor if the tab is not under the cursor while dragging
+ if (
+ screen < elementMoving.screenY + translateY ||
+ screen > elementMoving.screenY + tabHeight + translateY
+ ) {
+ translateY = screen - elementMoving.screenY - tabHeight / 2;
+ }
+
+ dragData.translateX = translateX;
+ dragData.translateY = translateY;
+
+ // Determine what tab we're dragging over.
+ // * Single tab dragging: Point of reference is the center of the dragged tab. If that
+ // point touches a background tab, the dragged tab would take that
+ // tab's position when dropped.
+ // * Multiple tabs dragging: All dragged tabs are one "giant" tab with two
+ // points of reference (center of tabs on the extremities). When
+ // mouse is moving from top to bottom, the bottom reference gets activated,
+ // otherwise the top reference will be used. Everything else works the same
+ // as single tab dragging.
+ // * We're doing a binary search in order to reduce the amount of
+ // tabs we need to check.
+
+ tabs = tabs.filter((t) => !movingTabs.includes(t) || t == elementMoving);
+ let firstTabCenterX = firstMovingTabScreenX + translateX + tabWidth / 2;
+ let lastTabCenterX = lastMovingTabScreenX + translateX + tabWidth / 2;
+ let tabCenterX = directionX ? lastTabCenterX : firstTabCenterX;
+ let firstTabCenterY = firstMovingTabScreenY + translateY + tabHeight / 2;
+ let lastTabCenterY = lastMovingTabScreenY + translateY + tabHeight / 2;
+ let tabCenterY = directionY ? lastTabCenterY : firstTabCenterY;
+
+ let shiftNumber = this.#maxTabsPerRow - movingTabs.length;
+
+ let getTabShift = (tab, dropIndex) => {
+ if (tab.elementIndex < elementMoving.elementIndex && tab.elementIndex >= dropIndex) {
+ // If tab is at the end of a row, shift back and down
+ let tabRow = Math.ceil((tab.elementIndex + 1) / this.#maxTabsPerRow);
+ let shiftedTabRow = Math.ceil(
+ (tab.elementIndex + 1 + movingTabs.length) / this.#maxTabsPerRow
+ );
+ if (tab.elementIndex && tabRow != shiftedTabRow) {
+ return [RTL_UI ? tabWidth * shiftNumber : -tabWidth * shiftNumber, shiftSizeY];
+ }
+ return [RTL_UI ? -shiftSizeX : shiftSizeX, 0];
+ }
+ if (tab.elementIndex > elementMoving.elementIndex && tab.elementIndex < dropIndex) {
+ // If tab is not index 0 and at the start of a row, shift across and up
+ let tabRow = Math.floor(tab.elementIndex / this.#maxTabsPerRow);
+ let shiftedTabRow = Math.floor(
+ (tab.elementIndex - movingTabs.length) / this.#maxTabsPerRow
+ );
+ if (tab.elementIndex && tabRow != shiftedTabRow) {
+ return [RTL_UI ? -tabWidth * shiftNumber : tabWidth * shiftNumber, -shiftSizeY];
+ }
+ return [RTL_UI ? shiftSizeX : -shiftSizeX, 0];
+ }
+ return [0, 0];
+ };
+
+ let low = 0;
+ let high = tabs.length - 1;
+ let newIndex = -1;
+ let oldIndex = dragData.animDropElementIndex ?? movingTabs[0].elementIndex;
+ while (low <= high) {
+ let mid = Math.floor((low + high) / 2);
+ if (tabs[mid] == elementMoving && ++mid > high) {
+ break;
+ }
+ let [shiftX, shiftY] = getTabShift(tabs[mid], oldIndex);
+ screenX = tabs[mid].screenX + shiftX;
+ screenY = tabs[mid].screenY + shiftY;
+
+ if (screenY + tabHeight < tabCenterY) {
+ low = mid + 1;
+ } else if (screenY > tabCenterY) {
+ high = mid - 1;
+ } else if (RTL_UI ? screenX + tabWidth < tabCenterX : screenX > tabCenterX) {
+ high = mid - 1;
+ } else if (RTL_UI ? screenX > tabCenterX : screenX + tabWidth < tabCenterX) {
+ low = mid + 1;
+ } else {
+ newIndex = tabs[mid].elementIndex;
+ break;
+ }
+ }
+
+ if (newIndex >= oldIndex && newIndex < tabs.length) {
+ newIndex++;
+ }
+
+ if (newIndex < 0) {
+ newIndex = oldIndex;
+ }
+
+ if (newIndex == dragData.animDropElementIndex) {
+ return;
+ }
+
+ dragData.animDropElementIndex = newIndex;
+ dragData.dropElement = tabs[Math.min(newIndex, tabs.length - 1)];
+ dragData.dropBefore = newIndex < tabs.length;
+
+ // Shift background tabs to leave a gap where the dragged tab
+ // would currently be dropped.
+ for (let tab of tabs) {
+ if (tab != draggedTab) {
+ let [shiftX, shiftY] = getTabShift(tab, newIndex);
+ tab.style.transform = shiftX || shiftY ? `translate(${shiftX}px, ${shiftY}px)` : '';
+ }
+ }
+ }
+
+ #maybeClearVerticalPinnedGridDragOver(draggedTab) {
+ if (this._fakeEssentialTab) {
+ this._fakeEssentialTab.remove();
+ delete this._fakeEssentialTab;
+ draggedTab.style.visibility = '';
+ for (let tab of this._tabbrowserTabs.visibleTabs.slice(0, gBrowser._numZenEssentials)) {
+ tab.style.transform = '';
+ }
+ gZenWorkspaces.updateTabsContainers();
+ }
+ }
+
+ #makeDragImageEssential(event) {
+ const dt = event.dataTransfer;
+ const draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
+ const dragData = draggedTab._dragData;
+ const [wrapper] = this.originalDragImageArgs;
+ const tab = wrapper.firstElementChild;
+ tab.setAttribute('zen-essential', 'true');
+ tab.setAttribute('pinned', 'true');
+ tab.setAttribute('selected', 'true');
+ tab.style.minWidth = tab.style.maxWidth = wrapper.style.width = '54px';
+ tab.style.minHeight = tab.style.maxHeight = wrapper.style.height = '50px';
+ const offsetY = dragData.offsetY;
+ const offsetX = dragData.offsetX;
+ // Apply a transform translate to the tab in order to center it within the drag image
+ tab.style.transform = `translate(${(54 - offsetX) / 2}px, ${(50 - offsetY) / 2}px)`;
+ gZenPinnedTabManager.setEssentialTabIcon(tab);
+ dt.updateDragImage(wrapper, -16, -16);
+ }
+
+ #makeDragImageNonEssential(event) {
+ const dt = event.dataTransfer;
+ const draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
+ const wrapper = this.originalDragImageArgs[0];
+ const tab = wrapper.firstElementChild;
+ tab.style.setProperty('transition', 'none', 'important');
+ tab.removeAttribute('zen-essential');
+ tab.removeAttribute('pinned');
+ tab.style.minWidth = tab.style.maxWidth = '';
+ tab.style.minHeight = tab.style.maxHeight = '';
+ tab.style.transform = '';
+ const rect = window.windowUtils.getBoundsWithoutFlushing(draggedTab);
+ wrapper.style.width = rect.width + 'px';
+ wrapper.style.height = rect.height + 'px';
+ setTimeout(() => {
+ tab.style.transition = '';
+ dt.updateDragImage(...this.originalDragImageArgs);
+ }, 50);
+ }
+ };
+}
diff --git a/src/zen/drag-and-drop/components.conf b/src/zen/drag-and-drop/components.conf
new file mode 100644
index 000000000..576fc3af0
--- /dev/null
+++ b/src/zen/drag-and-drop/components.conf
@@ -0,0 +1,14 @@
+# 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/.
+
+Classes = [
+ {
+ 'cid': '{f8714110-1fb1-4129-abad-887a64e4085e}',
+ 'interfaces': ['nsIZenDragAndDrop'],
+ 'contract_ids': ['@mozilla.org/zen/drag-and-drop;1'],
+ 'type': 'zen::nsZenDragAndDrop',
+ 'headers': ['mozilla/nsZenDragAndDrop.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+]
diff --git a/src/zen/drag-and-drop/jar.inc.mn b/src/zen/drag-and-drop/jar.inc.mn
new file mode 100644
index 000000000..d5c31bbbf
--- /dev/null
+++ b/src/zen/drag-and-drop/jar.inc.mn
@@ -0,0 +1,5 @@
+# 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/.
+
+ content/browser/zen-components/ZenDragAndDrop.js (../../zen/drag-and-drop/ZenDragAndDrop.js)
diff --git a/src/zen/drag-and-drop/moz.build b/src/zen/drag-and-drop/moz.build
new file mode 100644
index 000000000..9b7aa4564
--- /dev/null
+++ b/src/zen/drag-and-drop/moz.build
@@ -0,0 +1,31 @@
+#
+# 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/.
+
+XPIDL_SOURCES += [
+ "nsIZenDragAndDrop.idl",
+]
+
+EXPORTS.mozilla += [
+ "nsZenDragAndDrop.h",
+]
+
+SOURCES += [
+ "nsZenDragAndDrop.cpp",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+LOCAL_INCLUDES += [
+ "/dom/base",
+ "/layout/base",
+ "/widget",
+]
+
+FINAL_LIBRARY = "xul"
+XPIDL_MODULE = "zen_dnd"
diff --git a/src/zen/drag-and-drop/nsIZenDragAndDrop.idl b/src/zen/drag-and-drop/nsIZenDragAndDrop.idl
new file mode 100644
index 000000000..c8fddbf20
--- /dev/null
+++ b/src/zen/drag-and-drop/nsIZenDragAndDrop.idl
@@ -0,0 +1,25 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * @brief Interface for Zen's drag and drop functionality.
+ */
+[scriptable, uuid(f8714110-1fb1-4129-abad-887a64e4085e)]
+interface nsIZenDragAndDrop : nsISupports {
+ /**
+ * @brief Indicate that a drag operation has started. Note
+ * that this should only be called for zen's drag and drop
+ * operations for the tabs.
+ * @param opacity The opacity of the drag image.
+ */
+ void onDragStart(in float opacity);
+
+ /**
+ * @brief Indicate that a drag operation has ended.
+ */
+ void onDragEnd();
+};
+
diff --git a/src/zen/drag-and-drop/nsZenDragAndDrop.cpp b/src/zen/drag-and-drop/nsZenDragAndDrop.cpp
new file mode 100644
index 000000000..48246e842
--- /dev/null
+++ b/src/zen/drag-and-drop/nsZenDragAndDrop.cpp
@@ -0,0 +1,48 @@
+/* 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/. */
+
+#include "nsZenDragAndDrop.h"
+#include "nsBaseDragService.h"
+
+namespace zen {
+namespace {
+
+static constexpr auto kZenDefaultDragImageOpacity =
+#if defined(MOZ_WIDGET_GTK)
+// For GTK, the default is 0.5 (DRAG_IMAGE_ALPHA_LEVEL) to match
+// the native behavior. Make sure its synced with the following variable:
+// https://searchfox.org/firefox-main/rev/14c08f0368ead8bfdddec62f43e0bb5c8fd61289/widget/gtk/nsDragService.cpp#75
+ 0.5f;
+#else
+// For other platforms, the default is whatever the value of DRAG_TRANSLUCENCY
+// is, defined in nsBaseDragService.h
+ DRAG_TRANSLUCENCY;
+#endif
+
+} // namespace:
+
+// Use the macro to inject all of the definitions for nsISupports.
+NS_IMPL_ISUPPORTS(nsZenDragAndDrop, nsIZenDragAndDrop)
+
+nsZenDragAndDrop::nsZenDragAndDrop() {
+ (void)this->OnDragEnd();
+}
+
+auto nsZenDragAndDrop::GetZenDragAndDropInstance() -> nsCOMPtr {
+ return do_GetService(ZEN_BOOSTS_BACKEND_CONTRACTID);
+}
+
+NS_IMETHODIMP
+nsZenDragAndDrop::OnDragStart(float opacity) {
+ mDragImageOpacity = opacity;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsZenDragAndDrop::OnDragEnd() {
+ mDragImageOpacity = kZenDefaultDragImageOpacity;
+ return NS_OK;
+}
+
+} // namespace: zen
diff --git a/src/zen/drag-and-drop/nsZenDragAndDrop.h b/src/zen/drag-and-drop/nsZenDragAndDrop.h
new file mode 100644
index 000000000..d03990bfa
--- /dev/null
+++ b/src/zen/drag-and-drop/nsZenDragAndDrop.h
@@ -0,0 +1,45 @@
+/* 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/. */
+
+#ifndef mozilla_ZenDragAndDrop_h__
+#define mozilla_ZenDragAndDrop_h__
+
+#include "nsIZenDragAndDrop.h"
+#include "nsCOMPtr.h"
+
+#define ZEN_BOOSTS_BACKEND_CONTRACTID "@mozilla.org/zen/drag-and-drop;1"
+
+namespace zen {
+
+/**
+ * @brief Implementation of the nsIZenDragAndDrop interface.
+ * When we want to do a drag and drop operation, web standards
+ * don't really allow much customization of the drag image.
+ * This class allows Zen to have more control over the drag
+ * and drop operations for the tabs.
+ */
+class nsZenDragAndDrop final : public nsIZenDragAndDrop {
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIZENDRAGANDDROP
+
+ public:
+ explicit nsZenDragAndDrop();
+ auto GetDragImageOpacity() const { return mDragImageOpacity; }
+
+ /**
+ * @brief Get the singleton instance of nsZenDragAndDrop. There may be occasions
+ * where it won't be available (e.g. on the content process), so this may return
+ * nullptr.
+ * @return nsZenDragAndDrop* The singleton instance, or nullptr if not available
+ */
+ static auto GetZenDragAndDropInstance() -> nsCOMPtr;
+
+ private:
+ ~nsZenDragAndDrop() = default;
+ float mDragImageOpacity{};
+};
+
+} // namespace zen
+
+#endif
diff --git a/src/zen/folders/ZenFolder.mjs b/src/zen/folders/ZenFolder.mjs
index 3c3413e6a..d05dc396c 100644
--- a/src/zen/folders/ZenFolder.mjs
+++ b/src/zen/folders/ZenFolder.mjs
@@ -6,7 +6,7 @@ export class nsZenFolder extends MozTabbrowserTabGroup {
#initialized = false;
static markup = `
-
+
diff --git a/src/zen/folders/ZenFolders.mjs b/src/zen/folders/ZenFolders.mjs
index e1fcd7c97..a8ff7497b 100644
--- a/src/zen/folders/ZenFolders.mjs
+++ b/src/zen/folders/ZenFolders.mjs
@@ -33,8 +33,6 @@ function formatRelativeTime(timestamp) {
class nsZenFolders extends nsZenDOMOperatedFeature {
#ZEN_MAX_SUBFOLDERS = Services.prefs.getIntPref('zen.folders.max-subfolders', 5);
- #ZEN_EDGE_ZONE_THRESHOLD =
- Services.prefs.getIntPref('zen.view.drag-and-drop.edge-zone-threshold', 25) / 100;
#popup = null;
#popupTimer = null;
@@ -1065,18 +1063,16 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
* @param {Array|null} movingTabs The tabs being moved.
*/
highlightGroupOnDragOver(folder, movingTabs) {
- if (folder === this.#lastHighlightedGroup) return;
+ if (folder === this.#lastHighlightedGroup) return true;
const tab = movingTabs ? movingTabs[0] : null;
if (this.#lastHighlightedGroup && this.#lastHighlightedGroup !== folder) {
- this.#lastHighlightedGroup.removeAttribute('selected');
if (this.#lastHighlightedGroup.collapsed) {
this.updateFolderIcon(this.#lastHighlightedGroup, 'close');
}
this.#lastHighlightedGroup = null;
}
-
if (
- folder &&
+ folder?.isZenFolder &&
(!folder.hasAttribute('split-view-group') || !folder.hasAttribute('selected')) &&
folder !== tab?.group &&
!(
@@ -1084,13 +1080,13 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
movingTabs?.some((t) => gBrowser.isTabGroupLabel(t))
)
) {
- folder.setAttribute('selected', 'true');
- folder.style.transform = '';
if (folder.collapsed) {
this.updateFolderIcon(folder, 'open');
}
this.#lastHighlightedGroup = folder;
+ return true;
}
+ return false;
}
/**
@@ -1103,54 +1099,6 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
}
}
- /**
- * Handles the dragover logic when dragging a tab or tab group label over another tab group label.
- * This function determines where the dragged item should be visually dropped (before/after the group, or inside it)
- * and updates related styling and highlighting.
- *
- * @param {MozTabbrowserTabGroupLabel} currentDropElement The tab group label currently being dragged over.
- * @param {MozTabbrowserTab|MozTabbrowserTabGroupLabel} draggedTab The tab or tab group label being dragged.
- * @param {number} overlapPercent The percentage of overlap between the dragged item and the drop target.
- * @param {Array} movingTabs An array of tabs that are currently being dragged together.
- * @param {boolean} currentDropBefore Indicates if the current drop position is before the middle of the drop element.
- * @param {string|undefined} currentColorCode The current color code for dragover highlighting.
- * @returns {{dropElement: MozTabbrowserTabGroup|MozTabbrowserTab|MozTabbrowserTabGroupLabel, colorCode: string|undefined, dropBefore: boolean}}
- * An object containing the updated drop element, color code for highlighting, and drop position.
- */
- handleDragOverTabGroupLabel(
- currentDropElement,
- draggedTab,
- overlapPercent,
- movingTabs,
- currentDropBefore,
- currentColorCode
- ) {
- let dropElement = currentDropElement;
- let dropBefore = currentDropBefore;
- let colorCode = currentColorCode;
-
- const dropElementGroup = dropElement?.isZenFolder ? dropElement : dropElement?.group;
- const isSplitGroup = dropElement?.group?.hasAttribute('split-view-group');
- let firstGroupElem = dropElementGroup?.groupStartElement.nextElementSibling;
- if (gBrowser.isTabGroup(firstGroupElem)) firstGroupElem = firstGroupElem.labelElement;
-
- const isInMiddleZone =
- overlapPercent >= this.#ZEN_EDGE_ZONE_THRESHOLD &&
- overlapPercent <= 1 - this.#ZEN_EDGE_ZONE_THRESHOLD;
- const shouldDropInside = isInMiddleZone && !isSplitGroup;
-
- if (shouldDropInside) {
- dropElement = firstGroupElem;
- dropBefore = true;
- this.highlightGroupOnDragOver(dropElementGroup, movingTabs);
- } else {
- colorCode = undefined;
- this.highlightGroupOnDragOver(null);
- }
-
- return { dropElement, colorCode, dropBefore };
- }
-
#normalizeGroupItems(items) {
return items
.filter((item) => !item.hasAttribute('zen-empty-tab'))
diff --git a/src/zen/moz.build b/src/zen/moz.build
index eb681597f..4910bc43d 100644
--- a/src/zen/moz.build
+++ b/src/zen/moz.build
@@ -8,6 +8,7 @@ EXTRA_PP_COMPONENTS += [
DIRS += [
"common",
+ "drag-and-drop",
"glance",
"mods",
"tests",
diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs
index c8f2dfd17..e01e27bd3 100644
--- a/src/zen/sessionstore/ZenWindowSync.sys.mjs
+++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs
@@ -464,6 +464,12 @@ class nsZenWindowSync {
}
const relativeTab = this.#getItemFromWindow(aWindow, originalSibling.id);
if (relativeTab) {
+ gBrowser.tabContainer.tabDragAndDrop.handle_drop_transition(
+ relativeTab,
+ aTargetItem,
+ [aTargetItem],
+ false
+ );
relativeTab.after(aTargetItem);
}
});
diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs
index cab32e721..c9e99b861 100644
--- a/src/zen/split-view/ZenViewSplitter.mjs
+++ b/src/zen/split-view/ZenViewSplitter.mjs
@@ -228,9 +228,26 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
}
}
+ #getDragImageForSplit(tab) {
+ const element = window.MozXULElement.parseXULToFragment(
+ `
+
+
+
+
+ `
+ ).querySelector('#zen-split-view-drag-image');
+ const image = element.querySelector('image');
+ const label = element.querySelector('label');
+ image.src = tab.getAttribute('image');
+ label.textContent = tab.label;
+ document.documentElement.appendChild(element);
+ this._dndElement = element;
+ return element;
+ }
+
onBrowserDragOverToSplit(event) {
if (this.fakeBrowser) {
- this.onBrowserDragEndToSplit(event);
return;
}
var dt = event.dataTransfer;
@@ -239,23 +256,20 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
// tab copy or move
draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
// not our drop then
- if (!draggedTab || gBrowser.selectedTab.hasAttribute('zen-empty-tab')) {
+ if (!gBrowser.isTab(draggedTab) || gBrowser.selectedTab.hasAttribute('zen-empty-tab')) {
return;
}
gBrowser.tabContainer.tabDragAndDrop.finishMoveTogetherSelectedTabs(draggedTab);
}
if (
- !draggedTab ||
- this._canDrop ||
- this._hasAnimated ||
- this.fakeBrowser ||
!this._lastOpenedTab ||
- (this._lastOpenedTab &&
- this._lastOpenedTab.getAttribute('zen-workspace-id') !==
- draggedTab.getAttribute('zen-workspace-id') &&
- !this._lastOpenedTab.hasAttribute('zen-essential')) ||
- draggedTab === this._lastOpenedTab
+ (this._lastOpenedTab.getAttribute('zen-workspace-id') !==
+ draggedTab.getAttribute('zen-workspace-id') &&
+ !this._lastOpenedTab.hasAttribute('zen-essential'))
) {
+ this._lastOpenedTab = gBrowser.selectedTab;
+ }
+ if (!draggedTab || this._canDrop || this._hasAnimated || this.fakeBrowser) {
return;
}
if (draggedTab.splitView) {
@@ -285,19 +299,25 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
return;
}
dt.mozCursor = 'default';
+ if (!this._dndElement) {
+ const originalDNDArgs = gBrowser.tabContainer.tabDragAndDrop.originalDragImageArgs;
+ requestAnimationFrame(() => {
+ dt.updateDragImage(
+ this.#getDragImageForSplit(draggedTab),
+ originalDNDArgs[1],
+ originalDNDArgs[2]
+ );
+ });
+ gBrowser.tabContainer.tabDragAndDrop.clearDragOverVisuals();
+ }
const oldTab = this._lastOpenedTab;
this._canDrop = true;
+ Services.zen.playHapticFeedback();
{
this._draggingTab = draggedTab;
gBrowser.selectedTab = oldTab;
this._hasAnimated = true;
this.tabBrowserPanel.setAttribute('dragging-split', 'true');
- for (const tab of gBrowser.tabs) {
- tab.style.removeProperty('transform');
- if (tab.group) {
- tab.group.style.removeProperty('transform');
- }
- }
// Add a min width to all the browser elements to prevent them from resizing
const panelsWidth = gBrowser.tabbox.getBoundingClientRect().width;
let numOfTabsToDivide = 2;
@@ -330,11 +350,6 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
this.fakeBrowser.setAttribute('has-split-view', 'true');
}
gBrowser.tabbox.appendChild(this.fakeBrowser);
- this.fakeBrowser.style.setProperty(
- '--zen-split-view-fake-icon',
- `url(${draggedTab.getAttribute('image')})`
- );
- draggedTab._visuallySelected = true;
this.fakeBrowser.setAttribute('side', side);
this._finishAllAnimatingPromise = Promise.all([
gZenUIManager.motion.animate(
@@ -371,13 +386,14 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
]);
if (this._finishAllAnimatingPromise) {
this._finishAllAnimatingPromise.then(() => {
- draggedTab.linkedBrowser.docShellIsActive = false;
- draggedTab.linkedBrowser
- .closest('.browserSidebarContainer')
- .classList.remove('deck-selected');
+ if (draggedTab !== oldTab) {
+ draggedTab.linkedBrowser.docShellIsActive = false;
+ draggedTab.linkedBrowser
+ .closest('.browserSidebarContainer')
+ .classList.remove('deck-selected');
+ }
this.fakeBrowser.addEventListener('dragleave', this.onBrowserDragEndToSplit);
this._canDrop = true;
- draggedTab._visuallySelected = true;
});
}
}
@@ -390,12 +406,11 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
const panelsRect = gBrowser.tabbox.getBoundingClientRect();
const fakeBrowserRect = this.fakeBrowser && this.fakeBrowser.getBoundingClientRect();
if (
- ((event.target.closest('#tabbrowser-tabbox') && event.target != this.fakeBrowser) ||
- (fakeBrowserRect &&
- event.clientX > fakeBrowserRect.left &&
- event.clientX < fakeBrowserRect.left + fakeBrowserRect.width &&
- event.clientY > fakeBrowserRect.top &&
- event.clientY < fakeBrowserRect.top + fakeBrowserRect.height) ||
+ ((fakeBrowserRect &&
+ event.clientX > fakeBrowserRect.left &&
+ event.clientX < fakeBrowserRect.left + fakeBrowserRect.width &&
+ event.clientY > fakeBrowserRect.top &&
+ event.clientY < fakeBrowserRect.top + fakeBrowserRect.height) ||
(event.screenX === 0 && event.screenY === 0)) && // It's equivalent to 0 if the event has been dropped
!cancelled
) {
@@ -415,50 +430,47 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
if (!this.fakeBrowser) {
return;
}
- this.fakeBrowser.classList.add('fade-out');
const side = this.fakeBrowser.getAttribute('side');
- if (this._draggingTab) this._draggingTab.setAttribute('zen-has-splitted', 'true');
this._lastOpenedTab = gBrowser.selectedTab;
this._draggingTab = null;
- try {
- this._canDrop = false;
- Promise.all([
- gZenUIManager.motion.animate(
- gBrowser.tabbox,
- side === 'left'
+ gBrowser.tabContainer.tabDragAndDrop.clearSpaceSwitchTimer();
+ event.dataTransfer.updateDragImage(
+ ...gBrowser.tabContainer.tabDragAndDrop.originalDragImageArgs
+ );
+ this._canDrop = false;
+ Promise.all([
+ gZenUIManager.motion.animate(
+ gBrowser.tabbox,
+ side === 'left'
+ ? {
+ paddingLeft: [`${halfWidth}px`, 0],
+ }
+ : {
+ paddingRight: [`${halfWidth}px`, 0],
+ },
+ {
+ duration: 0.1,
+ easing: 'ease-out',
+ }
+ ),
+ gZenUIManager.motion.animate(
+ this.fakeBrowser,
+ {
+ width: [`${halfWidth - padding * 2}px`, 0],
+ ...(side === 'left'
? {
- paddingLeft: [`${halfWidth}px`, 0],
+ marginLeft: [`${-halfWidth}px`, 0],
}
- : {
- paddingRight: [`${halfWidth}px`, 0],
- },
- {
- duration: 0.1,
- easing: 'ease-out',
- }
- ),
- gZenUIManager.motion.animate(
- this.fakeBrowser,
- {
- width: [`${halfWidth - padding * 2}px`, 0],
- ...(side === 'left'
- ? {
- marginLeft: [`${-halfWidth}px`, 0],
- }
- : {}),
- },
- {
- duration: 0.1,
- easing: 'ease-out',
- }
- ),
- ]).then(() => {
- this._maybeRemoveFakeBrowser();
- });
- } catch {
- this._canDrop = false;
+ : {}),
+ },
+ {
+ duration: 0.1,
+ easing: 'ease-out',
+ }
+ ),
+ ]).finally(() => {
this._maybeRemoveFakeBrowser();
- }
+ });
}
/**
@@ -1667,11 +1679,14 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
_maybeRemoveFakeBrowser(select = true) {
gBrowser.tabbox.removeAttribute('style');
this.tabBrowserPanel.removeAttribute('dragging-split');
+ if (this._dndElement) {
+ this._dndElement.remove();
+ delete this._dndElement;
+ }
if (this.fakeBrowser) {
delete this._hasAnimated;
this.fakeBrowser.remove();
this.fakeBrowser = null;
- if (this._draggingTab) this._draggingTab._visuallySelected = false;
if (select) {
gBrowser.selectedTab = this._draggingTab;
this._draggingTab = null;
@@ -1728,6 +1743,12 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
return false;
}
+ const droppedOnTab = gZenGlanceManager.getTabOrGlanceParent(gBrowser.getTabForBrowser(browser));
+ if (droppedOnTab === this._draggingTab) {
+ this.createEmptySplit(dropSide == 'right');
+ return true;
+ }
+
gBrowser.selectedTab = this._draggingTab;
this._draggingTab = null;
const browserContainer = draggedTab.linkedBrowser?.closest('.browserSidebarContainer');
@@ -1735,7 +1756,6 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
browserContainer.style.opacity = '0';
}
- const droppedOnTab = gZenGlanceManager.getTabOrGlanceParent(gBrowser.getTabForBrowser(browser));
if (droppedOnTab && droppedOnTab !== draggedTab) {
// Calculate which side of the target browser the drop occurred
// const browserRect = browser.getBoundingClientRect();
@@ -1977,13 +1997,14 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
}
}
- createEmptySplit() {
+ createEmptySplit(rightSide = true) {
const selectedTab = gBrowser.selectedTab;
const emptyTab = gZenWorkspaces._emptyTab;
+ let tabs = rightSide ? [selectedTab, emptyTab] : [emptyTab, selectedTab];
const data = {
- tabs: [selectedTab, emptyTab],
+ tabs: tabs,
gridType: 'grid',
- layoutTree: this.calculateLayoutTree([selectedTab, emptyTab], 'grid'),
+ layoutTree: this.calculateLayoutTree(tabs, 'grid'),
};
this._data.push(data);
this.activateSplitView(data);
@@ -2016,7 +2037,11 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
this.removeTabFromGroup(emptyTab, groupIndex, { forUnsplit: true });
gBrowser.selectedTab = selectedTab;
this.resetTabState(emptyTab, false);
- this.splitTabs([selectedTab, newSelectedTab], 'grid', 1);
+ this.splitTabs(
+ rightSide ? [selectedTab, newSelectedTab] : [newSelectedTab, selectedTab],
+ 'grid',
+ rightSide ? 1 : 0
+ );
} else {
cleanup();
}
diff --git a/src/zen/split-view/zen-decks.css b/src/zen/split-view/zen-decks.css
index 6678a395f..b3fb72cbe 100644
--- a/src/zen/split-view/zen-decks.css
+++ b/src/zen/split-view/zen-decks.css
@@ -210,31 +210,28 @@
right: var(--zen-element-separation);
}
}
+}
- &::after {
- content: '';
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- width: 3.5rem;
- pointer-events: none;
- height: 3.5rem;
- background: var(--zen-split-view-fake-icon);
- background-size: contain;
- background-repeat: no-repeat;
- background-position: center;
- opacity: 0.8;
- transition: opacity 0.2s;
- transition-delay: 0.1s;
+#zen-split-view-drag-image {
+ width: 200px;
+ height: 250px;
+ border-radius: 16px;
+ background: black;
+ justify-content: center;
+ align-items: center;
+ padding: 10px;
+ gap: 20px;
+ position: relative;
- @starting-style {
- opacity: 0;
- }
+ & image {
+ width: 24px;
+ height: 24px;
}
- &.fade-out::after {
- opacity: 0;
- transition-delay: 0s;
+ & label {
+ color: white;
+ font-size: 14px;
+ font-weight: bold;
+ text-align: center;
}
}
diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs
index 1ab19ee08..dc1e602c4 100644
--- a/src/zen/tabs/ZenPinnedTabManager.mjs
+++ b/src/zen/tabs/ZenPinnedTabManager.mjs
@@ -84,12 +84,16 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
onTabIconChanged(tab, url = null) {
tab.dispatchEvent(new CustomEvent('ZenTabIconChanged', { bubbles: true, detail: { tab } }));
- const iconUrl = url ?? tab.iconImage.src;
if (tab.hasAttribute('zen-essential')) {
- tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`);
+ this.setEssentialTabIcon(tab, url);
}
}
+ setEssentialTabIcon(tab, url = null) {
+ const iconUrl = url ?? tab.getAttribute('image') ?? '';
+ tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`);
+ }
+
_onTabResetPinButton(event, tab) {
event.stopPropagation();
this._resetTabToStoredState(tab);
@@ -534,15 +538,16 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
moveToAnotherTabContainerIfNecessary(event, movingTabs) {
+ movingTabs = [...movingTabs];
if (!this.enabled) {
return false;
}
- movingTabs = [...movingTabs];
try {
- const pinnedTabsTarget =
- event.target.closest('.zen-current-workspace-indicator') || this._isGoingToPinnedTabs;
+ const pinnedTabsTarget = event.target.closest(
+ ':is(.zen-current-workspace-indicator, .zen-workspace-pinned-tabs-section)'
+ );
const essentialTabsTarget = event.target.closest('.zen-essentials-container');
- const tabsTarget = !this._isGoingToPinnedTabs;
+ const tabsTarget = !pinnedTabsTarget;
// TODO: Solve the issue of adding a tab between two groups
// Remove group labels from the moving tabs and replace it
@@ -685,9 +690,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
removeTabContainersDragoverClass(hideIndicator = true) {
- if (this._dragIndicator) {
- Services.zen.playHapticFeedback();
- }
this.dragIndicator.remove();
this._dragIndicator = null;
if (hideIndicator) {
@@ -695,95 +697,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
}
- onDragFinish() {
- for (const item of this.dragShiftableItems) {
- item.style.transform = '';
- }
- delete this._topToNormalTabs;
- for (const item of gBrowser.tabContainer.ariaFocusableItems) {
- if (gBrowser.isTab(item)) {
- let isVisible = true;
- let parent = item.group;
- while (parent) {
- if (!parent.visible) {
- isVisible = false;
- break;
- }
- parent = parent.group;
- }
- if (!isVisible) {
- continue;
- }
- }
- const itemToAnimate =
- item.group?.hasAttribute('split-view-group') || gBrowser.isTabGroupLabel(item)
- ? item.group
- : item;
- itemToAnimate.style.removeProperty('--zen-folder-indent');
- }
- this.removeTabContainersDragoverClass();
- }
-
- get dragShiftableItems() {
- const separator = gZenWorkspaces.pinnedTabsContainer.querySelector(
- '.pinned-tabs-container-separator'
- );
- // Make sure to always return the separator at the start of the array
- return Services.prefs.getBoolPref('zen.view.show-newtab-button-top')
- ? [separator, gZenWorkspaces.activeWorkspaceElement.newTabButton]
- : [separator];
- }
-
- animateSeparatorMove(movingTabs, dropElement, isPinned) {
- let draggedTab = movingTabs[0];
- if (gBrowser.isTabGroupLabel(draggedTab) && draggedTab.group.isZenFolder) {
- this._isGoingToPinnedTabs = true;
- return;
- }
- if (draggedTab?.group?.hasAttribute('split-view-group')) {
- draggedTab = draggedTab.group;
- }
- const itemsToCheck = this.dragShiftableItems;
- let translate = movingTabs[isPinned ? movingTabs.length - 1 : 0].getBoundingClientRect().top;
- if (isPinned) {
- const rect = draggedTab.getBoundingClientRect();
- translate += rect.height;
- }
- const draggingTabHeight = movingTabs.reduce((acc, item) => {
- return acc + window.windowUtils.getBoundsWithoutFlushing(item).height;
- }, 0);
- if (typeof this._topToNormalTabs === 'undefined') {
- const rects = itemsToCheck.map((item) => window.windowUtils.getBoundsWithoutFlushing(item));
- this._topToNormalTabs = rects[0].top + rects.at(-1).height / (isPinned ? 2 : 4);
- }
- let topToNormalTabs = this._topToNormalTabs;
- const isGoingToPinnedTabs =
- translate < topToNormalTabs && gBrowser.pinnedTabCount - gBrowser._numZenEssentials > 0;
- const multiplier = isGoingToPinnedTabs !== isPinned ? (isGoingToPinnedTabs ? 1 : -1) : 0;
- this._isGoingToPinnedTabs = isGoingToPinnedTabs;
- if (!dropElement) {
- itemsToCheck.forEach((item) => {
- item.style.transform = `translateY(${draggingTabHeight * multiplier}px)`;
- });
- }
- }
-
- getLastTabBound(lastBound, lastTab, isDraggingFolder = false) {
- if (!lastTab.pinned || isDraggingFolder) {
- return lastBound;
- }
- const shiftedItems = this.dragShiftableItems;
- let totalHeight = shiftedItems.reduce((acc, item) => {
- return acc + window.windowUtils.getBoundsWithoutFlushing(item).height;
- }, 0);
- if (shiftedItems.length === 1) {
- // Means the new tab button is not at the top or not visible
- const lastTabRect = window.windowUtils.getBoundsWithoutFlushing(lastTab);
- totalHeight += lastTabRect.height;
- }
- return lastBound + totalHeight + 6;
- }
-
get dragIndicator() {
if (!this._dragIndicator) {
this._dragIndicator = document.createElement('div');
diff --git a/src/zen/tabs/zen-tabs/vertical-tabs.css b/src/zen/tabs/zen-tabs/vertical-tabs.css
index 0ef1e524a..516d52a20 100644
--- a/src/zen/tabs/zen-tabs/vertical-tabs.css
+++ b/src/zen/tabs/zen-tabs/vertical-tabs.css
@@ -715,7 +715,6 @@
}
& .zen-essentials-container {
- will-change: transform;
justify-content: center;
grid-template-columns: 1fr !important;
padding: 0 !important;
@@ -1108,8 +1107,6 @@
}
.zen-essentials-container {
- will-change: transform;
-
overflow: hidden;
gap: 4px;
transition:
@@ -1166,7 +1163,7 @@
}
}
-.zen-essentials-container > .tabbrowser-tab,
+.tabbrowser-tab[zen-essential='true'],
#zen-welcome-initial-essentials-browser-sidebar-essentials .tabbrowser-tab {
--toolbarbutton-inner-padding: 0;
max-width: unset;
@@ -1288,9 +1285,9 @@
width: calc(var(--indicator-width) - 2 * var(--zen-drag-indicator-height) - 4px);
height: var(--zen-drag-indicator-height);
transition:
- top 0.1s ease-out,
- left 0.1s ease-out,
- width 0.1s ease-out;
+ top 0.05s ease-out,
+ left 0.05s ease-out,
+ width 0.05s ease-out;
&::before {
left: calc(-2 * var(--zen-drag-indicator-height));
@@ -1373,7 +1370,14 @@
}
}
-.tabbrowser-tab[zen-dragtarget],
-.tab-group-label-container[zen-dragtarget] {
- z-index: 9 !important;
+/* Drag and drop */
+
+#zen-dragover-background {
+ position: absolute;
+ z-index: -1;
+ /* Extra width to cover the sidebar splitter */
+ width: calc(100% + var(--zen-toolbox-padding));
+ left: 0;
+ pointer-events: none;
+ background: var(--zen-primary-color);
}
diff --git a/src/zen/workspaces/ZenWorkspace.mjs b/src/zen/workspaces/ZenWorkspace.mjs
index a888b6732..b890ef8f8 100644
--- a/src/zen/workspaces/ZenWorkspace.mjs
+++ b/src/zen/workspaces/ZenWorkspace.mjs
@@ -43,7 +43,7 @@ export class nsZenWorkspace extends MozXULElement {
static get markup() {
return `
-
+
diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs
index d70e70dc7..b228b6f4f 100644
--- a/src/zen/workspaces/ZenWorkspaces.mjs
+++ b/src/zen/workspaces/ZenWorkspaces.mjs
@@ -1568,11 +1568,7 @@ class nsZenWorkspaces {
}
async changeWorkspace(workspace, ...args) {
- if (
- !this.workspaceEnabled ||
- this.#inChangingWorkspace ||
- gNavToolbox.hasAttribute('movingtab')
- ) {
+ if (!this.workspaceEnabled || this.#inChangingWorkspace) {
return;
}
this.#inChangingWorkspace = true;
@@ -2433,7 +2429,7 @@ class nsZenWorkspaces {
return workspaceData;
}
- async updateTabsContainers(target = undefined, forAnimation = false) {
+ updateTabsContainers(target = undefined, forAnimation = false) {
this.makeSureEmptyTabIsFirst();
if (target && !target.target?.parentNode) {
target = null;
@@ -2443,7 +2439,7 @@ class nsZenWorkspaces {
if (target?.type === 'TabClose' || target?.type === 'TabOpen') {
animateContainer = target.target.pinned;
}
- await this.onPinnedTabsResize(
+ this.onPinnedTabsResize(
// This is what happens when we join a resize observer, an event listener
// while using it as a method.
[{ target: (target?.target ? target.target : target) ?? this.pinnedTabsContainer }],
@@ -2491,7 +2487,7 @@ class nsZenWorkspaces {
}
}
- async onPinnedTabsResize(entries, forAnimation = false, animateContainer = false) {
+ onPinnedTabsResize(entries, forAnimation = false, animateContainer = false) {
if (
document.documentElement.hasAttribute('inDOMFullscreen') ||
!this._hasInitializedTabsStrip ||
@@ -2515,9 +2511,7 @@ class nsZenWorkspaces {
// Get all workspaces that have the same userContextId
const activeWorkspace = this.getActiveWorkspace();
const userContextId = activeWorkspace.containerTabId;
- const workspaces = this._workspaceCache.filter(
- (w) => w.containerTabId === userContextId && w.uuid !== originalWorkspaceId
- );
+ const workspaces = this.getWorkspaces().filter((w) => w.containerTabId === userContextId);
workspacesIds.push(...workspaces.map((w) => w.uuid));
} else {
workspacesIds.push(originalWorkspaceId);
@@ -2690,7 +2684,7 @@ class nsZenWorkspaces {
return tab;
}
- async changeWorkspaceShortcut(offset = 1, whileScrolling = false) {
+ async changeWorkspaceShortcut(offset = 1, whileScrolling = false, disableWrap = false) {
// Cycle through workspaces
let workspaces = this.getWorkspaces();
let activeWorkspace = this.getActiveWorkspace();
@@ -2698,7 +2692,7 @@ class nsZenWorkspaces {
// note: offset can be negative
let targetIndex = workspaceIndex + offset;
- if (this.shouldWrapAroundNavigation) {
+ if (this.shouldWrapAroundNavigation && !disableWrap) {
// Add length to handle negative indices and loop
targetIndex = (targetIndex + workspaces.length) % workspaces.length;
} else {