feat: Added support for essential tabs, b=no-bug, c=split-view, tabs, workspaces

This commit is contained in:
mr. m
2025-12-27 11:49:46 +01:00
parent 2b2f7626b5
commit 4e46907bdd
7 changed files with 401 additions and 109 deletions

View File

@@ -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..1ac51353e63000f49075b12da83292312996a73c 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 @@
@@ -22,7 +22,18 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da8329231
if (
(dropEffect == "move" || dropEffect == "copy") &&
document == draggedTab.ownerDocument &&
@@ -266,6 +272,15 @@
@@ -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();
@@ -38,7 +49,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da8329231
if (draggedTab && dropEffect == "copy") {
let duplicatedDraggedTab;
let duplicatedTabs = [];
@@ -291,8 +306,9 @@
@@ -291,8 +302,9 @@
let translateOffsetY = oldTranslateY % tabHeight;
let newTranslateX = oldTranslateX - translateOffsetX;
let newTranslateY = oldTranslateY - translateOffsetY;
@@ -50,7 +61,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da8329231
if (this._isContainerVerticalPinnedGrid(draggedTab)) {
// Update both translate axis for pinned vertical expanded tabs
@@ -308,8 +324,8 @@
@@ -308,8 +320,8 @@
}
} else {
let tabs = this._tabbrowserTabs.ariaFocusableItems.slice(
@@ -61,7 +72,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da8329231
);
let size = this._tabbrowserTabs.verticalMode ? "height" : "width";
let screenAxis = this._tabbrowserTabs.verticalMode
@@ -362,11 +378,13 @@
@@ -362,11 +374,13 @@
this._dragToPinPromoCard,
];
let shouldPin =
@@ -75,7 +86,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da8329231
isTab(draggedTab) &&
draggedTab.pinned &&
this._tabbrowserTabs.arrowScrollbox.contains(event.target);
@@ -384,6 +402,7 @@
@@ -384,6 +398,7 @@
(oldTranslateY && oldTranslateY != newTranslateY);
} else if (this._tabbrowserTabs.verticalMode) {
shouldTranslate &&= oldTranslateY && oldTranslateY != newTranslateY;
@@ -83,7 +94,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da8329231
} else {
shouldTranslate &&= oldTranslateX && oldTranslateX != newTranslateX;
}
@@ -440,7 +459,7 @@
@@ -440,7 +455,7 @@
item.removeAttribute("tabdrop-samewindow");
resolve();
};
@@ -92,7 +103,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da8329231
postTransitionCleanup();
} else {
let onTransitionEnd = transitionendEvent => {
@@ -581,6 +600,7 @@
@@ -581,6 +596,7 @@
let nextItem = this._tabbrowserTabs.ariaFocusableItems[newIndex];
let tabGroup = isTab(nextItem) && nextItem.group;
@@ -100,7 +111,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da8329231
gBrowser.loadTabs(urls, {
inBackground,
replace,
@@ -618,7 +638,16 @@
@@ -618,7 +634,16 @@
this._expandGroupOnDrop(draggedTab);
}
this._resetTabsAfterDrop(draggedTab.ownerDocument);
@@ -118,7 +129,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da8329231
if (
dt.mozUserCancelled ||
dt.dropEffect != "none" ||
@@ -822,7 +851,10 @@
@@ -822,7 +847,10 @@
_getDragTarget(event, { ignoreSides = false } = {}) {
let { target } = event;
while (target) {
@@ -130,7 +141,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da8329231
break;
}
target = target.parentNode;
@@ -839,14 +871,17 @@
@@ -839,14 +867,17 @@
return null;
}
}
@@ -150,7 +161,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da8329231
!this._tabbrowserTabs.expandOnHover
);
}
@@ -877,7 +912,8 @@
@@ -877,7 +908,8 @@
isTabGroupLabel(draggedTab) &&
draggedTab._dragData?.expandGroupOnDrop
) {
@@ -160,19 +171,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da8329231
}
}
@@ -942,10 +978,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(
@@ -1055,7 +1088,6 @@
@@ -1055,7 +1087,6 @@
// using updateDragImage. On Linux, we can use a panel.
if (platform == "win" || platform == "macosx") {
captureListener = function () {
@@ -180,7 +179,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da8329231
};
} else {
// Create a panel to use it in setDragImage
@@ -1093,7 +1125,6 @@
@@ -1093,7 +1124,6 @@
);
dragImageOffset = dragImageOffset * scale;
}
@@ -188,7 +187,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da8329231
// _dragData.offsetX/Y give the coordinates that the mouse should be
// positioned relative to the corner of the new window created upon
@@ -1112,7 +1143,7 @@
@@ -1112,7 +1142,7 @@
let dropEffect = this.getDropEffectForTabDrag(event);
let isMovingInTabStrip = !fromTabList && dropEffect == "move";
let collapseTabGroupDuringDrag =
@@ -197,7 +196,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da8329231
tab._dragData = {
offsetX: this._tabbrowserTabs.verticalMode
@@ -1122,7 +1153,7 @@
@@ -1122,7 +1152,7 @@
? event.screenY - window.screenY - tabOffset
: event.screenY - window.screenY,
scrollPos:
@@ -206,7 +205,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da8329231
? this._tabbrowserTabs.pinnedTabsContainer.scrollPosition
: this._tabbrowserTabs.arrowScrollbox.scrollPosition,
screenX: event.screenX,
@@ -1149,6 +1180,7 @@
@@ -1149,6 +1179,7 @@
if (collapseTabGroupDuringDrag) {
tab.group.collapsed = true;
@@ -214,7 +213,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da8329231
}
}
}
@@ -1173,6 +1205,7 @@
@@ -1173,6 +1204,7 @@
if (tabStripItemElement.hasAttribute("dragtarget")) {
return;
}
@@ -222,7 +221,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da8329231
let isPinned = tab.pinned;
let numPinned = gBrowser.pinnedTabCount;
let allTabs = this._tabbrowserTabs.ariaFocusableItems;
@@ -2457,7 +2490,7 @@
@@ -2457,7 +2489,7 @@
tab.style.left = "";
tab.style.top = "";
tab.style.maxWidth = "";
@@ -231,7 +230,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da8329231
}
for (let label of draggedTabDocument.getElementsByClassName(
"tab-group-label-container"
@@ -2467,7 +2500,7 @@
@@ -2467,7 +2499,7 @@
label.style.left = "";
label.style.top = "";
label.style.maxWidth = "";

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tab.js b/browser/components/tabbrowser/content/tab.js
index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..69af8c986add4f74f1c3105ccd6e5804fd33b80f 100644
index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7600f564a 100644
--- a/browser/components/tabbrowser/content/tab.js
+++ b/browser/components/tabbrowser/content/tab.js
@@ -21,6 +21,7 @@
@@ -101,15 +101,6 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..69af8c986add4f74f1c3105ccd6e5804
}
get splitview() {
@@ -446,7 +464,7 @@
// to detach tabs. Ensure that we do not show the drag image returning
// to its point of origin when this happens, as it makes the drag
// finishing feel very slow.
- event.dataTransfer.mozShowFailAnimation = false;
+ event.dataTransfer.mozShowFailAnimation = true;
if (event.eventPhase == Event.CAPTURING_PHASE) {
this.style.MozUserFocus = "";
} else if (
@@ -473,6 +491,8 @@
this.style.MozUserFocus = "ignore";
} else if (

View File

@@ -52,6 +52,7 @@
#lastDropTarget = null;
originalDragImageArgs = [];
#isOutOfWindow = false;
#maxTabsPerRow = 0;
constructor(tabbrowserTabs) {
super(tabbrowserTabs);
@@ -68,6 +69,8 @@
super.init();
this.handle_windowDragEnter = this.handle_windowDragEnter.bind(this);
window.addEventListener('dragleave', this.handle_windowDragLeave.bind(this), true);
const dragOverBind = this.handle_dragover.bind(this);
gZenWorkspaces.workspaceIcons.addEventListener('dragover', dragOverBind);
}
startTabDrag(event, tab, ...args) {
@@ -92,6 +95,10 @@
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';
@@ -99,6 +106,17 @@
}
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;
@@ -116,17 +134,15 @@
dot.style.color = 'white';
wrapper.appendChild(dot);
}
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;
}
_animateTabMove(event) {
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
if (event.target.closest('#zen-essentials') && isTab(draggedTab)) {
return this.#animateVerticalPinnedGridDragOver(event);
} else if (this._fakeEssentialTab) {
this.#makeDragImageNonEssential(event);
}
let dragData = draggedTab._dragData;
let movingTabs = dragData.movingTabs;
let movingTabsSet = dragData.movingTabsSet;
@@ -532,9 +548,6 @@
handle_dragover(event) {
super.handle_dragover(event);
if (event.target.closest('#tabbrowser-tabbox')) {
gZenViewSplitter.onBrowserDragOverToSplit(event);
}
}
handle_windowDragEnter(event) {
@@ -563,12 +576,16 @@
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');
wrappingDiv.style.borderRadius = canvas.style.borderRadius = '8px';
wrappingDiv.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);
@@ -585,12 +602,6 @@
}
}
handle_drop(event) {
const dt = event.dataTransfer;
dt.updateDragImage(...this.originalDragImageArgs);
super.handle_drop(event);
}
handle_drop_transition(dropElement, draggedTab, movingTabs, dropBefore) {
if (isTabGroupLabel(dropElement)) {
dropElement = dropElement.group;
@@ -599,7 +610,9 @@
draggedTab = draggedTab.group;
}
if (
!gZenStartup.isReady ||
gReduceMotion ||
!dropElement ||
dropElement.group !== draggedTab.group ||
dropElement.hasAttribute('zen-essential') ||
draggedTab.hasAttribute('zen-essential')
@@ -666,10 +679,12 @@
}
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;
@@ -715,26 +730,33 @@
const separation = 4;
const dropZoneSelector = ':is(.tabbrowser-tab, .zen-drop-target, .tab-group-label)';
let shouldPlayHapticFeedback = false;
let showIndicatorUnderNewTabButton = false;
let dropElement = event.target.closest(dropZoneSelector);
let dropBefore;
if (!dropElement) {
const numEssentials = gBrowser._numZenEssentials;
const numPinned = gBrowser.pinnedTabCount - numEssentials;
const tabToUse = event.target.closest(dropZoneSelector);
if (!tabToUse) {
this.clearDragOverVisuals();
return;
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];
}
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);
if (this._isContainerVerticalPinnedGrid(dropElement) && isTab(draggedTab)) {
console.log('TODO: Handle essential tab dragover in vertical pinned grid');
return;
}
this.#maybeClearVerticalPinnedGridDragOver(draggedTab);
if (this.#lastDropTarget !== dropElement) {
shouldPlayHapticFeedback = this.#lastDropTarget !== null;
this.#removeDragOverBackground();
@@ -742,7 +764,7 @@
let isZenFolder = dropElement.parentElement?.isZenFolder;
let canHightlightGroup =
gZenFolders.highlightGroupOnDragOver(dropElement.parentElement, movingTabs) || !isZenFolder;
let rect = dropElement.getBoundingClientRect();
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.
@@ -758,7 +780,10 @@
this.clearDragOverVisuals();
return;
}
if (isTab(dropElement) || dropIntoFolder) {
if (isTab(dropElement) || dropIntoFolder || showIndicatorUnderNewTabButton) {
if (showIndicatorUnderNewTabButton) {
rect = window.windowUtils.getBoundsWithoutFlushing(this.#dragShiftableItems.at(-1));
}
const indicator = gZenPinnedTabManager.dragIndicator;
let top = 0;
threshold =
@@ -808,5 +833,277 @@
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 (!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 4px 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);
}
};
}

View File

@@ -262,17 +262,14 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
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) {
@@ -315,17 +312,12 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
}
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;
@@ -394,10 +386,12 @@ 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;
});
@@ -1748,6 +1742,12 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
return false;
}
const droppedOnTab = gZenGlanceManager.getTabOrGlanceParent(gBrowser.getTabForBrowser(browser));
if (droppedOnTab === gBrowser.selectedTab) {
this.createEmptySplit(dropSide == 'right');
return true;
}
gBrowser.selectedTab = this._draggingTab;
this._draggingTab = null;
const browserContainer = draggedTab.linkedBrowser?.closest('.browserSidebarContainer');
@@ -1755,7 +1755,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();
@@ -1997,13 +1996,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);
@@ -2036,7 +2036,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();
}

View File

@@ -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);

View File

@@ -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:
@@ -1373,6 +1370,8 @@
}
}
/* Drag and drop */
#zen-dragover-background {
position: absolute;
z-index: -1;

View File

@@ -2404,7 +2404,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;
@@ -2414,7 +2414,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 }],
@@ -2462,7 +2462,7 @@ class nsZenWorkspaces {
}
}
async onPinnedTabsResize(entries, forAnimation = false, animateContainer = false) {
onPinnedTabsResize(entries, forAnimation = false, animateContainer = false) {
if (
document.documentElement.hasAttribute('inDOMFullscreen') ||
!this._hasInitializedTabsStrip ||
@@ -2486,9 +2486,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);