Compare commits

..

32 Commits

Author SHA1 Message Date
mr. m
5e17ef46ca gh-14521: Fixed theme settings group being visible (gh-14524) 2026-07-08 20:59:48 +02:00
Ashvin Jangid
decd9548dd gh-14507: Fix essentials being added to folder without being removed (gh-14508)
Co-authored-by: mr. m <mr.m@tuta.com>
2026-07-07 21:35:23 +02:00
mr. m
27d290c77d gh-14514: Sync upstream Firefox to version 152.0.5 (gh-14513) 2026-07-07 21:31:01 +02:00
mr. m
5ecbc13eb6 gh-14470: Fixed patch export for sync modules (gh-14490) 2026-07-06 09:55:03 +02:00
mr. m
58611690b5 gh-14470: Fixed wrong sync engine name (gh-14486) 2026-07-05 20:32:03 +02:00
Andrey Bochkarev
68c8fe18bc gh-14242: Fix route tab doesn't change container (gh-14482) 2026-07-05 17:01:20 +02:00
Andrey Bochkarev
3a72290f16 gh-13870: Fix essentials overlap and fix two space wrap-around (gh-14476) 2026-07-05 12:45:35 +02:00
Ashvin Jangid
79d3d6a993 gh-14460: prevent nested glance tabs (gh-14480)
Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2026-07-05 12:45:07 +02:00
InlitX
e9c35b0f92 gh-10749: Localize command palette actions, emoji picker, etc (gh-14473) 2026-07-05 12:19:18 +02:00
mr. m
647e219639 gh-14470: Add space and container sync (gh-14471)
Co-authored-by: Kristijan Ribarić <96492763+kristijanribaric@users.noreply.github.com>
Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2026-07-04 11:18:08 +02:00
mr. m
a685738db4 gh-14431: Fixed pinned tabs broken on latest twilight (gh-14446) 2026-07-02 11:01:21 +02:00
Ashvin Jangid
70f19e6d3d gh-14431: Do not allow glance tabs to be pinned (gh-14432) 2026-07-01 12:41:31 +02:00
Raghav
312ad69d65 gh-12948: Removed ai related stuff from settings search (gh-14412) 2026-06-30 22:58:34 +02:00
mr. m
5d10922d91 gh-14427: Sync upstream Firefox to version 152.0.4 (gh-14426)
This PR syncs the upstream Firefox to version 152.0.4.
2026-06-30 19:55:10 +02:00
mr. m
44f7616238 Merge commit from fork 2026-06-30 13:01:01 +02:00
mr. m
dc3ff8b1c9 gh-14382: Fixed overflowing addons not having context menus (gh-14423) 2026-06-30 12:28:46 +02:00
Bernhard
06392af296 gh-14390: fix profile picker menu when creating new workspace (gh-14421) 2026-06-30 12:06:49 +02:00
mr. m
87220f71f4 gh-14417: Fixed tab renaming selection range not starting from the start (gh-14418) 2026-06-29 17:10:23 +02:00
Ashvin Jangid
84ba80bc11 no-bug: set last selected workspace tab to background tab (gh-14395) 2026-06-28 10:40:13 +02:00
Bernhard
e59e37fcbb no-bug: Appending ellipsis to menu items triggering further actions (gh-14388) 2026-06-27 21:12:58 +02:00
mr. m
f4781daa49 gh-14367: Sync upstream Firefox to version 152.0.3 (gh-14366)
This PR syncs the upstream Firefox to version 152.0.3.

*  All patches applied cleanly.

---------

Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2026-06-25 16:36:06 +02:00
mr. m
4e9c958f8d gh-14183: Dont open space routing tabs in background (gh-14360) 2026-06-24 11:25:39 +02:00
fen4flo
54692261f8 no-bug: Move code inside container null check inside ZenSpaceManager (gh-14356) 2026-06-24 11:15:05 +02:00
Andrey Bochkarev
1fdc1d4475 no-bug: Fix glance tab positioning and prevent split-view (gh-14343) 2026-06-24 11:14:45 +02:00
mr. m
9f9ae52c63 gh-14351: Sync upstream Firefox to version 152.0.2 (gh-14350)
This PR syncs the upstream Firefox to version 152.0.2.

*  All patches applied cleanly.
2026-06-23 16:33:12 +02:00
fen4flo
3c3322058e gh-14044: Implement 'Add Route for Domain' in tab context menu (gh-14279)
Signed-off-by: fen4flo <75260616+FlorianButz@users.noreply.github.com>
Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2026-06-23 16:07:17 +02:00
Bernhard
31bb9d606d gh-12153: edit pinned-page url directly via submenu (gh-14329) 2026-06-23 15:43:57 +02:00
moshyfawn
b6cb8338e3 gh-14341: Fix about dialog footer alignment and restore legal links (gh-14342) 2026-06-23 15:39:29 +02:00
Bernhard
183c583841 gh-14320: fix workspace change icon picker state when opening (gh-14321)
The picker reset its page with a smooth scrollIntoView while the popup
was hiding, and positioned the page before the popup was laid out, so it
could animate/jump on open. Reset instantly on popupshown (after layout)
instead, and drop the on-hide scroll so there's no visible animation.
2026-06-21 21:43:44 +02:00
fen4flo
40080a25dc gh-11245: Fix tab placement with Move Tab / Space Routing (gh-14324)
Fixes #11245
2026-06-21 15:18:02 +02:00
Connor Griffin
733061fbe3 gh-11766: Fixed video fullscreen not working while in glance (gh-14272)
Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2026-06-21 11:20:37 +02:00
Andrey Bochkarev
61e631902c gh-14131: Prevent sidebar from flickering when moving a tab (gh-14293)
Co-authored-by: mr. m <mr.m@tuta.com>
2026-06-20 17:18:41 +02:00
75 changed files with 1991 additions and 7274 deletions

View File

@@ -34,8 +34,8 @@ Zen is a firefox-based browser with the aim of pushing your productivity to a ne
### Firefox Versions
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `152.0.1`! 🚀
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 152.0.1`!
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `152.0.5`! 🚀
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 152.0.5`!
### Contributing

View File

@@ -1 +1 @@
2960f4c1ce58d289d3b9ec885695f0017d2636ab
6bd10168f81fb90f878132d87cf681200afbcd91

View File

@@ -24,3 +24,5 @@ files:
translation: browser/browser/zen-boosts.ftl
- source: en-US/browser/browser/zen-space-routing.ftl
translation: browser/browser/zen-space-routing.ftl
- source: en-US/browser/browser/zen-command-palette.ftl
translation: browser/browser/zen-command-palette.ftl

View File

@@ -0,0 +1,36 @@
# 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/.
zen-action-toggle-compact-mode = Toggle Compact Mode
zen-action-open-theme-picker = Open Theme Picker
zen-action-new-split-view = New Split View
zen-action-new-folder = New Folder
zen-action-copy-current-url = Copy Current URL
zen-action-settings = Settings
zen-action-open-private-window = Open Private Window
zen-action-open-new-window = Open New Window
zen-action-new-blank-window = New Blank Window
zen-action-pin-tab = Pin Tab
zen-action-unpin-tab = Unpin Tab
zen-action-open-space-routing = Open Space Routing
zen-action-new-boost = New Boost
zen-action-next-space = Next Space
zen-action-previous-space = Previous Space
zen-action-close-tab = Close Tab
zen-action-reload-tab = Reload Tab
zen-action-reload-tab-without-cache = Reload Tab Without Cache
zen-action-next-tab = Next Tab
zen-action-previous-tab = Previous Tab
zen-action-capture-screenshot = Capture Screenshot
zen-action-toggle-tabs-on-right = Toggle Tabs on right
zen-action-add-to-essentials = Add to Essentials
zen-action-remove-from-essentials = Remove from Essentials
zen-action-find-in-page = Find in Page
zen-action-manage-extensions = Manage Extensions
zen-action-switch-to-automatic-appearance = Switch to Automatic Appearance
zen-action-switch-to-light-mode = Switch to Light Mode
zen-action-switch-to-dark-mode = Switch to Dark Mode
zen-action-print = Print
zen-action-focus-on = Focus on
zen-action-extension = Extension

View File

@@ -6,13 +6,13 @@ zen-folders-search-placeholder =
.placeholder = Search { $folder-name }...
zen-folders-panel-rename-folder =
.label = Rename Folder
.label = Rename Folder
zen-folders-panel-unpack-folder =
.label = Unpack Folder
zen-folders-new-subfolder =
.label = New Subfolder
.label = New Subfolder
zen-folders-panel-delete-folder =
.label = Delete Folder
@@ -21,7 +21,7 @@ zen-folders-panel-convert-folder-to-space =
.label = Convert folder to Space
zen-folders-panel-change-folder-space =
.label = Change Space...
.label = Change Space
zen-folders-unload-all-tooltip =
.tooltiptext = Unload active in this folder

View File

@@ -19,17 +19,23 @@ tab-context-zen-add-essential-badge = { $num } / { $max }
tab-context-zen-remove-essential =
.label = Remove from Essentials
.accesskey = R
tab-context-zen-replace-pinned-url-with-current =
tab-context-zen-edit-pinned-page =
.label =
{ $isEssential ->
[true] Replace Essential URL with Current
*[false] Replace Pinned URL with Current
[true] Edit Essential URL
*[false] Edit Pinned URL
}
.accesskey = P
tab-context-zen-replace-pinned-url-with-current =
.label = Replace with Current URL
.accesskey = C
tab-context-zen-edit-pinned-url =
.label = Edit…
.accesskey = E
tab-context-zen-edit-title =
.label = Change Label...
.label = Change Label
tab-context-zen-edit-icon =
.label = Change Icon...
.label = Change Icon
zen-themes-corrupted = Your { -brand-short-name } mods file is corrupted. They have been reset to the default theme.
zen-shortcuts-corrupted = Your { -brand-short-name } shortcuts file is corrupted. They have been reset to the default shortcuts.
@@ -55,6 +61,10 @@ zen-general-confirm =
.label = Confirm
zen-pinned-tab-replaced = Pinned tab URL has been replaced with the current URL!
zen-pinned-tab-url-edited = Pinned tab URL has been updated!
zen-pinned-tab-url-invalid = That doesn't look like a valid URL.
zen-pinned-tab-edit-url-title = Edit Pinned URL
zen-pinned-tab-edit-url-label = Enter the URL this pinned tab should point to:
zen-tabs-renamed = Tab has been successfully renamed!
zen-background-tab-opened-toast = New background tab opened!
zen-workspace-renamed-toast = Workspace has been successfully renamed!
@@ -77,6 +87,8 @@ zen-icons-picker-emoji =
.label = Emojis
zen-icons-picker-svg =
.label = Icons
zen-emojis-picker-search =
.placeholder = Search emojis
urlbar-search-mode-zen_actions = Actions
zen-site-data-settings = Settings

View File

@@ -25,3 +25,9 @@ zen-space-routing-open-in = Open In
zen-space-routing-url = URL
zen-space-routing-tab-routed-toast = New tab opened in { $targetWorkspace }
tab-context-zen-add-domain-to-sr =
.label =
{ $tabCount ->
[one] Add Route for Domain
*[other] Add Route for Domains
}

View File

@@ -20,7 +20,7 @@ zen-toolbar-context-compact-mode-hide-both =
.accesskey = H
zen-toolbar-context-move-to-folder =
.label = Move to Folder...
.label = Move to Folder
.accesskey = M
zen-toolbar-context-new-folder =
@@ -31,7 +31,7 @@ sidebar-zen-expand =
.label = Expand Sidebar
sidebar-zen-create-new =
.label = Create New...
.label = Create New
tabbrowser-unload-tab-button =
.tooltiptext =

View File

@@ -24,10 +24,10 @@ zen-workspaces-panel-context-delete =
.accesskey = D
zen-workspaces-panel-change-name =
.label = Change Name
.label = Change Name
zen-workspaces-panel-change-icon =
.label = Change Icon
.label = Change Icon
zen-workspaces-panel-context-default-profile =
.label = Set Profile
@@ -42,7 +42,7 @@ zen-workspaces-how-to-reorder-title = How to reorder spaces
zen-workspaces-how-to-reorder-desc = Drag the space icons at the bottom of the sidebar to reorder them
zen-workspaces-change-theme =
.label = Edit Theme
.label = Edit Theme
zen-workspaces-panel-context-open =
.label = Open Workspace
@@ -72,7 +72,7 @@ zen-workspace-creation-name =
.placeholder = Space Name
zen-move-tab-to-workspace-button =
.label = Move To...
.label = Move To
.tooltiptext = Move all tabs in this window to a Space
zen-workspaces-panel-context-reorder =

View File

@@ -23,3 +23,6 @@
- name: browser.ml.linkPreview.enabled
value: false
- name: browser.preferences.aiControls
value: false

View File

@@ -95,9 +95,6 @@
- name: browser.search.widget.new
value: true
- name: layout.css.corner-shape.enabled
value: true
# Disabled from https://searchfox.org/firefox-main/rev/d6bfff43852356ca98af848b4705d37f8d41856f/modules/libpref/init/StaticPrefList.yaml#2008
# Only enabled for windows, doesn't really fit inside Zen.
- name: browser.startup.preXulSkeletonUI

12
prefs/zen/sync.yaml Normal file
View File

@@ -0,0 +1,12 @@
# 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/.
- name: services.sync.engine.spaces
value: true
condition: "@IS_TWILIGHT@"
- name: services.sync.engine.spaces
value: false
locked: true
condition: "!@IS_TWILIGHT@"

View File

@@ -32,9 +32,6 @@
- name: zen.workspaces.scroll-modifier-key
value: ctrl
- name: services.sync.engine.workspaces
value: false
- name: zen.workspaces.separate-essentials
value: true

View File

@@ -0,0 +1,18 @@
diff --git a/browser/base/content/aboutDialog.css b/browser/base/content/aboutDialog.css
index 017125bc2510e5f5e317a5e78c40d6aa9ded76ca..d343d8c62a2251e3c3a33ae8f2ab9c4c68218c22 100644
--- a/browser/base/content/aboutDialog.css
+++ b/browser/base/content/aboutDialog.css
@@ -135,6 +135,13 @@
margin: 0 40px;
}
+#trademark {
+ font-size: xx-small;
+ text-align: center;
+ margin-block: 10px;
+ color: var(--text-color-deemphasized);
+}
+
#currentChannel {
margin: 0;
padding: 0;

View File

@@ -1,5 +1,5 @@
diff --git a/browser/base/content/aboutDialog.xhtml b/browser/base/content/aboutDialog.xhtml
index 3ffd464b960a4299a7dd0cd87e4fc2f781b9d593..ef9f42d1f0196902b4af31f4496891fcd6319831 100644
index 3ffd464b960a4299a7dd0cd87e4fc2f781b9d593..7a831c8ee2b73bb89bf8a82ac24958b55c16a5aa 100644
--- a/browser/base/content/aboutDialog.xhtml
+++ b/browser/base/content/aboutDialog.xhtml
@@ -102,10 +102,6 @@
@@ -39,8 +39,8 @@ index 3ffd464b960a4299a7dd0cd87e4fc2f781b9d593..ef9f42d1f0196902b4af31f4496891fc
<label is="text-link" class="bottom-link" useoriginprincipal="true" href="about:license" data-l10n-id="bottomLinks-license"/>
- <label is="text-link" class="bottom-link" href="https://www.mozilla.org/about/legal/terms/firefox/" data-l10n-id="bottom-links-terms"/>
- <label is="text-link" class="bottom-link" href="https://www.mozilla.org/privacy/firefox/?utm_source=firefox-browser&#38;utm_medium=firefox-desktop&#38;utm_campaign=about-dialog" data-l10n-id="bottom-links-privacy"/>
+ <label is="text-link" class="bottom-link" href="about:rights" data-l10n-id="bottomLinks-rights"/>
+ <label is="text-link" class="bottom-link" href="https://www.zen-browser.app/privacy-policy/" data-l10n-id="bottomLinks-privacy"/>
+ <label is="text-link" class="bottom-link" href="about:rights" data-l10n-id="bottom-links-terms"/>
+ <label is="text-link" class="bottom-link" href="https://www.zen-browser.app/privacy-policy/" data-l10n-id="bottom-links-privacy"/>
</hbox>
<description id="trademark" data-l10n-id="trademarkInfo"></description>
</vbox>

View File

@@ -35,6 +35,7 @@
<command id="cmd_zenToggleTabsOnRight" />
<command id="cmd_zenReplacePinnedUrlWithCurrent" />
<command id="cmd_zenEditPinnedUrl" />
<command id="cmd_contextZenAddToEssentials" />
<command id="cmd_contextZenRemoveFromEssentials" />

View File

@@ -14,7 +14,7 @@
<hbox id="PanelUI-zen-emojis-picker-pages">
<vbox emojis="true">
<hbox id="PanelUI-zen-emojis-picker-header">
<html:input type="search" id="PanelUI-zen-emojis-picker-search" placeholder="Search emojis" />
<html:input type="search" id="PanelUI-zen-emojis-picker-search" data-l10n-id="zen-emojis-picker-search" />
</hbox>
<hbox id="PanelUI-zen-emojis-picker-list" />
</vbox>

View File

@@ -131,10 +131,10 @@
</box>
<html:input type="range" value="0.4" step="0.001" id="PanelUI-zen-gradient-generator-opacity"
#ifdef XP_MACOSX
max="0.8"
max="0.9"
min="0.30"
#else
max="0.8"
max="0.9"
min="0.25"
#endif
/>

View File

@@ -0,0 +1,20 @@
diff --git a/browser/components/preferences/config/account-sync.mjs b/browser/components/preferences/config/account-sync.mjs
index b503987a08e2ce64bfc01c4e3a21e5e19ef834f0..b1c03a0b219fd3eddd93c1deaeb18ab79c85f168 100644
--- a/browser/components/preferences/config/account-sync.mjs
+++ b/browser/components/preferences/config/account-sync.mjs
@@ -42,6 +42,7 @@ Preferences.addAll([
{ id: "services.sync.engine.creditcards", type: "bool" },
{ id: "services.sync.engine.addons", type: "bool" },
{ id: "services.sync.engine.prefs", type: "bool" },
+ { id: "services.sync.engine.spaces", type: "bool" },
]);
/**
@@ -546,6 +547,7 @@ const SYNC_ENGINE_SETTINGS = [
type: "payments",
},
{ id: "syncAddons", pref: "services.sync.engine.addons", type: "addons" },
+ { id: "syncSpaces", pref: "services.sync.engine.spaces", type: "workspaces" },
{ id: "syncSettings", pref: "services.sync.engine.prefs", type: "settings" },
];

View File

@@ -6,7 +6,7 @@ index 64aa0d98a0622c01f3dcfff1a04bfcda368354d2..2013e04b0881ad2295d6897b91e1573c
{ id: "services.sync.engine.passwords", type: "bool" },
{ id: "services.sync.engine.addresses", type: "bool" },
{ id: "services.sync.engine.creditcards", type: "bool" },
+ { id: "services.sync.engine.workspaces", type: "bool" },
+ { id: "services.sync.engine.spaces", type: "bool" },
]);
let gSyncChooseWhatToSync = {

View File

@@ -20,7 +20,7 @@ index a893c5ec3d007820d98f5d92dd039640faa2c181..9cbd00102e44ccf98b37845474d92d57
+ <html:div class="sync-engine-workspaces">
+ <checkbox
+ data-l10n-id="sync-engine-workspaces"
+ preference="services.sync.engine.workspaces"
+ preference="services.sync.engine.spaces"
+ />
+ </html:div>
</html:div>

View File

@@ -10,7 +10,7 @@ index 57add34d876fb885275f1147209c6fbeee367a7c..be0ab43b299317c0022a5e719f47a070
],
resizeCallback: async ({ title, frame }) => {
// Search within main document and highlight matched keyword.
@@ -437,6 +438,8 @@ const CONFIG_PANES = Object.freeze({
@@ -437,6 +437,8 @@ const CONFIG_PANES = Object.freeze({
tabsBrowsing: {
l10nId: "tabs-browsing-section",
groupIds: [

View File

@@ -6,7 +6,7 @@ index c379e1a5f82692406a92d9fcd3bca2769dfac5b2..af037dd3d995813d966524ac44a3795d
<image class="sync-engine-image sync-engine-prefs" alt=""/>
<label data-l10n-id="sync-currently-syncing-settings"/>
</html:div>
+ <html:div engine_preference="services.sync.engine.workspaces">
+ <html:div engine_preference="services.sync.engine.spaces">
+ <image class="sync-engine-image sync-engine-workspaces" alt=""/>
+ <label data-l10n-id="sync-currently-syncing-workspaces"/>
+ </html:div>

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f3682852f8a52bd 100644
index 08b5b56e069d038d72c87355920c4ce8a55ed805..e1a3a18081c7a8153f8d12c187bf4dd522944fcb 100644
--- a/browser/components/tabbrowser/content/tabbrowser.js
+++ b/browser/components/tabbrowser/content/tabbrowser.js
@@ -511,6 +511,7 @@
@@ -109,7 +109,13 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
tab.linkedPanel = uniqueId;
this._selectedTab = tab;
this._selectedBrowser = browser;
@@ -1131,13 +1192,18 @@
@@ -1126,18 +1187,24 @@
aTab,
{ telemetrySource = this.TabMetrics.METRIC_SOURCE.UNKNOWN } = {}
) {
+ aTab = gZenGlanceManager.getTabOrGlanceParent(aTab);
if (aTab.pinned || aTab == FirefoxViewHandler.tab) {
return;
}
this.showTab(aTab);
@@ -129,7 +135,13 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
aTab.setAttribute("pinned", "true");
this._updateTabBarForPinnedTabs();
@@ -1150,11 +1216,19 @@
@@ -1145,16 +1212,25 @@
}
unpinTab(aTab) {
+ aTab = gZenGlanceManager.getTabOrGlanceParent(aTab);
if (!aTab.pinned) {
return;
}
this.#handleTabMove(aTab, () => {
@@ -150,7 +162,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
});
aTab.style.marginInlineStart = "";
@@ -1369,6 +1443,9 @@
@@ -1369,6 +1445,9 @@
let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"];
@@ -160,7 +172,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
if (
aIconURL &&
!LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol))
@@ -1378,6 +1455,9 @@
@@ -1378,6 +1457,9 @@
);
return;
}
@@ -170,7 +182,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
let browser = this.getBrowserForTab(aTab);
browser.mIconURL = aIconURL;
@@ -1700,7 +1780,6 @@
@@ -1700,7 +1782,6 @@
// Preview mode should not reset the owner
if (!this._previewMode && !oldTab.selected) {
@@ -178,7 +190,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
}
let lastRelatedTab = this._lastRelatedTabMap.get(oldTab);
@@ -1791,6 +1870,7 @@
@@ -1791,6 +1872,7 @@
if (!this._previewMode) {
newTab.recordTimeFromUnloadToReload();
newTab.updateLastAccessed();
@@ -186,7 +198,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
oldTab.updateLastAccessed();
// if this is the foreground window, update the last-seen timestamps.
if (this.documentGlobal == BrowserWindowTracker.getTopWindow()) {
@@ -2005,6 +2085,9 @@
@@ -2005,6 +2087,9 @@
}
let activeEl = document.activeElement;
@@ -196,7 +208,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
// If focus is on the old tab, move it to the new tab.
if (activeEl == oldTab) {
newTab.focus();
@@ -2043,7 +2126,7 @@
@@ -2043,7 +2128,7 @@
// Focus the location bar if it was previously focused for that tab.
// In full screen mode, only bother making the location bar visible
// if the tab is a blank one.
@@ -205,7 +217,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
let selectURL = () => {
if (this._asyncTabSwitching) {
// Set _awaitingSetURI flag to suppress popup notification
@@ -2331,7 +2414,12 @@
@@ -2331,7 +2416,12 @@
return this._setTabLabel(aTab, aLabel);
}
@@ -219,7 +231,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
if (!aLabel || (isURL && /^about:reader\?url=/.test(aLabel))) {
return false;
}
@@ -2457,7 +2545,7 @@
@@ -2457,7 +2547,7 @@
newIndex = this.selectedTab._tPos + 1;
}
@@ -228,7 +240,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
if (this.isTabGroupLabel(targetTab)) {
throw new Error(
"Replacing a tab group label with a tab is not supported"
@@ -2737,6 +2825,7 @@
@@ -2737,6 +2827,7 @@
uriIsAboutBlank,
userContextId,
skipLoad,
@@ -236,7 +248,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
} = {}) {
let b = document.createXULElement("browser");
// Use the JSM global to create the permanentKey, so that if the
@@ -2810,8 +2899,7 @@
@@ -2810,8 +2901,7 @@
// we use a different attribute name for this?
b.setAttribute("name", name);
}
@@ -246,7 +258,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
b.setAttribute("transparent", "true");
}
@@ -2981,7 +3069,7 @@
@@ -2981,7 +3071,7 @@
let panel = this.getPanel(browser);
let uniqueId = this._generateUniquePanelID();
@@ -255,7 +267,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
aTab.linkedPanel = uniqueId;
// Inject the <browser> into the DOM if necessary.
@@ -3041,8 +3129,8 @@
@@ -3041,8 +3131,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) {
@@ -266,7 +278,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
} else {
aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1;
}
@@ -3227,7 +3315,6 @@
@@ -3227,7 +3317,6 @@
this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, {
tabIndex: tab._tPos + 1,
userContextId: tab.userContextId,
@@ -274,7 +286,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
focusUrlBar: true,
});
resolve(this.selectedBrowser);
@@ -3337,6 +3424,10 @@
@@ -3337,6 +3426,10 @@
schemelessInput,
hasValidUserGestureActivation = false,
textDirectiveUserActivation = false,
@@ -285,7 +297,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
} = {}
) {
// all callers of addTab that pass a params object need to pass
@@ -3347,10 +3438,25 @@
@@ -3347,10 +3440,25 @@
);
}
@@ -296,7 +308,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
+
+ let hasZenDefaultUserContextId = false;
+ let zenForcedWorkspaceId = undefined;
+ if (beforeRouteResult.isRouteFound && typeof userContextId !== "undefined") {
+ if (beforeRouteResult.isRouteFound && typeof userContextId === "undefined") {
+ userContextId = beforeRouteResult.userContextId;
+ hasZenDefaultUserContextId = true;
+ } else if (typeof gZenWorkspaces !== "undefined" && !_forZenEmptyTab) {
@@ -311,7 +323,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
// If we're opening a foreground tab, set the owner by default.
ownerTab ??= inBackground ? null : this.selectedTab;
@@ -3358,6 +3464,7 @@
@@ -3358,6 +3466,7 @@
if (this.selectedTab.owner) {
this.selectedTab.owner = null;
}
@@ -319,7 +331,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
// Find the tab that opened this one, if any. This is used for
// determining positioning, and inherited attributes such as the
@@ -3410,6 +3517,22 @@
@@ -3410,6 +3519,22 @@
noInitialLabel,
skipBackgroundNotify,
});
@@ -342,7 +354,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
if (insertTab) {
// Insert the tab into the tab container in the correct position.
this.#insertTabAtIndex(t, {
@@ -3418,6 +3541,7 @@
@@ -3418,6 +3543,7 @@
ownerTab,
openerTab,
pinned,
@@ -350,7 +362,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
bulkOrderedOpen,
tabGroup: tabGroup ?? openerTab?.group,
});
@@ -3436,6 +3560,7 @@
@@ -3436,6 +3562,7 @@
openWindowInfo,
skipLoad,
triggeringRemoteType,
@@ -358,7 +370,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
}));
if (focusUrlBar) {
@@ -3560,6 +3685,12 @@
@@ -3560,6 +3687,12 @@
}
}
@@ -371,7 +383,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
// Additionally send pinned tab events
if (pinned) {
this.#notifyPinnedStatus(t);
@@ -3570,6 +3701,15 @@
@@ -3570,6 +3703,15 @@
if (!inBackground) {
this.selectedTab = t;
}
@@ -387,7 +399,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
return t;
}
@@ -3802,6 +3942,7 @@
@@ -3802,6 +3944,7 @@
isAdoptingGroup = false,
isUserTriggered = false,
telemetryUserCreateSource = "unknown",
@@ -395,7 +407,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
} = {}
) {
if (
@@ -3812,9 +3953,6 @@
@@ -3812,9 +3955,6 @@
!this.isSplitViewWrapper(tabOrSplitView)
)
) {
@@ -405,7 +417,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
}
if (!color) {
@@ -3835,9 +3973,14 @@
@@ -3835,9 +3975,14 @@
label,
isAdoptingGroup
);
@@ -422,7 +434,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
);
group.addTabs(tabsAndSplitViews);
@@ -3958,7 +4101,7 @@
@@ -3958,7 +4103,7 @@
}
this.#handleTabMove(tab, () =>
@@ -431,7 +443,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
);
}
@@ -4044,6 +4187,7 @@
@@ -4044,6 +4189,7 @@
color: group.color,
insertBefore: newTabs[0],
isAdoptingGroup: true,
@@ -439,7 +451,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
});
}
@@ -4254,6 +4398,7 @@
@@ -4254,6 +4400,7 @@
openWindowInfo,
skipLoad,
triggeringRemoteType,
@@ -447,7 +459,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
}
) {
// If we don't have a preferred remote type (or it is `NOT_REMOTE`), and
@@ -4323,6 +4468,7 @@
@@ -4323,6 +4470,7 @@
openWindowInfo,
name,
skipLoad,
@@ -455,7 +467,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
});
}
@@ -4536,9 +4682,9 @@
@@ -4536,9 +4684,9 @@
}
// Add a new tab if needed.
@@ -467,7 +479,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
let url = "about:blank";
if (tabData.entries?.length) {
@@ -4575,8 +4721,10 @@
@@ -4575,8 +4723,10 @@
insertTab: false,
skipLoad: true,
preferredRemoteType,
@@ -479,7 +491,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
if (select) {
tabToSelect = tab;
}
@@ -4598,7 +4746,8 @@
@@ -4598,7 +4748,8 @@
this.pinTab(tab);
// Then ensure all the tab open/pinning information is sent.
this._fireTabOpen(tab, {});
@@ -489,7 +501,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
let { groupId } = tabData;
const tabGroup = tabGroupWorkingData.get(groupId);
// if a tab refers to a tab group we don't know, skip any group
@@ -4618,7 +4767,10 @@
@@ -4618,7 +4769,10 @@
tabGroup.stateData.id,
tabGroup.stateData.color,
tabGroup.stateData.collapsed,
@@ -501,7 +513,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
);
tabsFragment.appendChild(tabGroup.node);
}
@@ -4673,9 +4825,21 @@
@@ -4673,9 +4827,21 @@
// to remove the old selected tab.
if (tabToSelect) {
let leftoverTab = this.selectedTab;
@@ -523,12 +535,15 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
if (tabs.length > 1 || !tabs[0].selected) {
this._updateTabsAfterInsert();
@@ -4866,11 +5030,14 @@
@@ -4866,11 +5032,17 @@
if (ownerTab) {
tab.owner = ownerTab;
}
+ if ((!tab.pinned && tabGroup?.isZenFolder && !Services.prefs.getBoolPref('zen.folders.owned-tabs-in-folder')) || (tabGroup && tabGroup.hasAttribute("split-view-group"))) {
+ tabGroup = null;
+ }
+ if (openerTab?.hasAttribute("zen-glance-tab")) {
+ openerTab = gZenGlanceManager.getTabOrGlanceParent(openerTab);
+ }
// Ensure we have an index if one was not provided.
@@ -539,7 +554,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
if (
!bulkOrderedOpen &&
((openerTab &&
@@ -4882,7 +5049,7 @@
@@ -4882,7 +5054,7 @@
let lastRelatedTab =
openerTab && this._lastRelatedTabMap.get(openerTab);
let previousTab = lastRelatedTab || openerTab || this.selectedTab;
@@ -548,7 +563,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
tabGroup = previousTab.group;
}
if (
@@ -4898,7 +5065,7 @@
@@ -4898,7 +5070,7 @@
previousTab.splitview
) + 1;
} else if (previousTab.visible) {
@@ -557,7 +572,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
} else if (previousTab == FirefoxViewHandler.tab) {
elementIndex = 0;
}
@@ -4926,14 +5093,14 @@
@@ -4926,14 +5098,14 @@
}
// Ensure index is within bounds.
if (tab.pinned) {
@@ -576,7 +591,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
if (pinned && !itemAfter?.pinned) {
itemAfter = null;
@@ -4950,7 +5117,7 @@
@@ -4950,7 +5122,7 @@
this.tabContainer._invalidateCachedTabs();
@@ -585,7 +600,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
if (
(this.isTab(itemAfter) && itemAfter.group == tabGroup) ||
this.isSplitViewWrapper(itemAfter)
@@ -4981,7 +5148,11 @@
@@ -4981,7 +5153,11 @@
const tabContainer = pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
@@ -597,7 +612,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
}
if (tab.group?.collapsed) {
@@ -4996,6 +5167,7 @@
@@ -4996,6 +5172,7 @@
if (pinned) {
this._updateTabBarForPinnedTabs();
}
@@ -605,7 +620,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
TabBarVisibility.update();
}
@@ -5544,6 +5716,7 @@
@@ -5544,6 +5721,7 @@
telemetrySource,
} = {}
) {
@@ -613,7 +628,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
// When 'closeWindowWithLastTab' pref is enabled, closing all tabs
// can be considered equivalent to closing the window.
if (
@@ -5633,6 +5806,7 @@
@@ -5633,6 +5811,7 @@
if (lastToClose) {
this.removeTab(lastToClose, aParams);
}
@@ -621,7 +636,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
} catch (e) {
console.error(e);
}
@@ -5678,6 +5852,14 @@
@@ -5678,6 +5857,14 @@
return;
}
@@ -636,7 +651,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
let isVisibleTab = aTab.visible;
// We have to sample the tab width now, since _beginRemoveTab might
// end up modifying the DOM in such a way that aTab gets a new
@@ -5685,6 +5867,9 @@
@@ -5685,6 +5872,9 @@
// state).
let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width;
let isLastTab = this.#isLastTabInWindow(aTab);
@@ -646,7 +661,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
if (
!this._beginRemoveTab(aTab, {
closeWindowFastpath: true,
@@ -5696,13 +5881,14 @@
@@ -5696,13 +5886,14 @@
telemetrySource,
})
) {
@@ -662,7 +677,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
let lockTabSizing =
!this.tabContainer.verticalMode &&
!aTab.pinned &&
@@ -5733,7 +5919,13 @@
@@ -5733,7 +5924,13 @@
// We're not animating, so we can cancel the animation stopwatch.
Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId);
aTab._closeTimeAnimTimerId = null;
@@ -677,7 +692,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
return;
}
@@ -5867,7 +6059,7 @@
@@ -5867,7 +6064,7 @@
closeWindowWithLastTab != null
? closeWindowWithLastTab
: !window.toolbar.visible ||
@@ -686,7 +701,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
if (closeWindow) {
// We've already called beforeunload on all the relevant tabs if we get here,
@@ -5891,6 +6083,7 @@
@@ -5891,6 +6088,7 @@
newTab = true;
}
@@ -694,7 +709,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
aTab._endRemoveArgs = [closeWindow, newTab];
// swapBrowsersAndCloseOther will take care of closing the window without animation.
@@ -5931,13 +6124,7 @@
@@ -5931,13 +6129,7 @@
aTab._mouseleave();
if (newTab) {
@@ -709,7 +724,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
} else {
TabBarVisibility.update();
}
@@ -6070,6 +6257,7 @@
@@ -6070,6 +6262,7 @@
this.tabs[i]._tPos = i;
}
@@ -717,7 +732,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
if (!this._windowIsClosing) {
// update tab close buttons state
this.tabContainer._updateCloseButtons();
@@ -6255,6 +6443,7 @@
@@ -6255,6 +6448,7 @@
memory_after: await getTotalMemoryUsage(),
time_to_unload_in_ms: timeElapsed,
});
@@ -725,7 +740,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
}
/**
@@ -6300,6 +6489,7 @@
@@ -6300,6 +6494,7 @@
}
let excludeTabs = new Set(aExcludeTabs);
@@ -733,7 +748,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
// If this tab has a successor, it should be selectable, since
// hiding or closing a tab removes that tab as a successor.
@@ -6312,15 +6502,22 @@
@@ -6312,15 +6507,22 @@
!excludeTabs.has(aTab.owner) &&
Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
) {
@@ -758,7 +773,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
let tab = this.tabContainer.findNextTab(aTab, {
direction: 1,
filter: _tab => remainingTabs.includes(_tab),
@@ -6334,7 +6531,7 @@
@@ -6334,7 +6536,7 @@
}
if (tab) {
@@ -767,7 +782,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
}
// If no qualifying visible tab was found, see if there is a tab in
@@ -6355,7 +6552,7 @@
@@ -6355,7 +6557,7 @@
});
}
@@ -776,7 +791,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
}
_blurTab(aTab) {
@@ -6366,7 +6563,7 @@
@@ -6366,7 +6568,7 @@
* @returns {boolean}
* False if swapping isn't permitted, true otherwise.
*/
@@ -785,7 +800,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
// Do not allow transfering a private tab to a non-private window
// and vice versa.
if (
@@ -6420,6 +6617,7 @@
@@ -6420,6 +6622,7 @@
// fire the beforeunload event in the process. Close the other
// window if this was its last tab.
if (
@@ -793,7 +808,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
!remoteBrowser._beginRemoveTab(aOtherTab, {
adoptedByTab: aOurTab,
closeWindowWithLastTab: true,
@@ -6431,7 +6629,7 @@
@@ -6431,7 +6634,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.
@@ -802,7 +817,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
if (closeWindow) {
let win = aOtherTab.documentGlobal;
win.windowUtils.suppressAnimation(true);
@@ -6565,11 +6763,13 @@
@@ -6565,11 +6768,13 @@
}
// Finish tearing down the tab that's going away.
@@ -816,7 +831,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
this.setTabTitle(aOurTab);
@@ -6771,10 +6971,10 @@
@@ -6771,10 +6976,10 @@
SessionStore.deleteCustomTabValue(aTab, "hiddenBy");
}
@@ -829,7 +844,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
aTab.selected ||
aTab.closing ||
// Tabs that are sharing the screen, microphone or camera cannot be hidden.
@@ -6834,7 +7034,8 @@
@@ -6834,7 +7039,8 @@
* @param {object} [aOptions={}]
* Key-value pairs that will be serialized into the features string.
*/
@@ -839,7 +854,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
if (this.tabs.length == 1) {
return null;
}
@@ -6851,7 +7052,7 @@
@@ -6851,7 +7057,7 @@
// tell a new window to take the "dropped" tab
let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
args.appendElement(aTab.splitview ?? aTab);
@@ -848,7 +863,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
private: PrivateBrowsingUtils.isWindowPrivate(window),
features: Object.entries(aOptions)
.map(([key, value]) => `${key}=${value}`)
@@ -6859,6 +7060,8 @@
@@ -6859,6 +7065,8 @@
openerWindow: window,
args,
});
@@ -857,7 +872,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
}
/**
@@ -6971,7 +7174,7 @@
@@ -6971,7 +7179,7 @@
* `true` if element is a `<tab-group>`
*/
isTabGroup(element) {
@@ -866,7 +881,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
}
/**
@@ -7056,8 +7259,8 @@
@@ -7056,8 +7264,8 @@
}
// Don't allow mixing pinned and unpinned tabs.
@@ -877,7 +892,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
} else {
tabIndex = Math.max(tabIndex, this.pinnedTabCount);
}
@@ -7103,8 +7306,8 @@
@@ -7103,8 +7311,8 @@
this.#handleTabMove(
element,
() => {
@@ -888,7 +903,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
neighbor = neighbor.group;
}
if (neighbor?.splitview) {
@@ -7115,6 +7318,12 @@
@@ -7115,6 +7323,12 @@
return;
}
}
@@ -901,7 +916,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
if (movingForwards && neighbor) {
neighbor.after(element);
@@ -7173,23 +7382,31 @@
@@ -7173,23 +7387,31 @@
#moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) {
if (this.isTabGroupLabel(targetElement)) {
targetElement = targetElement.group;
@@ -939,7 +954,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
} 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
@@ -7202,12 +7419,35 @@
@@ -7202,12 +7424,35 @@
// 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.
@@ -976,7 +991,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
// We want to include the splitview wrapper if it's the targetElement, but
// not in the case where we want to reverse tabs within the same splitview.
@@ -7216,6 +7456,7 @@
@@ -7216,6 +7461,7 @@
}
let getContainer = () =>
@@ -984,7 +999,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
element.pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
@@ -7224,11 +7465,15 @@
@@ -7224,11 +7470,15 @@
element,
() => {
if (moveBefore) {
@@ -1001,7 +1016,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
}
},
metricsContext
@@ -7302,11 +7547,15 @@
@@ -7302,11 +7552,15 @@
* @param {TabMetricsContext} [metricsContext]
*/
moveTabToExistingGroup(aTab, aGroup, metricsContext) {
@@ -1020,7 +1035,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
}
if (aTab.group && aTab.group.id === aGroup.id) {
return;
@@ -7378,6 +7627,7 @@
@@ -7378,6 +7632,7 @@
let state = {
tabIndex: tab._tPos,
@@ -1028,7 +1043,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
};
if (tab.visible) {
state.elementIndex = tab.elementIndex;
@@ -7409,7 +7659,7 @@
@@ -7409,7 +7664,7 @@
let changedSplitView =
previousTabState.splitViewId != currentTabState.splitViewId;
@@ -1037,7 +1052,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
tab.dispatchEvent(
new CustomEvent("TabMove", {
bubbles: true,
@@ -7456,6 +7706,10 @@
@@ -7456,6 +7711,10 @@
moveActionCallback();
@@ -1048,7 +1063,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
// Clear tabs cache after moving nodes because the order of tabs may have
// changed.
this.tabContainer._invalidateCachedTabs();
@@ -7506,7 +7760,22 @@
@@ -7506,7 +7765,22 @@
* @returns {object}
* The new tab in the current window, null if the tab couldn't be adopted.
*/
@@ -1072,7 +1087,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
// Swap the dropped tab with a new one we create and then close
// it in the other window (making it seem to have moved between
// windows). We also ensure that the tab we create to swap into has
@@ -7549,6 +7818,8 @@
@@ -7549,6 +7823,8 @@
}
params.skipLoad = true;
let newTab = this.addWebTab("about:blank", params);
@@ -1081,7 +1096,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
aTab.container.tabDragAndDrop.finishAnimateTabMove();
@@ -8259,7 +8530,7 @@
@@ -8259,7 +8535,7 @@
// preventDefault(). It will still raise the window if appropriate.
return;
}
@@ -1090,7 +1105,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
window.focus();
aEvent.preventDefault();
}
@@ -8276,7 +8547,6 @@
@@ -8276,7 +8552,6 @@
on_TabGroupCollapse(aEvent) {
aEvent.target.tabs.forEach(tab => {
@@ -1098,7 +1113,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
});
}
@@ -8630,7 +8900,9 @@
@@ -8630,7 +8905,9 @@
let filter = this._tabFilters.get(tab);
if (filter) {
@@ -1108,7 +1123,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
let listener = this._tabListeners.get(tab);
if (listener) {
@@ -9435,6 +9707,7 @@
@@ -9435,6 +9712,7 @@
aWebProgress.isTopLevel
) {
this.mTab.setAttribute("busy", "true");
@@ -1116,7 +1131,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
gBrowser._tabAttrModified(this.mTab, ["busy"]);
this.mTab._notselectedsinceload = !this.mTab.selected;
}
@@ -9515,6 +9788,7 @@
@@ -9515,6 +9793,7 @@
// known defaults. Note we use the original URL since about:newtab
// redirects to a prerendered page.
const shouldRemoveFavicon =
@@ -1124,7 +1139,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
!this.mBrowser.mIconURL &&
!ignoreBlank &&
!(originalLocation.spec in FAVICON_DEFAULTS);
@@ -9689,13 +9963,6 @@
@@ -9689,13 +9968,6 @@
this.mBrowser.originalURI = aRequest.originalURI;
}
@@ -1138,7 +1153,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..555ffd4772d9d4903491fdff9f368285
}
let userContextId = this.mBrowser.getAttribute("usercontextid") || 0;
@@ -10587,7 +10854,8 @@ var TabContextMenu = {
@@ -10587,7 +10859,8 @@ var TabContextMenu = {
);
contextUnpinSelectedTabs.hidden =
!this.contextTab.pinned || !this.multiselected;

View File

@@ -1,8 +1,23 @@
diff --git a/browser/components/tabbrowser/content/tabs.js b/browser/components/tabbrowser/content/tabs.js
index 568f3a7cc7051ff8cb569f6bcb8018a5212f7072..3036768b8911b4fbc28df7528f7189d9ea21b6f6 100644
index 568f3a7cc7051ff8cb569f6bcb8018a5212f7072..b9a1cfe3a4a5035d9b06b0b3826a97c52cfcb39e 100644
--- a/browser/components/tabbrowser/content/tabs.js
+++ b/browser/components/tabbrowser/content/tabs.js
@@ -220,7 +220,7 @@
@@ -197,8 +197,12 @@
XPCOMUtils.defineLazyPreferenceGetter(
this,
"_sidebarPositionStart",
- "sidebar.position_start",
- true
+ "zen.tabs.vertical.right-side",
+ true,
+ null,
+ newValue => {
+ return !newValue;
+ }
);
if (gMultiProcessBrowser) {
@@ -220,7 +224,7 @@
this.tooltip = "tabbrowser-tab-tooltip";
@@ -11,7 +26,7 @@ index 568f3a7cc7051ff8cb569f6bcb8018a5212f7072..3036768b8911b4fbc28df7528f7189d9
this.tabDragAndDrop.init();
}
@@ -444,7 +444,7 @@
@@ -444,7 +448,7 @@
// and we're not hitting the scroll buttons.
if (
event.button != 0 ||
@@ -20,7 +35,7 @@ index 568f3a7cc7051ff8cb569f6bcb8018a5212f7072..3036768b8911b4fbc28df7528f7189d9
event.composedTarget.localName == "toolbarbutton"
) {
return;
@@ -525,7 +525,6 @@
@@ -525,7 +529,6 @@
});
}
} else if (isTabGroupLabel(event.target)) {
@@ -28,7 +43,7 @@ index 568f3a7cc7051ff8cb569f6bcb8018a5212f7072..3036768b8911b4fbc28df7528f7189d9
} else if (
event.originalTarget.closest("scrollbox") &&
!Services.prefs.getBoolPref(
@@ -561,6 +560,9 @@
@@ -561,6 +564,9 @@
}
on_keydown(event) {
@@ -38,7 +53,7 @@ index 568f3a7cc7051ff8cb569f6bcb8018a5212f7072..3036768b8911b4fbc28df7528f7189d9
let { altKey, shiftKey } = event;
let [accel, nonAccel] =
AppConstants.platform == "macosx"
@@ -755,7 +757,6 @@
@@ -755,7 +761,6 @@
this._updateCloseButtons();
if (!this.#animatingGroups.size) {
@@ -46,7 +61,7 @@ index 568f3a7cc7051ff8cb569f6bcb8018a5212f7072..3036768b8911b4fbc28df7528f7189d9
}
document
@@ -822,7 +823,7 @@
@@ -822,7 +827,7 @@
}
get newTabButton() {
@@ -55,7 +70,7 @@ index 568f3a7cc7051ff8cb569f6bcb8018a5212f7072..3036768b8911b4fbc28df7528f7189d9
}
get verticalMode() {
@@ -838,6 +839,7 @@
@@ -838,6 +843,7 @@
}
get overflowing() {
@@ -63,7 +78,7 @@ index 568f3a7cc7051ff8cb569f6bcb8018a5212f7072..3036768b8911b4fbc28df7528f7189d9
return this.hasAttribute("overflow");
}
@@ -851,29 +853,56 @@
@@ -851,29 +857,56 @@
if (pinnedChildren?.at(-1)?.id == "pinned-tabs-container-periphery") {
pinnedChildren.pop();
}
@@ -93,7 +108,7 @@ index 568f3a7cc7051ff8cb569f6bcb8018a5212f7072..3036768b8911b4fbc28df7528f7189d9
+ } else if (!isTab(tab)) {
+ tabs.splice(i, 1);
+ }
}
+ }
+ };
+ expandTabs(pinnedTabs);
+ expandTabs(unpinnedChildren);
@@ -114,7 +129,7 @@ index 568f3a7cc7051ff8cb569f6bcb8018a5212f7072..3036768b8911b4fbc28df7528f7189d9
+ // remove the separator from the list
+ allTabs.splice(i, 1);
+ i--;
+ }
}
+ i++;
}
-
@@ -130,7 +145,7 @@ index 568f3a7cc7051ff8cb569f6bcb8018a5212f7072..3036768b8911b4fbc28df7528f7189d9
}
get allSplitViews() {
@@ -958,29 +987,28 @@
@@ -958,29 +991,28 @@
return this.#focusableItems;
}
@@ -170,7 +185,7 @@ index 568f3a7cc7051ff8cb569f6bcb8018a5212f7072..3036768b8911b4fbc28df7528f7189d9
this.#focusableItems = focusableItems;
return this.#focusableItems;
@@ -993,6 +1021,7 @@
@@ -993,6 +1025,7 @@
* focusable (ex, we don't want the splitview container to be focusable, only its children).
*/
get dragAndDropElements() {
@@ -178,7 +193,7 @@ index 568f3a7cc7051ff8cb569f6bcb8018a5212f7072..3036768b8911b4fbc28df7528f7189d9
if (this.#dragAndDropElements) {
return this.#dragAndDropElements;
}
@@ -1063,6 +1092,7 @@
@@ -1063,6 +1096,7 @@
_invalidateCachedTabs() {
this.#allTabs = null;
this._invalidateCachedVisibleTabs();
@@ -186,7 +201,7 @@ index 568f3a7cc7051ff8cb569f6bcb8018a5212f7072..3036768b8911b4fbc28df7528f7189d9
}
_invalidateCachedVisibleTabs() {
@@ -1082,7 +1112,8 @@
@@ -1082,7 +1116,8 @@
isContainerVerticalPinnedGrid(tab) {
return (
@@ -196,7 +211,7 @@ index 568f3a7cc7051ff8cb569f6bcb8018a5212f7072..3036768b8911b4fbc28df7528f7189d9
this.verticalMode &&
this.hasAttribute("expanded") &&
!this.expandOnHover
@@ -1176,7 +1207,7 @@
@@ -1176,7 +1211,7 @@
if (node == null) {
// We have a container for non-tab elements at the end of the scrollbox.
@@ -205,7 +220,7 @@ index 568f3a7cc7051ff8cb569f6bcb8018a5212f7072..3036768b8911b4fbc28df7528f7189d9
}
node.before(tab);
@@ -1271,7 +1302,7 @@
@@ -1271,7 +1306,7 @@
// There are separate "new tab" buttons for horizontal tabs toolbar, vertical tabs and
// for when the tab strip is overflowed (which is shared by vertical and horizontal tabs);
// Attach the long click popup to all of them.
@@ -214,7 +229,7 @@ index 568f3a7cc7051ff8cb569f6bcb8018a5212f7072..3036768b8911b4fbc28df7528f7189d9
const newTab2 = this.newTabButton;
const newTabVertical = document.getElementById(
"vertical-tabs-newtab-button"
@@ -1376,8 +1407,10 @@
@@ -1376,8 +1411,10 @@
*/
_handleTabSelect(aInstant) {
let selectedTab = this.selectedItem;
@@ -225,7 +240,7 @@ index 568f3a7cc7051ff8cb569f6bcb8018a5212f7072..3036768b8911b4fbc28df7528f7189d9
selectedTab._notselectedsinceload = false;
}
@@ -1386,7 +1419,7 @@
@@ -1386,7 +1423,7 @@
* @param {boolean} [shouldScrollInstantly=false]
*/
#ensureTabIsVisible(tab, shouldScrollInstantly = false) {
@@ -234,7 +249,7 @@ index 568f3a7cc7051ff8cb569f6bcb8018a5212f7072..3036768b8911b4fbc28df7528f7189d9
if (arrowScrollbox?.overflowing) {
arrowScrollbox.ensureElementIsVisible(tab, shouldScrollInstantly);
}
@@ -1513,7 +1546,7 @@
@@ -1513,7 +1550,7 @@
}
_notifyBackgroundTab(aTab) {

View File

@@ -1,9 +1,9 @@
diff --git a/browser/components/urlbar/UrlbarUtils.sys.mjs b/browser/components/urlbar/UrlbarUtils.sys.mjs
index 2f03bdf632c6ca15c728df2801162da5159682da..6834daede8cb11854f137602100183d85a930686 100644
index aa7c67f2bfaa3f15d592a14b527a8721dfc9bc6f..d2fe012796ea3a5be05090a732011b3cf92b78be 100644
--- a/browser/components/urlbar/UrlbarUtils.sys.mjs
+++ b/browser/components/urlbar/UrlbarUtils.sys.mjs
@@ -109,6 +109,8 @@ export var UrlbarUtils = {
RESTRICT_SEARCH_KEYWORD: "restrictSearchKeyword",
@@ -110,6 +110,8 @@ export var UrlbarUtils = {
SEMANTIC_HISTORY: "semanticHistory",
SUGGESTED_INDEX: "suggestedIndex",
TAIL_SUGGESTION: "tailSuggestion",
+ ZEN_ACTION: "zenAction",
@@ -11,7 +11,7 @@ index 2f03bdf632c6ca15c728df2801162da5159682da..6834daede8cb11854f137602100183d8
}),
// Defines provider types.
@@ -170,6 +172,8 @@ export var UrlbarUtils = {
@@ -171,6 +173,8 @@ export var UrlbarUtils = {
OTHER_NETWORK: 6,
ADDON: 7,
ACTIONS: 8,
@@ -20,7 +20,7 @@ index 2f03bdf632c6ca15c728df2801162da5159682da..6834daede8cb11854f137602100183d8
}),
// Per-result exposure telemetry.
@@ -319,6 +323,14 @@ export var UrlbarUtils = {
@@ -320,6 +324,14 @@ export var UrlbarUtils = {
telemetryLabel: "actions",
uiLabel: "urlbar-searchmode-actions2",
},
@@ -35,7 +35,7 @@ index 2f03bdf632c6ca15c728df2801162da5159682da..6834daede8cb11854f137602100183d8
]);
},
@@ -611,6 +623,12 @@ export var UrlbarUtils = {
@@ -612,6 +624,12 @@ export var UrlbarUtils = {
return this.RESULT_GROUP.HEURISTIC_FALLBACK;
case "UrlbarProviderHistoryUrlHeuristic":
return this.RESULT_GROUP.HEURISTIC_HISTORY_URL;

View File

@@ -484,13 +484,10 @@ groupbox h2 {
#helpButton,
#support-firefox,
#tabGroupSuggestions,
#web-appearance-manage-themes-link,
#setting-control-sidebarChatbotFieldset,
#aiControlsDescription,
#category-ai-features,
#setting-control-supportFirefox,
.mission-message,
html|setting-group:is([data-subcategory="layout"], [groupid="support"]) {
html|setting-group:is([data-subcategory="layout"], [groupid="support"], [groupid="browserTheme"]) {
display: none !important;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +0,0 @@
diff --git a/servo/components/style/values/generics/border.rs b/servo/components/style/values/generics/border.rs
--- a/servo/components/style/values/generics/border.rs
+++ b/servo/components/style/values/generics/border.rs
@@ -296,9 +296,9 @@
pub fn all(s: S) -> Self {
Self {
top_left: s.clone(),
top_right: s.clone(),
bottom_right: s.clone(),
- bottom_left: s.clone(),
+ bottom_left: s,
}
}
}

View File

@@ -1,38 +0,0 @@
diff --git a/gfx/wr/glsl-to-cxx/src/hir.rs b/gfx/wr/glsl-to-cxx/src/hir.rs
--- a/gfx/wr/glsl-to-cxx/src/hir.rs
+++ b/gfx/wr/glsl-to-cxx/src/hir.rs
@@ -3531,10 +3531,11 @@
None,
Type::new(Float),
vec![Type::new(Vec2)],
);
declare_function(state, "pow", None, Type::new(Vec3), vec![Type::new(Vec3)]);
+ declare_function(state, "pow", None, Type::new(Vec2), vec![Type::new(Vec2)]);
declare_function(state, "pow", None, Type::new(Float), vec![Type::new(Float)]);
declare_function(state, "exp", None, Type::new(Float), vec![Type::new(Float)]);
declare_function(state, "exp2", None, Type::new(Float), vec![Type::new(Float)]);
declare_function(state, "log", None, Type::new(Float), vec![Type::new(Float)]);
declare_function(state, "log2", None, Type::new(Float), vec![Type::new(Float)]);
diff --git a/gfx/wr/swgl/src/glsl.h b/gfx/wr/swgl/src/glsl.h
--- a/gfx/wr/swgl/src/glsl.h
+++ b/gfx/wr/swgl/src/glsl.h
@@ -800,10 +800,18 @@
Float pow(Float x, Float y) {
return if_then_else((x == 0) | (x == 1), x, approx_pow2(approx_log2(x) * y));
}
+vec2 pow(vec2 a, vec2 b) {
+ return vec2(pow(a.x, b.x), pow(a.y, b.y));
+}
+
+vec2_scalar pow(vec2_scalar a, vec2_scalar b) {
+ return vec2_scalar(pow(a.x, b.x), pow(a.y, b.y));
+}
+
#define exp __glsl_exp
SI float exp(float x) { return expf(x); }
Float exp(Float y) {

View File

@@ -1,19 +0,0 @@
diff --git a/gfx/wr/swgl/src/glsl.h b/gfx/wr/swgl/src/glsl.h
--- a/gfx/wr/swgl/src/glsl.h
+++ b/gfx/wr/swgl/src/glsl.h
@@ -1611,10 +1611,14 @@
vec3_scalar make_vec3(const vec2_scalar& v, float z) {
return vec3_scalar{v.x, v.y, z};
}
+vec3_scalar make_vec3(float x, const vec2_scalar& v) {
+ return vec3_scalar{x, v.x, v.y};
+}
+
vec3_scalar make_vec3(float x, float y, float z) {
return vec3_scalar{x, y, z};
}
vec3_scalar make_vec3(int32_t x, int32_t y, float z) {

View File

@@ -1,22 +0,0 @@
diff --git a/gfx/wr/swgl/src/glsl.h b/gfx/wr/swgl/src/glsl.h
--- a/gfx/wr/swgl/src/glsl.h
+++ b/gfx/wr/swgl/src/glsl.h
@@ -1599,10 +1599,17 @@
x += a.x;
y += a.y;
z += a.z;
return *this;
}
+
+ vec3& operator*=(Float a) {
+ x *= a;
+ y *= a;
+ z *= a;
+ return *this;
+ }
};
vec3_scalar force_scalar(const vec3& v) {
return vec3_scalar{force_scalar(v.x), force_scalar(v.y), force_scalar(v.z)};
}

View File

@@ -1,20 +0,0 @@
diff --git a/gfx/wr/swgl/src/glsl.h b/gfx/wr/swgl/src/glsl.h
--- a/gfx/wr/swgl/src/glsl.h
+++ b/gfx/wr/swgl/src/glsl.h
@@ -469,10 +469,15 @@
vec2_scalar_ref& operator=(const vec2_scalar& a) {
x = a.x;
y = a.y;
return *this;
}
+ vec2_scalar_ref& operator+=(vec2_scalar a) {
+ x += a.x;
+ y += a.y;
+ return *this;
+ }
vec2_scalar_ref& operator*=(vec2_scalar a) {
x *= a.x;
y *= a.y;
return *this;
}

View File

@@ -1,67 +0,0 @@
diff --git a/gfx/wr/glsl-to-cxx/src/hir.rs b/gfx/wr/glsl-to-cxx/src/hir.rs
--- a/gfx/wr/glsl-to-cxx/src/hir.rs
+++ b/gfx/wr/glsl-to-cxx/src/hir.rs
@@ -3537,10 +3537,12 @@
declare_function(state, "pow", None, Type::new(Float), vec![Type::new(Float)]);
declare_function(state, "exp", None, Type::new(Float), vec![Type::new(Float)]);
declare_function(state, "exp2", None, Type::new(Float), vec![Type::new(Float)]);
declare_function(state, "log", None, Type::new(Float), vec![Type::new(Float)]);
declare_function(state, "log2", None, Type::new(Float), vec![Type::new(Float)]);
+ declare_function(state, "isnan", None, Type::new(Bool), vec![Type::new(Float)]);
+ declare_function(state, "isinf", None, Type::new(Bool), vec![Type::new(Float)]);
for t in &[Float, Vec2] {
// recip is non-standard
declare_function(
state,
"recip",
diff --git a/gfx/wr/swgl/src/glsl.h b/gfx/wr/swgl/src/glsl.h
--- a/gfx/wr/swgl/src/glsl.h
+++ b/gfx/wr/swgl/src/glsl.h
@@ -205,10 +205,46 @@
#else
return (Float){sqrtf(v.x), sqrtf(v.y), sqrtf(v.z), sqrtf(v.w)};
#endif
}
+// NOTE: the Bool type is actually int under the hood,
+// and is used for bitwise return value masking in the
+// generated code ("ret_mask" variable).
+//
+// The ret_mask is initialized to -1 (0xffffffff), and
+// is then subsequently masked with condition results.
+//
+// If we use the boolean result directly here (0 or 1),
+// the bitwise AND ends up removing just one bit from
+// the mask, and it doesn't work.
+//
+// Taking the negative transforms 1 (single bit) into -1
+// (all bits 1), and correctly broadcasts the boolean
+// result into all bits of the ret_mask.
+//
+// If the condition is false, 0 becomes -0 which is the
+// same bit pattern for integers (all bits 0).
+
+SI Bool isnan(Float v) {
+ return (Bool){
+ -(fpclassify(v.x) == FP_NAN),
+ -(fpclassify(v.y) == FP_NAN),
+ -(fpclassify(v.z) == FP_NAN),
+ -(fpclassify(v.w) == FP_NAN)
+ };
+}
+
+SI Bool isinf(Float v) {
+ return (Bool){
+ -(fpclassify(v.x) == FP_INFINITE),
+ -(fpclassify(v.y) == FP_INFINITE),
+ -(fpclassify(v.z) == FP_INFINITE),
+ -(fpclassify(v.w) == FP_INFINITE)
+ };
+}
+
SI float recip(float x) {
#if USE_SSE2
return _mm_cvtss_f32(_mm_rcp_ss(_mm_set_ss(x)));
#else
return 1.0f / x;

View File

@@ -1,49 +0,0 @@
diff --git a/gfx/wr/webrender/res/debug.glsl b/gfx/wr/webrender/res/debug.glsl
new file mode 100644
--- /dev/null
+++ b/gfx/wr/webrender/res/debug.glsl
@@ -0,0 +1,43 @@
+/* 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/. */
+
+// Display a signed distance field as color
+//
+// It will be orange outside the shape (positive distance), and blue
+// inside (negative distance).
+//
+// Color will cycle every 10px.
+//
+// The shape outline (distance = 0) is drawn as a thin white outline.
+//
+// NaNs and infinite values will be drawn as red and yellow respectively.
+//
+// Example usage:
+//
+// #include debug
+//
+// void main(void) {
+// float d = compute_some_distance_in_pixels(...);
+// oFragColor = debug_sdf(d);
+// }
+//
+vec4 debug_sdf(float d) {
+ if (isnan(d)) {
+ return vec4(1.0, 0.0, 0.0, 1.0);
+ }
+ if (isinf(d)) {
+ return vec4(1.0, 1.0, 0.0, 1.0);
+ }
+
+ vec3 color = (d > 0.0) ? vec3(0.9, 0.6, 0.3) : vec3(0.6, 0.8, 1.0);
+ color *= 1.0 - exp(-0.32 * abs(d));
+ color *= fract(d / 10.0) * 0.4 + 0.6;
+#ifdef SWGL
+ // SWGL doesn't support smoothstep() for now
+ color = mix(color, vec3(1.0), step(abs(d), 2.0));
+#else
+ color = mix(color, vec3(1.0), smoothstep(2.0, 0.0, abs(d)));
+#endif
+ return vec4(color, 1.0);
+}

View File

@@ -1,456 +0,0 @@
diff --git a/gfx/wr/wrench/reftests/clip/clip-superellipse.png b/gfx/wr/wrench/reftests/clip/clip-superellipse.png
new file mode 100644
index 0000000000000000000000000000000000000000..4470f8b707c255bc3e4eb026368945267ee895e6
GIT binary patch
literal 7953
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
G@_zvPNe@N<
literal 0
Hc$@<O00001
diff --git a/gfx/wr/wrench/reftests/clip/clip-superellipse.yaml b/gfx/wr/wrench/reftests/clip/clip-superellipse.yaml
new file mode 100644
--- /dev/null
+++ b/gfx/wr/wrench/reftests/clip/clip-superellipse.yaml
@@ -0,0 +1,174 @@
+---
+root:
+ items:
+ - type: clip
+ id: 100
+ complex:
+ - rect: [20, 20, 100, 100]
+ radius:
+ top-left: [32, 16]
+ top-right: [32, 16]
+ bottom-right: [32, 16]
+ bottom-left: [32, 16]
+ shape-top-left: 0.5
+ shape-top-right: +Inf
+ shape-bottom-left: -2.1
+ shape-bottom-right: 0
+ - type: clip-chain
+ id: 200
+ clips: [100]
+ - type: rect
+ bounds: [20, 20, 100, 100]
+ color: red
+ clip-chain: 200
+
+ - type: clip
+ id: 101
+ complex:
+ - rect: [130, 20, 100, 100]
+ radius:
+ top-left: [32, 16]
+ top-right: [32, 16]
+ bottom-right: [32, 16]
+ bottom-left: [32, 16]
+ shape-top-left: 0.5
+ shape-top-right: +Inf
+ shape-bottom-left: -2.1
+ shape-bottom-right: 0
+ clip-mode: clip-out
+ - type: clip-chain
+ id: 201
+ clips: [101]
+ - type: rect
+ bounds: [130, 20, 100, 100]
+ color: green
+ clip-chain: 201
+
+ - type: clip
+ id: 102
+ complex:
+ - rect: [20, 130, 100, 100]
+ radius:
+ top-left: [16, 32]
+ top-right: [16, 32]
+ bottom-right: [16, 32]
+ bottom-left: [16, 32]
+ shape-top-left: 0.5
+ shape-top-right: +Inf
+ shape-bottom-left: -2.1
+ shape-bottom-right: 0
+ - type: clip-chain
+ id: 202
+ clips: [102]
+ - type: rect
+ bounds: [20, 130, 100, 100]
+ color: red
+ clip-chain: 202
+
+ - type: clip
+ id: 103
+ complex:
+ - rect: [130, 130, 100, 100]
+ radius:
+ top-left: [16, 32]
+ top-right: [16, 32]
+ bottom-right: [16, 32]
+ bottom-left: [16, 32]
+ shape-top-left: 0.5
+ shape-top-right: +Inf
+ shape-bottom-left: -2.1
+ shape-bottom-right: 0
+ clip-mode: clip-out
+ - type: clip-chain
+ id: 203
+ clips: [103]
+ - type: rect
+ bounds: [130, 130, 100, 100]
+ color: green
+ clip-chain: 203
+
+ - type: clip
+ id: 104
+ complex:
+ - rect: [20, 240, 100, 100]
+ radius:
+ top-left: [128, 32]
+ top-right: [128, 32]
+ bottom-right: [128, 32]
+ bottom-left: [128, 32]
+ shape-top-left: 0.5
+ shape-top-right: +Inf
+ shape-bottom-left: -2.1
+ shape-bottom-right: 0
+ - type: clip-chain
+ id: 204
+ clips: [104]
+ - type: rect
+ bounds: [20, 240, 100, 100]
+ color: red
+ clip-chain: 204
+
+ - type: clip
+ id: 105
+ complex:
+ - rect: [130, 240, 100, 100]
+ radius:
+ top-left: [128, 32]
+ top-right: [128, 32]
+ bottom-right: [128, 32]
+ bottom-left: [128, 32]
+ shape-top-left: 0.5
+ shape-top-right: +Inf
+ shape-bottom-left: -2.1
+ shape-bottom-right: 0
+ clip-mode: clip-out
+ - type: clip-chain
+ id: 205
+ clips: [105]
+ - type: rect
+ bounds: [130, 240, 100, 100]
+ color: green
+ clip-chain: 205
+
+ - type: clip
+ id: 106
+ complex:
+ - rect: [20, 350, 100, 100]
+ radius:
+ top-left: [32, 128]
+ top-right: [32, 128]
+ bottom-right: [32, 128]
+ bottom-left: [32, 128]
+ shape-top-left: 0.5
+ shape-top-right: +Inf
+ shape-bottom-left: -2.1
+ shape-bottom-right: 0
+ - type: clip-chain
+ id: 206
+ clips: [106]
+ - type: rect
+ bounds: [20, 350, 100, 100]
+ color: red
+ clip-chain: 206
+
+ - type: clip
+ id: 107
+ complex:
+ - rect: [130, 350, 100, 100]
+ radius:
+ top-left: [32, 128]
+ top-right: [32, 128]
+ bottom-right: [32, 128]
+ bottom-left: [32, 128]
+ shape-top-left: 0.5
+ shape-top-right: +Inf
+ shape-bottom-left: -2.1
+ shape-bottom-right: 0
+ clip-mode: clip-out
+ - type: clip-chain
+ id: 207
+ clips: [107]
+ - type: rect
+ bounds: [130, 350, 100, 100]
+ color: green
+ clip-chain: 207
diff --git a/gfx/wr/wrench/reftests/clip/reftest.list b/gfx/wr/wrench/reftests/clip/reftest.list
--- a/gfx/wr/wrench/reftests/clip/reftest.list
+++ b/gfx/wr/wrench/reftests/clip/reftest.list
@@ -1,8 +1,9 @@
platform(linux,mac) == border-with-rounded-clip.yaml border-with-rounded-clip.png
fuzzy-if(platform(swgl),1,4) == clip-mode.yaml clip-mode.png
fuzzy-if(platform(swgl),1,80) == clip-ellipse.yaml clip-ellipse.png
+fuzzy-if(platform(android),128,200) fuzzy-if(platform(swgl),1,350) == clip-superellipse.yaml clip-superellipse.png
fuzzy(1,1000) platform(linux,mac) == clip-45-degree-rotation.yaml clip-45-degree-rotation-ref.png
== clip-3d-transform.yaml clip-3d-transform-ref.yaml
fuzzy(1,4) == clip-corner-overlap.yaml clip-corner-overlap-ref.yaml
== custom-clip-chain-node-ancestors.yaml custom-clip-chain-node-ancestors-ref.yaml
== fixed-position-clipping.yaml fixed-position-clipping-ref.yaml
diff --git a/gfx/wr/wrench/reftests/mask/reftest.list b/gfx/wr/wrench/reftests/mask/reftest.list
--- a/gfx/wr/wrench/reftests/mask/reftest.list
+++ b/gfx/wr/wrench/reftests/mask/reftest.list
@@ -14,5 +14,6 @@
platform(linux,mac) == checkerboard.yaml checkerboard.png
skip_on(android,device) fuzzy(2,1900) == checkerboard.yaml checkerboard-tiling.yaml # Fails on a Pixel2
== missing-mask.yaml missing-mask-ref.yaml
platform(linux) == scaled-filter-raster-root.yaml scaled-filter-raster-root.png
platform(linux,mac) == mask-multiple-coord-systems.yaml mask-multiple-coord-systems.png
+fuzzy(1,10) fuzzy-if(platform(swgl),5,100) == shaped-corners.yaml shaped-corners.png
diff --git a/gfx/wr/wrench/reftests/mask/shaped-corners.png b/gfx/wr/wrench/reftests/mask/shaped-corners.png
new file mode 100644
index 0000000000000000000000000000000000000000..0599a1f93a7d3564c6da950908fe495c98216f7c
GIT binary patch
literal 2026
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
TV>Bg#Z@5WtKp0!@_p|UHon4m#
literal 0
Hc$@<O00001
diff --git a/gfx/wr/wrench/reftests/mask/shaped-corners.yaml b/gfx/wr/wrench/reftests/mask/shaped-corners.yaml
new file mode 100644
--- /dev/null
+++ b/gfx/wr/wrench/reftests/mask/shaped-corners.yaml
@@ -0,0 +1,12 @@
+---
+root:
+ items:
+ - type: clip
+ id: 2
+ complex:
+ - rect: [10, 10, 200, 200]
+ radius: [200, 200, 200, 200, 8.2, 0, -2.8, +Inf]
+ - type: rect
+ clip-chain: [2]
+ bounds: [10, 10, 200, 200]
+ color: blue
diff --git a/gfx/wr/wrench/src/yaml_helper.rs b/gfx/wr/wrench/src/yaml_helper.rs
--- a/gfx/wr/wrench/src/yaml_helper.rs
+++ b/gfx/wr/wrench/src/yaml_helper.rs
@@ -206,10 +206,12 @@
impl YamlHelper for Yaml {
fn as_f32(&self) -> Option<f32> {
match *self {
Yaml::Integer(iv) => Some(iv as f32),
Yaml::Real(ref sv) => f32::from_str(sv.as_str()).ok(),
+ Yaml::String(ref sv) if sv == "+Inf" => Some(f32::INFINITY),
+ Yaml::String(ref sv) if sv == "-Inf" => Some(f32::NEG_INFINITY),
_ => None,
}
}
fn as_force_f32(&self) -> Option<f32> {
@@ -483,24 +485,48 @@
shape_top_right: 1.0,
shape_bottom_left: 1.0,
shape_bottom_right: 1.0,
})
}
+ Yaml::Array(ref array) if array.len() == 8 => {
+ let top_left = array[0].as_border_radius_component();
+ let top_right = array[1].as_border_radius_component();
+ let bottom_left = array[2].as_border_radius_component();
+ let bottom_right = array[3].as_border_radius_component();
+ let shape_top_left = array[4].as_f32().unwrap();
+ let shape_top_right = array[5].as_f32().unwrap();
+ let shape_bottom_left = array[6].as_f32().unwrap();
+ let shape_bottom_right = array[7].as_f32().unwrap();
+ Some(BorderRadius {
+ top_left,
+ top_right,
+ bottom_left,
+ bottom_right,
+ shape_top_left,
+ shape_top_right,
+ shape_bottom_left,
+ shape_bottom_right,
+ })
+ }
Yaml::Hash(_) => {
let top_left = self["top-left"].as_border_radius_component();
let top_right = self["top-right"].as_border_radius_component();
let bottom_left = self["bottom-left"].as_border_radius_component();
let bottom_right = self["bottom-right"].as_border_radius_component();
+ let shape_top_left = self["shape-top-left"].as_f32().unwrap_or(1.0);
+ let shape_top_right = self["shape-top-right"].as_f32().unwrap_or(1.0);
+ let shape_bottom_left = self["shape-bottom-left"].as_f32().unwrap_or(1.0);
+ let shape_bottom_right = self["shape-bottom-right"].as_f32().unwrap_or(1.0);
Some(BorderRadius {
top_left,
top_right,
bottom_left,
bottom_right,
- shape_top_left: 1.0,
- shape_top_right: 1.0,
- shape_bottom_left: 1.0,
- shape_bottom_right: 1.0,
+ shape_top_left,
+ shape_top_right,
+ shape_bottom_left,
+ shape_bottom_right,
})
}
_ => {
panic!("Invalid border radius specified: {:?}", self);
}

View File

@@ -1,592 +0,0 @@
diff --git a/gfx/wr/webrender/res/border_shared.glsl b/gfx/wr/webrender/res/border_shared.glsl
new file mode 100644
--- /dev/null
+++ b/gfx/wr/webrender/res/border_shared.glsl
@@ -0,0 +1,47 @@
+/* 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 gpu_buffer
+
+#define SEGMENT_TOP_LEFT 0
+#define SEGMENT_TOP_RIGHT 1
+#define SEGMENT_BOTTOM_RIGHT 2
+#define SEGMENT_BOTTOM_LEFT 3
+#define SEGMENT_LEFT 4
+#define SEGMENT_TOP 5
+#define SEGMENT_RIGHT 6
+#define SEGMENT_BOTTOM 7
+
+#ifdef WR_VERTEX_SHADER
+
+PER_INSTANCE in vec2 aTaskOrigin;
+PER_INSTANCE in int aFlags;
+PER_INSTANCE in int aGpuDataAddress;
+PER_INSTANCE in vec4 aClipParams1;
+PER_INSTANCE in vec4 aClipParams2;
+
+struct BorderInstanceGpuData {
+ vec4 rect;
+ vec4 color0;
+ vec4 color1;
+ vec2 widths;
+ vec2 radii;
+ float shape;
+};
+
+BorderInstanceGpuData fetch_gpu_data(int index) {
+ BorderInstanceGpuData data;
+
+ vec4 texels[5] = fetch_from_gpu_buffer_5f(index);
+ data.rect = texels[0];
+ data.color0 = texels[1];
+ data.color1 = texels[2];
+ data.widths = texels[3].xy;
+ data.radii = texels[3].zw;
+ data.shape = texels[4].x;
+
+ return data;
+}
+
+#endif
diff --git a/gfx/wr/webrender/res/cs_border_segment.glsl b/gfx/wr/webrender/res/cs_border_segment.glsl
--- a/gfx/wr/webrender/res/cs_border_segment.glsl
+++ b/gfx/wr/webrender/res/cs_border_segment.glsl
@@ -1,10 +1,10 @@
/* 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 shared,rect,ellipse
+#include shared,rect,border_shared,ellipse
// For edges, the colors are the same. For corners, these
// are the colors of each edge making up the corner.
flat varying mediump vec4 vColor00;
flat varying mediump vec4 vColor01;
@@ -44,19 +44,10 @@
flat varying mediump vec4 vClipParams2;
// Local space position
varying highp vec2 vPos;
-#define SEGMENT_TOP_LEFT 0
-#define SEGMENT_TOP_RIGHT 1
-#define SEGMENT_BOTTOM_RIGHT 2
-#define SEGMENT_BOTTOM_LEFT 3
-#define SEGMENT_LEFT 4
-#define SEGMENT_TOP 5
-#define SEGMENT_RIGHT 6
-#define SEGMENT_BOTTOM 7
-
// Border styles as defined in webrender_api/types.rs
#define BORDER_STYLE_NONE 0
#define BORDER_STYLE_SOLID 1
#define BORDER_STYLE_DOUBLE 2
#define BORDER_STYLE_DOTTED 3
@@ -72,20 +63,10 @@
#define CLIP_DASH_EDGE 2
#define CLIP_DOT 3
#ifdef WR_VERTEX_SHADER
-PER_INSTANCE in vec2 aTaskOrigin;
-PER_INSTANCE in vec4 aRect;
-PER_INSTANCE in vec4 aColor0;
-PER_INSTANCE in vec4 aColor1;
-PER_INSTANCE in int aFlags;
-PER_INSTANCE in vec2 aWidths;
-PER_INSTANCE in vec2 aRadii;
-PER_INSTANCE in vec4 aClipParams1;
-PER_INSTANCE in vec4 aClipParams2;
-
vec2 get_outer_corner_scale(int segment) {
vec2 p;
switch (segment) {
case SEGMENT_TOP_LEFT:
@@ -153,16 +134,18 @@
return result;
}
void main(void) {
+ BorderInstanceGpuData data = fetch_gpu_data(aGpuDataAddress);
+
int segment = aFlags & 0xff;
int style0 = (aFlags >> 8) & 0xff;
int style1 = (aFlags >> 16) & 0xff;
int clip_mode = (aFlags >> 24) & 0x0f;
- vec2 size = aRect.zw - aRect.xy;
+ vec2 size = data.rect.zw - data.rect.xy;
vec2 outer_scale = get_outer_corner_scale(segment);
vec2 outer = outer_scale * size;
vec2 clip_sign = 1.0 - 2.0 * outer_scale;
// Set some flags used by the FS to determine the
@@ -176,19 +159,19 @@
edge_axis = ivec2(0, 1);
edge_reference = outer;
break;
case SEGMENT_TOP_RIGHT:
edge_axis = ivec2(1, 0);
- edge_reference = vec2(outer.x - aWidths.x, outer.y);
+ edge_reference = vec2(outer.x - data.widths.x, outer.y);
break;
case SEGMENT_BOTTOM_RIGHT:
edge_axis = ivec2(0, 1);
- edge_reference = outer - aWidths;
+ edge_reference = outer - data.widths;
break;
case SEGMENT_BOTTOM_LEFT:
edge_axis = ivec2(1, 0);
- edge_reference = vec2(outer.x, outer.y - aWidths.y);
+ edge_reference = vec2(outer.x, outer.y - data.widths.y);
break;
case SEGMENT_TOP:
case SEGMENT_BOTTOM:
edge_axis = ivec2(1, 1);
break;
@@ -199,23 +182,23 @@
}
vSegmentClipMode = vec2(float(segment), float(clip_mode));
vStyleEdgeAxis = vec4(float(style0), float(style1), float(edge_axis.x), float(edge_axis.y));
- vPartialWidths = vec4(aWidths / 3.0, aWidths / 2.0);
+ vPartialWidths = vec4(data.widths / 3.0, data.widths / 2.0);
vPos = size * aPosition.xy;
- vec4[2] color0 = get_colors_for_side(aColor0, style0);
+ vec4[2] color0 = get_colors_for_side(data.color0, style0);
vColor00 = color0[0];
vColor01 = color0[1];
- vec4[2] color1 = get_colors_for_side(aColor1, style1);
+ vec4[2] color1 = get_colors_for_side(data.color1, style1);
vColor10 = color1[0];
vColor11 = color1[1];
- vClipCenter_Sign = vec4(outer + clip_sign * aRadii, clip_sign);
- vClipRadii = vec4(aRadii, max(aRadii - aWidths, 0.0));
- vColorLine = vec4(outer, aWidths.y * -clip_sign.y, aWidths.x * clip_sign.x);
- vEdgeReference = vec4(edge_reference, edge_reference + aWidths);
+ vClipCenter_Sign = vec4(outer + clip_sign * data.radii, clip_sign);
+ vClipRadii = vec4(data.radii, max(data.radii - data.widths, 0.0));
+ vColorLine = vec4(outer, data.widths.y * -clip_sign.y, data.widths.x * clip_sign.x);
+ vEdgeReference = vec4(edge_reference, edge_reference + data.widths);
vClipParams1 = aClipParams1;
vClipParams2 = aClipParams2;
// For the case of dot and dash clips, optimize the number of pixels that
// are hit to just include the dot itself.
@@ -234,17 +217,17 @@
// This is a gross approximation which works out because dashes don't have
// a strong curvature and we will overshoot by inflating the geometry by
// this amount on each side (sqrt(2) * length(dash) would be enough and we
// compute 2 * approx_length(dash)).
float dash_length = length(aClipParams1.xy - aClipParams2.xy);
- float width = max(aWidths.x, aWidths.y);
+ float width = max(data.widths.x, data.widths.y);
// expand by a small amout for AA just like we do for dots.
vec2 r = vec2(max(dash_length, width)) + 2.0;
vPos = clamp(vPos, center - r, center + r);
}
- gl_Position = uTransform * vec4(aTaskOrigin + aRect.xy + vPos, 0.0, 1.0);
+ gl_Position = uTransform * vec4(aTaskOrigin + data.rect.xy + vPos, 0.0, 1.0);
}
#endif
#ifdef WR_FRAGMENT_SHADER
vec4 evaluate_color_for_style_in_corner(
diff --git a/gfx/wr/webrender/res/cs_border_solid.glsl b/gfx/wr/webrender/res/cs_border_solid.glsl
--- a/gfx/wr/webrender/res/cs_border_solid.glsl
+++ b/gfx/wr/webrender/res/cs_border_solid.glsl
@@ -1,10 +1,10 @@
/* 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 shared,rect,ellipse
+#include shared,rect,border_shared,ellipse
#define DONT_MIX 0
#define MIX_AA 1
#define MIX_NO_AA 2
@@ -37,27 +37,12 @@
flat varying highp vec2 vVerticalClipRadii;
// Local space position
varying highp vec2 vPos;
-#define SEGMENT_TOP_LEFT 0
-#define SEGMENT_TOP_RIGHT 1
-#define SEGMENT_BOTTOM_RIGHT 2
-#define SEGMENT_BOTTOM_LEFT 3
-
#ifdef WR_VERTEX_SHADER
-PER_INSTANCE in vec2 aTaskOrigin;
-PER_INSTANCE in vec4 aRect;
-PER_INSTANCE in vec4 aColor0;
-PER_INSTANCE in vec4 aColor1;
-PER_INSTANCE in int aFlags;
-PER_INSTANCE in vec2 aWidths;
-PER_INSTANCE in vec2 aRadii;
-PER_INSTANCE in vec4 aClipParams1;
-PER_INSTANCE in vec4 aClipParams2;
-
vec2 get_outer_corner_scale(int segment) {
vec2 p;
switch (segment) {
case SEGMENT_TOP_LEFT:
@@ -80,15 +65,17 @@
return p;
}
void main(void) {
+ BorderInstanceGpuData data = fetch_gpu_data(aGpuDataAddress);
+
int segment = aFlags & 0xff;
bool do_aa = ((aFlags >> 24) & 0xf0) != 0;
vec2 outer_scale = get_outer_corner_scale(segment);
- vec2 size = aRect.zw - aRect.xy;
+ vec2 size = data.rect.zw - data.rect.xy;
vec2 outer = outer_scale * size;
vec2 clip_sign = 1.0 - 2.0 * outer_scale;
int mix_colors;
switch (segment) {
@@ -105,15 +92,15 @@
}
vMixColors.x = mix_colors;
vPos = size * aPosition.xy;
- vColor0 = aColor0;
- vColor1 = aColor1;
- vClipCenter_Sign = vec4(outer + clip_sign * aRadii, clip_sign);
- vClipRadii = vec4(aRadii, max(aRadii - aWidths, 0.0));
- vColorLine = vec4(outer, aWidths.y * -clip_sign.y, aWidths.x * clip_sign.x);
+ vColor0 = data.color0;
+ vColor1 = data.color1;
+ vClipCenter_Sign = vec4(outer + clip_sign * data.radii, clip_sign);
+ vClipRadii = vec4(data.radii, max(data.radii - data.widths, 0.0));
+ vColorLine = vec4(outer, data.widths.y * -clip_sign.y, data.widths.x * clip_sign.x);
vec2 horizontal_clip_sign = vec2(-clip_sign.x, clip_sign.y);
vHorizontalClipCenter_Sign = vec4(aClipParams1.xy +
horizontal_clip_sign * aClipParams1.zw,
horizontal_clip_sign);
@@ -123,11 +110,11 @@
vVerticalClipCenter_Sign = vec4(aClipParams2.xy +
vertical_clip_sign * aClipParams2.zw,
vertical_clip_sign);
vVerticalClipRadii = aClipParams2.zw;
- gl_Position = uTransform * vec4(aTaskOrigin + aRect.xy + vPos, 0.0, 1.0);
+ gl_Position = uTransform * vec4(aTaskOrigin + data.rect.xy + vPos, 0.0, 1.0);
}
#endif
#ifdef WR_FRAGMENT_SHADER
void main(void) {
diff --git a/gfx/wr/webrender/src/border.rs b/gfx/wr/webrender/src/border.rs
--- a/gfx/wr/webrender/src/border.rs
+++ b/gfx/wr/webrender/src/border.rs
@@ -6,13 +6,14 @@
use api::{NormalBorder as ApiNormalBorder, RepeatMode};
use api::units::*;
use crate::clip::ClipNodeId;
use crate::ellipse::Ellipse;
use euclid::vec2;
+use crate::renderer::GpuBufferBuilderF;
use crate::scene_building::SceneBuilder;
use crate::spatial_tree::SpatialNodeIndex;
-use crate::gpu_types::{BorderInstance, BorderSegment, BrushFlags};
+use crate::gpu_types::{BorderInstance, BorderInstanceGpuData, BorderSegment, BrushFlags};
use crate::prim_store::{BorderSegmentInfo, BrushSegment, NinePatchDescriptor};
use crate::prim_store::borders::NormalBorderPrim;
use crate::util::{lerp, RectHelpers};
use crate::internal_types::LayoutPrimitiveInfo;
use crate::segment::EdgeMask;
@@ -125,10 +126,11 @@
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct BorderSegmentCacheKey {
pub size: LayoutSizeAu,
pub radius: LayoutSizeAu,
+ pub shape: u32,
pub side0: BorderSideAu,
pub side1: BorderSideAu,
pub segment: BorderSegment,
pub do_aa: bool,
pub h_adjacent_corner_outer: LayoutPointAu,
@@ -762,10 +764,11 @@
),
border.left,
border.top,
LayoutSize::new(widths.left, widths.top),
border.radius.top_left,
+ border.radius.shape_top_left,
BorderSegment::TopLeft,
EdgeMask::TOP | EdgeMask::LEFT,
rect.top_right(),
border.radius.top_right,
rect.bottom_left(),
@@ -789,10 +792,11 @@
),
border.top,
border.right,
LayoutSize::new(widths.right, widths.top),
border.radius.top_right,
+ border.radius.shape_top_right,
BorderSegment::TopRight,
EdgeMask::TOP | EdgeMask::RIGHT,
rect.min,
border.radius.top_left,
rect.max,
@@ -816,10 +820,11 @@
),
border.right,
border.bottom,
LayoutSize::new(widths.right, widths.bottom),
border.radius.bottom_right,
+ border.radius.shape_bottom_right,
BorderSegment::BottomRight,
EdgeMask::BOTTOM | EdgeMask::RIGHT,
rect.bottom_left(),
border.radius.bottom_left,
rect.top_right(),
@@ -843,10 +848,11 @@
),
border.bottom,
border.left,
LayoutSize::new(widths.left, widths.bottom),
border.radius.bottom_left,
+ border.radius.shape_bottom_left,
BorderSegment::BottomLeft,
EdgeMask::BOTTOM | EdgeMask::LEFT,
rect.max,
border.radius.bottom_right,
rect.min,
@@ -881,30 +887,37 @@
color1: ColorF,
segment: BorderSegment,
instances: &mut Vec<BorderInstance>,
widths: DeviceSize,
radius: DeviceSize,
+ shape: f32,
do_aa: bool,
h_adjacent_corner_outer: DevicePoint,
h_adjacent_corner_radius: DeviceSize,
v_adjacent_corner_outer: DevicePoint,
v_adjacent_corner_radius: DeviceSize,
+ gpu_buffer_builder: &mut GpuBufferBuilderF,
) {
let base_flags = (segment as i32) |
((style0 as i32) << 8) |
((style1 as i32) << 16) |
((do_aa as i32) << 28);
- let base_instance = BorderInstance {
- task_origin: DevicePoint::zero(),
+ let instance_gpu_data = BorderInstanceGpuData {
local_rect: task_rect,
- flags: base_flags,
color0: color0.premultiplied(),
color1: color1.premultiplied(),
widths,
radius,
+ shape
+ };
+
+ let base_instance = BorderInstance {
+ task_origin: DevicePoint::zero(),
+ flags: base_flags,
clip_params: [0.0; 8],
+ gpu_data_address: instance_gpu_data.write(gpu_buffer_builder)
};
match segment {
BorderSegment::TopLeft |
BorderSegment::TopRight |
@@ -1018,10 +1031,11 @@
non_overlapping_rect: LayoutRect,
side0: BorderSide,
side1: BorderSide,
widths: LayoutSize,
radius: LayoutSize,
+ shape: f32,
segment: BorderSegment,
edge_flags: EdgeMask,
h_adjacent_corner_outer: LayoutPoint,
h_adjacent_corner_radius: LayoutSize,
v_adjacent_corner_outer: LayoutPoint,
@@ -1137,10 +1151,11 @@
do_aa,
side0: side0.into(),
side1: side1.into(),
segment,
radius: radius.to_au(),
+ shape: shape.to_bits(),
size: widths.to_au(),
h_adjacent_corner_outer: (h_corner_outer - image_rect.min).to_point().to_au(),
h_adjacent_corner_radius: h_corner_radius.to_au(),
v_adjacent_corner_outer: (v_corner_outer - image_rect.min).to_point().to_au(),
v_adjacent_corner_radius: v_corner_radius.to_au(),
@@ -1200,10 +1215,11 @@
cache_key: BorderSegmentCacheKey {
do_aa,
side0: side.into(),
side1: side.into(),
radius: LayoutSizeAu::zero(),
+ shape: 0,
size: size.to_au(),
segment,
h_adjacent_corner_outer: LayoutPointAu::zero(),
h_adjacent_corner_radius: LayoutSizeAu::zero(),
v_adjacent_corner_outer: LayoutPointAu::zero(),
@@ -1217,10 +1233,11 @@
pub fn build_border_instances(
cache_key: &BorderSegmentCacheKey,
cache_size: DeviceIntSize,
border: &ApiNormalBorder,
scale: LayoutToDeviceScale,
+ gpu_buffer_builder: &mut GpuBufferBuilderF,
) -> Vec<BorderInstance> {
let mut instances = Vec::new();
let (side0, side1, flip0, flip1) = match cache_key.segment {
BorderSegment::Left => (&border.left, &border.left, false, false),
@@ -1247,10 +1264,11 @@
let color0 = side0.border_color(flip0);
let color1 = side1.border_color(flip1);
let widths = (LayoutSize::from_au(cache_key.size) * scale).ceil();
let radius = (LayoutSize::from_au(cache_key.radius) * scale).ceil();
+ let shape = f32::from_bits(cache_key.shape);
let h_corner_outer = (LayoutPoint::from_au(cache_key.h_adjacent_corner_outer) * scale).round();
let h_corner_radius = (LayoutSize::from_au(cache_key.h_adjacent_corner_radius) * scale).ceil();
let v_corner_outer = (LayoutPoint::from_au(cache_key.v_adjacent_corner_outer) * scale).round();
let v_corner_radius = (LayoutSize::from_au(cache_key.v_adjacent_corner_radius) * scale).ceil();
@@ -1263,15 +1281,17 @@
color1,
cache_key.segment,
&mut instances,
widths,
radius,
+ shape,
border.do_aa,
h_corner_outer,
h_corner_radius,
v_corner_outer,
v_corner_radius,
+ gpu_buffer_builder,
);
instances
}
diff --git a/gfx/wr/webrender/src/gpu_types.rs b/gfx/wr/webrender/src/gpu_types.rs
--- a/gfx/wr/webrender/src/gpu_types.rs
+++ b/gfx/wr/webrender/src/gpu_types.rs
@@ -192,22 +192,40 @@
Top,
Right,
Bottom,
}
+pub struct BorderInstanceGpuData {
+ pub local_rect: DeviceRect,
+ pub color0: PremultipliedColorF,
+ pub color1: PremultipliedColorF,
+ pub widths: DeviceSize,
+ pub radius: DeviceSize,
+ pub shape: f32,
+}
+
+impl BorderInstanceGpuData {
+ pub fn write(&self, gpu_buffer_builder: &mut GpuBufferBuilderF) -> GpuBufferAddress {
+ let mut writer = gpu_buffer_builder.write_blocks(5);
+ writer.push_one(self.local_rect);
+ writer.push_one(self.color0);
+ writer.push_one(self.color1);
+ writer.push_one([self.widths.width, self.widths.height, self.radius.width, self.radius.height]);
+ writer.push_one([self.shape, 0.0, 0.0, 0.0]);
+
+ writer.finish()
+ }
+}
+
#[derive(Debug, Clone)]
#[repr(C)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct BorderInstance {
pub task_origin: DevicePoint,
- pub local_rect: DeviceRect,
- pub color0: PremultipliedColorF,
- pub color1: PremultipliedColorF,
pub flags: i32,
- pub widths: DeviceSize,
- pub radius: DeviceSize,
+ pub gpu_data_address: GpuBufferAddress,
pub clip_params: [f32; 8],
}
#[derive(Copy, Clone, Debug)]
#[cfg_attr(feature = "capture", derive(Serialize))]
diff --git a/gfx/wr/webrender/src/prim_store/borders.rs b/gfx/wr/webrender/src/prim_store/borders.rs
--- a/gfx/wr/webrender/src/prim_store/borders.rs
+++ b/gfx/wr/webrender/src/prim_store/borders.rs
@@ -230,19 +230,20 @@
false, // TODO(gw): We don't calculate opacity for borders yet!
RenderTaskParent::Surface,
&mut frame_state.frame_gpu_data.f32,
frame_state.rg_builder,
&mut frame_state.surface_builder,
- &mut |rg_builder, _| {
+ &mut |rg_builder, gpu_buffer_builder| {
rg_builder.add().init(RenderTask::new_dynamic(
cache_size,
RenderTaskKind::new_border_segment(
build_border_instances(
&segment.cache_key,
cache_size,
&self.border,
scale,
+ gpu_buffer_builder,
)
),
))
}
);
diff --git a/gfx/wr/webrender/src/renderer/vertex.rs b/gfx/wr/webrender/src/renderer/vertex.rs
--- a/gfx/wr/webrender/src/renderer/vertex.rs
+++ b/gfx/wr/webrender/src/renderer/vertex.rs
@@ -63,16 +63,12 @@
pub const BORDER: VertexDescriptor = VertexDescriptor {
vertex_attributes: &[VertexAttribute::quad_instance_vertex()],
instance_attributes: &[
VertexAttribute::f32x2("aTaskOrigin"),
- VertexAttribute::f32x4("aRect"),
- VertexAttribute::f32x4("aColor0"),
- VertexAttribute::f32x4("aColor1"),
VertexAttribute::i32("aFlags"),
- VertexAttribute::f32x2("aWidths"),
- VertexAttribute::f32x2("aRadii"),
+ VertexAttribute::gpu_buffer_address("aGpuDataAddress"),
VertexAttribute::f32x4("aClipParams1"),
VertexAttribute::f32x4("aClipParams2"),
],
};

View File

@@ -2,24 +2,6 @@
// 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/.
[
{
"type": "phabricator",
"ids": [
"D296935",
"D303334",
"D297660",
"D304517",
"D306773",
"D306774",
"D306775",
"D306776",
"D306777",
"D306778",
"D306779"
],
"name": "Corner shape support"
},
{
"type": "phabricator",
"id": "D299584",

View File

@@ -0,0 +1,15 @@
diff --git a/services/sync/modules/service.sys.mjs b/services/sync/modules/service.sys.mjs
index 4f4efe572909441abdd81909282936702c225621..81ab06a9414ed656b644a21b40b683f06a4f8dbf 100644
--- a/services/sync/modules/service.sys.mjs
+++ b/services/sync/modules/service.sys.mjs
@@ -99,6 +99,10 @@ function getEngineModules() {
whenTrue: "ExtensionStorageEngineKinto",
whenFalse: "ExtensionStorageEngineBridge",
};
+ result.Spaces = {
+ module: "resource:///modules/zen/ZenWorkspacesSync.sys.mjs",
+ symbol: "ZenWorkspacesEngine",
+ };
return result;
}

View File

@@ -0,0 +1,37 @@
diff --git a/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs b/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
index 5702ff28cc22206f5ce16584dac8a78d816562ce..3d08ecc97ce5995b30d8a4af0c33df329b428008 100644
--- a/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
+++ b/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
@@ -270,11 +270,29 @@ _ContextualIdentityService.prototype = {
});
},
- create(name, icon, color) {
+ create(name, icon, color, id = null) {
this.ensureDataReady();
- // Retrieve the next userContextId available.
- let userContextId = ++this._lastUserContextId;
+ if (id !== null) {
+ id = typeof id == "string" ? Number(id) : id;
+ if (!Number.isSafeInteger(id) || id <= 0) {
+ throw new Error(`Invalid userContextId '${id}'`);
+ }
+ }
+
+ let userContextId;
+ if (
+ id !== null &&
+ !this._identities.some(identity => identity.userContextId === id)
+ ) {
+ userContextId = id;
+ this._lastUserContextId = Math.max(
+ this._lastUserContextId,
+ userContextId
+ );
+ } else {
+ userContextId = ++this._lastUserContextId;
+ }
// Throw an error if the next userContextId available is invalid (the one associated to
// MAX_USER_CONTEXT_ID is already reserved to "userContextIdInternal.webextStorageLocal", which

View File

@@ -16,3 +16,4 @@ category app-startup nsBrowserGlue @mozilla.org/browser/browserglue;1 applicatio
#include common/Components.manifest
#include sessionstore/SessionComponents.manifest
#include live-folders/LiveFoldersComponents.manifest
#include space-routing/SpaceRoutingComponents.manifest

View File

@@ -6,29 +6,29 @@ import { nsZenDOMOperatedFeature } from "chrome://browser/content/zen-components
// prettier-ignore
const SVG_ICONS = [
"airplane.svg", "american-football.svg", "baseball.svg", "basket.svg",
"bed.svg", "bell.svg", "bookmark.svg", "book.svg",
"briefcase.svg", "brush.svg", "bug.svg", "build.svg",
"cafe.svg", "call.svg", "card.svg", "chat.svg",
"checkbox.svg", "circle.svg", "cloud.svg", "code.svg",
"coins.svg", "construct.svg", "cutlery.svg", "egg.svg",
"extension-puzzle.svg", "eye.svg", "fast-food.svg", "fish.svg",
"flag.svg", "flame.svg", "flask.svg", "folder.svg",
"game-controller.svg", "globe-1.svg", "globe.svg", "grid-2x2.svg",
"grid-3x3.svg", "heart.svg", "ice-cream.svg", "image.svg",
"inbox.svg", "key.svg", "layers.svg", "leaf.svg",
"lightning.svg", "location.svg", "lock-closed.svg", "logo-rss.svg",
"logo-usd.svg", "mail.svg", "map.svg", "megaphone.svg",
"moon.svg", "music.svg", "navigate.svg", "nuclear.svg",
"page.svg", "palette.svg", "paw.svg", "people.svg",
"pizza.svg", "planet.svg", "present.svg", "rocket.svg",
"school.svg", "shapes.svg", "shirt.svg", "skull.svg",
"squares.svg", "square.svg", "star-1.svg", "star.svg",
"stats-chart.svg", "sun.svg", "tada.svg", "terminal.svg",
"ticket.svg", "time.svg", "trash.svg", "triangle.svg",
"video.svg", "volume-high.svg", "wallet.svg", "warning.svg",
"water.svg", "weight.svg",
];
"airplane.svg", "american-football.svg", "baseball.svg", "basket.svg",
"bed.svg", "bell.svg", "bookmark.svg", "book.svg",
"briefcase.svg", "brush.svg", "bug.svg", "build.svg",
"cafe.svg", "call.svg", "card.svg", "chat.svg",
"checkbox.svg", "circle.svg", "cloud.svg", "code.svg",
"coins.svg", "construct.svg", "cutlery.svg", "egg.svg",
"extension-puzzle.svg", "eye.svg", "fast-food.svg", "fish.svg",
"flag.svg", "flame.svg", "flask.svg", "folder.svg",
"game-controller.svg", "globe-1.svg", "globe.svg", "grid-2x2.svg",
"grid-3x3.svg", "heart.svg", "ice-cream.svg", "image.svg",
"inbox.svg", "key.svg", "layers.svg", "leaf.svg",
"lightning.svg", "location.svg", "lock-closed.svg", "logo-rss.svg",
"logo-usd.svg", "mail.svg", "map.svg", "megaphone.svg",
"moon.svg", "music.svg", "navigate.svg", "nuclear.svg",
"page.svg", "palette.svg", "paw.svg", "people.svg",
"pizza.svg", "planet.svg", "present.svg", "rocket.svg",
"school.svg", "shapes.svg", "shirt.svg", "skull.svg",
"squares.svg", "square.svg", "star-1.svg", "star.svg",
"stats-chart.svg", "sun.svg", "tada.svg", "terminal.svg",
"ticket.svg", "time.svg", "trash.svg", "triangle.svg",
"video.svg", "volume-high.svg", "wallet.svg", "warning.svg",
"water.svg", "weight.svg",
];
class nsZenEmojiPicker extends nsZenDOMOperatedFeature {
#panel;
@@ -47,6 +47,7 @@ class nsZenEmojiPicker extends nsZenDOMOperatedFeature {
init() {
this.#panel = document.getElementById("PanelUI-zen-emojis-picker");
this.#panel.addEventListener("popupshowing", this);
this.#panel.addEventListener("popupshown", this);
this.#panel.addEventListener("popuphidden", this);
this.#panel.addEventListener("command", this);
this.searchInput.addEventListener("input", this);
@@ -57,6 +58,9 @@ class nsZenEmojiPicker extends nsZenDOMOperatedFeature {
case "popupshowing":
this.#onPopupShowing(event);
break;
case "popupshown":
this.#onPopupShown(event);
break;
case "popuphidden":
this.#onPopupHidden(event);
break;
@@ -103,17 +107,20 @@ class nsZenEmojiPicker extends nsZenDOMOperatedFeature {
return document.getElementById("PanelUI-zen-emojis-picker-search");
}
#changePage(toSvg = false) {
#changePage(toSvg = false, { animate = true } = {}) {
const pages = document.getElementById("PanelUI-zen-emojis-picker-pages");
const itemToScroll = toSvg
? this.svgList
: document
.getElementById("PanelUI-zen-emojis-picker-pages")
.querySelector('[emojis="true"]');
itemToScroll.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "start",
});
: pages.querySelector('[emojis="true"]');
if (animate) {
itemToScroll.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "start",
});
} else {
pages.scrollLeft = toSvg ? itemToScroll.offsetLeft : 0;
}
const button = document.getElementById(
`PanelUI-zen-emojis-picker-change-${toSvg ? "svg" : "emojis"}`
);
@@ -180,9 +187,6 @@ class nsZenEmojiPicker extends nsZenDOMOperatedFeature {
});
emojiList.appendChild(item);
}
setTimeout(() => {
this.searchInput.focus();
}, 500);
}
const svgList = this.svgList;
for (const icon of SVG_ICONS) {
@@ -199,14 +203,23 @@ class nsZenEmojiPicker extends nsZenDOMOperatedFeature {
}
}
#onPopupShown(event) {
if (event.target !== this.#panel) {
return;
}
const allowEmojis = !this.#panel.hasAttribute("only-svg-icons");
if (allowEmojis) {
this.searchInput.focus({ preventScroll: true });
}
this.#changePage(false, { animate: false });
}
#onPopupHidden(event) {
if (event.target !== this.#panel) {
return;
}
this.#clearEmojis();
this.#changePage(false);
const emojiList = this.emojiList;
emojiList.innerHTML = "";

View File

@@ -1579,6 +1579,7 @@ window.gZenVerticalTabsManager = {
overflowElements.appendChild(child);
} else {
const element = document.getElementById("page-action-buttons");
child.setAttribute("context", "toolbar-context-menu");
element.before(child);
}
return;
@@ -1612,7 +1613,9 @@ window.gZenVerticalTabsManager = {
// it will reset to the original name anyway
if (hasChanged || (this._tabEdited.zenStaticLabel && newName)) {
this._tabEdited.zenStaticLabel = newName;
gBrowser._setTabLabel(this._tabEdited, newName);
gBrowser._setTabLabel(this._tabEdited, newName, {
_zenChangeLabelFlag: true,
});
gZenUIManager.showToast("zen-tabs-renamed");
} else {
delete this._tabEdited.zenStaticLabel;
@@ -1708,7 +1711,8 @@ window.gZenVerticalTabsManager = {
this._tabEdited.after(input);
}
input.focus();
input.select();
input.setSelectionRange(0, input.value.length, "backward");
input.scrollLeft = 0;
input.addEventListener("blur", this._renameTabHalt);
},

View File

@@ -185,7 +185,7 @@
.toolbarbutton-1:not(#tabs-newtab-button),
.urlbar-page-action,
.identity-box-button {
--tab-border-radius: 8px;
--tab-border-radius: 6px;
--toolbarbutton-border-radius: var(--tab-border-radius);
--toolbarbutton-padding-inner: 6px;
--toolbarbutton-padding-outer: 1px;

View File

@@ -213,8 +213,6 @@
--toolbarbutton-border-radius: 6px;
--urlbar-margin-inline: 1px !important;
--zen-squircle-value: 1.4;
--tab-icon-overlay-stroke: light-dark(white, black) !important;
--tab-close-button-padding: 4px !important;
@@ -345,9 +343,3 @@
}
%include zen-buttons.css
@media (-moz-pref('layout.css.corner-shape.enabled')) {
*:not(.no-squircles) {
corner-shape: superellipse(var(--zen-squircle-value));
}
}

View File

@@ -35,13 +35,6 @@ export class ZenSpaceRoutingNavigation extends ZenUIComponent {
return;
}
// The tab we spawn for a route must be allowed to load once without being
// redirected again, regardless of when its workspace attribute lands.
if (aBrowser._zenSkipNavRouteOnce) {
aBrowser._zenSkipNavRouteOnce = false;
return;
}
let uri;
try {
uri = aRequest.QueryInterface(Ci.nsIChannel).URI;
@@ -80,16 +73,49 @@ export class ZenSpaceRoutingNavigation extends ZenUIComponent {
}
const currentWorkspaceId = tab.getAttribute("zen-workspace-id");
if (
!win.gZenSpaceRoutingManager.shouldRedirectNavigation(
const targetWorkspaceId =
win.gZenSpaceRoutingManager.getRedirectTargetWorkspaceId(
uri.spec,
currentWorkspaceId,
win
)
) {
);
if (!targetWorkspaceId) {
return;
}
// A brand-new tab whose very first real navigation this is (a
// target="_blank" link, window.open(), or a freshly opened tab) is still
// showing its initial about:blank document. There is nothing to preserve,
// so rather than cancelling the load and spawning a duplicate tab - which
// would leave this one behind empty - just move this very tab into the
// destination space and let the in-flight load finish in place.
const isInitialDocument =
aBrowser.browsingContext?.currentWindowGlobal?.isInitialDocument ?? false;
if (isInitialDocument) {
const wasSelected = tab.selected;
// Defer so we don't mutate the tab strip from inside a progress notification.
win.setTimeout(() => {
if (!tab.isConnected) {
return;
}
gBrowser.selectedTab = tab.owner;
win.gZenWorkspaces.moveTabToWorkspace(tab, targetWorkspaceId);
if (wasSelected) {
const targetWorkspace =
win.gZenWorkspaces.getWorkspaceFromId(targetWorkspaceId);
if (targetWorkspace) {
win.gZenWorkspaces.lastSelectedWorkspaceTabs[targetWorkspaceId] =
tab;
win.gZenWorkspaces.changeWorkspace(targetWorkspace);
}
}
}, 0);
return;
}
// An already-loaded page is navigating in place. Preserve it in its current
// tab and re-open the destination in a new routed tab instead.
//
// Under Fission the parent-side aRequest is a RemoteWebProgress stand-in
// whose cancel()/loadInfo throw NS_ERROR_NOT_IMPLEMENTED (the real channel
// lives in the content process). Stop the in-place load through the browser,
@@ -111,13 +137,14 @@ export class ZenSpaceRoutingNavigation extends ZenUIComponent {
// Defer so we don't mutate the tab strip from inside a progress notification.
win.setTimeout(() => {
const newTab = gBrowser.addTab(urlToOpen, {
gBrowser.addTab(urlToOpen, {
triggeringPrincipal: principal,
ownerTab: tab.isConnected ? tab : null,
// The user was actively navigating this tab, so follow the navigation
// into the routed tab instead of opening it in the background (addTab
// defaults inBackground to true).
inBackground: false,
});
if (newTab?.linkedBrowser) {
newTab.linkedBrowser._zenSkipNavRouteOnce = true;
}
}, 0);
}
}

View File

@@ -78,6 +78,9 @@ document.addEventListener(
case "cmd_zenReplacePinnedUrlWithCurrent":
gZenPinnedTabManager.replacePinnedUrlWithCurrent();
break;
case "cmd_zenEditPinnedUrl":
gZenPinnedTabManager.editPinnedUrl();
break;
case "cmd_contextZenAddToEssentials":
gZenPinnedTabManager.addToEssentials();
break;
@@ -112,7 +115,9 @@ document.addEventListener(
});
break;
case "cmd_zenTogglePinTab": {
const currentTab = gBrowser.selectedTab;
const currentTab = gZenGlanceManager.getTabOrGlanceParent(
gBrowser.selectedTab
);
if (currentTab && !currentTab.hasAttribute("zen-empty-tab")) {
if (currentTab.pinned) {
gBrowser.unpinTab(currentTab);

View File

@@ -51,6 +51,10 @@
--zen-toolbox-max-width: 74px !important;
--zen-compact-float: var(--zen-element-separation);
@media (-moz-mac-tahoe-theme) {
--zen-compact-mode-no-padding-radius-fix: -2px;
}
:root[zen-no-padding='true'] & {
--zen-compact-float: 10px;
--zen-compact-top-toolbar-hidden-fix: var(--zen-compact-float);

View File

@@ -723,8 +723,11 @@
const { isNearLeftEdge, isNearRightEdge } =
this.#shouldSwitchSpace(event);
if (isNearLeftEdge || isNearRightEdge) {
if (!this.#changeSpaceTimer) {
if (!this.#changeSpaceTimer && !this.#isOutOfWindow) {
this.#changeSpaceTimer = setTimeout(() => {
if (this.#isOutOfWindow) {
return;
}
this.clearDragOverVisuals();
gZenWorkspaces
.changeWorkspaceShortcut(
@@ -956,8 +959,10 @@
if (ownerGlobal?.gZenCompactModeManager) {
// Sometimes, dragend doesn't always get called when dragging
// to different windows, see gh-8643.
delete ownerGlobal.gZenCompactModeManager._isTabBeingDragged;
ownerGlobal.gZenCompactModeManager._clearAllHoverStates();
requestAnimationFrame(() => {
delete ownerGlobal.gZenCompactModeManager._isTabBeingDragged;
ownerGlobal.gZenCompactModeManager._clearAllHoverStates();
});
}
this.clearSpaceSwitchTimer();
gZenFolders.highlightGroupOnDragOver(null);

View File

@@ -289,6 +289,9 @@ export class nsZenFolder extends MozTabbrowserTabGroup {
addTabs(tabs) {
let tabsFromOutside = [];
for (let tab of tabs) {
if (tab.hasAttribute("zen-essential")) {
gZenPinnedTabManager.removeEssentials(tab, false);
}
if (tab.group !== this) {
tabsFromOutside.push(tab);
}

View File

@@ -622,15 +622,13 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
}
createFolder(tabs = [], options = {}) {
const filteredTabs = tabs
.filter(tab => !tab.hasAttribute("zen-essential"))
.map(tab => {
gBrowser.pinTab(tab);
if (tab?.group?.hasAttribute("split-view-group")) {
tab = tab.group;
}
return tab;
});
const filteredTabs = tabs.map(tab => {
gBrowser.pinTab(tab);
if (tab?.group?.hasAttribute("split-view-group")) {
tab = tab.group;
}
return tab;
});
const workspacePinned = gZenWorkspaces.workspaceElement(
options.workspaceId

View File

@@ -53,6 +53,10 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
#setupEventListeners() {
window.addEventListener("TabClose", this.onTabClose.bind(this));
window.addEventListener("TabSelect", this.onLocationChange.bind(this));
window.addEventListener(
"MozDOMFullscreen:Entered",
this.onFullscreenEntered.bind(this)
);
document
.getElementById("tabbrowser-tabpanels")
@@ -81,8 +85,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
menuitem.addEventListener("command", () =>
this.openGlance({
url: gContextMenu.linkURL,
triggeringPrincipal:
Services.scriptSecurityManager.getSystemPrincipal(),
triggeringPrincipal: gContextMenu.principal,
})
);
@@ -350,6 +353,28 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
return this.#lastLinkClickData;
}
/**
* Check whether a glance load is permitted for its triggering principal.
*
* @param {object} data - Glance data including URL and triggeringPrincipal
* @returns {boolean} Whether the load is allowed
*/
#isGlanceLoadAllowed(data) {
const { url, triggeringPrincipal } = data ?? {};
if (typeof url !== "string" || !url.length || !triggeringPrincipal) {
return false;
}
try {
Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
triggeringPrincipal,
url
);
} catch {
return false;
}
return true;
}
/**
* Open a glance overlay with the specified data
*
@@ -367,6 +392,13 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
return Promise.resolve(this.#currentTab);
}
// Existing-tab glances perform no navigation and are exempt; for fresh
// loads, refuse any URL the triggering principal isn't allowed to load
// (e.g. a web page linking to file://).
if (!existingTab && !this.#isGlanceLoadAllowed(data)) {
return Promise.resolve(null);
}
if (!data.height || !data.width) {
data = {
...data,
@@ -1414,6 +1446,23 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
}
}
/**
* Handle DOM Fullscreen request while inside glance
*
* @param {Event} event - The MozDOMFullscreen:Entered event
*/
onFullscreenEntered(event) {
const browser = this.#currentBrowser;
if (!browser) {
return;
}
if (event.target === browser) {
this.fullyOpenGlance();
}
}
/**
* Manage tab close for glance tabs
*
@@ -1502,6 +1551,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
return (
owner &&
owner.pinned &&
!owner.hasAttribute("glance-id") &&
this._lazyPref.SHOULD_OPEN_EXTERNAL_TABS_IN_GLANCE &&
owner.linkedBrowser?.browsingContext?.isAppTab &&
this.tabDomainsDiffer(owner, uri) &&

View File

@@ -41,11 +41,11 @@
&:hover {
background: light-dark(rgb(41, 41, 41), rgb(204, 204, 204));
scale: 1.02;
scale: 1.05;
}
&:hover:active {
scale: 0.98;
scale: 0.95;
}
& label {

View File

@@ -4,8 +4,8 @@
<html:template id="zen-glance-sidebar-template">
<vbox class="zen-glance-sidebar-container">
<toolbarbutton class="no-squircles zen-glance-sidebar-close toolbarbutton-1" command="cmd_zenGlanceClose" data-l10n-id="zen-general-confirm" />
<toolbarbutton class="no-squircles zen-glance-sidebar-open toolbarbutton-1" command="cmd_zenGlanceExpand" />
<toolbarbutton class="no-squircles zen-glance-sidebar-split toolbarbutton-1" command="cmd_zenGlanceSplit" />
<toolbarbutton class="zen-glance-sidebar-close toolbarbutton-1" command="cmd_zenGlanceClose" data-l10n-id="zen-general-confirm" />
<toolbarbutton class="zen-glance-sidebar-open toolbarbutton-1" command="cmd_zenGlanceExpand" />
<toolbarbutton class="zen-glance-sidebar-split toolbarbutton-1" command="cmd_zenGlanceSplit" />
</vbox>
</html:template>

View File

@@ -20,4 +20,5 @@ DIRS += [
"share",
"spaces",
"space-routing",
"sync",
]

View File

@@ -10,6 +10,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZenLiveFoldersManager:
"resource:///modules/zen/ZenLiveFoldersManager.sys.mjs",
ZenSyncStore: "resource:///modules/zen/ZenSyncManager.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
@@ -609,6 +610,7 @@ export class nsZenSessionManager {
}
);
this.#collectWindowData(windows);
lazy.ZenSyncStore.notifyAboutChanges();
// This would save the data to disk asynchronously or when quitting the app.
let sidebar = this.#sidebarWithoutCloning;
this.#file.data = sidebar;
@@ -622,6 +624,15 @@ export class nsZenSessionManager {
this.log(`Saving Zen session data with ${sidebar.tabs?.length || 0} tabs`);
}
/**
* Returns the sidebar data object.
*
* @returns {object}
*/
getSidebarData() {
return this.#sidebarWithoutCloning;
}
/**
* Called when the last known backup should be deleted and a new one
* created. This uses the #deferredBackupTask to debounce clusters of

View File

@@ -1232,19 +1232,38 @@ class nsZenWindowSync {
activeIndex = Math.min(activeIndex, entries.length - 1);
activeIndex = Math.max(activeIndex, 0);
let entryToUse = (entries[activeIndex] || entries[0]) ?? null;
const initialState = {
entry: {
url: entryToUse?.url,
title: entryToUse?.title,
},
image,
};
this.#runOnAllWindows(null, win => {
const targetTab = this.getItemFromWindow(win, aTab.id);
if (targetTab) {
targetTab._zenPinnedInitialState = initialState;
}
});
this.#setPinnedInitialState(
aTab,
{ url: entryToUse?.url, title: entryToUse?.title },
image
);
});
}
/**
* Sets the canonical pinned URL for a tab across all windows. Used to let the
* user edit a pinned tab's URL directly.
*
* @param {object} aTab - The tab to set the pinned URL for.
* @param {string} aUrl - The URL to store as the canonical pinned URL.
* @param {string} [aImage] - Optional Icon to store.
*/
setPinnedUrl(aTab, aUrl, aImage) {
this.log(`Setting pinned url for tab ${aTab.id}`);
this.#setPinnedInitialState(
aTab,
{ url: aUrl, title: aTab.zenStaticLabel },
aImage
);
}
#setPinnedInitialState(aTab, aEntry, aImage) {
const initialState = { entry: aEntry, image: aImage };
this.#runOnAllWindows(null, win => {
const targetTab = this.getItemFromWindow(win, aTab.id);
if (targetTab) {
targetTab._zenPinnedInitialState = initialState;
}
});
}

View File

@@ -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/.
category browser-window-delayed-startup resource:///modules/zen/spacerouting/ZenSpaceRoutingManager.sys.mjs gZenSpaceRoutingManager.onDelayedBrowserStartup

View File

@@ -18,6 +18,58 @@ class nsZenSpaceRoutingManager {
this.#readFromDisk();
}
/**
* Auto invoked for every window on delayed startup
*
* @param {nsIDOMWindow} window - The browser window that just started up
*/
onDelayedBrowserStartup(window) {
const element = window.MozXULElement.parseXULToFragment(`
<menuseparator/>
<menuitem id="context_zen-add-domain-to-routing"
data-lazy-l10n-id="tab-context-zen-add-domain-to-sr"
data-l10n-args='{"tabCount": 1}'/>
`);
window.document.getElementById("context_undoCloseTab").after(element);
window.document
.getElementById("context_zen-add-domain-to-routing")
.addEventListener("command", this.#onAddSelectedToRouting.bind(this));
window.document
.getElementById("tabContextMenu")
.addEventListener(
"popupshowing",
this.#updateTabCloseCountState.bind(this)
);
}
/**
* Updates the "context_zen-add-domain-to-routing" command
* to reflect the number of selected tabs, when applicable.
*
* @param {Event} event - The event param
*/
#updateTabCloseCountState(event) {
const window = event.target.documentGlobal;
window.document.l10n.setArgs(
window.document.getElementById("context_zen-add-domain-to-routing"),
{ tabCount: window.gBrowser.selectedTabs.length }
);
}
/**
* Callback for whenever the menuitem command is ran
*
* @param {Event} event - The event parameter
*/
#onAddSelectedToRouting(event) {
const window = event.target.documentGlobal;
const tabs = window.TabContextMenu.contextTab.multiselected
? window.gBrowser.selectedTabs
: [window.TabContextMenu.contextTab];
this.addRouteForSelected(tabs, window);
}
/**
* Callback that will be executed from tabbrowser.js
* This method can be used to stop the tab from being created.
@@ -52,7 +104,7 @@ class nsZenSpaceRoutingManager {
break;
default: {
const targetWorkspace =
win?.gZenWorkspaces?.getWorkspaceFromId(targetRoute);
win.gZenWorkspaces.getWorkspaceFromId(targetRoute);
if (targetWorkspace) {
userContextId = targetWorkspace.containerTabId;
@@ -107,8 +159,26 @@ class nsZenSpaceRoutingManager {
* @returns {boolean} True when the navigation should open in a new routed tab
*/
shouldRedirectNavigation(uriString, currentWorkspaceId, win) {
return !!this.getRedirectTargetWorkspaceId(
uriString,
currentWorkspaceId,
win
);
}
/**
* Resolves the destination space for an in-place top-level navigation, or
* null when the navigation should be left alone (no rule, the destination is
* "most-recent-space", the tab already lives there, or the space is gone).
*
* @param {string} uriString - The destination URI
* @param {string|null} currentWorkspaceId - The zen-workspace-id of the navigating tab
* @param {Window} win - The owning browser window
* @returns {string|null} The target workspace id, or null to leave the navigation in place
*/
getRedirectTargetWorkspaceId(uriString, currentWorkspaceId, win) {
if (!win?.gZenWorkspaces?.workspaceEnabled) {
return false;
return null;
}
const targetRoute = this.routeUri(uriString, { fromExternal: false });
@@ -118,11 +188,13 @@ class nsZenSpaceRoutingManager {
targetRoute === "most-recent-space" ||
targetRoute === currentWorkspaceId
) {
return false;
return null;
}
// Only redirect when the destination space actually exists.
return !!win.gZenWorkspaces.getWorkspaceFromId(targetRoute);
return win.gZenWorkspaces.getWorkspaceFromId(targetRoute)
? targetRoute
: null;
}
/**
@@ -173,10 +245,6 @@ class nsZenSpaceRoutingManager {
if (targetWorkspace) {
workspaces.moveTabToWorkspace(newTab, targetWorkspace.uuid);
if (inBackground) {
return;
}
const mostRecentWindow =
Services.wm.getMostRecentWindow("navigator:browser");
const isOriginatingWindow = win === mostRecentWindow;
@@ -184,7 +252,10 @@ class nsZenSpaceRoutingManager {
win.gZenWorkspaces.lastSelectedWorkspaceTabs[
targetWorkspace.uuid
] = newTab;
await win.gZenWorkspaces.changeWorkspace(targetWorkspace);
if (!inBackground) {
await win.gZenWorkspaces.changeWorkspace(targetWorkspace);
}
}
}
}
@@ -403,6 +474,37 @@ class nsZenSpaceRoutingManager {
this.#file.data.defaultRouteExternal = routeType;
}
/**
* Adds a new route for all given tabs
*
* @param {Array<object>} selectedTabs - The tabs that should be routed
* @param {Window} parentWindow - The window from which this is being executed
*/
addRouteForSelected(selectedTabs, parentWindow) {
const newRoute = this.createNewRoute();
let routeReference = "";
if (selectedTabs.length == 1) {
newRoute.matchType = "contains";
routeReference = selectedTabs[0].linkedBrowser.currentURI.host;
} else {
newRoute.matchType = "regex";
routeReference = "(";
for (let i = 0; i < selectedTabs.length; i++) {
const domain = selectedTabs[i].linkedBrowser.currentURI.host;
routeReference += domain.replaceAll(".", "\.");
if (i != selectedTabs.length - 1) {
routeReference += "|";
}
}
routeReference += ")";
}
newRoute.reference = routeReference;
this.updateRoute(newRoute);
this.openSpaceRoutingDialog(parentWindow);
}
/**
* Saves all routes. The list of
* routes is stripped of empty routes

View File

@@ -182,8 +182,8 @@ class nsZenWorkspaceCreation extends MozXULElement {
this.onProfileCommand.bind(this)
);
this.profilesPopup.addEventListener(
"popupshown",
this.onProfilePopupShown.bind(this)
"popupshowing",
this.onProfilePopupShowing.bind(this)
);
this.profilesPopup.addEventListener(
"command",
@@ -192,7 +192,7 @@ class nsZenWorkspaceCreation extends MozXULElement {
this.currentProfile = {
id: 0,
name: "Default",
name: ContextualIdentityService.formatContextLabel("user-context-none"),
};
} else {
this.inputProfile.parentNode.hidden = true;
@@ -296,7 +296,7 @@ class nsZenWorkspaceCreation extends MozXULElement {
this.profilesPopup.openPopup(event.target, "after_start");
}
onProfilePopupShown(event) {
onProfilePopupShowing(event) {
return window.createUserContextMenu(event, {
isContextMenu: true,
showDefaultTab: true,

View File

@@ -11,6 +11,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
ZenSyncStore: "resource:///modules/zen/ZenSyncManager.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "browserBackgroundElement", () => {
@@ -148,7 +149,7 @@ class nsZenWorkspaces {
if (!this.privateWindowOrDisabled) {
const observerFunction = async () => {
delete this._workspaceBookmarksCache;
await this.workspaceBookmarks();
await this.#initializeWorkspaceBookmarks();
this._invalidateBookmarkContainers();
};
Services.obs.addObserver(observerFunction, "workspace-bookmarks-updated");
@@ -168,6 +169,63 @@ class nsZenWorkspaces {
}
}
/**
* Applies live sync changes: updates workspace cache, removes deleted items,
* then creates/updates pulled items.
*
* @param {{ spaces: Array}} pulled Reconcile-pulled items.
* @param {{ spaces: Array}} removals Items to remove.
*/
async _applySyncChanges(pulled, removals = {}) {
if (!this.shouldHaveWorkspaces || this.privateWindowOrDisabled) {
return;
}
await this.promiseInitialized;
// 1. Update workspace cache (remove deleted, merge pulled)
const removedSpaceIds = new Set((removals.spaces || []).map(s => s.uuid));
if (removedSpaceIds.size || pulled.spaces?.length) {
const localMap = new Map(
this.getWorkspaces()
.filter(w => !removedSpaceIds.has(w.uuid))
.map(w => [w.uuid, w])
);
for (const space of pulled.spaces || []) {
if (!space?.uuid) {
continue;
}
const existing = localMap.get(space.uuid);
localMap.set(space.uuid, existing ? { ...existing, ...space } : space);
}
await this.propagateWorkspaces(
this.#getOrderedWorkspacesByPosition(Array.from(localMap.values()))
);
this.#propagateWorkspaceData();
}
}
#getOrderedWorkspacesByPosition(workspaces) {
return [...workspaces]
.map((workspace, index) => ({ workspace, index }))
.sort((a, b) => {
const aPosition =
typeof a.workspace.position === "number"
? a.workspace.position
: a.index;
const bPosition =
typeof b.workspace.position === "number"
? b.workspace.position
: b.index;
return aPosition - bPosition || a.index - b.index;
})
.map(({ workspace }) => {
// strip the position property that comes from pulled workspaces
const rest = { ...workspace };
delete rest.position;
return rest;
});
}
#afterLoadInit() {
const onResize = (...args) => {
requestAnimationFrame(() => {
@@ -391,7 +449,7 @@ class nsZenWorkspaces {
container = 0;
}
let essentialsContainer = document.querySelector(
`.zen-essentials-container[container="${container}"]:not([cloned])`
`.zen-essentials-container[container="${container}"]`
);
if (!essentialsContainer) {
essentialsContainer = document.createXULElement("hbox");
@@ -691,17 +749,17 @@ class nsZenWorkspaces {
return spacesForSS;
}
async workspaceBookmarks() {
async #initializeWorkspaceBookmarks() {
if (this.privateWindowOrDisabled) {
this._workspaceBookmarksCache = {
bookmarks: [],
lastChangeTimestamp: 0,
};
return this._workspaceBookmarksCache;
return;
}
if (this._workspaceBookmarksCache) {
return this._workspaceBookmarksCache;
return;
}
const [bookmarks, lastChangeTimestamp] = await Promise.all([
@@ -710,8 +768,6 @@ class nsZenWorkspaces {
]);
this._workspaceBookmarksCache = { bookmarks, lastChangeTimestamp };
return this._workspaceCache;
}
restoreWorkspacesFromSessionStore(aWinData = {}) {
@@ -771,7 +827,7 @@ class nsZenWorkspaces {
return (async () => {
await this.#waitForPromises();
this.#afterLoadInit();
await this.workspaceBookmarks();
await this.#initializeWorkspaceBookmarks();
await this.changeWorkspace(activeWorkspace, { onInit: true });
this.#fixTabPositions();
this.onWindowResize();
@@ -797,6 +853,13 @@ class nsZenWorkspaces {
})();
}
#markWorkspaceChanged(workspaceId) {
lazy.ZenSyncStore.markItemChanged({
type: "space",
id: workspaceId,
});
}
async selectStartPage() {
if (!this.workspaceEnabled || gZenUIManager.testingEnabled) {
return;
@@ -1214,12 +1277,19 @@ class nsZenWorkspaces {
} else {
workspacesData.push(workspaceData);
}
// mark item as changed for sync
this.#markWorkspaceChanged(workspaceData.uuid);
this.#propagateWorkspaceData();
}
removeWorkspace(windowID) {
let { promise, resolve } = Promise.withResolvers();
this.#deleteWorkspaceOwnedTabs(windowID);
// mark item as changed for sync
this.#markWorkspaceChanged(windowID);
let workspacesData = this.getWorkspaces();
// Remove the workspace from the cache
workspacesData = workspacesData.filter(
@@ -1351,30 +1421,50 @@ class nsZenWorkspaces {
if (this.privateWindowOrDisabled) {
return;
}
const workspaces = this._workspaceCache;
const workspaces = this.getWorkspaces();
// Track previous positions so we only notify observers for workspaces whose
// position changed during the reorder.
const previousPositions = new Map(
workspaces.map((workspace, index) => [workspace.uuid, index])
);
const workspace = workspaces.find(w => w.uuid === id);
if (!workspace) {
console.warn(`Workspace with ID ${id} not found for reordering.`);
return;
}
// Remove the workspace from its current position
const currentIndex = workspaces.indexOf(workspace);
if (currentIndex === -1) {
console.warn(`Workspace with ID ${id} not found in the list.`);
return;
}
workspaces.splice(currentIndex, 1);
// Insert the workspace at the new position
if (newPosition < 0 || newPosition > workspaces.length) {
if (newPosition < 0 || newPosition >= workspaces.length) {
console.warn(
`Invalid position ${newPosition} for reordering workspace with ID ${id}.`
);
return;
}
workspaces.splice(currentIndex, 1);
workspaces.splice(newPosition, 0, workspace);
// Propagate the changes if the order has changed
if (currentIndex !== newPosition) {
this.#propagateWorkspaceData();
this._workspaceCache = workspaces;
for (const [i, ws] of workspaces.entries()) {
if (previousPositions.get(ws.uuid) === i) {
continue;
}
// mark item as changed for sync
this.#markWorkspaceChanged(ws.uuid);
}
this.#propagateWorkspaceData(workspaces);
}
}
@@ -1507,18 +1597,26 @@ class nsZenWorkspaces {
}
if (container) {
const newtabPlacement = Services.prefs.getBoolPref(
"zen.view.show-newtab-button-top",
false
);
const insertElement = newtabPlacement
? container.firstChild
: container.lastChild;
if (tab.group?.hasAttribute("split-view-group")) {
gBrowser.zenHandleTabMove(tab.group, () => {
for (const subTab of tab.group.tabs) {
subTab.setAttribute("zen-workspace-id", workspaceID);
}
container.insertBefore(tab.group, container.lastChild);
container.insertBefore(tab.group, insertElement);
});
continue;
}
gBrowser.zenHandleTabMove(tab, () => {
tab.setAttribute("zen-workspace-id", workspaceID);
container.insertBefore(tab, container.lastChild);
container.insertBefore(tab, insertElement);
});
}
// also change glance tab if it's the same tab
@@ -1766,6 +1864,11 @@ class nsZenWorkspaces {
}
const newTransform = diff * 100;
element.style.transform = `translateX(${newTransform + offsetPixels / 2}%)`;
// A special case for two spaces
if (spaceLen === 2 && diff !== 0) {
const side = offsetPixels > 0 ? -100 : 100;
element.style.transform = `translateX(${side + offsetPixels / 2}%)`;
}
}
// Hide other essentials with different containerTabId
for (const container of otherContainersEssentials) {
@@ -1919,28 +2022,20 @@ class nsZenWorkspaces {
diff += spaceLen;
}
const isGoingLeft = diff < 0;
const clonedEssentials = [];
const essentialsAnimData = [];
if (shouldAnimate && this.shouldAnimateEssentials && previousWorkspace) {
const containerIds = new Map();
for (const workspace of workspaces) {
const essentialsContainer = this.getEssentialsSection(
workspace.containerTabId
);
let lastCloned = clonedEssentials[clonedEssentials.length - 1];
if (lastCloned && lastCloned.contextId == workspace.containerTabId) {
lastCloned.repeat++;
lastCloned.workspaces.push(workspace);
continue;
const containerId = workspace.containerTabId;
if (!containerIds.has(containerId)) {
containerIds.set(containerId, []);
}
essentialsContainer.setAttribute("hidden", "true");
const essentialsClone = essentialsContainer.cloneNode(true);
essentialsClone.removeAttribute("hidden");
essentialsClone.setAttribute("cloned", "true");
clonedEssentials.push({
container: essentialsClone,
workspaces: [workspace],
contextId: workspace.containerTabId,
originalContainer: essentialsContainer,
repeat: 0,
containerIds.get(containerId).push(workspace);
}
for (const [containerId, spaces] of containerIds) {
essentialsAnimData.push({
element: this.getEssentialsSection(containerId),
workspaces: spaces,
});
}
}
@@ -2004,6 +2099,12 @@ class nsZenWorkspaces {
offset += spaceLen;
}
offset = offset * 100;
// A special case for two spaces
if (spaceLen === 2 && offset !== 0) {
const currentTransform =
parseFloat(element.style.transform.split("(")[1]) || 0;
offset = currentTransform >= 0 ? 100 : -100;
}
const newTransform = `translateX(${offset}%)`;
// Only animate the workspace that is coming in, to avoid having multiple workspaces
// animating off-screen at the same time which can cause performance issues. With an off
@@ -2047,190 +2148,59 @@ class nsZenWorkspaces {
}
}
if (this.shouldAnimateEssentials && previousWorkspace) {
// Animate essentials
const newWorkspaceEssentialsContainer = clonedEssentials.find(cloned =>
cloned.workspaces.some(w => w.uuid === newWorkspace.uuid)
);
// Get a list of essentials containers that are in between the first and last workspace
const essentialsContainersInBetween = clonedEssentials.filter(cloned => {
const essentialsWorkspaces = cloned.workspaces;
const firstIndex = workspaces.findIndex(
w => w.uuid === essentialsWorkspaces[0].uuid
for (const data of essentialsAnimData) {
const container = data.element;
const essentialsWorkspaces = data.workspaces;
const containsPrev = essentialsWorkspaces.some(
w => w.uuid === previousWorkspace.uuid
);
const lastIndex = workspaces.findIndex(
w =>
w.uuid ===
essentialsWorkspaces[essentialsWorkspaces.length - 1].uuid
const containsNew = essentialsWorkspaces.some(
w => w.uuid === newWorkspace.uuid
);
const [start, end] = [
Math.min(previousWorkspaceIndex, newWorkspaceIndex),
Math.max(previousWorkspaceIndex, newWorkspaceIndex),
];
// Check if any part of the container overlaps with the movement range
return firstIndex <= end && lastIndex >= start;
});
for (const cloned of clonedEssentials) {
const container = cloned.container;
const essentialsWorkspaces = cloned.workspaces;
const repeats = cloned.repeat;
// Animate like the workspaces above expect essentials are a bit more
// complicated because they are not based on workspaces but on containers
// So, if we have the following arangement:
// | [workspace1] [workspace2] [workspace3] [workspace4]
// | [container1] [container1] [container2] [container1]
// And if we are changing from workspace 1 to workspace 4,
// we should be doing the following:
// First container (repeat 2 times) will stay in place until
// we reach container 3, then animate to the left and container 2
// also move to the left after that while container 1 in workspace 4
// will slide in from the right
// Get the index from first and last workspace
const firstWorkspaceIndex = workspaces.findIndex(
w => w.uuid === essentialsWorkspaces[0].uuid
);
const lastWorkspaceIndex = workspaces.findIndex(
w =>
w.uuid ===
essentialsWorkspaces[essentialsWorkspaces.length - 1].uuid
);
cloned.originalContainer.style.removeProperty("transform");
// Check if the container is even going to appear on the screen, to save on animation
if (
// We also need to check if the container is even going to appear on the screen.
// In order to do this, we need to check if the container is between the first and last workspace.
// Note that essential containers can have multiple workspaces,
// so we need to check if any of the workspaces in the container are between the
// first and last workspace.
!essentialsContainersInBetween.find(
ce =>
ce.workspaces.some(
w => w.uuid === essentialsWorkspaces[0].uuid
) &&
ce.workspaces.some(
w =>
w.uuid ===
essentialsWorkspaces[essentialsWorkspaces.length - 1].uuid
)
)
) {
if (!containsPrev && !containsNew) {
container.setAttribute("hidden", "true");
continue;
}
cloned.originalContainer.parentNode.appendChild(container);
let stepsInBetween =
Math.abs(
newWorkspaceIndex -
(isGoingLeft ? firstWorkspaceIndex : lastWorkspaceIndex)
) + 1;
const usingSameContainer =
newWorkspaceEssentialsContainer?.workspaces.some(
w => w.uuid === newWorkspace.uuid
) &&
newWorkspaceEssentialsContainer?.workspaces.some(
w => w.uuid === previousWorkspace.uuid
);
let newOffset =
-(
newWorkspaceIndex -
(isGoingLeft ? firstWorkspaceIndex : lastWorkspaceIndex) +
(!isGoingLeft ? repeats - 1 : -repeats + 1)
) * 100;
let existingOffset =
-(
newWorkspaceIndex -
(isGoingLeft ? lastWorkspaceIndex : firstWorkspaceIndex) +
(isGoingLeft ? repeats - 1 : -repeats + 1)
) * 100;
container.removeAttribute("hidden");
// If we are on the same container and both new and old workspace are in the same "essentialsWorkspaces"
// we can simply not animate the essentials
if (
usingSameContainer &&
essentialsWorkspaces.some(w => w.uuid === newWorkspace.uuid) &&
essentialsWorkspaces.some(w => w.uuid === previousWorkspace.uuid)
) {
newOffset = 0;
existingOffset = 0;
if (containsPrev && containsNew) {
container.style.transform = "translateX(0%)";
continue;
}
const needsOffsetAdjustment =
stepsInBetween > essentialsWorkspaces.length || usingSameContainer;
let existingOffset, newOffset;
const currentTransform =
parseFloat(container.style.transform.split("(")[1]) || 0;
if (containsPrev && !containsNew) {
existingOffset = currentTransform;
newOffset = isGoingLeft ? 100 : -100;
} else {
existingOffset = currentTransform || (isGoingLeft ? -100 : 100);
newOffset = 0;
}
if (repeats > 0 && needsOffsetAdjustment) {
if (!isGoingLeft) {
if (existingOffset !== 0) {
existingOffset += 100;
}
if (newOffset !== 0) {
newOffset += 100;
}
if (spaceLen === 2) {
if (containsPrev && !containsNew) {
existingOffset = currentTransform;
newOffset = currentTransform >= 0 ? 100 : -100;
} else {
if (existingOffset !== 0) {
existingOffset -= 100;
}
if (newOffset !== 0) {
newOffset -= 100;
}
existingOffset = currentTransform >= 0 ? 100 : -100;
newOffset = 0;
}
}
// Special case: going forward from single reused container to a new one
if (
!usingSameContainer &&
!isGoingLeft &&
lastWorkspaceIndex === newWorkspaceIndex - 1
) {
existingOffset = 0;
newOffset = -100;
stepsInBetween = 1;
}
if (
!usingSameContainer &&
isGoingLeft &&
firstWorkspaceIndex === newWorkspaceIndex + 1
) {
existingOffset = 0;
newOffset = 100;
stepsInBetween = 1;
}
if (
!usingSameContainer &&
isGoingLeft &&
(firstWorkspaceIndex === newWorkspaceIndex - 1 ||
firstWorkspaceIndex === newWorkspaceIndex)
) {
existingOffset = -100;
newOffset = 0;
stepsInBetween = 1;
}
if (
!usingSameContainer &&
!isGoingLeft &&
firstWorkspaceIndex === newWorkspaceIndex
) {
existingOffset = 100;
newOffset = 0;
stepsInBetween = 1;
}
const newTransform = `translateX(${newOffset}%)`;
let existingTransform = `translateX(${existingOffset}%)`;
if (container.style.transform && container.style.transform !== "none") {
existingTransform = container.style.transform;
}
if (shouldAnimate) {
container.style.transform = existingTransform;
animations.push(
gZenUIManager.motion.animate(
container,
{
transform: [
existingTransform,
new Array(stepsInBetween).fill(newTransform).join(","),
],
transform: [existingTransform, newTransform],
},
{
type: "spring",
@@ -2259,17 +2229,12 @@ class nsZenWorkspaces {
this.#currentSpaceSwitchContext.animations = [];
document.documentElement.removeAttribute("animating-background");
if (shouldAnimate) {
for (const cloned of clonedEssentials) {
cloned.container.remove();
for (const data of essentialsAnimData) {
data.element.style.removeProperty("transform");
}
this._alwaysAnimatePaddingTop = true;
this.updateTabsContainers();
}
const essentialsContainer = this.getEssentialsSection(
newWorkspace.containerTabId
);
essentialsContainer.removeAttribute("hidden");
essentialsContainer.style.transform = "none";
gBrowser.tabContainer._invalidateCachedTabs();
gZenUIManager.tabsWrapper.style.removeProperty("scrollbar-width");
this._animatingChange = false;
@@ -2290,12 +2255,12 @@ class nsZenWorkspaces {
}
onBeforeTabSelect(aTab) {
if (this.#inChangingWorkspace) {
if (this.#inChangingWorkspace || !aTab) {
// Just in case, Let's not do these checks while we are
// in the middle of changing workspace,
return false;
}
const tabSpace = aTab?.getAttribute("zen-workspace-id");
const tabSpace = aTab.getAttribute("zen-workspace-id");
if (
tabSpace &&
tabSpace !== this.activeWorkspace &&
@@ -2485,7 +2450,7 @@ class nsZenWorkspaces {
for (const tab of gBrowser.tabs) {
if (
!tab.hasAttribute("zen-workspace-id") &&
!tab.hasAttribute("zen-workspace-id")
!tab.hasAttribute("zen-essential")
) {
tab.setAttribute("zen-workspace-id", workspace.uuid);
}
@@ -3144,9 +3109,6 @@ class nsZenWorkspaces {
...normalContainers,
];
for (const container of containers) {
if (container.hasAttribute("cloned")) {
continue;
}
for (const tab of container.children) {
if (gBrowser.isTab(tab)) {
tabs.push(tab);

View File

@@ -31,7 +31,6 @@
& toolbarbutton {
margin: 0;
max-width: 28px;
border-radius: var(--toolbarbutton-border-radius);
height: 28px;
display: flex;
justify-content: center;

View File

@@ -1229,7 +1229,11 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
const newTab = this.openAndSwitchToTab(url, {
skipRoute: true,
inBackground: false,
triggeringPrincipal: window.gContextMenu.principal,
});
if (!newTab) {
return;
}
this.splitTabs([currentTab, newTab], undefined, 1);
}
@@ -1240,13 +1244,17 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
*/
contextSplitTabs(otherTabHint = null) {
let tabs;
let currentTab = TabContextMenu.contextTab || gBrowser.selectedTab;
let currentTab = gZenGlanceManager.getTabOrGlanceParent(
TabContextMenu.contextTab || gBrowser.selectedTab
);
if (currentTab.multiselected) {
tabs = gBrowser.selectedTabs;
} else if (!currentTab.selected && !currentTab.splitView) {
tabs = [
currentTab,
...gBrowser.selectedTabs.filter(t => t !== currentTab),
...gBrowser.selectedTabs.filter(
t => t !== currentTab && !t.hasAttribute("zen-glance-tab")
),
];
} else {
tabs = [currentTab];
@@ -1975,9 +1983,24 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
* @returns {tab} The tab that was opened
*/
openAndSwitchToTab(url, options) {
const triggeringPrincipal = options?.triggeringPrincipal;
if (!triggeringPrincipal) {
return null;
}
try {
Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
triggeringPrincipal,
url
);
} catch {
return null;
}
const parentWindow = window.parent;
const targetWindow = parentWindow || window;
const tab = targetWindow.gBrowser.addTrustedTab(url, options);
const tab = targetWindow.gBrowser.addTab(url, {
...options,
triggeringPrincipal,
});
targetWindow.gBrowser.selectedTab = tab;
return tab;
}

View File

@@ -0,0 +1,160 @@
/* 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/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
ContextualIdentityService:
"resource://gre/modules/ContextualIdentityService.sys.mjs",
ZenWindowSync: "resource:///modules/zen/ZenWindowSync.sys.mjs",
});
function normalizeUserContextId(value) {
const normalized = typeof value === "string" ? Number(value) : value;
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
return null;
}
return normalized;
}
class ZenSyncManager {
getSidebarData() {
return lazy.ZenSessionStore.getSidebarData();
}
/**
* Whether to ignore changes to items. This is used to prevent
* infinite loops when applying incoming sync changes.
*
* @type {boolean}
*/
#ignoreChanges = false;
#changedItems = new Map();
markItemChanged(item) {
if (item.type && item.id && !this.#ignoreChanges) {
const key = `${item.type}~${item.id}`;
this.#changedItems.set(key, { type: item.type, id: item.id });
}
}
#getChangedItems() {
return Array.from(this.#changedItems.values());
}
#clearChangedItems() {
this.#changedItems.clear();
}
notifyAboutChanges() {
const changedItems = this.#getChangedItems();
for (const item of changedItems) {
Services.obs.notifyObservers(
{ wrappedJSObject: item },
"zen-workspace-item-changed"
);
}
this.#clearChangedItems();
}
async applyIncomingBatch(pulled, removals) {
try {
this.#ignoreChanges = true;
this.#applyIncomingContainers(
pulled.containers || [],
removals.containers || []
);
const win = lazy.ZenWindowSync.firstSyncedWindow;
if (win?.gZenWorkspaces) {
await win.gZenWorkspaces._applySyncChanges(pulled, removals);
}
} catch (e) {
console.error("ZenSyncManager: Failed to apply incoming sync data:", e);
throw e;
} finally {
this.#ignoreChanges = false;
}
}
#applyIncomingContainers(pulledContainers, removedContainers) {
const localContainersById = new Map(
lazy.ContextualIdentityService.getPublicIdentities().map(container => [
container.userContextId,
container,
])
);
for (const container of pulledContainers) {
if (!container.name) {
continue;
}
const userContextId = normalizeUserContextId(container.userContextId);
if (userContextId === null) {
console.warn(
"ZenSyncManager: Ignoring incoming container with invalid userContextId",
{ container }
);
continue;
}
const existsLocally = localContainersById.has(userContextId);
if (existsLocally) {
lazy.ContextualIdentityService.update(
userContextId,
container.name,
container.icon,
container.color
);
continue;
}
const createdIdentity = lazy.ContextualIdentityService.create(
container.name,
container.icon,
container.color,
userContextId
);
if (createdIdentity) {
localContainersById.set(createdIdentity.userContextId, createdIdentity);
}
if (createdIdentity && createdIdentity.userContextId !== userContextId) {
console.warn("ZenSyncManager: Container sync created unexpected ID", {
requestedId: userContextId,
createdId: createdIdentity.userContextId,
name: container.name,
});
}
}
for (const container of removedContainers) {
const userContextId = normalizeUserContextId(container.userContextId);
if (userContextId === null) {
console.warn(
"ZenSyncManager: Ignoring container removal with invalid userContextId",
{ container }
);
continue;
}
if (!localContainersById.has(userContextId)) {
continue;
}
try {
lazy.ContextualIdentityService.remove(userContextId);
localContainersById.delete(userContextId);
} catch {
// Container may already be gone locally.
}
}
}
}
export const ZenSyncStore = new ZenSyncManager();

View File

@@ -0,0 +1,404 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import {
Store,
SyncEngine,
Tracker,
} from "resource://services-sync/engines.sys.mjs";
import { CryptoWrapper } from "resource://services-sync/record.sys.mjs";
import { SCORE_INCREMENT_XLARGE } from "resource://services-sync/constants.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZenSyncStore: "resource:///modules/zen/ZenSyncManager.sys.mjs",
ContextualIdentityService:
"resource://gre/modules/ContextualIdentityService.sys.mjs",
});
const RECORD_ID_PREFIX_BY_TYPE = Object.freeze({
space: "s",
container: "c",
});
const RECORD_TYPE_BY_PREFIX = Object.freeze({
s: "space",
c: "container",
});
/**
* Sync record wrapper for workspace and container items stored in the
* Workspaces engine collection.
*/
export class ZenWorkspacesRecord extends CryptoWrapper {
_logName = "Sync.Record.ZenWorkspaces";
}
ZenWorkspacesRecord.prototype.type = "workspaces";
function parseRecordId(id) {
const sep = id.indexOf("~");
if (sep === -1) {
return null;
}
const prefix = id.slice(0, sep);
const key = id.slice(sep + 1);
return { type: RECORD_TYPE_BY_PREFIX[prefix] || prefix, key };
}
function createRecordId(type, id) {
const prefix = RECORD_ID_PREFIX_BY_TYPE[type];
if (!prefix) {
throw new Error(`Unknown Workspaces Sync record type: ${type}`);
}
return `${prefix}~${id}`;
}
function normalizeUserContextId(value) {
const normalized = typeof value === "string" ? Number(value) : value;
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
return null;
}
return normalized;
}
/**
* Strips the sync-envelope fields (`id` and `type`) from incoming record data
* and restores the item's real identity key where needed
*
* @param {object} data
*/
function stripSyncFields(data) {
const rest = { ...data };
delete rest.id;
delete rest.type;
return rest;
}
/**
* Sync store implementation that serializes local workspace and container
* state into records and applies incoming remote changes.
*/
class ZenWorkspacesStore extends Store {
constructor(name, engine) {
super(name, engine);
}
async getAllIDs() {
const ids = {};
const sidebar = lazy.ZenSyncStore.getSidebarData();
for (const space of sidebar.spaces || []) {
if (space.uuid) {
ids[createRecordId("space", space.uuid)] = true;
}
}
for (const c of lazy.ContextualIdentityService.getPublicIdentities()) {
ids[createRecordId("container", c.userContextId)] = true;
}
return ids;
}
async itemExists(id) {
const parsed = parseRecordId(id);
if (!parsed) {
return false;
}
const sidebar = lazy.ZenSyncStore.getSidebarData();
switch (parsed.type) {
case "space":
return (sidebar.spaces || []).some(s => s.uuid === parsed.key);
case "container":
return lazy.ContextualIdentityService.getPublicIdentities().some(
c => String(c.userContextId) === parsed.key
);
default:
return false;
}
}
async createRecord(id, collection) {
const record = new ZenWorkspacesRecord(collection, id);
const parsed = parseRecordId(id);
if (!parsed) {
record.deleted = true;
return record;
}
const sidebar = lazy.ZenSyncStore.getSidebarData();
switch (parsed.type) {
case "space": {
const spaces = sidebar.spaces || [];
const idx = spaces.findIndex(s => s.uuid === parsed.key);
if (idx === -1) {
record.deleted = true;
return record;
}
const rest = { ...spaces[idx] };
delete rest.syncStatus;
record.cleartext = { id, type: "space", ...rest, position: idx };
break;
}
case "container": {
const container =
lazy.ContextualIdentityService.getPublicIdentities().find(
c => String(c.userContextId) === parsed.key
);
if (!container) {
record.deleted = true;
return record;
}
record.cleartext = {
id,
type: "container",
userContextId: container.userContextId,
name: container.name,
icon: container.icon,
color: container.color,
};
break;
}
default:
record.deleted = true;
}
return record;
}
async applyIncomingBatch(records, _countTelemetry) {
const pulled = { spaces: [], containers: [] };
const removals = { spaces: [], containers: [] };
for (const record of records) {
if (record.deleted) {
this._collectRemoval(record.id, removals);
continue;
}
const data = record.cleartext;
if (!data?.type) {
continue;
}
const clean = stripSyncFields(data);
switch (data.type) {
case "space":
pulled.spaces.push(clean);
break;
case "container":
pulled.containers.push(clean);
break;
}
}
// Suppress change tracking while applying incoming data to prevent
// feedback loops where applied items get re-uploaded immediately.
this.engine._tracker.ignoreAll = true;
try {
await lazy.ZenSyncStore.applyIncomingBatch(pulled, removals);
} finally {
this.engine._tracker.ignoreAll = false;
}
return [];
}
_collectRemoval(id, removals) {
const parsed = parseRecordId(id);
if (!parsed) {
return;
}
switch (parsed.type) {
case "space":
removals.spaces.push({ uuid: parsed.key });
break;
case "container": {
const userContextId = normalizeUserContextId(parsed.key);
if (userContextId === null) {
console.warn(
"ZenWorkspacesStore: Ignoring container removal with invalid userContextId",
{ id }
);
break;
}
removals.containers.push({ userContextId });
break;
}
}
}
async create(record) {
await this._applySingle(record);
}
async update(record) {
await this._applySingle(record);
}
async _applySingle(record) {
this.engine._tracker.ignoreAll = true;
try {
if (record.deleted) {
const removals = { spaces: [], containers: [] };
this._collectRemoval(record.id, removals);
await lazy.ZenSyncStore.applyIncomingBatch(
{ spaces: [], containers: [] },
removals
);
return;
}
const data = record.cleartext;
if (!data?.type) {
return;
}
const clean = stripSyncFields(data);
const pulled = { spaces: [], containers: [] };
switch (data.type) {
case "space":
pulled.spaces.push(clean);
break;
case "container":
pulled.containers.push(clean);
break;
}
await lazy.ZenSyncStore.applyIncomingBatch(pulled, {
spaces: [],
containers: [],
});
} finally {
this.engine._tracker.ignoreAll = false;
}
}
async remove() {
// No-op: never delete user data on wipe
}
async wipe() {
// No-op: never delete user data on wipe
}
changeItemID() {
// No-op
}
}
/**
* Sync tracker that watches workspace and contextual identity observers and
* marks the corresponding record IDs as changed.
*/
class ZenWorkspacesTracker extends Tracker {
#changedIDs = {};
#ignoreAll = false;
get ignoreAll() {
return this.#ignoreAll;
}
set ignoreAll(value) {
this.#ignoreAll = value;
}
onStart() {
Services.obs.addObserver(this, "zen-workspace-item-changed");
Services.obs.addObserver(this, "contextual-identity-created");
Services.obs.addObserver(this, "contextual-identity-updated");
Services.obs.addObserver(this, "contextual-identity-deleted");
}
onStop() {
Services.obs.removeObserver(this, "zen-workspace-item-changed");
Services.obs.removeObserver(this, "contextual-identity-created");
Services.obs.removeObserver(this, "contextual-identity-updated");
Services.obs.removeObserver(this, "contextual-identity-deleted");
}
observe(subject, topic, _data) {
if (this.#ignoreAll) {
return;
}
if (topic === "zen-workspace-item-changed") {
const type = subject?.wrappedJSObject?.type;
const id = subject?.wrappedJSObject?.id;
if (type && id) {
this._trackChange({ type, id });
}
} else if (topic.startsWith("contextual-identity-")) {
const id = subject?.wrappedJSObject?.userContextId;
if (id && normalizeUserContextId(id) !== null) {
this._trackChange({ type: "container", id });
}
}
}
_trackChange(data) {
if (data.type && data.id) {
const id = createRecordId(data.type, data.id);
this.#changedIDs[id] = Date.now() / 1000;
this.score += SCORE_INCREMENT_XLARGE;
}
}
async getChangedIDs() {
return { ...this.#changedIDs };
}
async addChangedID(id, when) {
this.#changedIDs[id] = when;
return true;
}
async removeChangedID(...ids) {
for (const id of ids) {
delete this.#changedIDs[id];
}
return true;
}
clearChangedIDs() {
this.#changedIDs = {};
}
}
/**
* Sync engine entrypoint that wires the Workspaces record, store, and tracker
* implementations into Firefox Sync.
*/
export class ZenWorkspacesEngine extends SyncEngine {
static get name() {
return "Workspaces";
}
constructor(service) {
super("Workspaces", service);
}
get _storeObj() {
return ZenWorkspacesStore;
}
get _trackerObj() {
return ZenWorkspacesTracker;
}
get _recordObj() {
return ZenWorkspacesRecord;
}
get version() {
return 2;
}
get syncPriority() {
return 8;
}
get allowSkippedRecord() {
return false;
}
}

8
src/zen/sync/moz.build Normal file
View File

@@ -0,0 +1,8 @@
# 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/.
EXTRA_JS_MODULES.zen += [
"ZenSyncManager.sys.mjs",
"ZenWorkspacesSync.sys.mjs",
]

View File

@@ -246,6 +246,66 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
gZenUIManager.showToast("zen-pinned-tab-replaced");
}
async editPinnedUrl(tab = undefined) {
tab ??= TabContextMenu.contextTab;
if (!tab || !tab.pinned) {
return;
}
const initialUrl =
tab._zenPinnedInitialState?.entry?.url ||
tab.linkedBrowser?.currentURI?.spec;
const [title, label] = await document.l10n.formatValues([
{ id: "zen-pinned-tab-edit-url-title" },
{ id: "zen-pinned-tab-edit-url-label" },
]);
const result = { value: initialUrl ?? "" };
const confirmed = Services.prompt.prompt(
window,
title,
label,
result,
null,
{ value: false }
);
if (!confirmed) {
return;
}
let uri;
try {
uri = Services.uriFixup.getFixupURIInfo(
result.value.trim(),
Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS
).preferredURI;
} catch (_) {}
if (!uri) {
gZenUIManager.showToast("zen-pinned-tab-url-invalid");
return;
}
const url = uri.spec;
// Skip when the value wasn't actually changed from what was prefilled.
if (!url || url === initialUrl) {
return;
}
const image = tab.zenStaticIcon || (await this.#getCachedFavicon(uri));
window.gZenWindowSync.setPinnedUrl(tab, url, image);
this.#resetTabToStoredState(tab);
gZenUIManager.showToast("zen-pinned-tab-url-edited");
}
async #getCachedFavicon(uri) {
try {
const favicon = await PlacesUtils.favicons.getFaviconForPage(uri);
return favicon?.dataURI?.spec;
} catch (ex) {
console.error("Failed to get favicon for edited pinned url:", ex);
return null;
}
}
_initClosePinnedTabShortcut() {
let cmdClose = document.getElementById("cmd_close");
@@ -545,11 +605,20 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
const elements = window.MozXULElement.parseXULToFragment(`
<menuseparator id="context_zen-pinned-tab-separator" hidden="true"/>
<menuitem id="context_zen-replace-pinned-url-with-current"
data-lazy-l10n-id="tab-context-zen-replace-pinned-url-with-current"
data-l10n-args="{&quot;isEssential&quot;:&quot;&quot;}"
hidden="true"
command="cmd_zenReplacePinnedUrlWithCurrent"/>
<menu id="context_zen-edit-pinned-page"
data-lazy-l10n-id="tab-context-zen-edit-pinned-page"
data-l10n-args="{&quot;isEssential&quot;:&quot;&quot;}"
hidden="true">
<menupopup>
<menuitem id="context_zen-replace-pinned-url-with-current"
data-lazy-l10n-id="tab-context-zen-replace-pinned-url-with-current"
data-l10n-args="{&quot;isEssential&quot;:&quot;&quot;}"
command="cmd_zenReplacePinnedUrlWithCurrent"/>
<menuitem id="context_zen-edit-pinned-url"
data-lazy-l10n-id="tab-context-zen-edit-pinned-url"
command="cmd_zenEditPinnedUrl"/>
</menupopup>
</menu>
<menuitem id="context_zen-reset-pinned-tab"
data-lazy-l10n-id="tab-context-zen-reset-pinned-tab"
data-l10n-args="{&quot;isEssential&quot;:&quot;&quot;}"
@@ -619,15 +688,24 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
const zenResetPinnedTab = document.getElementById(
"context_zen-reset-pinned-tab"
);
const zenEditPinnedPage = document.getElementById(
"context_zen-edit-pinned-page"
);
const zenReplacePinnedUrl = document.getElementById(
"context_zen-replace-pinned-url-with-current"
);
[zenResetPinnedTab, zenReplacePinnedUrl].forEach(element => {
[zenResetPinnedTab, zenEditPinnedPage].forEach(element => {
if (element) {
element.hidden = !isVisible;
document.l10n.setArgs(element, { isEssential });
}
});
[zenResetPinnedTab, zenEditPinnedPage, zenReplacePinnedUrl].forEach(
element => {
if (element) {
document.l10n.setArgs(element, { isEssential });
}
}
);
zenAddEssential.hidden = isEssential || !!contextTab.group;
document.l10n
.formatValue("tab-context-zen-add-essential-badge", {
@@ -857,7 +935,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
) {
return;
}
// Remove # and ? from the URL
// Remove # from the URL
const pinUrl = tab._zenPinnedInitialState.entry.url.split("#")[0];
const currentUrl = location.split("#")[0];
// Add an indicator that the pin has been changed
@@ -897,10 +975,14 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
} else {
tab.setAttribute("zen-pinned-changed", "true");
}
tab.style.setProperty(
"--zen-original-tab-icon",
`url(${tab._zenPinnedInitialState.image})`
);
if (tab._zenPinnedInitialState.image) {
tab.style.setProperty(
"--zen-original-tab-icon",
`url(${tab._zenPinnedInitialState.image})`
);
} else {
tab.style.removeProperty("--zen-original-tab-icon");
}
}
removeTabContainersDragoverClass(hideIndicator = true) {

View File

@@ -235,7 +235,7 @@
}
@media (-moz-platform: macos) {
--border-radius-medium: 14px;
--border-radius-medium: 12px;
--tab-border-radius: 8px;
}
@@ -1156,10 +1156,6 @@
pointer-events: none;
position: fixed; /* Fix position to prevent scrolling */
}
&[cloned] {
pointer-events: none;
}
}
.tabbrowser-tab[zen-essential="true"],
@@ -1232,8 +1228,6 @@
background: var(--zen-essential-tab-selected-bg);
margin: var(--zen-essential-bg-margin);
border-radius: calc(var(--border-radius-medium) - var(--zen-essential-bg-margin));
/* stylelint-disable-next-line property-no-unknown */
corner-shape: var(--zen-squircle-value);
position: absolute;
inset: 0;
z-index: 0;

View File

@@ -13,6 +13,8 @@ prefs = ["zen.workspaces.separate-essentials=false"]
["browser_pinned_created.js"]
["browser_pinned_edit_url.js"]
["browser_pinned_nounload_reset.js"]
["browser_pinned_reset_button.js"]

View File

@@ -0,0 +1,384 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
async function pinTab(url) {
const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
gBrowser.pinTab(tab);
await gBrowser.TabStateFlusher.flush(tab.linkedBrowser);
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise(r => setTimeout(r, 500));
return tab;
}
// XPCOM service methods can't be stubbed in place (non-configurable), so we
// swap the whole service object out for a mock and restore it afterwards.
function mockPrompt(value) {
const original = Services.prompt;
Services.prompt = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIPromptService]),
prompt(win, title, label, result) {
if (value === null) {
return false; // user cancelled
}
result.value = value;
return true;
},
};
return () => {
Services.prompt = original;
};
}
function mockFavicons(faviconSpec) {
const original = PlacesUtils.favicons;
const mock = {
callCount: 0,
defaultFavicon: { spec: "data:image/png;base64,DEFAULT" },
getFaviconForPage() {
mock.callCount++;
return Promise.resolve(
faviconSpec ? { dataURI: { spec: faviconSpec } } : null
);
},
};
PlacesUtils.favicons = mock;
return {
mock,
restore: () => {
PlacesUtils.favicons = original;
},
};
}
add_task(async function test_EditPinnedUrl_SurvivesRebuild() {
// Pinned tab at url1 (loaded), then select a different tab (unfocus it).
const tab = await pinTab("https://example.com/1");
const other = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"https://example.com/other"
);
const editedUrl = "https://example.com/edited";
const restorePrompt = mockPrompt(editedUrl);
const favicons = mockFavicons("data:image/png;base64,iVBORw0KGgo=");
try {
await gZenPinnedTabManager.editPinnedUrl(tab);
// Close + re-open rebuilds the tab: the in-memory _zenPinnedInitialState is
// gone and gets reconstructed from the persisted session via
// setPinnedTabState (exactly what #onSessionStoreInitialized does).
delete tab._zenPinnedInitialState;
await window.gZenWindowSync.setPinnedTabState(tab);
Assert.equal(
tab._zenPinnedInitialState.entry.url,
editedUrl,
"After the tab is rebuilt, the pinned URL should still be the edited one"
);
} finally {
restorePrompt();
favicons.restore();
await BrowserTestUtils.removeTab(other);
await BrowserTestUtils.removeTab(tab);
}
});
add_task(async function test_EditPinnedUrl_ActiveTabNavigates() {
// Editing the active (focused) pinned tab applies the new URL immediately:
// the live tab navigates to it (matching Arc's behavior).
const tab = await pinTab("https://example.com/1");
Assert.equal(gBrowser.selectedTab, tab, "the pinned tab should be active");
const editedUrl = "https://example.com/edited";
const restorePrompt = mockPrompt(editedUrl);
const favicons = mockFavicons("data:image/png;base64,iVBORw0KGgo=");
try {
await gZenPinnedTabManager.editPinnedUrl(tab);
await BrowserTestUtils.waitForCondition(
() => tab.linkedBrowser.currentURI.spec === editedUrl,
"the active pinned tab to navigate to the edited URL"
);
Assert.equal(
tab.linkedBrowser.currentURI.spec,
editedUrl,
"Editing the active pinned tab should navigate it to the new URL"
);
} finally {
restorePrompt();
favicons.restore();
await BrowserTestUtils.removeTab(tab);
}
});
add_task(
async function test_EditPinnedUrl_FaviconLookupErrorLeavesImageEmpty() {
const tab = await pinTab("https://example.com/1");
const restorePrompt = mockPrompt("https://example.org/edited");
const favicons = mockFavicons(null);
// Simulate a Places DB failure so #getCachedFavicon hits its catch branch.
favicons.mock.getFaviconForPage = () =>
Promise.reject(new Error("simulated favicon DB failure"));
try {
await gZenPinnedTabManager.editPinnedUrl(tab);
Assert.equal(
tab._zenPinnedInitialState.entry.url,
"https://example.org/edited",
"The URL should still be updated when the favicon lookup fails"
);
ok(
!tab._zenPinnedInitialState.image,
"The image should be left empty (populated by the next navigation)"
);
} finally {
restorePrompt();
favicons.restore();
await BrowserTestUtils.removeTab(tab);
}
}
);
add_task(async function test_EditPinnedUrl_UpdatesUrlAndFavicon() {
const tab = await pinTab("https://example.com/1");
const faviconSpec = "data:image/png;base64,iVBORw0KGgo=";
const restorePrompt = mockPrompt("https://example.org/edited");
const favicons = mockFavicons(faviconSpec);
try {
await gZenPinnedTabManager.editPinnedUrl(tab);
Assert.equal(
tab._zenPinnedInitialState.entry.url,
"https://example.org/edited",
"The pinned URL should be updated to the edited value"
);
Assert.equal(
tab._zenPinnedInitialState.image,
faviconSpec,
"The stored icon should be the cached favicon for the new URL"
);
} finally {
restorePrompt();
favicons.restore();
await BrowserTestUtils.removeTab(tab);
}
});
add_task(async function test_EditPinnedUrl_NoCachedFaviconLeavesImageEmpty() {
const tab = await pinTab("https://example.com/1");
const restorePrompt = mockPrompt("https://example.org/edited");
const favicons = mockFavicons(null); // no cached favicon for the new URL
try {
await gZenPinnedTabManager.editPinnedUrl(tab);
ok(
!tab._zenPinnedInitialState.image,
"Without a cached favicon the image is left empty, not the default; the " +
"next navigation captures the real icon"
);
} finally {
restorePrompt();
favicons.restore();
await BrowserTestUtils.removeTab(tab);
}
});
add_task(async function test_EditPinnedUrl_ClearsStaleTitle() {
const tab = await pinTab("https://example.com/1");
const restorePrompt = mockPrompt("https://example.org/edited");
const favicons = mockFavicons("data:image/png;base64,iVBORw0KGgo=");
try {
await gZenPinnedTabManager.editPinnedUrl(tab);
ok(
!tab._zenPinnedInitialState.entry.title,
"The previous title is cleared so the new page's title is used on load"
);
} finally {
restorePrompt();
favicons.restore();
await BrowserTestUtils.removeTab(tab);
}
});
add_task(async function test_EditPinnedUrl_KeepsCustomLabel() {
const tab = await pinTab("https://example.com/1");
tab.zenStaticLabel = "My Pinned Tab";
const restorePrompt = mockPrompt("https://example.org/edited");
const favicons = mockFavicons("data:image/png;base64,iVBORw0KGgo=");
try {
await gZenPinnedTabManager.editPinnedUrl(tab);
Assert.equal(
tab._zenPinnedInitialState.entry.title,
"My Pinned Tab",
"An explicit custom label is preserved across a URL edit"
);
} finally {
restorePrompt();
favicons.restore();
delete tab.zenStaticLabel;
await BrowserTestUtils.removeTab(tab);
}
});
add_task(async function test_EditPinnedUrl_KeepsCustomIcon() {
const tab = await pinTab("https://example.com/1");
const customIcon = "data:image/svg+xml,custom-icon";
tab.zenStaticIcon = customIcon;
const restorePrompt = mockPrompt("https://example.org/edited");
const favicons = mockFavicons("data:image/png;base64,iVBORw0KGgo=");
try {
await gZenPinnedTabManager.editPinnedUrl(tab);
Assert.equal(
tab._zenPinnedInitialState.entry.url,
"https://example.org/edited",
"The pinned URL should still be updated when a custom icon is set"
);
Assert.equal(
tab._zenPinnedInitialState.image,
customIcon,
"A user-set custom icon should be preserved, not overridden by a favicon"
);
Assert.equal(
favicons.mock.callCount,
0,
"Favicon lookup should be skipped when a custom icon is set"
);
} finally {
restorePrompt();
favicons.restore();
delete tab.zenStaticIcon;
await BrowserTestUtils.removeTab(tab);
}
});
add_task(async function test_EditPinnedUrl_InvalidUrlKeepsState() {
const tab = await pinTab("https://example.com/1");
const originalUrl = tab._zenPinnedInitialState.entry.url;
const restorePrompt = mockPrompt(" "); // whitespace only -> not a valid URL
const favicons = mockFavicons("data:image/png;base64,iVBORw0KGgo=");
try {
await gZenPinnedTabManager.editPinnedUrl(tab);
Assert.equal(
tab._zenPinnedInitialState.entry.url,
originalUrl,
"The pinned URL should be unchanged for invalid input"
);
ok(
!tab.hasAttribute("zen-pinned-changed"),
"The tab should not be marked as changed for invalid input"
);
} finally {
restorePrompt();
favicons.restore();
await BrowserTestUtils.removeTab(tab);
}
});
add_task(async function test_EditPinnedUrl_CancelKeepsState() {
const tab = await pinTab("https://example.com/1");
const originalUrl = tab._zenPinnedInitialState.entry.url;
const restorePrompt = mockPrompt(null); // user cancels the dialog
try {
await gZenPinnedTabManager.editPinnedUrl(tab);
Assert.equal(
tab._zenPinnedInitialState.entry.url,
originalUrl,
"The pinned URL should be unchanged when the dialog is cancelled"
);
} finally {
restorePrompt();
await BrowserTestUtils.removeTab(tab);
}
});
add_task(async function test_EditPinnedUrl_FixesSchemeTypo() {
const tab = await pinTab("https://example.com/1");
const restorePrompt = mockPrompt("htps://example.org/typo");
const favicons = mockFavicons("data:image/png;base64,iVBORw0KGgo=");
try {
await gZenPinnedTabManager.editPinnedUrl(tab);
Assert.equal(
tab._zenPinnedInitialState.entry.url,
"https://example.org/typo",
"A mistyped scheme (htps://) should be auto-fixed to https://"
);
} finally {
restorePrompt();
favicons.restore();
await BrowserTestUtils.removeTab(tab);
}
});
add_task(async function test_EditPinnedUrl_AddsMissingScheme() {
const tab = await pinTab("https://example.com/1");
const restorePrompt = mockPrompt("example.org/no-scheme");
const favicons = mockFavicons("data:image/png;base64,iVBORw0KGgo=");
try {
await gZenPinnedTabManager.editPinnedUrl(tab);
const stored = tab._zenPinnedInitialState.entry.url;
ok(
/^https?:\/\//.test(stored),
`A scheme should be prepended when omitted (got "${stored}")`
);
ok(
stored.endsWith("example.org/no-scheme"),
`Host and path should be preserved (got "${stored}")`
);
} finally {
restorePrompt();
favicons.restore();
await BrowserTestUtils.removeTab(tab);
}
});
add_task(async function test_EditPinnedUrl_PrefillsWithStoredUrl() {
const tab = await pinTab("https://example.com/1");
// The stored pinned URL differs from the live browser URL (e.g. it was pinned
// as http but the server redirected the tab to https).
tab._zenPinnedInitialState = {
entry: { url: "http://example.com/pinned" },
image: "",
};
let prefilled;
const originalPrompt = Services.prompt;
Services.prompt = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIPromptService]),
prompt(win, title, label, result) {
prefilled = result.value;
return false; // cancel, we only care about the prefilled value
},
};
try {
await gZenPinnedTabManager.editPinnedUrl(tab);
Assert.equal(
prefilled,
"http://example.com/pinned",
"The edit dialog should prefill with the stored pinned URL, not the live browser URL"
);
} finally {
Services.prompt = originalPrompt;
await BrowserTestUtils.removeTab(tab);
}
});

View File

@@ -27,6 +27,10 @@ ChromeUtils.defineESModuleGetters(lazy, {
UrlUtils: "resource://gre/modules/UrlUtils.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "l10n", () => {
return new Localization(["browser/zen-command-palette.ftl"], true);
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"enabledPref",
@@ -182,7 +186,7 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider {
.workspaceElement(workspace.uuid)
?.style.getPropertyValue("--zen-primary-color");
actions.push({
label: "Focus on",
label: lazy.l10n.formatValueSync("zen-action-focus-on"),
extraPayload: {
workspaceId: workspace.uuid,
prettyName: workspace.name,
@@ -215,7 +219,7 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider {
.map(addon => {
return {
icon: "chrome://browser/skin/zen-icons/extension.svg",
label: "Extension",
label: lazy.l10n.formatValueSync("zen-action-extension"),
commandId: `zen:extension-${addon.id}`,
extraPayload: {
extensionId: addon.id,

View File

@@ -13,58 +13,62 @@ XPCOMUtils.defineLazyPreferenceGetter(
2
);
ChromeUtils.defineLazyGetter(lazy, "l10n", () => {
return new Localization(["browser/zen-command-palette.ftl"], true);
});
function isNotEmptyTab(window) {
return !window.gBrowser.selectedTab.hasAttribute("zen-empty-tab");
}
const globalActionsTemplate = [
{
label: "Toggle Compact Mode",
l10nId: "zen-action-toggle-compact-mode",
command: "cmd_zenCompactModeToggle",
icon: "chrome://browser/skin/zen-icons/sidebar.svg",
},
{
label: "Open Theme Picker",
l10nId: "zen-action-open-theme-picker",
command: "cmd_zenOpenZenThemePicker",
icon: "chrome://browser/skin/zen-icons/edit-theme.svg",
},
{
label: "New Split View",
l10nId: "zen-action-new-split-view",
command: "cmd_zenNewEmptySplit",
icon: "chrome://browser/skin/zen-icons/split.svg",
},
{
label: "New Folder",
l10nId: "zen-action-new-folder",
command: "cmd_zenOpenFolderCreation",
icon: "chrome://browser/skin/zen-icons/folder.svg",
},
{
label: "Copy Current URL",
l10nId: "zen-action-copy-current-url",
command: "cmd_zenCopyCurrentURL",
icon: "chrome://browser/skin/zen-icons/link.svg",
},
{
label: "Settings",
l10nId: "zen-action-settings",
command: window => window.openPreferences(),
icon: "chrome://browser/skin/zen-icons/settings.svg",
},
{
label: "Open Private Window",
l10nId: "zen-action-open-private-window",
command: "Tools:PrivateBrowsing",
icon: "chrome://browser/skin/zen-icons/private-window.svg",
},
{
label: "Open New Window",
l10nId: "zen-action-open-new-window",
command: "cmd_newNavigator",
icon: "chrome://browser/skin/zen-icons/window.svg",
},
{
label: "New Blank Window",
l10nId: "zen-action-new-blank-window",
command: "cmd_zenNewNavigatorUnsynced",
icon: "chrome://browser/skin/zen-icons/window.svg",
},
{
label: "Pin Tab",
l10nId: "zen-action-pin-tab",
command: "cmd_zenTogglePinTab",
icon: "chrome://browser/skin/zen-icons/pin.svg",
isAvailable: window => {
@@ -73,7 +77,7 @@ const globalActionsTemplate = [
},
},
{
label: "Unpin Tab",
l10nId: "zen-action-unpin-tab",
command: "cmd_zenTogglePinTab",
icon: "chrome://browser/skin/zen-icons/unpin.svg",
isAvailable: window => {
@@ -82,12 +86,12 @@ const globalActionsTemplate = [
},
},
{
label: "Open Space Routing",
l10nId: "zen-action-open-space-routing",
command: "cmd_zenOpenSpaceRoutingSettings",
icon: "chrome://browser/skin/zen-icons/selectable/airplane.svg",
},
{
label: "New Boost",
l10nId: "zen-action-new-boost",
icon: "chrome://browser/skin/zen-icons/boost.svg",
isAvailable: window => {
if (!isNotEmptyTab(window)) {
@@ -130,7 +134,7 @@ const globalActionsTemplate = [
},
},
{
label: "Next Space",
l10nId: "zen-action-next-space",
command: "cmd_zenWorkspaceForward",
icon: "chrome://browser/skin/zen-icons/forward.svg",
isAvailable: window => {
@@ -138,7 +142,7 @@ const globalActionsTemplate = [
},
},
{
label: "Previous Space",
l10nId: "zen-action-previous-space",
command: "cmd_zenWorkspaceBackward",
icon: "chrome://browser/skin/zen-icons/back.svg",
isAvailable: window => {
@@ -147,7 +151,7 @@ const globalActionsTemplate = [
},
},
{
label: "Close Tab",
l10nId: "zen-action-close-tab",
command: "cmd_close",
icon: "chrome://browser/skin/zen-icons/close.svg",
isAvailable: window => {
@@ -155,27 +159,27 @@ const globalActionsTemplate = [
},
},
{
label: "Reload Tab",
l10nId: "zen-action-reload-tab",
command: "Browser:Reload",
icon: "chrome://browser/skin/zen-icons/reload.svg",
},
{
label: "Reload Tab Without Cache",
l10nId: "zen-action-reload-tab-without-cache",
command: "Browser:ReloadSkipCache",
icon: "chrome://browser/skin/zen-icons/reload.svg",
},
{
label: "Next Tab",
l10nId: "zen-action-next-tab",
command: "Browser:NextTab",
icon: "chrome://browser/skin/zen-icons/forward.svg",
},
{
label: "Previous Tab",
l10nId: "zen-action-previous-tab",
command: "Browser:PrevTab",
icon: "chrome://browser/skin/zen-icons/back.svg",
},
{
label: "Capture Screenshot",
l10nId: "zen-action-capture-screenshot",
command: "Browser:Screenshot",
icon: "chrome://browser/skin/zen-icons/screenshot.svg",
isAvailable: window => {
@@ -183,12 +187,12 @@ const globalActionsTemplate = [
},
},
{
label: "Toggle Tabs on right",
l10nId: "zen-action-toggle-tabs-on-right",
command: "cmd_zenToggleTabsOnRight",
icon: "chrome://browser/skin/zen-icons/sidebars-right.svg",
},
{
label: "Add to Essentials",
l10nId: "zen-action-add-to-essentials",
command: window =>
window.gZenPinnedTabManager.addToEssentials(window.gBrowser.selectedTab),
isAvailable: window => {
@@ -201,7 +205,7 @@ const globalActionsTemplate = [
icon: "chrome://browser/skin/zen-icons/essential-add.svg",
},
{
label: "Remove from Essentials",
l10nId: "zen-action-remove-from-essentials",
command: window =>
window.gZenPinnedTabManager.removeEssentials(window.gBrowser.selectedTab),
isAvailable: window =>
@@ -209,7 +213,7 @@ const globalActionsTemplate = [
icon: "chrome://browser/skin/zen-icons/essential-remove.svg",
},
{
label: "Find in Page",
l10nId: "zen-action-find-in-page",
command: "cmd_find",
icon: "chrome://browser/skin/zen-icons/search-page.svg",
isAvailable: window => {
@@ -217,12 +221,12 @@ const globalActionsTemplate = [
},
},
{
label: "Manage Extensions",
l10nId: "zen-action-manage-extensions",
command: "Tools:Addons",
icon: "chrome://browser/skin/zen-icons/extension.svg",
},
{
label: "Switch to Automatic Appearance",
l10nId: "zen-action-switch-to-automatic-appearance",
command: () => Services.prefs.setIntPref("zen.view.window.scheme", 2),
icon: "chrome://browser/skin/zen-icons/sparkles.svg",
isAvailable: () => {
@@ -230,7 +234,7 @@ const globalActionsTemplate = [
},
},
{
label: "Switch to Light Mode",
l10nId: "zen-action-switch-to-light-mode",
command: () => Services.prefs.setIntPref("zen.view.window.scheme", 1),
icon: "chrome://browser/skin/zen-icons/face-sun.svg",
isAvailable: () => {
@@ -238,7 +242,7 @@ const globalActionsTemplate = [
},
},
{
label: "Switch to Dark Mode",
l10nId: "zen-action-switch-to-dark-mode",
command: () => Services.prefs.setIntPref("zen.view.window.scheme", 0),
icon: "chrome://browser/skin/zen-icons/moon-stars.svg",
isAvailable: () => {
@@ -246,7 +250,7 @@ const globalActionsTemplate = [
},
},
{
label: "Print",
l10nId: "zen-action-print",
command: "cmd_print",
icon: "chrome://browser/skin/zen-icons/print.svg",
isAvailable: window => {
@@ -266,7 +270,10 @@ export const globalActions = globalActionsTemplate.map(action => ({
commandId:
typeof action.command === "string"
? action.command
: `zen:global-action-${action.label.toLowerCase().replace(/\s+/g, "-")}`,
: `zen:global-action-${action.l10nId.replace("zen-action-", "")}`,
extraPayload: {},
...action,
get label() {
return lazy.l10n.formatValueSync(action.l10nId);
},
}));

View File

@@ -5,8 +5,8 @@
"binaryName": "zen",
"version": {
"product": "firefox",
"version": "152.0.1",
"candidate": "152.0.1",
"version": "152.0.5",
"candidate": "152.0.5",
"candidateBuild": 1
},
"buildOptions": {
@@ -20,7 +20,7 @@
"brandShortName": "Zen",
"brandFullName": "Zen Browser",
"release": {
"displayVersion": "1.21.3b",
"displayVersion": "1.21.6b",
"github": {
"repo": "zen-browser/desktop"
},
@@ -54,4 +54,4 @@
"licenseType": "MPL-2.0"
},
"updateHostname": "updates.zen-browser.app"
}
}

View File

@@ -346,6 +346,17 @@ fn expand_pref_values(prefs: &mut [Preference]) {
}
pref.value = new_value.clone();
}
// Also change the condition if it contains placeholders
if let Some(condition) = &pref.condition {
let mut new_condition = condition.clone();
for (key, value) in &env_values {
let placeholder = format!("@{}@", key);
if new_condition.contains(&placeholder) {
new_condition = new_condition.replace(&placeholder, if *value { "1" } else { "0" });
}
pref.condition = Some(new_condition.clone());
}
}
}
}