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 8bfbe7f70..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..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 = ""; diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch index 59c4830d4..dd0edd05f 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..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 ( diff --git a/src/zen/drag-and-drop/ZenDragAndDrop.js b/src/zen/drag-and-drop/ZenDragAndDrop.js index 08523915c..4e192289d 100644 --- a/src/zen/drag-and-drop/ZenDragAndDrop.js +++ b/src/zen/drag-and-drop/ZenDragAndDrop.js @@ -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); + } }; } diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index d0392f9f9..71c3b225d 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -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(); } diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index 8229a3fb0..78c270558 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); diff --git a/src/zen/tabs/zen-tabs/vertical-tabs.css b/src/zen/tabs/zen-tabs/vertical-tabs.css index 63054b122..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: @@ -1373,6 +1370,8 @@ } } +/* Drag and drop */ + #zen-dragover-background { position: absolute; z-index: -1; diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs index 872b6af93..a3b570de9 100644 --- a/src/zen/workspaces/ZenWorkspaces.mjs +++ b/src/zen/workspaces/ZenWorkspaces.mjs @@ -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);