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] 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 = ` - +