From e81af6ff71c8ebe1aec6ed5c2eaf70659939ae86 Mon Sep 17 00:00:00 2001 From: "mr. m" Date: Thu, 25 Dec 2025 02:17:43 +0100 Subject: [PATCH] feat: Add support for multi tabs dragging, b=no-bug, c=tabs --- .../tabbrowser/content/drag-and-drop-js.patch | 86 +----------------- .../tabbrowser/content/tab-js.patch | 11 ++- src/zen/drag-and-drop/ZenDragAndDrop.js | 88 ++++++++++++++----- src/zen/tabs/zen-tabs/vertical-tabs.css | 2 +- 4 files changed, 78 insertions(+), 109 deletions(-) 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 d04c7ac74..8bfbe7f70 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..6c24d636459d1d83b6d8dcf8157b3f25b0158436 100644 +index 97b931c3c7385a52d20204369fcf6d6999053687..1ac51353e63000f49075b12da83292312996a73c 100644 --- a/browser/components/tabbrowser/content/drag-and-drop.js +++ b/browser/components/tabbrowser/content/drag-and-drop.js @@ -32,6 +32,9 @@ @@ -222,87 +222,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..6c24d636459d1d83b6d8dcf8157b3f25 let isPinned = tab.pinned; let numPinned = gBrowser.pinnedTabCount; let allTabs = this._tabbrowserTabs.ariaFocusableItems; -@@ -1608,8 +1641,9 @@ - - _animateExpandedPinnedTabMove(event) { - let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0); -+ let zenFakeTab = this._invisibleTempTab || draggedTab; - let dragData = draggedTab._dragData; -- let movingTabs = dragData.movingTabs; -+ let movingTabs = this._invisibleTempTab ? [this._invisibleTempTab] : dragData.movingTabs; - - dragData.animLastScreenX ??= dragData.screenX; - dragData.animLastScreenY ??= dragData.screenY; -@@ -1624,10 +1658,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; -@@ -1635,7 +1666,9 @@ - dragData.animLastScreenX = screenX; - - let { width: tabWidth, height: tabHeight } = -- draggedTab.getBoundingClientRect(); -+ zenFakeTab.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 +1705,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); - -@@ -1704,7 +1737,6 @@ - // * 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 == draggedTab); - let firstTabCenterX = firstMovingTabScreenX + translateX + tabWidth / 2; - let lastTabCenterX = lastMovingTabScreenX + translateX + tabWidth / 2; - let tabCenterX = directionX ? lastTabCenterX : firstTabCenterX; -@@ -1716,7 +1748,7 @@ - - let getTabShift = (tab, dropIndex) => { - if ( -- tab.elementIndex < draggedTab.elementIndex && -+ tab.elementIndex < zenFakeTab.elementIndex && - tab.elementIndex >= dropIndex - ) { - // If tab is at the end of a row, shift back and down -@@ -1733,7 +1765,7 @@ - return [RTL_UI ? -shiftSizeX : shiftSizeX, 0]; - } - if ( -- tab.elementIndex > draggedTab.elementIndex && -+ tab.elementIndex > zenFakeTab.elementIndex && - tab.elementIndex < dropIndex - ) { - // If tab is not index 0 and at the start of a row, shift across and up -@@ -1759,7 +1791,7 @@ - dragData.animDropElementIndex ?? movingTabs[0].elementIndex; - while (low <= high) { - let mid = Math.floor((low + high) / 2); -- if (tabs[mid] == draggedTab && ++mid > high) { -+ if (tabs[mid] == zenFakeTab && ++mid > high) { - break; - } - let [shiftX, shiftY] = getTabShift(tabs[mid], oldIndex); -@@ -2457,7 +2489,7 @@ +@@ -2457,7 +2490,7 @@ tab.style.left = ""; tab.style.top = ""; tab.style.maxWidth = ""; @@ -311,7 +231,7 @@ index 97b931c3c7385a52d20204369fcf6d6999053687..6c24d636459d1d83b6d8dcf8157b3f25 } for (let label of draggedTabDocument.getElementsByClassName( "tab-group-label-container" -@@ -2467,7 +2499,7 @@ +@@ -2467,7 +2500,7 @@ label.style.left = ""; label.style.top = ""; label.style.maxWidth = ""; diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch index dd0edd05f..59c4830d4 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..69af8c986add4f74f1c3105ccd6e5804fd33b80f 100644 --- a/browser/components/tabbrowser/content/tab.js +++ b/browser/components/tabbrowser/content/tab.js @@ -21,6 +21,7 @@ @@ -101,6 +101,15 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7 } 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 ( diff --git a/src/zen/drag-and-drop/ZenDragAndDrop.js b/src/zen/drag-and-drop/ZenDragAndDrop.js index 28d77f502..08523915c 100644 --- a/src/zen/drag-and-drop/ZenDragAndDrop.js +++ b/src/zen/drag-and-drop/ZenDragAndDrop.js @@ -76,11 +76,55 @@ super.startTabDrag(event, tab, ...args); const dt = event.dataTransfer; - const { offsetX, offsetY } = this.#getDragImageOffset(event, tab); - this.originalDragImageArgs = [tab, offsetX, offsetY]; + 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 (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); + } + 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); + } + 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); let dragData = draggedTab._dragData; @@ -541,6 +585,12 @@ } } + 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; @@ -623,11 +673,14 @@ this.originalDragImageArgs = []; window.removeEventListener('dragover', this.handle_windowDragEnter, { capture: true }); this.#isOutOfWindow = false; - this.#clearInvisibleTempTab(); if (this._browserDragImageWrapper) { this._browserDragImageWrapper.remove(); delete this._browserDragImageWrapper; } + if (this._tempDragImageParent) { + this._tempDragImageParent.remove(); + delete this._tempDragImageParent; + } } #applyDragOverBackground(element) { @@ -658,14 +711,6 @@ gZenPinnedTabManager.removeTabContainersDragoverClass(); } - #clearInvisibleTempTab() { - if (this._invisibleTempTab) { - this._invisibleTempTab.remove(); - delete this._invisibleTempTab; - this._tabbrowserTabs._invalidateCachedTabs(); - } - } - #applyDragoverIndicator(event, tabs, movingTabs, draggedTab) { const separation = 4; const dropZoneSelector = ':is(.tabbrowser-tab, .zen-drop-target, .tab-group-label)'; @@ -687,19 +732,8 @@ } dropElement = elementToMove(dropElement); if (this._isContainerVerticalPinnedGrid(dropElement) && isTab(draggedTab)) { - if (!draggedTab.hasAttribute('zen-essential') && !this._invisibleTempTab) { - this._invisibleTempTab = draggedTab.cloneNode(true); - this._invisibleTempTab.setAttribute('zen-essential', 'true'); - //this._invisibleTempTab.style.visibility = 'hidden'; - this._tabbrowserTabs.ariaFocusableItems[gBrowser._numZenEssentials - 1].after( - this._invisibleTempTab - ); - this._tabbrowserTabs._invalidateCachedTabs(); - } - this._animateExpandedPinnedTabMove(event); + console.log('TODO: Handle essential tab dragover in vertical pinned grid'); return; - } else if (this._invisibleTempTab) { - this.#clearInvisibleTempTab(); } if (this.#lastDropTarget !== dropElement) { shouldPlayHapticFeedback = this.#lastDropTarget !== null; @@ -761,7 +795,13 @@ return [dropBefore, dropElement]; } - #getDragImageOffset(event, tab) { + #getDragImageOffset(event, tab, draggingTabs) { + if (draggingTabs.length > 1) { + return { + offsetX: 18, + offsetY: 18, + }; + } const rect = tab.getBoundingClientRect(); return { offsetX: event.clientX - rect.left, diff --git a/src/zen/tabs/zen-tabs/vertical-tabs.css b/src/zen/tabs/zen-tabs/vertical-tabs.css index 1687026e3..63054b122 100644 --- a/src/zen/tabs/zen-tabs/vertical-tabs.css +++ b/src/zen/tabs/zen-tabs/vertical-tabs.css @@ -1166,7 +1166,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;