Compare commits

..

26 Commits

Author SHA1 Message Date
doruk
22961e97d8 gh-15363: Fix boost font shuffle off-by-one index (gh-15365) 2026-09-11 14:35:24 +02:00
mr. m
cb6a607c67 gh-15362: Prevent window captions appearing as squircles (gh-15364) 2026-09-11 14:25:26 +02:00
mr. m
267c9aabe1 gh-15353: Prevent popups from being too rounded on linux (gh-15354) 2026-09-11 09:37:47 +02:00
doruk
d54e12359c gh-15355: Fix folder unload TypeError by using String.includes (gh-15347) 2026-09-11 09:37:37 +02:00
mr. m
c89645a787 gh-15344: Fixed restoring tabs with static icons being cleared (gh-15345) 2026-09-11 01:23:58 +02:00
mr. m
0a548c7572 gh-15329: Prevent split view preview from opening for multiple tabs (gh-15340) 2026-09-10 22:20:12 +02:00
mr. m
6d158223b5 gh-15123: Fixed not spawining any native messaging host (gh-15328) 2026-09-10 10:47:35 +02:00
mr. m
76fcd5c822 gh-15026: Revert "Improve scrolling performance" (gh-15327) 2026-09-10 10:32:48 +02:00
Brandon Quon
ed6900d762 gh-14938: Fix CWLO for only pinned/essential tabs (gh-15324) 2026-09-10 10:27:53 +02:00
mr. m
2e9aecd41b gh-6302: Remove corner shape for browser stack rendering (gh-15318) 2026-09-09 19:35:46 +02:00
mr. m
da995e8653 gh-15313: Make sure to bootstrap before doing release builds (gh-15314) 2026-09-09 13:15:17 +02:00
mr. m
64d73ab3e3 gh-14956: Check if the closing tab exists on the tab list before switching (gh-15312) 2026-09-09 13:13:40 +02:00
Brandon Quon
9551444c61 gh-15260: Fix essentials appearing during workspace creation (gh-15307)
Co-authored-by: mr. m <mr.m@tuta.com>
2026-09-09 08:34:59 +02:00
mr. m
fdf9588b7b gh-15026: Improve scrolling performance (gh-15306) 2026-09-09 02:13:38 +02:00
mr. m
b07b17c87e gh-15304: Revert urlbar when searching with an alias (gh-15305) 2026-09-09 01:39:13 +02:00
mr. m
c9d3cb3dc9 gh-15297: Fixed synced tabs losing their icon when they are unloaded (gh-15298) 2026-09-08 20:14:23 +02:00
KiKaraage
2c030369c5 gh-15288: add missing margin for space selector in BMPanel (gh-15286)
Signed-off-by: KiKaraage <kikaraage@tuta.io>
2026-09-08 18:48:07 +02:00
Brandon Quon
88a90206ce gh-14755: fix duplicated owned tabs leaving folders (gh-15285) 2026-09-08 18:24:10 +02:00
Andrey Bochkarev
6d398dec63 gh-15295: Correct tab index calculation for CWLO (gh-15293) 2026-09-08 18:22:23 +02:00
mr. m
243a8791a0 gh-15279: Wait for all windows to be restored before syncing (gh-15294) 2026-09-08 18:13:33 +02:00
Brandon Quon
412731f37e gh-15155: Fix tab moving to bottom of tab list (gh-15246) 2026-09-07 22:54:02 +02:00
mr. m
7a78848177 gh-15266: Add support for sharing spaces, folders and split views (gh-15283) 2026-09-07 22:28:09 +02:00
mr. m
a323036995 gh-15266: Add support for sharing spaces, folders and split views (gh-15267) 2026-09-07 01:11:02 +02:00
Harsh Varshney
abc69a1fcb gh-15188: Fixed invisible download progress bar in Library when unselected (gh-15251)
Signed-off-by: Harsh Varshney <92783708+Iron0utlaw@users.noreply.github.com>
2026-09-06 17:52:30 +02:00
mr. m
8df45e5c9a gh-15247: Add support for cross-device normal tabs (gh-15250) 2026-09-06 02:42:59 +02:00
mr. m
58e83c137f gh-15244: Fixed creatin a new space not selecting the empty tab (gh-15245) 2026-09-05 20:25:24 +02:00
101 changed files with 6326 additions and 2425 deletions

View File

@@ -23,7 +23,6 @@ jobs:
fail-fast: false
matrix:
arch: [x86_64, aarch64]
runs-on: ${{ matrix.arch == 'x86_64' && 'macos-15-intel' || 'macos-26' }}
steps:

View File

@@ -27,7 +27,6 @@ jobs:
fail-fast: false
matrix:
arch: [x86_64, aarch64]
runs-on: ${{ inputs.release-branch == 'release' && 'windows-latest' || 'windows-latest' }}
steps:

View File

@@ -53,7 +53,10 @@ fi
if test "$ZEN_RELEASE"; then
ac_add_options --enable-clang-plugin
ac_add_options --enable-bootstrap=-sccache
if ! test "$ZEN_DISABLE_BOOTSTRAP"; then
ac_add_options --enable-bootstrap=-sccache
fi
ac_add_options --enable-optimize
@@ -69,16 +72,11 @@ if test "$ZEN_RELEASE"; then
ac_add_options --disable-geckodriver
ac_add_options --disable-rust-tests
if ! test "$ZEN_DISABLE_LTO"; then
# only enable full LTO when ZEN_RELEASE_BRANCH is 'release'
if test "$ZEN_RELEASE_BRANCH" = "release"; then
# TODO: make it "full" once we have the resources to build it
export MOZ_LTO=cross,full
ac_add_options --enable-lto=cross,full
else
export MOZ_LTO=cross,thin
ac_add_options --enable-lto=cross,thin
fi
if test "$ZEN_DISABLE_LTO"; then
ac_add_options --disable-lto
else
export MOZ_LTO=cross,thin
ac_add_options --enable-lto=cross,thin
fi
mk_add_options MOZILLA_OFFICIAL=1

View File

@@ -9,6 +9,9 @@ if test "$ZEN_RELEASE"; then
# Enable Profile Guided Optimization
if ! test "$ZEN_GA_DISABLE_PGO"; then
export MOZ_LTO=cross,full
ac_add_options --enable-lto=cross,full
if test "$ZEN_GA_GENERATE_PROFILE"; then
mk_add_options "export MOZ_AUTOMATION_PACKAGE_GENERATED_SOURCES=0"
ac_add_options --enable-profile-generate=cross

View File

@@ -45,6 +45,9 @@ if test "$ZEN_CROSS_COMPILING"; then
ac_add_options --enable-profile-use=cross
ac_add_options --with-pgo-profile-path=$(echo ~)/artifact/merged.profdata
ac_add_options --with-pgo-jarlog=$(echo ~)/artifact/en-US.log
export MOZ_LTO=cross,full
ac_add_options --enable-lto=cross,full
fi
fi
fi

View File

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

View File

@@ -68,6 +68,10 @@ zen-settings-workspaces-sync =
.label = Sync your sidebar across devices
.description = Keep your workspaces, pinned tabs and folders in sync on all your devices through your Mozilla account.
zen-settings-normal-tabs-sync =
.label = Include unpinned tabs
.description = Also sync the regular tabs of each space, not just your pinned tabs and folders.
zen-settings-workspaces-sync-unpinned-tabs =
.label = Sync only pinned tabs in workspaces

View File

@@ -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/.
zen-share-space =
.label = Share Space…
zen-share-folder =
.label = Share Folder…
zen-share-split-view =
.label = Share Split View…
zen-share-link-copied-toast = Share link copied!
zen-share-link-expires-description = Anyone with the link can open it until { $date }
zen-share-link-permanent-description = Anyone with the link can open it
zen-share-error-toast = Couldn't create a share link
zen-share-error-auth-description = The share server rejected your key
zen-share-error-too-large-description = This share is too big to upload
zen-share-error-rate-limited-description = You're sharing too fast, try again in a minute
zen-share-error-network-description = Couldn't reach the share server
zen-share-error-empty-description = There's nothing shareable here
zen-share-error-server-description = The share server returned an error
zen-share-overlay-loading = Loading shared tabs…
zen-share-overlay-from-space = A Space from { $name }
zen-share-overlay-from-folder = A Folder from { $name }
zen-share-overlay-shared-with-you = Shared with you
zen-share-overlay-add-space = Add Space to Zen
zen-share-overlay-add-folder = Add Folder to Zen
zen-share-imported-toast = Added to your browser!
zen-share-import-error-toast = Couldn't import this share
zen-share-import-error-dead-description = The link has expired or doesn't exist
zen-share-import-error-invalid-description = The shared data is invalid
zen-share-confirm-title = Share link
zen-share-confirm-dialog =
.buttonlabelaccept = Share
zen-share-confirm-description = Zen uploads a copy of these tabs to the share server and copies a link anyone can open. The link expires after 30 days.
zen-share-confirm-anonymous = Sharing is anonymous unless you add a name.
zen-share-confirm-name-input =
.placeholder = Your name (optional)
zen-share-confirm-dont-ask =
.label = Don't ask me again

View File

@@ -24,6 +24,11 @@ zen-welcome-default-search-description = Choose your default search engine. You
zen-welcome-essentials-title = Choose the apps you use most.
zen-welcome-essentials-description = Select your favorite apps to keep them handy in your sidebar.
zen-welcome-block-ads-title = Block ads and trackers?
zen-welcome-block-ads-description = { -brand-short-name } can install uBlock Origin for you, so pages load cleaner and faster. You can remove it any time.
zen-welcome-block-ads-yes = <strong>YES</strong>, block ads for me.
zen-welcome-block-ads-no = <strong>NO</strong>, show me everything.
zen-welcome-workspace-colors-title = Your space, your colors.
zen-welcome-workspace-colors-description = Personalize your browser by giving each space its own unique color identity.

View File

@@ -2,5 +2,18 @@
# 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: zen.share.enabled
- name: zen.share.base-url
value: "https://share.zen-browser.app"
- name: zen.share.secret-key
value: ""
- name: zen.share.display-name
value: ""
# Allow zen.share.display-name to be synced across devices.
- name: services.sync.prefs.sync.zen.share.display-name
value: true
- name: zen.share.dont-ask-before-sharing
value: false

View File

@@ -8,3 +8,9 @@
- name: zen.spaces-sync.debug
value: false
- name: zen.spaces-sync.normal-tabs
value: false
- name: services.sync.prefs.sync.zen.spaces-sync.normal-tabs
value: true

View File

@@ -20,6 +20,9 @@
- name: zen.theme.acrylic-elements
value: "@IS_TWILIGHT@"
- name: zen.theme.squircle-browser-view
value: "@IS_TWILIGHT@"
- name: zen.theme.disable-lightweight
value: true

View File

@@ -21,3 +21,4 @@
#include ../../../zen/boosts/jar.inc.mn
#include ../../../zen/live-folders/jar.inc.mn
#include ../../../zen/space-routing/jar.inc.mn
#include ../../../zen/share/jar.inc.mn

View File

@@ -16,6 +16,7 @@
<command id="cmd_zenSplitViewUnsplit" />
<command id="cmd_zenSplitViewLinkInNewTab" />
<command id="cmd_zenSplitViewContextMenu" />
<command id="cmd_zenCtxShareSplitView" />
<command id="cmd_zenNewEmptySplit" />
<!-- Workspace commands -->
@@ -40,6 +41,7 @@
<command id="cmd_contextZenRemoveFromEssentials" />
<command id="cmd_zenCtxDeleteWorkspace" />
<command id="cmd_zenCtxShareWorkspace" />
<command id="cmd_zenUnloadWorkspace" />
<command id="cmd_zenUnloadAllOtherWorkspace" />
<command id="cmd_zenOpenSpaceRoutingSettings" />

View File

@@ -12,4 +12,5 @@
<link rel="localization" href="browser/zen-boosts.ftl"/>
<link rel="localization" href="browser/zen-live-folders.ftl"/>
<link rel="localization" href="browser/zen-space-routing.ftl"/>
<link rel="localization" href="browser/zen-share.ftl"/>
</linkset>

View File

@@ -32,6 +32,7 @@
<menuitem id="context_zenUnloadAllOtherWorkspace" data-l10n-id="zen-workspaces-panel-unload-others" command="cmd_zenUnloadAllOtherWorkspace"/>
<menuseparator/>
<menuitem data-l10n-id="zen-panel-ui-workspaces-create" command="cmd_zenOpenWorkspaceCreation"/>
<menuitem id="context_zenShareWorkspace" data-l10n-id="zen-share-space" command="cmd_zenCtxShareWorkspace"/>
<menuitem id="context_zenDeleteWorkspace" data-l10n-id="zen-workspaces-panel-context-delete" command="cmd_zenCtxDeleteWorkspace"/>
<menuseparator/>
<menuitem id="context_zenSpaceRoutingSettings" data-l10n-id="zen-space-routing-settings" command="cmd_zenOpenSpaceRoutingSettings"/>
@@ -55,6 +56,7 @@
<menupopup />
</menu>
<menuitem id="context_zenFolderToSpace" data-l10n-id="zen-folders-panel-convert-folder-to-space" />
<menuitem id="context_zenShareFolder" data-l10n-id="zen-share-folder"/>
<menuseparator />
<menuitem id="context_zenFolderUnpack" data-l10n-id="zen-folders-panel-unpack-folder"/>
<menuitem id="context_zenFolderDelete" data-l10n-id="zen-folders-panel-delete-folder"/>

View File

@@ -1,16 +1,17 @@
diff --git a/browser/components/preferences/config/account-sync.mjs b/browser/components/preferences/config/account-sync.mjs
index b01fd4bb163ad53139fcbd8e79e12ef994d1fa13..dd60be4fe268618d6596cd5cfac3342975bd241a 100644
index b01fd4bb163ad53139fcbd8e79e12ef994d1fa13..7e3c2f6852b77192b4505bb3d5a7b58586da6548 100644
--- a/browser/components/preferences/config/account-sync.mjs
+++ b/browser/components/preferences/config/account-sync.mjs
@@ -46,6 +46,7 @@ Preferences.addAll([
@@ -46,6 +46,8 @@ 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" },
+ { id: "zen.spaces-sync.normal-tabs", type: "bool" },
]);
/**
@@ -557,6 +558,7 @@ const SYNC_ENGINE_SETTINGS = [
@@ -557,6 +559,7 @@ const SYNC_ENGINE_SETTINGS = [
type: "payments",
},
{ id: "syncAddons", pref: "services.sync.engine.addons", type: "addons" },
@@ -18,7 +19,7 @@ index b01fd4bb163ad53139fcbd8e79e12ef994d1fa13..dd60be4fe268618d6596cd5cfac33429
{ id: "syncSettings", pref: "services.sync.engine.prefs", type: "settings" },
];
@@ -564,6 +566,12 @@ SYNC_ENGINE_SETTINGS.forEach(({ id, pref }) => {
@@ -564,6 +567,18 @@ SYNC_ENGINE_SETTINGS.forEach(({ id, pref }) => {
Preferences.addSetting({ id, pref });
});
@@ -27,11 +28,17 @@ index b01fd4bb163ad53139fcbd8e79e12ef994d1fa13..dd60be4fe268618d6596cd5cfac33429
+ id: "zenSyncWorkspaces",
+ pref: "services.sync.engine.spaces",
+});
+Preferences.addSetting({
+ id: "zenSyncNormalTabs",
+ pref: "zen.spaces-sync.normal-tabs",
+ deps: ["zenSyncWorkspaces"],
+ visible: deps => !!deps.zenSyncWorkspaces.value,
+});
+
Preferences.addSetting({
id: "syncEngines",
deps: SYNC_ENGINE_SETTINGS.map(({ id }) => id),
@@ -995,6 +1003,20 @@ SettingGroupManager.registerGroups({
@@ -995,6 +1010,27 @@ SettingGroupManager.registerGroups({
id: "syncEnginesList",
control: "sync-engines-list",
},
@@ -46,6 +53,13 @@ index b01fd4bb163ad53139fcbd8e79e12ef994d1fa13..dd60be4fe268618d6596cd5cfac33429
+ controlAttrs: {
+ inputlayout: "inline-end",
+ },
+ items: [
+ {
+ id: "zenSyncNormalTabs",
+ l10nId: "zen-settings-normal-tabs-sync",
+ control: "moz-checkbox",
+ },
+ ],
+ },
+ ],
+ },

View File

@@ -1,8 +1,8 @@
diff --git a/browser/components/preferences/main.js b/browser/components/preferences/main.js
index 88675d1bcbf000b10d85a9c0980bc6069d88052c..ed3a1e809eb0fcccc5a957be7affb77be8978e32 100644
index 5a56d348146c1c37c088ced60327fffebcf8fac0..abd65594d365fbc0a74af3953c71f410301898ad 100644
--- a/browser/components/preferences/main.js
+++ b/browser/components/preferences/main.js
@@ -528,6 +528,11 @@ function createStartupConfig(hidden = false) {
@@ -529,6 +529,11 @@ function createStartupConfig(hidden = false) {
},
],
},
@@ -14,7 +14,7 @@ index 88675d1bcbf000b10d85a9c0980bc6069d88052c..ed3a1e809eb0fcccc5a957be7affb77b
{
id: "windowsLaunchOnLogin",
l10nId: "windows-launch-on-login",
@@ -575,7 +580,7 @@ function createStartupConfig(hidden = false) {
@@ -576,7 +581,7 @@ function createStartupConfig(hidden = false) {
SettingGroupManager.registerGroups({
defaultBrowser: createDefaultBrowserConfig(),
startup: createStartupConfig(
@@ -23,7 +23,7 @@ index 88675d1bcbf000b10d85a9c0980bc6069d88052c..ed3a1e809eb0fcccc5a957be7affb77b
),
});
@@ -628,7 +633,7 @@ function getBundleForLocales(newLocales) {
@@ -636,7 +641,7 @@ function getBundleForLocales(newLocales) {
])
);
return new Localization(

View File

@@ -38,9 +38,16 @@ var gZenMarketplaceManager = {
this.__hasInitializedEvents = true;
await this._buildModsList();
Services.prefs.addObserver(gZenMods.updatePref, this);
window.addEventListener(
"unload",
() => {
Services.prefs.removeObserver(gZenMods.updatePref, this);
},
{ once: true }
);
await this._buildModsList();
const checkForUpdateClick = (event) => {
if (event.target === checkForUpdates) {
@@ -69,7 +76,6 @@ var gZenMarketplaceManager = {
});
window.addEventListener("unload", () => {
Services.prefs.removeObserver(gZenMods.updatePref, this);
this.__hasInitializedEvents = false;
document.removeEventListener("ZenModsMarketplace:CheckForUpdatesFinished", this);
@@ -735,7 +741,6 @@ var gZenWorkspacesSettings = {
Services.prefs.addObserver("zen.glance.enabled", tabsUnloaderPrefListener); // We can use the same listener for both prefs
Services.prefs.addObserver("zen.workspaces.separate-essentials", tabsUnloaderPrefListener);
Services.prefs.addObserver("zen.glance.activation-method", tabsUnloaderPrefListener);
Services.prefs.addObserver("zen.window-sync.sync-only-pinned-tabs", tabsUnloaderPrefListener);
Services.prefs.addObserver(
"zen.tabs.ctrl-tab.ignore-essential-tabs",
@@ -744,7 +749,6 @@ var gZenWorkspacesSettings = {
Services.prefs.addObserver("browser.ctrlTab.sortByRecentlyUsed", toggleZenCycleByAttrWarning);
window.addEventListener("unload", () => {
Services.prefs.removeObserver("zen.glance.enabled", tabsUnloaderPrefListener);
Services.prefs.removeObserver("zen.glance.activation-method", tabsUnloaderPrefListener);
Services.prefs.removeObserver("zen.workspaces.separate-essentials", tabsUnloaderPrefListener);
Services.prefs.removeObserver(
"zen.window-sync.sync-only-pinned-tabs",

View File

@@ -1,8 +1,8 @@
diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs
index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2ab6e6c967 100644
index 76a984f6e21409941ea038218d161e67f2e0c23d..52408568a33a89c0e3d0d1514152ded2eaa1afed 100644
--- a/browser/components/sessionstore/SessionStore.sys.mjs
+++ b/browser/components/sessionstore/SessionStore.sys.mjs
@@ -129,6 +129,11 @@ const TAB_EVENTS = [
@@ -126,6 +126,11 @@ const TAB_EVENTS = [
"TabSplitViewActivate",
"SplitViewRemoved",
"SplitViewCreated",
@@ -14,7 +14,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
];
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
@@ -201,6 +206,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
@@ -198,6 +203,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs",
TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
@@ -23,7 +23,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
});
ChromeUtils.defineLazyGetter(lazy, "blankURI", () => {
@@ -1317,10 +1324,7 @@ var SessionStoreInternal = {
@@ -1314,10 +1321,7 @@ var SessionStoreInternal = {
*/
get willAutoRestore() {
return (
@@ -35,7 +35,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
);
},
@@ -2008,6 +2012,11 @@ var SessionStoreInternal = {
@@ -2005,6 +2009,11 @@ var SessionStoreInternal = {
case "TabPinned":
case "TabUnpinned":
case "SwapDocShells":
@@ -47,7 +47,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
this.saveStateDelayed(win);
break;
case "TabGroupCreate":
@@ -2117,6 +2126,10 @@ var SessionStoreInternal = {
@@ -2114,6 +2123,10 @@ var SessionStoreInternal = {
this._windows[aWindow.__SSi].isTaskbarTab = true;
}
@@ -58,7 +58,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
if (lazy.AIWindow.isAIWindowActiveAndEnabled(aWindow)) {
this._windows[aWindow.__SSi].isAIWindow = true;
}
@@ -2153,7 +2166,7 @@ var SessionStoreInternal = {
@@ -2150,7 +2163,7 @@ var SessionStoreInternal = {
let isTaskbarTab = this._windows[aWindow.__SSi].isTaskbarTab;
// A regular window is not a private window, taskbar tab window, or popup window
let isRegularWindow =
@@ -67,7 +67,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
// perform additional initialization when the first window is loading
if (lazy.RunState.isStopped) {
@@ -2165,7 +2178,7 @@ var SessionStoreInternal = {
@@ -2162,7 +2175,7 @@ var SessionStoreInternal = {
// to disk to NOW() to enforce a full interval before the next write.
lazy.SessionSaver.updateLastSaveTime();
@@ -76,7 +76,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
this._log.debug(
"initializeWindow, the window is private or a web app. Saving SessionStartup.state for possibly restoring later"
);
@@ -2209,6 +2222,7 @@ var SessionStoreInternal = {
@@ -2206,6 +2219,7 @@ var SessionStoreInternal = {
null,
"sessionstore-one-or-no-tab-restored"
);
@@ -84,7 +84,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
this._deferredAllWindowsRestored.resolve();
}
// this window was opened by _openWindowWithState
@@ -2258,7 +2272,6 @@ var SessionStoreInternal = {
@@ -2255,7 +2269,6 @@ var SessionStoreInternal = {
if (closedWindowState) {
let newWindowState;
if (
@@ -92,7 +92,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
!lazy.SessionStartup.willRestore()
) {
// We want to split the window up into pinned tabs and unpinned tabs.
@@ -2294,6 +2307,7 @@ var SessionStoreInternal = {
@@ -2291,6 +2304,7 @@ var SessionStoreInternal = {
}
if (newWindowState) {
@@ -100,7 +100,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
// Ensure that the window state isn't hidden
this._restoreCount = 1;
let state = { windows: [newWindowState] };
@@ -2330,6 +2344,9 @@ var SessionStoreInternal = {
@@ -2327,6 +2341,9 @@ var SessionStoreInternal = {
});
this._shouldRestoreLastSession = false;
}
@@ -110,7 +110,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
if (this._restoreLastWindow && aWindow.toolbar.visible) {
// always reset (if not a popup window)
@@ -2480,7 +2497,7 @@ var SessionStoreInternal = {
@@ -2477,7 +2494,7 @@ var SessionStoreInternal = {
var tabbrowser = aWindow.gBrowser;
@@ -119,7 +119,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
TAB_EVENTS.forEach(function (aEvent) {
tabbrowser.tabContainer.removeEventListener(aEvent, this, true);
@@ -2531,7 +2548,7 @@ var SessionStoreInternal = {
@@ -2528,7 +2545,7 @@ var SessionStoreInternal = {
let isLastRegularWindow =
Object.values(this._windows).filter(
@@ -128,7 +128,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
).length == 1;
this._log.debug(
`onClose, closing window isLastRegularWindow? ${isLastRegularWindow}`
@@ -2588,8 +2605,8 @@ var SessionStoreInternal = {
@@ -2585,8 +2602,8 @@ var SessionStoreInternal = {
// 2) Flush the window.
// 3) When the flush is complete, revisit our decision to store the window
// in _closedWindows, and add/remove as necessary.
@@ -139,7 +139,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
}
completionPromise = lazy.TabStateFlusher.flushWindow(aWindow).then(() => {
@@ -2609,8 +2626,9 @@ var SessionStoreInternal = {
@@ -2606,8 +2623,9 @@ var SessionStoreInternal = {
// Save non-private windows if they have at
// least one saveable tab or are the last window.
@@ -151,7 +151,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
if (!isLastWindow && winData.closedId > -1) {
this._addClosedAction(
@@ -2686,7 +2704,7 @@ var SessionStoreInternal = {
@@ -2683,7 +2701,7 @@ var SessionStoreInternal = {
* to call this method again asynchronously (for example, after
* a window flush).
*/
@@ -160,7 +160,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
// Make sure SessionStore is still running, and make sure that we
// haven't chosen to forget this window.
if (
@@ -2703,6 +2721,7 @@ var SessionStoreInternal = {
@@ -2700,6 +2718,7 @@ var SessionStoreInternal = {
// _closedWindows from a previous call to this function.
let winIndex = this._closedWindows.indexOf(winData);
let alreadyStored = winIndex != -1;
@@ -168,7 +168,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
// If sidebar command is truthy, i.e. sidebar is open, store sidebar settings
let shouldStore = hasSaveableTabs || isLastWindow;
@@ -3525,7 +3544,7 @@ var SessionStoreInternal = {
@@ -3522,7 +3541,7 @@ var SessionStoreInternal = {
if (!isPrivateWindow && tabState.isPrivate) {
return;
}
@@ -177,10 +177,14 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
return;
}
@@ -4264,6 +4283,12 @@ var SessionStoreInternal = {
@@ -4260,7 +4279,15 @@ var SessionStoreInternal = {
1,
Math.min(tabState.index, tabState.entries.length)
);
tabState.pinned = false;
- tabState.pinned = false;
+ tabState.pinned =
+ newTab.group?.isZenFolder === true &&
+ Services.prefs.getBoolPref("zen.folders.owned-tabs-in-folder");
+ tabState.zenEssential = false;
+ tabState.zenSyncId = null;
+ tabState.zenIsGlance = false;
@@ -190,7 +194,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
if (inBackground === false) {
aWindow.gBrowser.selectedTab = newTab;
@@ -4697,6 +4722,8 @@ var SessionStoreInternal = {
@@ -4694,6 +4721,8 @@ var SessionStoreInternal = {
// Append the tab if we're opening into a different window,
tabIndex: aSource == aTargetWindow ? pos : Infinity,
pinned: state.pinned,
@@ -199,7 +203,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
userContextId: state.userContextId,
skipLoad: true,
preferredRemoteType,
@@ -5194,9 +5221,10 @@ var SessionStoreInternal = {
@@ -5191,9 +5220,10 @@ var SessionStoreInternal = {
if (activePageData.title && activePageData.title != activePageData.url) {
win.gBrowser.setInitialTabTitle(tab, activePageData.title, {
isContentTitle: true,
@@ -211,7 +215,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
}
}
@@ -5561,7 +5589,7 @@ var SessionStoreInternal = {
@@ -5561,7 +5591,7 @@ var SessionStoreInternal = {
for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) {
let tab = tabbrowser.tabs[i];
@@ -220,7 +224,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
removableTabs.push(tab);
}
}
@@ -5674,7 +5702,7 @@ var SessionStoreInternal = {
@@ -5674,7 +5704,7 @@ var SessionStoreInternal = {
// collect the data for all windows
for (ix in this._windows) {
@@ -229,7 +233,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
// window data is still in _statesToRestore
continue;
}
@@ -5818,11 +5846,12 @@ var SessionStoreInternal = {
@@ -5818,11 +5848,12 @@ var SessionStoreInternal = {
}
let tabbrowser = aWindow.gBrowser;
@@ -243,7 +247,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
// update the internal state data for this window
for (let tab of tabs) {
if (tab == aWindow.FirefoxViewHandler.tab) {
@@ -5833,6 +5862,9 @@ var SessionStoreInternal = {
@@ -5833,6 +5864,9 @@ var SessionStoreInternal = {
tabsData.push(tabData);
}
@@ -253,7 +257,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
// update tab group state for this window
winData.groups = [];
for (let tabGroup of aWindow.gBrowser.tabGroups) {
@@ -5849,7 +5881,7 @@ var SessionStoreInternal = {
@@ -5849,7 +5883,7 @@ var SessionStoreInternal = {
// a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab,
// since it's only inserted into the tab strip after it's selected).
if (aWindow.FirefoxViewHandler.tab?.selected) {
@@ -262,7 +266,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
winData.title = tabbrowser.tabs[0].label;
}
winData.selected = selectedIndex;
@@ -6051,8 +6083,8 @@ var SessionStoreInternal = {
@@ -6051,8 +6085,8 @@ var SessionStoreInternal = {
// selectTab represents.
let selectTab = 0;
if (overwriteTabs) {
@@ -273,7 +277,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
selectTab = Math.min(selectTab, winData.tabs.length);
}
@@ -6074,6 +6106,7 @@ var SessionStoreInternal = {
@@ -6074,6 +6108,7 @@ var SessionStoreInternal = {
if (overwriteTabs) {
for (let i = tabbrowser.browsers.length - 1; i >= 0; i--) {
if (!tabbrowser.tabs[i].selected) {
@@ -281,7 +285,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
tabbrowser.removeTab(tabbrowser.tabs[i]);
}
}
@@ -6108,6 +6141,12 @@ var SessionStoreInternal = {
@@ -6108,6 +6143,12 @@ var SessionStoreInternal = {
savedTabGroup => !openTabGroupIdsInWindow.has(savedTabGroup.id)
);
}
@@ -294,7 +298,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
// Move the originally open tabs to the end.
if (initialTabs) {
@@ -6674,6 +6713,25 @@ var SessionStoreInternal = {
@@ -6674,6 +6715,25 @@ var SessionStoreInternal = {
// Most of tabData has been restored, now continue with restoring
// attributes that may trigger external events.
@@ -320,7 +324,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
if (tabData.pinned) {
tabbrowser.pinTab(tab);
@@ -6859,6 +6917,9 @@ var SessionStoreInternal = {
@@ -6859,6 +6919,9 @@ var SessionStoreInternal = {
aWindow.gURLBar.readOnly = false;
}
}
@@ -330,7 +334,14 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
let promiseParts = Promise.withResolvers();
aWindow.setTimeout(() => {
@@ -7654,7 +7715,7 @@ var SessionStoreInternal = {
@@ -7356,5 +7419,6 @@ var SessionStoreInternal = {
*/
_isCmdLineEmpty: function ssi_isCmdLineEmpty(aWindow, aState) {
var pinnedOnly =
+ !Services.prefs.getBoolPref("zen.workspaces.continue-where-left-off") &&
aState.windows &&
aState.windows.every(win => win.tabs.every(tab => tab.pinned));
@@ -7654,7 +7718,7 @@ var SessionStoreInternal = {
let groupsToSave = new Map();
for (let tIndex = 0; tIndex < window.tabs.length; ) {
@@ -339,7 +350,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
// Adjust window.selected
if (tIndex + 1 < window.selected) {
window.selected -= 1;
@@ -7669,7 +7730,7 @@ var SessionStoreInternal = {
@@ -7669,7 +7733,7 @@ var SessionStoreInternal = {
);
// We don't want to increment tIndex here.
continue;
@@ -348,7 +359,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
// Convert any open groups into saved groups.
let groupStateToSave = window.groups.find(
groupState => groupState.id == window.tabs[tIndex].groupId
@@ -8233,7 +8294,6 @@ var SessionStoreInternal = {
@@ -8233,7 +8297,6 @@ var SessionStoreInternal = {
timer.initWithCallback(
function () {
if (beats <= 0) {
@@ -356,7 +367,7 @@ index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2a
Glean.sessionRestore.shutdownFlushAllOutcomes.timed_out.add(1);
deferred.resolve();
}
@@ -8711,6 +8771,7 @@ var SessionStoreInternal = {
@@ -8711,6 +8774,7 @@ var SessionStoreInternal = {
if (
!savedTabGroupState.tabs.length ||
this.getSavedTabGroup(savedTabGroupState.id)

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/sessionstore/TabState.sys.mjs b/browser/components/sessionstore/TabState.sys.mjs
index a33deeb5b587d6a950960a1a4781176e4e730ad7..e96cfb9a5ffbe4feccdffa8b6d18ececb48d4c52 100644
index a33deeb5b587d6a950960a1a4781176e4e730ad7..7927471d389f2626e4cc1b0e7a6eab6446dca432 100644
--- a/browser/components/sessionstore/TabState.sys.mjs
+++ b/browser/components/sessionstore/TabState.sys.mjs
@@ -8,6 +8,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
@@ -39,3 +39,14 @@ index a33deeb5b587d6a950960a1a4781176e4e730ad7..e96cfb9a5ffbe4feccdffa8b6d18ecec
tabData.userContextId = tab.userContextId || 0;
@@ -132,6 +151,10 @@ class _TabState {
tabData.image = tabbrowser.getIcon(tab);
}
+ if (tab.zenStaticIcon) {
+ tabData.image = tab.zenStaticIcon;
+ }
+
// If there is a userTypedValue set, then either the user has typed something
// in the URL bar, or a new tab was opened with a URI to load.
// If so, we also track whether we were still in the process of loading something.

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
index 43180487a2fac15b7a65fac94f233eeed9094990..ae1288974cae29da929f27b78ff8db542566eba2 100644
index 43180487a2fac15b7a65fac94f233eeed9094990..ef6d30eb681caeae48d15280687b3bc37dbadc37 100644
--- a/browser/components/tabbrowser/content/tabbrowser.js
+++ b/browser/components/tabbrowser/content/tabbrowser.js
@@ -514,6 +514,7 @@
@@ -523,7 +523,7 @@ index 43180487a2fac15b7a65fac94f233eeed9094990..ae1288974cae29da929f27b78ff8db54
+ } else {
+ gZenWorkspaces._tabToRemoveForEmpty = leftoverTab;
+ if (Services.prefs.getBoolPref("zen.workspaces.continue-where-left-off")) {
+ gZenWorkspaces._tabToSelect = selectTab - 2; // -1 for the offset, and another -1 for the empty tab.
+ gZenWorkspaces._tabToSelect = selectTab - 1; // -1 for the empty tab.
+ }
+ if (gZenWorkspaces._initialTab && !gZenVerticalTabsManager._canReplaceNewTab) {
+ gZenWorkspaces._initialTab._shouldRemove = true;
@@ -751,7 +751,7 @@ index 43180487a2fac15b7a65fac94f233eeed9094990..ae1288974cae29da929f27b78ff8db54
}
/**
@@ -6422,6 +6618,7 @@
@@ -6422,11 +6618,12 @@
}
let excludeTabs = new Set(aExcludeTabs);
@@ -759,6 +759,12 @@ index 43180487a2fac15b7a65fac94f233eeed9094990..ae1288974cae29da929f27b78ff8db54
// If this tab has a successor, it should be selectable, since
// hiding or closing a tab removes that tab as a successor.
if (aTab.successor && !excludeTabs.has(aTab.successor)) {
- return aTab.successor;
+ return gZenWorkspaces.findTabToBlur(aTab.successor);
}
if (
@@ -6434,13 +6631,13 @@
!excludeTabs.has(aTab.owner) &&
Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
@@ -984,7 +990,7 @@ index 43180487a2fac15b7a65fac94f233eeed9094990..ae1288974cae29da929f27b78ff8db54
+ if (!gZenFolders.canDropElement(element, targetElement)) {
+ element = element.group;
+ }
+ if (!element.hasAttribute('zen-essential') && targetElement?.hasAttribute('zen-essential')) {
+ if (!element.hasAttribute('zen-essential') && targetElement?.hasAttribute('zen-essential')) {
+ targetElement = null;
+ moveBefore = false;
+ }

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/urlbar/content/UrlbarInput.mjs b/browser/components/urlbar/content/UrlbarInput.mjs
index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1a95e28b8 100644
index 319fb6870ec18d87b9f1263bd960edaa39b0447c..1746a052c304e7988447fdb869ad2e36d7d74e8e 100644
--- a/browser/components/urlbar/content/UrlbarInput.mjs
+++ b/browser/components/urlbar/content/UrlbarInput.mjs
@@ -88,6 +88,13 @@ const lazy = XPCOMUtils.declareLazy({
@@ -61,7 +61,17 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
}
/**
@@ -1726,7 +1755,11 @@ ${
@@ -1255,6 +1284,9 @@ ${
where,
query: searchString,
});
+ if (where != "current" && this.sapName != "searchbar") {
+ this.handleRevert();
+ }
this.controller.openSERP(
engine.id,
searchString,
@@ -1726,7 +1758,11 @@ ${
openParams.avoidBrowserFocus = keepViewOpen;
if (!this.#providesSearchMode(result) && !keepViewOpen) {
@@ -74,7 +84,7 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
}
if (isCanonized) {
@@ -3016,6 +3049,42 @@ ${
@@ -3016,6 +3052,42 @@ ${
await this.#updateLayoutBreakoutDimensions();
}
@@ -117,7 +127,7 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
startLayoutExtend() {
if (!this.#allowBreakout || this.hasAttribute("breakout-extend")) {
// Do not expand if the Urlbar does not support being expanded or it is
@@ -3030,6 +3099,13 @@ ${
@@ -3030,6 +3102,13 @@ ${
this.toggleAttribute("breakout-extend", true);
this.#updateTextboxPosition();
@@ -131,7 +141,7 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
// Enable the animation only after the first extend call to ensure it
// doesn't run when opening a new window.
if (!this.hasAttribute("breakout-extend-animate")) {
@@ -3053,6 +3129,29 @@ ${
@@ -3053,6 +3132,29 @@ ${
return;
}
@@ -161,7 +171,7 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
this.toggleAttribute("breakout-extend", false);
this.#updateTextboxPosition();
}
@@ -3091,7 +3190,7 @@ ${
@@ -3091,7 +3193,7 @@ ${
forceUnifiedSearchButtonAvailable = false
) {
let prevState = this.getAttribute("pageproxystate");
@@ -170,7 +180,7 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
this.setAttribute("pageproxystate", state);
this._inputContainer.setAttribute("pageproxystate", state);
this._identityBox?.setAttribute("pageproxystate", state);
@@ -3355,10 +3454,12 @@ ${
@@ -3355,10 +3457,12 @@ ${
}
this.style.top = px(
@@ -183,7 +193,7 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
);
}
@@ -3417,9 +3518,10 @@ ${
@@ -3417,9 +3521,10 @@ ${
return;
}
@@ -195,7 +205,7 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
);
if (this.#breakoutBlockerCount) {
@@ -3929,6 +4031,7 @@ ${
@@ -3929,6 +4034,7 @@ ${
}
_toggleActionOverride(event) {
@@ -203,7 +213,7 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
if (
event.keyCode == KeyEvent.DOM_VK_SHIFT ||
event.keyCode == KeyEvent.DOM_VK_ALT ||
@@ -4027,9 +4130,10 @@ ${
@@ -4027,9 +4133,10 @@ ${
if (!this.#isAddressbar) {
return val;
}
@@ -217,7 +227,7 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
// Only trim value if the directionality doesn't change to RTL and we're not
// showing a strikeout https protocol.
return this.controller.isTextDirectionRTL(trimmedValue) ||
@@ -4235,6 +4339,11 @@ ${
@@ -4235,6 +4342,11 @@ ${
keepViewOpen = false,
browserId = null,
}) {
@@ -229,7 +239,7 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
let userTypedValue;
if (this.#isAddressbar && where == "current") {
// Make sure URL is formatted properly (don't show punycode).
@@ -4576,6 +4685,7 @@ ${
@@ -4576,6 +4688,7 @@ ${
this.setResultForCurrentValue(null);
this.handleCommand();
this.controller.clearLastQueryContextCache();
@@ -237,7 +247,7 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
this._suppressStartQuery = false;
});
@@ -4583,7 +4693,6 @@ ${
@@ -4583,7 +4696,6 @@ ${
contextMenu.addEventListener("popupshowing", () => {
// Close the results pane when the input field contextual menu is open,
// because paste and go doesn't want a result selection.
@@ -245,7 +255,7 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
let controller =
this.document.commandDispatcher.getControllerForCommand("cmd_paste");
@@ -4850,7 +4959,11 @@ ${
@@ -4850,7 +4962,11 @@ ${
if (!engineName && !source && !this.hasAttribute("searchmode")) {
return;
}
@@ -258,7 +268,7 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
if (this._searchModeIndicatorTitle) {
this._searchModeIndicatorTitle.textContent = "";
this._searchModeIndicatorTitle.removeAttribute("data-l10n-id");
@@ -5149,6 +5262,7 @@ ${
@@ -5149,6 +5265,7 @@ ${
this.document.l10n.setAttributes(
this.inputField,
@@ -266,7 +276,7 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
l10nId,
l10nId == "urlbar-placeholder-with-name"
? { name: engineName }
@@ -5199,6 +5313,12 @@ ${
@@ -5199,6 +5316,12 @@ ${
}
lazy.logger.debug("Blur Event");
@@ -279,7 +289,7 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
// We cannot count every blur events after a missed engagement as abandoment
// because the user may have clicked on some view element that executes
// a command causing a focus change. For example opening preferences from
@@ -5276,6 +5396,11 @@ ${
@@ -5276,6 +5399,11 @@ ${
}
_on_click(event) {
@@ -291,7 +301,7 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
switch (event.target) {
case this.inputField:
case this._inputContainer:
@@ -5371,10 +5496,11 @@ ${
@@ -5371,10 +5499,11 @@ ${
}
if (untrim) {
this.setValue(this._untrimmedValue);
@@ -304,7 +314,7 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
this.view.autoOpen({ event });
} else {
if (this._untrimOnFocusAfterKeydown) {
@@ -5414,9 +5540,16 @@ ${
@@ -5414,9 +5543,16 @@ ${
}
_on_mousedown(event) {
@@ -322,7 +332,7 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
if (
event.composedTarget != this.inputField &&
event.composedTarget != this._inputContainer
@@ -5426,6 +5559,10 @@ ${
@@ -5426,6 +5562,10 @@ ${
this.focusedViaMousedown = !this.focused;
this.#preventClickSelectsAll = this.focused;
@@ -333,7 +343,7 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
// Keep the focus status, since the attribute may be changed
// upon calling this.focus().
@@ -5463,7 +5600,7 @@ ${
@@ -5463,7 +5603,7 @@ ${
// view open on tab switch, and the TabSelect event arrived earlier.
// Also ignore mousedown on the urlbarView context menu: opening/closing the
// view is already handled by the result opening flow.
@@ -342,7 +352,7 @@ index 319fb6870ec18d87b9f1263bd960edaa39b0447c..554cc90fb2307450ddc68033968cefd1
break;
}
@@ -5754,7 +5891,7 @@ ${
@@ -5754,7 +5894,7 @@ ${
// When we are in actions search mode we can show more results so
// increase the limit.
let maxResults =

View File

@@ -0,0 +1,13 @@
diff --git a/browser/extensions/newtab/lib/AboutPreferences.sys.mjs b/browser/extensions/newtab/lib/AboutPreferences.sys.mjs
index 4562efc05b76d5dddd35bda79bd7d5d98a51ffc9..ad9b6f55f5e77065ce74535daf6457a5c4b44058 100644
--- a/browser/extensions/newtab/lib/AboutPreferences.sys.mjs
+++ b/browser/extensions/newtab/lib/AboutPreferences.sys.mjs
@@ -355,7 +355,7 @@ export class AboutPreferences {
// Firefox 155+ registers `homepage` and `customHomepage` in
// components/preferences. They can't be registered twice, so skip them
// on 155+. Drop this condition once 155 reaches Release.
- if (Services.vc.compare(AppConstants.MOZ_APP_VERSION, "155.0a1") < 0) {
+ if (false) {
groups.homepage = this._setupHomepageGroup(window);
groups.customHomepage = this._setupCustomHomepageGroup(window);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,558 @@
diff --git a/gfx/wr/webrender/res/ps_quad_mask.glsl b/gfx/wr/webrender/res/ps_quad_mask.glsl
--- a/gfx/wr/webrender/res/ps_quad_mask.glsl
+++ b/gfx/wr/webrender/res/ps_quad_mask.glsl
@@ -19,12 +19,14 @@
// We pack 4 vec3 clip planes into 3 vec4 to save a varying slot.
flat varying highp vec4 vClipPlane_A;
flat varying highp vec4 vClipPlane_B;
flat varying highp vec4 vClipPlane_C;
+#ifdef WR_FEATURE_SUPERELLIPSE
flat varying highp vec4 vClipShape;
flat varying highp vec4 vClipClampedInset;
+#endif
#endif
flat varying highp vec2 vClipMode;
#ifdef WR_VERTEX_SHADER
@@ -40,12 +42,14 @@
vec4 radii;
#else
vec4 radii_top;
vec4 radii_bottom;
+#ifdef WR_FEATURE_SUPERELLIPSE
vec4 shape;
vec4 inset;
+#endif
#endif
float mode;
int space;
};
@@ -58,22 +62,29 @@
vec4 texels[3] = fetch_from_gpu_buffer_3f(index);
clip.rect = RectWithEndpoint(texels[0].xy, texels[0].zw);
clip.radii = texels[1];
clip.mode = texels[2].x;
#else
+#ifdef WR_FEATURE_SUPERELLIPSE
vec4 texels[6] = fetch_from_gpu_buffer_6f(index);
+#else
+ vec4 texels[4] = fetch_from_gpu_buffer_4f(index);
+#endif
clip.rect = RectWithEndpoint(texels[0].xy, texels[0].zw);
clip.radii_top = texels[1];
clip.radii_bottom = texels[2];
clip.mode = texels[3].x;
+#ifdef WR_FEATURE_SUPERELLIPSE
clip.shape = texels[4];
clip.inset = texels[5];
+#endif
#endif
return clip;
}
+#ifdef WR_FEATURE_SUPERELLIPSE
vec4 precalc_corner(vec2 center, vec2 radii, vec2 inset, vec2 clip_sign, float k) {
if (k == 1.0) {
// round/ellipse corner, precalc the ellipse parameters
return vec4(center, inverse_radii_squared(radii));
} else {
@@ -85,10 +96,11 @@
radii = offset_radii.zw;
}
return vec4(center, inverse_radii(radii));
}
}
+#endif
void pattern_vertex(PrimitiveInfo prim_info) {
Clip clip = fetch_clip(aClipData.y);
Transform clip_transform = fetch_transform(aClipData.x);
@@ -121,10 +133,11 @@
vec2 r_tl = clip.radii_top.xy;
vec2 r_tr = clip.radii_top.zw;
vec2 r_br = clip.radii_bottom.zw;
vec2 r_bl = clip.radii_bottom.xy;
+#ifdef WR_FEATURE_SUPERELLIPSE
vClipCenter_Radius_TL = precalc_corner(clip.rect.p0 + r_tl,
r_tl,
clip.inset.wx,
vec2(-1.0, -1.0),
clip.shape.x);
@@ -146,10 +159,25 @@
clip.rect.p1.y - r_bl.y),
r_bl,
clip.inset.wz,
vec2(-1.0, 1.0),
clip.shape.w);
+#else
+ vClipCenter_Radius_TL = vec4(clip.rect.p0 + r_tl,
+ inverse_radii_squared(r_tl));
+
+ vClipCenter_Radius_TR = vec4(clip.rect.p1.x - r_tr.x,
+ clip.rect.p0.y + r_tr.y,
+ inverse_radii_squared(r_tr));
+
+ vClipCenter_Radius_BR = vec4(clip.rect.p1 - r_br,
+ inverse_radii_squared(r_br));
+
+ vClipCenter_Radius_BL = vec4(clip.rect.p0.x + r_bl.x,
+ clip.rect.p1.y - r_bl.y,
+ inverse_radii_squared(r_bl));
+#endif
// We need to know the half-spaces of the corners separate from the center
// and radius. We compute a point that falls on the diagonal (which is just
// an inner vertex pushed out along one axis, but not on both) to get the
// plane offset of the half-space. We also compute the direction vector of
@@ -170,13 +198,15 @@
vClipPlane_A = vec4(tl.x, tl.y, tl.z, tr.x);
vClipPlane_B = vec4(tr.y, tr.z, br.x, br.y);
vClipPlane_C = vec4(br.z, bl.x, bl.y, bl.z);
+#ifdef WR_FEATURE_SUPERELLIPSE
vClipShape = clip.shape;
vClipClampedInset = min(clip.inset, 0.0);
#endif
+#endif
}
#endif
#ifdef WR_FRAGMENT_SHADER
@@ -205,11 +235,13 @@
vec3 plane_tr = vec3(vClipPlane_A.w, vClipPlane_B.x, vClipPlane_B.y);
vec3 plane_br = vec3(vClipPlane_B.z, vClipPlane_B.w, vClipPlane_C.x);
vec3 plane_bl = vec3(vClipPlane_C.y, vClipPlane_C.z, vClipPlane_C.w);
float dist;
+#ifdef WR_FEATURE_SUPERELLIPSE
if (vClipShape == vec4(1.0)) {
+#endif
dist = distance_to_rounded_rect(
clip_local_pos,
plane_tl,
vClipCenter_Radius_TL,
plane_tr,
@@ -218,10 +250,11 @@
vClipCenter_Radius_BR,
plane_bl,
vClipCenter_Radius_BL,
vTransformBounds
);
+#ifdef WR_FEATURE_SUPERELLIPSE
} else {
dist = distance_to_shaped_rect(
clip_local_pos,
vClipCenter_Radius_TL,
vClipCenter_Radius_TR,
@@ -230,10 +263,11 @@
vTransformBounds,
vClipShape,
vClipClampedInset
);
}
+#endif
#endif
// Compute AA for the given dist and range.
float alpha = distance_aa(aa_range, dist);
@@ -340,13 +374,13 @@
#define AA_RECT(local_pos) \
sd_round_box(local_pos, v_clip_size, v_clip_radii)
#else
// The span fast path only handles elliptical corners. For superellipse
// shapes, bail out and let SWGL fall back to the fragment shader.
- if (vClipShape != vec4(1.0)) {
+ #ifdef WR_FEATURE_SUPERELLIPSE
return;
- }
+ #endif
// Unpack the corner half-spaces packed into vClipPlane_A/B/C.
vec3 plane_tl = vec3(vClipPlane_A.x, vClipPlane_A.y, vClipPlane_A.z);
vec3 plane_tr = vec3(vClipPlane_A.w, vClipPlane_B.x, vClipPlane_B.y);
vec3 plane_br = vec3(vClipPlane_B.z, vClipPlane_B.w, vClipPlane_C.x);
diff --git a/gfx/wr/webrender/src/batch.rs b/gfx/wr/webrender/src/batch.rs
--- a/gfx/wr/webrender/src/batch.rs
+++ b/gfx/wr/webrender/src/batch.rs
@@ -1224,25 +1224,29 @@
/// A list of clip instances to be drawn into a target.
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct ClipMaskInstanceList {
pub mask_instances_fast: FrameVec<MaskInstance>,
+ pub mask_instances_superellipse: FrameVec<MaskInstance>,
pub mask_instances_slow: FrameVec<MaskInstance>,
pub mask_instances_fast_with_scissor: FastHashMap<DeviceIntRect, FrameVec<MaskInstance>>,
+ pub mask_instances_superellipse_with_scissor: FastHashMap<DeviceIntRect, FrameVec<MaskInstance>>,
pub mask_instances_slow_with_scissor: FastHashMap<DeviceIntRect, FrameVec<MaskInstance>>,
pub image_mask_instances: FastHashMap<TextureSource, FrameVec<PrimitiveInstanceData>>,
pub image_mask_instances_with_scissor: FastHashMap<(DeviceIntRect, TextureSource), FrameVec<PrimitiveInstanceData>>,
}
impl ClipMaskInstanceList {
pub fn new(memory: &FrameMemory) -> Self {
ClipMaskInstanceList {
mask_instances_fast: memory.new_vec(),
+ mask_instances_superellipse: memory.new_vec(),
mask_instances_slow: memory.new_vec(),
mask_instances_fast_with_scissor: FastHashMap::default(),
+ mask_instances_superellipse_with_scissor: FastHashMap::default(),
mask_instances_slow_with_scissor: FastHashMap::default(),
image_mask_instances: FastHashMap::default(),
image_mask_instances_with_scissor: FastHashMap::default(),
}
}
@@ -1250,20 +1254,24 @@
pub fn is_empty(&self) -> bool {
// Destructure self to make sure we don't forget to update this method if
// a new member is added.
let ClipMaskInstanceList {
mask_instances_fast,
+ mask_instances_superellipse,
mask_instances_slow,
mask_instances_fast_with_scissor,
+ mask_instances_superellipse_with_scissor,
mask_instances_slow_with_scissor,
image_mask_instances,
image_mask_instances_with_scissor,
} = self;
mask_instances_fast.is_empty()
+ && mask_instances_superellipse.is_empty()
&& mask_instances_slow.is_empty()
&& mask_instances_fast_with_scissor.is_empty()
+ && mask_instances_superellipse_with_scissor.is_empty()
&& mask_instances_slow_with_scissor.is_empty()
&& image_mask_instances.is_empty()
&& image_mask_instances_with_scissor.is_empty()
}
}
diff --git a/gfx/wr/webrender/src/quad.rs b/gfx/wr/webrender/src/quad.rs
--- a/gfx/wr/webrender/src/quad.rs
+++ b/gfx/wr/webrender/src/quad.rs
@@ -1807,11 +1807,11 @@
gpu_buffer: &mut GpuBufferBuilderF,
clip_rect: LayoutRect,
radius: &BorderRadius,
inset: LayoutSideOffsets,
mode: ClipMode,
-) -> (GpuBufferAddress, bool) {
+) -> (GpuBufferAddress, bool, bool) {
let radius = clamped_radius(radius, clip_rect.size());
if radius.can_use_fast_path_in(&clip_rect) {
let mut writer = gpu_buffer.write_blocks(3);
writer.push_one(clip_rect);
@@ -1821,13 +1821,15 @@
radius.bottom_left.width,
radius.top_left.width,
]);
writer.push_one([mode as i32 as f32, 0.0, 0.0, 0.0]);
- (writer.finish(), true)
+ (writer.finish(), true, false)
} else {
- let mut writer = gpu_buffer.write_blocks(6);
+ let superellipse = !radius.shapes_all_round();
+ let block_count = if superellipse { 6 } else { 4 };
+ let mut writer = gpu_buffer.write_blocks(block_count);
writer.push_one(clip_rect);
writer.push_one([
radius.top_left.width,
radius.top_left.height,
radius.top_right.width,
@@ -1838,19 +1840,21 @@
radius.bottom_left.height,
radius.bottom_right.width,
radius.bottom_right.height,
]);
writer.push_one([mode as i32 as f32, 0.0, 0.0, 0.0]);
- writer.push_one([
- radius.shape_top_left,
- radius.shape_top_right,
- radius.shape_bottom_right,
- radius.shape_bottom_left,
- ]);
- writer.push_one(inset);
+ if superellipse {
+ writer.push_one([
+ radius.shape_top_left,
+ radius.shape_top_right,
+ radius.shape_bottom_right,
+ radius.shape_bottom_left,
+ ]);
+ writer.push_one(inset);
+ }
- (writer.finish(), false)
+ (writer.finish(), false, superellipse)
}
}
pub fn prepare_clip_task(
clip_instance: &ClipNodeInstance,
@@ -1865,11 +1869,11 @@
gpu_buffer: &mut GpuBufferBuilderF,
transforms: &mut TransformPalette,
rg_builder: &mut RenderTaskGraphBuilder,
sub_tasks: &mut SubTaskRange,
) {
- let (clip_address, fast_path) = match clip_item.kind {
+ let (clip_address, fast_path, superellipse) = match clip_item.kind {
ClipItemKind::RoundedRectangle { radius, inset, mode } => {
write_rounded_rect_clip_blocks(
gpu_buffer,
clip_instance.clip_rect,
&radius,
@@ -1882,11 +1886,11 @@
writer.push_one(clip_instance.clip_rect);
writer.push_one([0.0, 0.0, 0.0, 0.0]);
writer.push_one([mode as i32 as f32, 0.0, 0.0, 0.0]);
let clip_address = writer.finish();
- (clip_address, true)
+ (clip_address, true, false)
}
ClipItemKind::Image { .. } => {
let transform_id = transforms.gpu.get_id_with_post_scale(
clip_instance.spatial_node_index,
raster_spatial_node_index,
@@ -2033,10 +2037,11 @@
clip_transform_id,
quad_flags,
clip_space,
needs_scissor_rect,
rounded_rect_fast_path: fast_path,
+ rounded_rect_superellipse: superellipse,
}),
);
}
fn create_quad_primitive(
diff --git a/gfx/wr/webrender/src/render_target.rs b/gfx/wr/webrender/src/render_target.rs
--- a/gfx/wr/webrender/src/render_target.rs
+++ b/gfx/wr/webrender/src/render_target.rs
@@ -441,11 +441,11 @@
// so the target must be initialized to one.
self.clears.push((target_rect, ColorF::WHITE));
let device_rect = DeviceRect::from_size(target_rect.size().to_f32());
- let (clip_address, fast_path) = quad::write_rounded_rect_clip_blocks(
+ let (clip_address, fast_path, superellipse) = quad::write_rounded_rect_clip_blocks(
&mut gpu_buffer_builder.f32,
region_task.clip_rect,
&region_task.radius,
region_task.inset,
region_task.mode,
@@ -495,10 +495,12 @@
unused: 0,
};
if fast_path {
self.clip_masks.mask_instances_fast.push(instance);
+ } else if superellipse {
+ self.clip_masks.mask_instances_superellipse.push(instance);
} else {
self.clip_masks.mask_instances_slow.push(instance);
}
},
);
@@ -925,19 +927,26 @@
if task.rounded_rect_fast_path {
results.mask_instances_fast_with_scissor
.entry(*target_rect)
.or_insert_with(|| memory.new_vec())
.push(instance);
+ } else if task.rounded_rect_superellipse {
+ results.mask_instances_superellipse_with_scissor
+ .entry(*target_rect)
+ .or_insert_with(|| memory.new_vec())
+ .push(instance);
} else {
results.mask_instances_slow_with_scissor
.entry(*target_rect)
.or_insert_with(|| memory.new_vec())
.push(instance);
}
} else {
if task.rounded_rect_fast_path {
results.mask_instances_fast.push(instance);
+ } else if task.rounded_rect_superellipse {
+ results.mask_instances_superellipse.push(instance);
} else {
results.mask_instances_slow.push(instance);
}
}
}
diff --git a/gfx/wr/webrender/src/render_task.rs b/gfx/wr/webrender/src/render_task.rs
--- a/gfx/wr/webrender/src/render_task.rs
+++ b/gfx/wr/webrender/src/render_task.rs
@@ -2240,10 +2240,11 @@
/// Transform from the quad primitive's space to the clip's local space.
pub clip_transform_id: GpuTransformId,
pub quad_flags: QuadFlags,
pub needs_scissor_rect: bool,
pub rounded_rect_fast_path: bool,
+ pub rounded_rect_superellipse: bool,
}
/// An clip applied to a render task using the multiply blend mode on top of
/// the content being clipped.
#[derive(Debug)]
diff --git a/gfx/wr/webrender/src/renderer/mod.rs b/gfx/wr/webrender/src/renderer/mod.rs
--- a/gfx/wr/webrender/src/renderer/mod.rs
+++ b/gfx/wr/webrender/src/renderer/mod.rs
@@ -2450,10 +2450,54 @@
}
self.device.disable_scissor();
}
+ if !masks.mask_instances_superellipse.is_empty() {
+ self.shaders.borrow_mut().ps_mask_superellipse().bind(
+ &mut self.device,
+ projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ &mut self.command_log,
+ );
+
+ self.draw_instanced_batch(
+ &masks.mask_instances_superellipse,
+ VertexArrayKind::Mask,
+ &BatchTextures::empty(),
+ stats,
+ );
+ }
+
+ if !masks.mask_instances_superellipse_with_scissor.is_empty() {
+ self.shaders.borrow_mut().ps_mask_superellipse().bind(
+ &mut self.device,
+ projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ &mut self.command_log,
+ );
+
+ self.device.enable_scissor();
+
+ for (scissor_rect, instances) in &masks.mask_instances_superellipse_with_scissor {
+ self.device.set_scissor_rect(draw_target.to_framebuffer_rect(*scissor_rect));
+
+ self.draw_instanced_batch(
+ instances,
+ VertexArrayKind::Mask,
+ &BatchTextures::empty(),
+ stats,
+ );
+ }
+
+ self.device.disable_scissor();
+ }
+
if !masks.image_mask_instances.is_empty() {
self.shaders.borrow_mut().ps_quad_textured().bind(
&mut self.device,
projection,
None,
diff --git a/gfx/wr/webrender/src/renderer/shade.rs b/gfx/wr/webrender/src/renderer/shade.rs
--- a/gfx/wr/webrender/src/renderer/shade.rs
+++ b/gfx/wr/webrender/src/renderer/shade.rs
@@ -65,10 +65,11 @@
];
const DITHERING_FEATURE: &str = "DITHERING";
const DUAL_SOURCE_FEATURE: &str = "DUAL_SOURCE_BLENDING";
const FAST_PATH_FEATURE: &str = "FAST_PATH";
+const SUPERELLIPSE_FEATURE: &str = "SUPERELLIPSE";
pub(crate) enum ShaderKind {
Primitive,
Cache(VertexArrayKind),
Text,
@@ -449,10 +450,11 @@
ps_quad_backdrop: ShaderHandle,
ps_quad_blend: ShaderHandle,
ps_quad_mix_blend: ShaderHandle,
ps_mask: ShaderHandle,
ps_mask_fast: ShaderHandle,
+ ps_mask_superellipse: ShaderHandle,
ps_clear: ShaderHandle,
ps_copy: ShaderHandle,
composite: CompositorShaders,
}
@@ -514,10 +516,17 @@
"ps_quad_mask",
&[FAST_PATH_FEATURE],
&shader_list,
)?;
+ let ps_mask_superellipse = loader.create_shader(
+ ShaderKind::Cache(VertexArrayKind::Mask),
+ "ps_quad_mask",
+ &[SUPERELLIPSE_FEATURE],
+ &shader_list,
+ )?;
+
let mut cs_scale = Vec::new();
let scale_shader_num = IMAGE_BUFFER_KINDS.len();
// PrimitiveShader is not clonable. Use push() to initialize the vec.
for _ in 0 .. scale_shader_num {
cs_scale.push(None);
@@ -786,10 +795,11 @@
ps_quad_backdrop,
ps_quad_blend,
ps_quad_mix_blend,
ps_mask,
ps_mask_fast,
+ ps_mask_superellipse,
ps_split_composite,
ps_clear,
ps_copy,
composite,
})
@@ -963,10 +973,11 @@
pub fn ps_quad_textured(&mut self) -> &mut LazilyCompiledShader {
self.loader.get(self.ps_quad_textured)
}
pub fn ps_mask(&mut self) -> &mut LazilyCompiledShader { self.loader.get(self.ps_mask) }
pub fn ps_mask_fast(&mut self) -> &mut LazilyCompiledShader { self.loader.get(self.ps_mask_fast) }
+ pub fn ps_mask_superellipse(&mut self) -> &mut LazilyCompiledShader { self.loader.get(self.ps_mask_superellipse) }
pub fn ps_clear(&mut self) -> &mut LazilyCompiledShader { self.loader.get(self.ps_clear) }
pub fn ps_copy(&mut self) -> &mut LazilyCompiledShader { self.loader.get(self.ps_copy) }
pub fn deinit(self, device: &mut Device) {
self.loader.deinit(device);
diff --git a/gfx/wr/webrender_build/src/shader_features.rs b/gfx/wr/webrender_build/src/shader_features.rs
--- a/gfx/wr/webrender_build/src/shader_features.rs
+++ b/gfx/wr/webrender_build/src/shader_features.rs
@@ -65,11 +65,11 @@
let mut shaders = ShaderFeatures::new();
// Cache shaders
shaders.insert("cs_blur", vec!["ALPHA_TARGET".to_string(), "COLOR_TARGET".to_string()]);
- shaders.insert("ps_quad_mask", vec![String::new(), "FAST_PATH".to_string()]);
+ shaders.insert("ps_quad_mask", vec![String::new(), "FAST_PATH".to_string(), "SUPERELLIPSE".to_string()]);
for name in &[
"cs_line_decoration",
"cs_border_segment",
"cs_border_solid",

View File

@@ -0,0 +1,307 @@
diff --git a/gfx/wr/webrender/res/ps_quad_box_shadow.glsl b/gfx/wr/webrender/res/ps_quad_box_shadow.glsl
--- a/gfx/wr/webrender/res/ps_quad_box_shadow.glsl
+++ b/gfx/wr/webrender/res/ps_quad_box_shadow.glsl
@@ -49,22 +49,26 @@
flat varying highp vec4 vElemCenter_Radius_TL;
flat varying highp vec4 vElemCenter_Radius_TR;
flat varying highp vec4 vElemCenter_Radius_BR;
flat varying highp vec4 vElemCenter_Radius_BL;
+#ifdef WR_FEATURE_SUPERELLIPSE
flat varying highp vec4 vElemShape;
+#endif
#ifdef WR_VERTEX_SHADER
void pattern_vertex(PrimitiveInfo info) {
vec4 data0 = fetch_from_gpu_buffer_1f(info.pattern_input.x);
vec4 data1 = fetch_from_gpu_buffer_1f(info.pattern_input.x + 1);
vec4 data2 = fetch_from_gpu_buffer_1f(info.pattern_input.x + 2);
vec4 data3 = fetch_from_gpu_buffer_1f(info.pattern_input.x + 3);
vec4 data4 = fetch_from_gpu_buffer_1f(info.pattern_input.x + 4);
vec4 data5 = fetch_from_gpu_buffer_1f(info.pattern_input.x + 5);
+#ifdef WR_FEATURE_SUPERELLIPSE
vec4 data6 = fetch_from_gpu_buffer_1f(info.pattern_input.x + 6);
+#endif
vec2 alloc_size = data0.xy;
vec2 dest_rect_size = data0.zw;
vec2 dest_rect_off = data1.xy;
v_uv_scale_inset = vec4(vec2(1.0) / alloc_size, data1.z, 0.0);
@@ -84,11 +88,11 @@
// Map nine-patch UV=1.0 to the true content edge (uv_p0 + content_device_size)
// rather than the rounded atlas entry edge (info.segment.uv_rect.p1). The atlas
// allocation is rounded up from content_device_size, so this edge always lies
// inside the entry; using it keeps the mapping stable to sub-texel precision as
// the blur animates (bug 2002194).
- vec2 content_device_size = data6.xy;
+ vec2 content_device_size = data5.xy;
vec2 texture_size = vec2(TEX_SIZE(sColor0));
vec2 uv_p0 = info.segment.uv_rect.p0;
vec2 uv_p1 = uv_p0 + content_device_size;
v_uv_rect = vec4(uv_p0, uv_p1) / texture_size.xyxy;
v_uv_bounds = vec4(
@@ -110,11 +114,13 @@
vElemCenter_Radius_TL = vec4(elem_p0 + r_tl, r_tl);
vElemCenter_Radius_TR = vec4(elem_p1.x - r_tr.x, elem_p0.y + r_tr.y, r_tr);
vElemCenter_Radius_BR = vec4(elem_p1 - r_br, r_br);
vElemCenter_Radius_BL = vec4(elem_p0.x + r_bl.x, elem_p1.y - r_bl.y, r_bl);
- vElemShape = data5;
+#ifdef WR_FEATURE_SUPERELLIPSE
+ vElemShape = data6;
+#endif
}
#endif
#ifdef WR_FRAGMENT_SHADER
@@ -170,22 +176,24 @@
vec3 elem_plane_bl = vec3(n_bl, dot(n_bl, vec2(c_bl.x, c_bl.y + r_bl.y)));
// Reconstruct the element rect bounds from the TL and BR corner data.
vec4 elem_bounds = vec4(c_tl - r_tl, c_br + r_br);
- vec4 elem_shape = vElemShape;
-
float elem_dist;
+#ifdef WR_FEATURE_SUPERELLIPSE
+ vec4 elem_shape = vElemShape;
if (elem_shape == vec4(1.0)) {
+#endif
elem_dist = distance_to_rounded_rect(
local_pos,
elem_plane_tl, vec4(c_tl, inverse_radii_squared(r_tl)),
elem_plane_tr, vec4(c_tr, inverse_radii_squared(r_tr)),
elem_plane_br, vec4(c_br, inverse_radii_squared(r_br)),
elem_plane_bl, vec4(c_bl, inverse_radii_squared(r_bl)),
elem_bounds
);
+#ifdef WR_FEATURE_SUPERELLIPSE
} else {
elem_dist = distance_to_shaped_rect(
local_pos,
vec4(c_tl, elem_shape.x == 1.0 ? inverse_radii_squared(r_tl) : inverse_radii(r_tl)),
vec4(c_tr, elem_shape.y == 1.0 ? inverse_radii_squared(r_tr) : inverse_radii(r_tr)),
@@ -194,10 +202,11 @@
elem_bounds,
elem_shape,
vec4(0.0)
);
}
+#endif
// Outset (inset=0): dist < 0 = inside element → should be clipped out → use -elem_dist.
// Inset (inset=1): dist < 0 = inside element → should be kept → use elem_dist.
float element_clip = distance_aa(aa_range, mix(-elem_dist, elem_dist, inset));
diff --git a/gfx/wr/webrender/src/pattern/box_shadow.rs b/gfx/wr/webrender/src/pattern/box_shadow.rs
--- a/gfx/wr/webrender/src/pattern/box_shadow.rs
+++ b/gfx/wr/webrender/src/pattern/box_shadow.rs
@@ -41,11 +41,14 @@
_sub_rect: Option<DeviceRect>,
_offset: LayoutVector2D,
_ctx: &PatternBuilderContext,
state: &mut PatternBuilderState,
) -> Pattern {
- let mut writer = state.frame_gpu_data.f32.write_blocks(7);
+ let superellipse = !self.element_radius.shapes_all_round();
+
+ let block_count = if superellipse { 7 } else { 6 };
+ let mut writer = state.frame_gpu_data.f32.write_blocks(block_count);
writer.push_one([
self.shadow_rect_alloc_size.width,
self.shadow_rect_alloc_size.height,
self.dest_rect_size.width,
self.dest_rect_size.height,
@@ -72,26 +75,28 @@
self.element_radius.bottom_right.width,
self.element_radius.bottom_right.height,
self.element_radius.bottom_left.width,
self.element_radius.bottom_left.height,
]);
- writer.push_one([
- self.element_radius.shape_top_left,
- self.element_radius.shape_top_right,
- self.element_radius.shape_bottom_right,
- self.element_radius.shape_bottom_left,
- ]);
writer.push_one([
self.content_device_size.width,
self.content_device_size.height,
0.0,
0.0,
]);
+ if superellipse {
+ writer.push_one([
+ self.element_radius.shape_top_left,
+ self.element_radius.shape_top_right,
+ self.element_radius.shape_bottom_right,
+ self.element_radius.shape_bottom_left,
+ ]);
+ }
let addr = writer.finish();
Pattern {
- kind: PatternKind::BoxShadow,
+ kind: if superellipse { PatternKind::BoxShadowSuperellipse } else { PatternKind::BoxShadow },
shader_input: PatternShaderInput(addr.as_int(), 0),
texture_input: PatternTextureInput::new(self.render_task),
base_color: self.color,
is_opaque: false,
blend_mode: BlendMode::PremultipliedAlpha,
diff --git a/gfx/wr/webrender/src/pattern/mod.rs b/gfx/wr/webrender/src/pattern/mod.rs
--- a/gfx/wr/webrender/src/pattern/mod.rs
+++ b/gfx/wr/webrender/src/pattern/mod.rs
@@ -29,36 +29,37 @@
Gradient = 1,
Repeat = 2,
Mask = 3,
BoxShadow = 4,
+ BoxShadowSuperellipse = 5,
// Variants of ColorOrTexture that use a non-default sampler type
// (samplerExternalOES / __samplerExternal2DY2YEXT / sampler2DRect). The
// quad shader is compiled in matching per-kind variants; see
// ps_quad_textured.glsl.
- TextureExternal = 5,
- TextureExternalBT709 = 6,
- TextureRect = 7,
+ TextureExternal = 6,
+ TextureExternalBT709 = 7,
+ TextureRect = 8,
// Samples up to three planes (sColor0/1/2) and converts from YUV to RGB.
// Like ColorOrTexture, the YUV pattern comes in sampler-type-specific
// variants so that the planes are sampled with the matching sColor
// declaration; see ps_quad_yuv.glsl. `Yuv` is the default (TEXTURE_2D).
- Yuv = 8,
- YuvTextureExternal = 9,
- YuvTextureExternalBT709 = 10,
- YuvTextureRect = 11,
+ Yuv = 9,
+ YuvTextureExternal = 10,
+ YuvTextureExternalBT709 = 11,
+ YuvTextureRect = 12,
// Samples a captured backdrop texture using a (bilerp) 4-corner uv quad.
- Backdrop = 12,
+ Backdrop = 13,
// Applies a filter to a sampled source texture; see ps_quad_blend.glsl.
- Blend = 13,
+ Blend = 14,
// Composites a picture over its backdrop with a software mix-blend-mode;
// samples two textures (backdrop + source). See ps_quad_mix_blend.glsl.
- MixBlend = 14,
+ MixBlend = 15,
// When adding patterns, don't forget to update the NUM_PATTERNS constant.
}
-pub const NUM_PATTERNS: u32 = 15;
+pub const NUM_PATTERNS: u32 = 16;
impl PatternKind {
pub fn from_u32(val: u32) -> Self {
assert!(val < NUM_PATTERNS);
unsafe { std::mem::transmute(val) }
diff --git a/gfx/wr/webrender/src/renderer/mod.rs b/gfx/wr/webrender/src/renderer/mod.rs
--- a/gfx/wr/webrender/src/renderer/mod.rs
+++ b/gfx/wr/webrender/src/renderer/mod.rs
@@ -251,10 +251,11 @@
BatchKind::Quad(PatternKind::TextureExternalBT709) => GPU_TAG_PRIMITIVE,
BatchKind::Quad(PatternKind::TextureRect) => GPU_TAG_PRIMITIVE,
BatchKind::Quad(PatternKind::Gradient) => GPU_TAG_GRADIENT,
BatchKind::Quad(PatternKind::Repeat) => GPU_TAG_REPEAT,
BatchKind::Quad(PatternKind::BoxShadow) => GPU_TAG_PRIMITIVE,
+ BatchKind::Quad(PatternKind::BoxShadowSuperellipse) => GPU_TAG_PRIMITIVE,
BatchKind::Quad(PatternKind::Yuv) => GPU_TAG_BRUSH_YUV_IMAGE,
BatchKind::Quad(PatternKind::YuvTextureExternal) => GPU_TAG_BRUSH_YUV_IMAGE,
BatchKind::Quad(PatternKind::YuvTextureExternalBT709) => GPU_TAG_BRUSH_YUV_IMAGE,
BatchKind::Quad(PatternKind::YuvTextureRect) => GPU_TAG_BRUSH_YUV_IMAGE,
BatchKind::Quad(PatternKind::Backdrop) => GPU_TAG_PRIMITIVE,
diff --git a/gfx/wr/webrender/src/renderer/shade.rs b/gfx/wr/webrender/src/renderer/shade.rs
--- a/gfx/wr/webrender/src/renderer/shade.rs
+++ b/gfx/wr/webrender/src/renderer/shade.rs
@@ -438,10 +438,11 @@
ps_quad_textured_external_bt709: Option<ShaderHandle>,
ps_quad_textured_rect: Option<ShaderHandle>,
ps_quad_repeat: ShaderHandle,
ps_quad_gradient: ShaderHandle,
ps_quad_box_shadow: ShaderHandle,
+ ps_quad_box_shadow_superellipse: ShaderHandle,
// ps_quad_yuv, like ps_quad_textured, comes in sampler-type-specific
// variants so the YUV planes are sampled with the matching sColor
// declaration. The variant is selected via PatternKind.
ps_quad_yuv: ShaderHandle,
ps_quad_yuv_external: Option<ShaderHandle>,
@@ -651,10 +652,17 @@
"ps_quad_box_shadow",
&[],
&shader_list,
)?;
+ let ps_quad_box_shadow_superellipse = loader.create_shader(
+ ShaderKind::Primitive,
+ "ps_quad_box_shadow",
+ &[SUPERELLIPSE_FEATURE],
+ &shader_list,
+ )?;
+
let ps_quad_yuv = loader.create_shader(
ShaderKind::Primitive,
"ps_quad_yuv",
&["TEXTURE_2D"],
&shader_list,
@@ -786,10 +794,11 @@
ps_quad_textured_external_bt709,
ps_quad_textured_rect,
ps_quad_repeat,
ps_quad_gradient,
ps_quad_box_shadow,
+ ps_quad_box_shadow_superellipse,
ps_quad_yuv,
ps_quad_yuv_external,
ps_quad_yuv_external_bt709,
ps_quad_yuv_rect,
ps_quad_backdrop,
@@ -867,10 +876,11 @@
PatternKind::TextureRect => self.ps_quad_textured_rect
.expect("bug: ps_quad_textured TEXTURE_RECT variant not loaded"),
PatternKind::Gradient => self.ps_quad_gradient,
PatternKind::Repeat => self.ps_quad_repeat,
PatternKind::BoxShadow => self.ps_quad_box_shadow,
+ PatternKind::BoxShadowSuperellipse => self.ps_quad_box_shadow_superellipse,
PatternKind::Yuv => self.ps_quad_yuv,
PatternKind::YuvTextureExternal => self.ps_quad_yuv_external
.expect("bug: ps_quad_yuv TEXTURE_EXTERNAL variant not loaded"),
PatternKind::YuvTextureExternalBT709 => self.ps_quad_yuv_external_bt709
.expect("bug: ps_quad_yuv TEXTURE_EXTERNAL_BT709 variant not loaded"),
@@ -923,10 +933,13 @@
self.ps_quad_repeat
}
BatchKind::Quad(PatternKind::BoxShadow) => {
self.ps_quad_box_shadow
}
+ BatchKind::Quad(PatternKind::BoxShadowSuperellipse) => {
+ self.ps_quad_box_shadow_superellipse
+ }
BatchKind::Quad(PatternKind::Yuv) => {
self.ps_quad_yuv
}
BatchKind::Quad(PatternKind::YuvTextureExternal) => {
self.ps_quad_yuv_external
diff --git a/gfx/wr/webrender_build/src/shader_features.rs b/gfx/wr/webrender_build/src/shader_features.rs
--- a/gfx/wr/webrender_build/src/shader_features.rs
+++ b/gfx/wr/webrender_build/src/shader_features.rs
@@ -166,11 +166,11 @@
}
shaders.insert("ps_quad_textured", ps_quad_textured_features);
shaders.insert("ps_quad_repeat", vec![base_prim_features.finish()]);
- shaders.insert("ps_quad_box_shadow", vec![base_prim_features.finish()]);
+ shaders.insert("ps_quad_box_shadow", vec![base_prim_features.finish(), "SUPERELLIPSE".to_string()]);
// Like ps_quad_textured, ps_quad_yuv needs per-texture-kind variants so that
// the YUV planes are sampled with the correct sampler type.
let mut ps_quad_yuv_features: Vec<String> = vec!["TEXTURE_2D".to_string()];
if flags.contains(ShaderFeatureFlags::GL) {

View File

@@ -0,0 +1,560 @@
diff --git a/gfx/wr/webrender/res/border_shared.glsl b/gfx/wr/webrender/res/border_shared.glsl
--- a/gfx/wr/webrender/res/border_shared.glsl
+++ b/gfx/wr/webrender/res/border_shared.glsl
@@ -27,25 +27,33 @@
vec4 rect;
vec4 color0;
vec4 color1;
vec2 widths;
vec2 radii;
+#ifdef WR_FEATURE_SUPERELLIPSE
float shape;
vec2 shape_offset;
vec2 inset;
+#endif
};
BorderInstanceGpuData fetch_gpu_data(int index) {
BorderInstanceGpuData data;
+#ifdef WR_FEATURE_SUPERELLIPSE
vec4 texels[6] = fetch_from_gpu_buffer_6f(index);
+#else
+ vec4 texels[4] = fetch_from_gpu_buffer_4f(index);
+#endif
data.rect = texels[0];
data.color0 = texels[1];
data.color1 = texels[2];
data.widths = texels[3].xy;
data.radii = texels[3].zw;
+#ifdef WR_FEATURE_SUPERELLIPSE
data.shape = texels[4].x;
data.shape_offset = texels[4].yz;
data.inset = texels[5].xy;
+#endif
return data;
}
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
@@ -30,11 +30,13 @@
flat varying highp vec4 vClipCenter_Sign;
// An outer and inner elliptical radii for border
// corner clipping.
flat varying highp vec4 vClipRadii;
+#ifdef WR_FEATURE_SUPERELLIPSE
flat varying highp vec4 vClipOffsets;
+#endif
// Reference point for determine edge clip lines.
flat varying highp vec4 vEdgeReference;
// Stores widths/2 and widths/3 to save doing this in FS.
@@ -192,26 +194,34 @@
vColor00 = color0[0];
vColor01 = color0[1];
vec4[2] color1 = get_colors_for_side(data.color1, style1);
vColor10 = color1[0];
vColor11 = color1[1];
+#ifdef WR_FEATURE_SUPERELLIPSE
vClipCenter_Sign = vec4(outer + clip_sign * (data.radii + data.shape_offset), clip_sign);
+#else
+ vClipCenter_Sign = vec4(outer + clip_sign * data.radii, clip_sign);
+#endif
vClipRadii = vec4(data.radii, max(data.radii - data.widths, 0.0));
+#ifdef WR_FEATURE_SUPERELLIPSE
vClipOffsets = vec4(0.0);
+#endif
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;
+#ifdef WR_FEATURE_SUPERELLIPSE
if (data.shape != 1.0)
{
vec2 reference_radii = (data.radii == vec2(0.0)) ? vec2(0.0) : data.radii + data.inset;
vec4 contour1 = compute_contoured_superellipse(reference_radii, data.shape, data.inset);
vec4 contour2 = compute_contoured_superellipse(reference_radii, data.shape, data.inset + data.widths);
vClipOffsets = vec4(contour1.xy, contour2.xy);
vClipRadii = vec4(contour1.zw, contour2.zw);
}
+#endif
// For the case of dot and dash clips, optimize the number of pixels that
// are hit to just include the dot itself.
if (clip_mode == CLIP_DOT) {
float radius = aClipParams1.z;
@@ -258,19 +268,22 @@
// also 0.67 of the radii. Use these to form a
// SDF subtraction which will clip out the inside
// third of the rounded edge.
float d_radii_a;
float d_radii_b;
+#ifdef WR_FEATURE_SUPERELLIPSE
if (data.shape == 1.0) {
+#endif
d_radii_a = distance_to_ellipse(
clip_relative_pos,
clip_radii.xy - vPartialWidths.xy
);
d_radii_b = distance_to_ellipse(
clip_relative_pos,
clip_radii.xy - 2.0 * vPartialWidths.xy
);
+#ifdef WR_FEATURE_SUPERELLIPSE
} else {
d_radii_a = distance_to_superellipse(
clip_relative_pos - mix(vClipOffsets.xy, vClipOffsets.zw, 1.0 / 3.0),
mix(clip_radii.xy, clip_radii.zw, 1.0 / 3.0),
data.shape
@@ -286,24 +299,28 @@
d_radii_a = max(d_radii_a, -min(included_region_a.x, included_region_a.y));
vec2 included_region_b = data.radii - 2.0 * vPartialWidths.xy - clip_relative_pos;
d_radii_b = max(d_radii_b, -min(included_region_b.x, included_region_b.y));
}
+#endif
float d = min(-d_radii_a, d_radii_b);
color0 *= distance_aa(aa_range, d);
break;
}
case BORDER_STYLE_GROOVE:
case BORDER_STYLE_RIDGE: {
float d;
+#ifdef WR_FEATURE_SUPERELLIPSE
if (data.shape == 1.0) {
+#endif
d = distance_to_ellipse(
clip_relative_pos,
clip_radii.xy - vPartialWidths.zw
);
+#ifdef WR_FEATURE_SUPERELLIPSE
} else {
d = distance_to_superellipse(
clip_relative_pos - mix(vClipOffsets.xy, vClipOffsets.zw, 0.5),
mix(clip_radii.xy, clip_radii.zw, 0.5),
data.shape
@@ -311,10 +328,11 @@
// Clamp to the middle of the adjacent borders
vec2 included_region = data.radii - vPartialWidths.zw - clip_relative_pos;
d = max(d, -min(included_region.x, included_region.y));
}
+#endif
float alpha = distance_aa(aa_range, d);
float swizzled_factor;
switch (segment) {
case SEGMENT_TOP_LEFT: swizzled_factor = 0.0; break;
case SEGMENT_TOP_RIGHT: swizzled_factor = mix_factor; break;
@@ -435,22 +453,26 @@
if (in_clip_region) {
float d_radii_a;
float d_radii_b;
+#ifdef WR_FEATURE_SUPERELLIPSE
if (data.shape == 1.0) {
+#endif
d_radii_a = distance_to_ellipse(clip_relative_pos, vClipRadii.xy);
d_radii_b = distance_to_ellipse(clip_relative_pos, vClipRadii.zw);
+#ifdef WR_FEATURE_SUPERELLIPSE
} else {
clip_relative_pos = abs(clip_relative_pos) - data.shape_offset;
d_radii_a = distance_to_superellipse(clip_relative_pos - vClipOffsets.xy, vClipRadii.xy, data.shape);
d_radii_b = distance_to_superellipse(clip_relative_pos - vClipOffsets.zw, vClipRadii.zw, data.shape);
// exclude the straight border part from the subtracted region
vec2 included_region = data.radii - data.widths - clip_relative_pos;
d_radii_b = max(d_radii_b, -min(included_region.x, included_region.y));
}
+#endif
float d_radii = max(d_radii_a, -d_radii_b);
d = max(d, d_radii);
color0 = 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
@@ -31,11 +31,13 @@
flat varying highp vec4 vClipCenter_Sign;
// An outer and inner elliptical radii for border
// corner clipping.
flat varying highp vec4 vClipRadii;
+#ifdef WR_FEATURE_SUPERELLIPSE
flat varying highp vec4 vClipOffsets;
+#endif
// Position, scale, and radii of horizontally and vertically adjacent corner clips.
flat varying highp vec4 vHorizontalClipCenter_Sign;
flat varying highp vec2 vHorizontalClipRadii;
flat varying highp vec4 vVerticalClipCenter_Sign;
@@ -74,11 +76,11 @@
void main(void) {
BorderInstanceGpuData data = fetch_gpu_data(aGpuDataAddress);
vGpuDataAddress.x = aGpuDataAddress;
int segment = aFlags & 0xff;
- bool do_aa = ((aFlags >> 24) & 0xf0) != 0;
+ bool do_aa = ((aFlags >> 28) & 0x1) != 0;
vec2 outer_scale = get_outer_corner_scale(segment);
vec2 size = data.rect.zw - data.rect.xy;
vec2 outer = outer_scale * size;
vec2 clip_sign = 1.0 - 2.0 * outer_scale;
@@ -100,23 +102,31 @@
vMixColors.x = mix_colors;
vPos = size * aPosition.xy;
vColor0 = data.color0;
vColor1 = data.color1;
+#ifdef WR_FEATURE_SUPERELLIPSE
vClipCenter_Sign = vec4(outer + clip_sign * (data.radii + data.shape_offset), clip_sign);
+#else
+ vClipCenter_Sign = vec4(outer + clip_sign * data.radii, clip_sign);
+#endif
vClipRadii = vec4(data.radii, max(data.radii - data.widths, 0.0));
+#ifdef WR_FEATURE_SUPERELLIPSE
vClipOffsets = vec4(0.0);
+#endif
vColorLine = vec4(outer, data.widths.y * -clip_sign.y, data.widths.x * clip_sign.x);
+#ifdef WR_FEATURE_SUPERELLIPSE
if (data.shape != 1.0)
{
vec2 reference_radii = (data.radii == vec2(0.0)) ? vec2(0.0) : data.radii + data.inset;
vec4 contour1 = compute_contoured_superellipse(reference_radii, data.shape, data.inset);
vec4 contour2 = compute_contoured_superellipse(reference_radii, data.shape, data.inset + data.widths);
vClipOffsets = vec4(contour1.xy, contour2.xy);
vClipRadii = vec4(contour1.zw, contour2.zw);
}
+#endif
vec2 horizontal_clip_sign = vec2(-clip_sign.x, clip_sign.y);
vHorizontalClipCenter_Sign = vec4(aClipParams1.xy +
horizontal_clip_sign * aClipParams1.zw,
horizontal_clip_sign);
@@ -156,22 +166,26 @@
float d = -1.0;
if (in_clip_region) {
float d_radii_a;
float d_radii_b;
+#ifdef WR_FEATURE_SUPERELLIPSE
if (data.shape == 1.0) {
+#endif
d_radii_a = distance_to_ellipse(clip_relative_pos, vClipRadii.xy);
d_radii_b = distance_to_ellipse(clip_relative_pos, vClipRadii.zw);
+#ifdef WR_FEATURE_SUPERELLIPSE
} else {
clip_relative_pos = abs(clip_relative_pos) - data.shape_offset;
d_radii_a = distance_to_superellipse(clip_relative_pos - vClipOffsets.xy, vClipRadii.xy, data.shape);
d_radii_b = distance_to_superellipse(clip_relative_pos - vClipOffsets.zw, vClipRadii.zw, data.shape);
// exclude the straight border part from the subtracted region
vec2 included_region = data.radii - data.widths - clip_relative_pos;
d_radii_b = max(d_radii_b, -min(included_region.x, included_region.y));
}
+#endif
d = max(d_radii_a, -d_radii_b);
}
// And again for horizontally-adjacent corner
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
@@ -855,14 +855,17 @@
h_adjacent_corner_radius: DeviceSize,
v_adjacent_corner_outer: DevicePoint,
v_adjacent_corner_radius: DeviceSize,
gpu_buffer_builder: &mut GpuBufferBuilderF,
) {
+ let superellipse = shape != 1.0;
+
let base_flags = (segment as i32) |
((style0 as i32) << 8) |
((style1 as i32) << 16) |
- ((do_aa as i32) << 28);
+ ((do_aa as i32) << 28) |
+ ((superellipse as i32) << 29);
let instance_gpu_data = BorderInstanceGpuData {
local_rect: task_rect,
color0: color0.premultiplied(),
color1: color1.premultiplied(),
@@ -875,11 +878,11 @@
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)
+ gpu_data_address: instance_gpu_data.write(superellipse, gpu_buffer_builder)
};
match segment {
BorderSegment::TopLeft |
BorderSegment::TopRight |
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,18 +192,21 @@
pub shape_offset: DeviceSize,
pub inset: DeviceSize,
}
impl BorderInstanceGpuData {
- pub fn write(&self, gpu_buffer_builder: &mut GpuBufferBuilderF) -> GpuBufferAddress {
- let mut writer = gpu_buffer_builder.write_blocks(6);
+ pub fn write(&self, superellipse: bool, gpu_buffer_builder: &mut GpuBufferBuilderF) -> GpuBufferAddress {
+ let block_count = if superellipse { 6 } else { 4 };
+ let mut writer = gpu_buffer_builder.write_blocks(block_count);
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, self.shape_offset.width, self.shape_offset.height, 0.0]);
- writer.push_one([self.inset.width, self.inset.height, 0.0, 0.0]);
+ if superellipse {
+ writer.push_one([self.shape, self.shape_offset.width, self.shape_offset.height, 0.0]);
+ writer.push_one([self.inset.width, self.inset.height, 0.0, 0.0]);
+ }
writer.finish()
}
}
diff --git a/gfx/wr/webrender/src/render_target.rs b/gfx/wr/webrender/src/render_target.rs
--- a/gfx/wr/webrender/src/render_target.rs
+++ b/gfx/wr/webrender/src/render_target.rs
@@ -33,10 +33,11 @@
use crate::spatial_tree::SpatialNodeIndex;
const STYLE_SOLID: i32 = ((BorderStyle::Solid as i32) << 8) | ((BorderStyle::Solid as i32) << 16);
const STYLE_MASK: i32 = 0x00FF_FF00;
+const SUPERELLIPSE_MASK: i32 = 1 << 29;
/// A tag used to identify the output format of a `RenderTarget`.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
@@ -163,10 +164,12 @@
pub clip_masks: ClipMaskInstanceList,
pub border_segments_complex: FrameVec<BorderInstance>,
pub border_segments_solid: FrameVec<BorderInstance>,
+ pub border_segments_complex_superellipse: FrameVec<BorderInstance>,
+ pub border_segments_solid_superellipse: FrameVec<BorderInstance>,
pub line_decorations: FrameVec<LineDecorationJob>,
// Clearing render targets has a fair amount of special cases.
// The general rules are:
// - Depth (for at least the used potion of the target) is always cleared if it
@@ -223,10 +226,12 @@
prim_instances: std::array::from_fn(|_| FastHashMap::default()),
prim_instances_with_scissor: FastHashMap::default(),
clip_masks: ClipMaskInstanceList::new(memory),
border_segments_complex: memory.new_vec(),
border_segments_solid: memory.new_vec(),
+ border_segments_complex_superellipse: memory.new_vec(),
+ border_segments_solid_superellipse: memory.new_vec(),
clears: memory.new_vec(),
line_decorations: memory.new_vec(),
}
}
@@ -547,14 +552,23 @@
let instances = task_info.instances.clone();
for mut instance in instances {
// TODO(gw): It may be better to store the task origin in
// the render task data instead of per instance.
instance.task_origin = task_origin;
+ let superellipse = (instance.flags & SUPERELLIPSE_MASK) != 0;
if instance.flags & STYLE_MASK == STYLE_SOLID {
- self.border_segments_solid.push(instance);
+ if superellipse {
+ self.border_segments_solid_superellipse.push(instance);
+ } else {
+ self.border_segments_solid.push(instance);
+ }
} else {
- self.border_segments_complex.push(instance);
+ if superellipse {
+ self.border_segments_complex_superellipse.push(instance);
+ } else {
+ self.border_segments_complex.push(instance);
+ }
}
}
}
RenderTaskKind::Image(..) |
RenderTaskKind::Cached(..) |
diff --git a/gfx/wr/webrender/src/renderer/mod.rs b/gfx/wr/webrender/src/renderer/mod.rs
--- a/gfx/wr/webrender/src/renderer/mod.rs
+++ b/gfx/wr/webrender/src/renderer/mod.rs
@@ -3394,11 +3394,13 @@
draw_target,
);
// Draw any borders for this target.
if !target.border_segments_solid.is_empty() ||
- !target.border_segments_complex.is_empty()
+ !target.border_segments_complex.is_empty() ||
+ !target.border_segments_solid_superellipse.is_empty() ||
+ !target.border_segments_complex_superellipse.is_empty()
{
let _timer = self.gpu_profiler.start_timer(GPU_TAG_CACHE_BORDER);
self.set_blend(true, FramebufferKind::Other);
self.set_blend_mode_premultiplied_alpha(FramebufferKind::Other);
@@ -3437,10 +3439,46 @@
&BatchTextures::empty(),
stats,
);
}
+ if !target.border_segments_solid_superellipse.is_empty() {
+ self.shaders.borrow_mut().cs_border_solid_superellipse().bind(
+ &mut self.device,
+ &projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ &mut self.command_log,
+ );
+
+ self.draw_instanced_batch(
+ &target.border_segments_solid_superellipse,
+ VertexArrayKind::Border,
+ &BatchTextures::empty(),
+ stats,
+ );
+ }
+
+ if !target.border_segments_complex_superellipse.is_empty() {
+ self.shaders.borrow_mut().cs_border_segment_superellipse().bind(
+ &mut self.device,
+ &projection,
+ None,
+ &mut self.renderer_errors,
+ &mut self.profile,
+ &mut self.command_log,
+ );
+
+ self.draw_instanced_batch(
+ &target.border_segments_complex_superellipse,
+ VertexArrayKind::Border,
+ &BatchTextures::empty(),
+ stats,
+ );
+ }
+
self.set_blend(false, FramebufferKind::Other);
}
// Draw any line decorations for this target.
if !target.line_decorations.is_empty() {
diff --git a/gfx/wr/webrender/src/renderer/shade.rs b/gfx/wr/webrender/src/renderer/shade.rs
--- a/gfx/wr/webrender/src/renderer/shade.rs
+++ b/gfx/wr/webrender/src/renderer/shade.rs
@@ -413,10 +413,12 @@
// draw intermediate results to cache targets. The results
// of these shaders are then used by the primitive shaders.
cs_blur_rgba8: ShaderHandle,
cs_border_segment: ShaderHandle,
cs_border_solid: ShaderHandle,
+ cs_border_segment_superellipse: ShaderHandle,
+ cs_border_solid_superellipse: ShaderHandle,
cs_scale: Vec<Option<ShaderHandle>>,
cs_line_decoration: ShaderHandle,
cs_svg_filter_node: ShaderHandle,
// The are "primitive shaders". These shaders draw and blend
@@ -774,19 +776,35 @@
"cs_border_solid",
&[],
&shader_list,
)?;
+ let cs_border_segment_superellipse = loader.create_shader(
+ ShaderKind::Cache(VertexArrayKind::Border),
+ "cs_border_segment",
+ &[SUPERELLIPSE_FEATURE],
+ &shader_list,
+ )?;
+
+ let cs_border_solid_superellipse = loader.create_shader(
+ ShaderKind::Cache(VertexArrayKind::Border),
+ "cs_border_solid",
+ &[SUPERELLIPSE_FEATURE],
+ &shader_list,
+ )?;
+
let composite = CompositorShaders::new(device, gl_type, &mut loader)?;
Ok(Shaders {
loader,
cs_blur_rgba8,
cs_border_segment,
- cs_line_decoration,
cs_border_solid,
+ cs_border_segment_superellipse,
+ cs_border_solid_superellipse,
+ cs_line_decoration,
cs_scale,
cs_svg_filter_node,
ps_text_run,
ps_text_run_dual_source,
ps_quad_textured,
@@ -979,10 +997,12 @@
}
pub fn cs_blur_rgba8(&mut self) -> &mut LazilyCompiledShader { self.loader.get(self.cs_blur_rgba8) }
pub fn cs_border_segment(&mut self) -> &mut LazilyCompiledShader { self.loader.get(self.cs_border_segment) }
pub fn cs_border_solid(&mut self) -> &mut LazilyCompiledShader { self.loader.get(self.cs_border_solid) }
+ pub fn cs_border_segment_superellipse(&mut self) -> &mut LazilyCompiledShader { self.loader.get(self.cs_border_segment_superellipse) }
+ pub fn cs_border_solid_superellipse(&mut self) -> &mut LazilyCompiledShader { self.loader.get(self.cs_border_solid_superellipse) }
pub fn cs_line_decoration(&mut self) -> &mut LazilyCompiledShader { self.loader.get(self.cs_line_decoration) }
pub fn cs_svg_filter_node(&mut self) -> &mut LazilyCompiledShader { self.loader.get(self.cs_svg_filter_node) }
pub fn ps_quad_textured(&mut self) -> &mut LazilyCompiledShader {
self.loader.get(self.ps_quad_textured)
}
diff --git a/gfx/wr/webrender_build/src/shader_features.rs b/gfx/wr/webrender_build/src/shader_features.rs
--- a/gfx/wr/webrender_build/src/shader_features.rs
+++ b/gfx/wr/webrender_build/src/shader_features.rs
@@ -69,17 +69,22 @@
shaders.insert("ps_quad_mask", vec![String::new(), "FAST_PATH".to_string(), "SUPERELLIPSE".to_string()]);
for name in &[
"cs_line_decoration",
- "cs_border_segment",
- "cs_border_solid",
"cs_svg_filter_node",
] {
shaders.insert(name, vec![String::new()]);
}
+ for name in &[
+ "cs_border_segment",
+ "cs_border_solid",
+ ] {
+ shaders.insert(name, vec![String::new(), "SUPERELLIPSE".to_string()]);
+ }
+
let mut base_prim_features = FeatureList::new();
let mut texture_types = vec!["TEXTURE_2D"];
if flags.contains(ShaderFeatureFlags::GL) {
texture_types.push("TEXTURE_RECT");

View File

@@ -0,0 +1,15 @@
diff --git a/devtools/client/responsive/test/browser/browser.toml b/devtools/client/responsive/test/browser/browser.toml
--- a/devtools/client/responsive/test/browser/browser.toml
+++ b/devtools/client/responsive/test/browser/browser.toml
@@ -275,7 +275,10 @@
skip-if = [
"os == 'win' && os_version == '11.26100' && arch == 'x86_64' && asan", # Bug 2016837
]
["browser_window_sizing.js"]
+skip-if = [
+ "os == 'win' && opt && standalone", # Bug 2063169
+]
["browser_zoom.js"]

View File

@@ -0,0 +1,34 @@
diff --git a/layout/painting/nsDisplayList.cpp b/layout/painting/nsDisplayList.cpp
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -4606,12 +4606,12 @@
wr::LayoutRect deviceClipRect = wr::ToLayoutRect(clipRect);
nscoord spread = shadow.spread.ToAppUnits();
float spreadRadius = float(spread) / float(appUnitsPerDevPixel);
- wr::BorderRadius borderRadius{};
- wr::BorderRadius shadowRadius{};
+ wr::BorderRadius borderRadius = wr::EmptyBorderRadius();
+ wr::BorderRadius shadowRadius = wr::EmptyBorderRadius();
if (hasBorderRadius) {
borderRadius = wr::ToBorderRadius(borderRadii);
if (spreadRadius) {
auto shadowRadii = borderRadii;
shadowRadii.AdjustOutwards(
@@ -4727,12 +4727,12 @@
float(shadow.base.blur.ToAppUnits()) / float(appUnitsPerDevPixel);
nscoord spread = shadow.spread.ToAppUnits();
float spreadRadius = spread / float(appUnitsPerDevPixel);
- wr::BorderRadius borderRadius{};
- wr::BorderRadius shadowRadius{};
+ wr::BorderRadius borderRadius = wr::EmptyBorderRadius();
+ wr::BorderRadius shadowRadius = wr::EmptyBorderRadius();
if (hasBorderRadius) {
borderRadius = wr::ToBorderRadius(innerRadii);
if (spreadRadius) {
RectCornerRadii shadowRadii = innerRadii;
shadowRadii.AdjustInwards(

View File

@@ -0,0 +1,53 @@
diff --git a/gfx/wr/webrender/res/ps_quad_mask.glsl b/gfx/wr/webrender/res/ps_quad_mask.glsl
--- a/gfx/wr/webrender/res/ps_quad_mask.glsl
+++ b/gfx/wr/webrender/res/ps_quad_mask.glsl
@@ -318,12 +318,30 @@
#endif
vec4 clip_dist =
mix(clip_rect, clip_rect.zwxy, lessThan(local_step, vec2(0.0)).xyxy)
- local_pos0.xyxy;
+
+ // Half-width, in local units, of the band around an edge over which
+ // distance_aa() produces partial coverage.
+ float aa_band = 0.5 * recip(aa_range);
+ // An axis the span does not step along has the same distance to its two
+ // edges for every pixel of the span, so the inside/outside test below
+ // cannot resolve an edge that runs parallel to the span and coverage ends
+ // up quantized to whole spans. Grow the rect by the AA band so that a span
+ // landing inside the band counts as inside, which leaves the corner
+ // selection below unchanged, and remember that the span is only partially
+ // covered so the opaque run can be dropped further down.
+ // `inset` is the distance from the span to the nearer of an axis' two
+ // edges, positive when the span is between them.
+ vec2 inset = min(-clip_dist.xy, clip_dist.zw);
+ bvec2 flat_axis = equal(local_step, vec2(0.0));
+ bvec2 partial = lessThan(inset, vec2(aa_band));
+ bool aa_span = (flat_axis.x && partial.x) || (flat_axis.y && partial.y);
+
clip_dist =
- mix(1.0e6 * step(0.0, clip_dist),
+ mix(1.0e6 * step(0.0, clip_dist - vec4(aa_band, aa_band, -aa_band, -aa_band)),
clip_dist * recip(local_step).xyxy,
notEqual(local_step, vec2(0.0)).xyxy);
float opaque_start = max(clip_dist.x, clip_dist.y);
float opaque_end = min(clip_dist.z, clip_dist.w);
@@ -404,10 +422,16 @@
aa_margin = max(aa_margin - max(aa_start - aa_end, 0.0), 0.0);
aa_start -= aa_margin;
aa_end += aa_margin;
+ if (aa_span) {
+ // No pixel of the span is fully covered, so collapse the opaque run and
+ // let the anti-aliased runs on either side of it cover the whole span.
+ opaque_end = min(opaque_end, opaque_start);
+ }
+
ivec4 steps = ivec4(clamp(
swgl_SpanLength -
swgl_StepSize *
vec4(floor(aa_start), ceil(opaque_start), floor(opaque_end), ceil(aa_end)),
0.0, swgl_SpanLength));

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,154 @@
diff --git a/toolkit/modules/subprocess/subprocess_shared_unix.js b/toolkit/modules/subprocess/subprocess_shared_unix.js
--- a/toolkit/modules/subprocess/subprocess_shared_unix.js
+++ b/toolkit/modules/subprocess/subprocess_shared_unix.js
@@ -52,16 +52,17 @@
close: [ctypes.default_abi, ctypes.int, ctypes.int /* fildes */],
dup: [ctypes.default_abi, ctypes.int, ctypes.int],
+ // Variadic arguments use a different calling convention on Apple silicon.
fcntl: [
ctypes.default_abi,
ctypes.int,
ctypes.int /* fildes */,
ctypes.int /* cmd */,
- ctypes.int /* ... */,
+ "...",
],
getcwd: [
ctypes.default_abi,
ctypes.char.ptr,
diff --git a/toolkit/modules/subprocess/subprocess_unix.sys.mjs b/toolkit/modules/subprocess/subprocess_unix.sys.mjs
--- a/toolkit/modules/subprocess/subprocess_unix.sys.mjs
+++ b/toolkit/modules/subprocess/subprocess_unix.sys.mjs
@@ -34,13 +34,13 @@
throw new Error("Unable to create pipe");
}
this.signalFd = fds[1];
- libc.fcntl(fds[0], LIBC.F_SETFL, LIBC.O_NONBLOCK);
- libc.fcntl(fds[0], LIBC.F_SETFD, LIBC.FD_CLOEXEC);
- libc.fcntl(fds[1], LIBC.F_SETFD, LIBC.FD_CLOEXEC);
+ libc.fcntl(fds[0], LIBC.F_SETFL, ctypes.int(LIBC.O_NONBLOCK));
+ libc.fcntl(fds[0], LIBC.F_SETFD, ctypes.int(LIBC.FD_CLOEXEC));
+ libc.fcntl(fds[1], LIBC.F_SETFD, ctypes.int(LIBC.FD_CLOEXEC));
this.call("init", [{ signalFd: fds[0] }]);
}
closePipe() {
diff --git a/toolkit/modules/subprocess/subprocess_unix.worker.js b/toolkit/modules/subprocess/subprocess_unix.worker.js
--- a/toolkit/modules/subprocess/subprocess_unix.worker.js
+++ b/toolkit/modules/subprocess/subprocess_unix.worker.js
@@ -349,13 +349,13 @@
our_pipes.push(new InputPipe(this, fds[1]));
} else {
our_pipes.push(new OutputPipe(this, fds[1]));
}
- libc.fcntl(fds[0], LIBC.F_SETFD, LIBC.FD_CLOEXEC);
- libc.fcntl(fds[1], LIBC.F_SETFD, LIBC.FD_CLOEXEC);
- libc.fcntl(fds[1], LIBC.F_SETFL, LIBC.O_NONBLOCK);
+ libc.fcntl(fds[0], LIBC.F_SETFD, ctypes.int(LIBC.FD_CLOEXEC));
+ libc.fcntl(fds[1], LIBC.F_SETFD, ctypes.int(LIBC.FD_CLOEXEC));
+ libc.fcntl(fds[1], LIBC.F_SETFL, ctypes.int(LIBC.O_NONBLOCK));
return fds[0];
};
their_pipes.set(0, pipe(false));
diff --git a/toolkit/modules/subprocess/test/xpcshell/test_subprocess_pipe_flags.js b/toolkit/modules/subprocess/test/xpcshell/test_subprocess_pipe_flags.js
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/subprocess/test/xpcshell/test_subprocess_pipe_flags.js
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(async function test_subprocess_pipe_flags() {
+ const { Subprocess, getSubprocessImplForTest } = ChromeUtils.importESModule(
+ "resource://gre/modules/Subprocess.sys.mjs"
+ );
+ const { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+ );
+ const constants = ChromeUtils.getLibcConstants();
+ const library = ctypes.open("a.out");
+ const fcntl = library.declare(
+ "fcntl",
+ ctypes.default_abi,
+ ctypes.int,
+ ctypes.int,
+ ctypes.int,
+ "..."
+ );
+ // Darwin's fcntl commands for reading descriptor and file status flags.
+ const F_GETFD = 1;
+ const F_GETFL = 3;
+ let process;
+
+ try {
+ const worker = getSubprocessImplForTest().Process.getWorker();
+ Assert.equal(
+ fcntl(worker.signalFd, F_GETFD),
+ constants.FD_CLOEXEC,
+ "The main-thread signal pipe has exactly FD_CLOEXEC set"
+ );
+
+ process = await Subprocess.call({
+ command: "/bin/cat",
+ stderr: "pipe",
+ disclaim: true,
+ });
+ const fds = await worker.call("getFds", [process.id]);
+ // Only inspect the descriptors while the worker owns them and cat is alive.
+ // Exact flags also rule out an unintended FD_CLOFORK on macOS.
+ for (const fd of fds) {
+ Assert.equal(
+ fcntl(fd, F_GETFD),
+ constants.FD_CLOEXEC,
+ "Worker pipes have exactly FD_CLOEXEC set"
+ );
+ Assert.equal(
+ fcntl(fd, F_GETFL) & constants.O_NONBLOCK,
+ constants.O_NONBLOCK,
+ "Worker pipes are nonblocking"
+ );
+ }
+
+ const output = process.stdout.readString(5);
+ await process.stdin.write("hello");
+ Assert.equal(await output, "hello", "The subprocess pipes transfer data");
+ await process.stdin.close();
+ Assert.equal(
+ (await process.wait()).exitCode,
+ 0,
+ "The subprocess exits cleanly"
+ );
+ } finally {
+ if (process && process.exitCode === null) {
+ await process.kill();
+ }
+ library.close();
+ }
+});
diff --git a/toolkit/modules/subprocess/test/xpcshell/xpcshell.toml b/toolkit/modules/subprocess/test/xpcshell/xpcshell.toml
--- a/toolkit/modules/subprocess/test/xpcshell/xpcshell.toml
+++ b/toolkit/modules/subprocess/test/xpcshell/xpcshell.toml
@@ -25,6 +25,9 @@
["test_subprocess_pathSearch.js"]
["test_subprocess_perf.js"]
requesttimeoutfactor = 2 # Slow on Windows
+["test_subprocess_pipe_flags.js"]
+run-if = ["os == 'mac'"]
+
["test_subprocess_polling.js"]

View File

@@ -7,6 +7,11 @@
"id": "D299584",
"name": "Native MacOS popovers fix"
},
{
"type": "phabricator",
"id": "D323933",
"name": "Issue 15123"
},
{
"type": "local",
// TODO: Convert into https://phabricator.services.mozilla.com/D298079
@@ -53,7 +58,14 @@
},
{
"type": "local",
"path": "firefox/css_corner_shape_rendering.patch"
// Sorts before bug_2063169/ on purpose: D319869 builds on the border
// shader hunks in here, and surfer applies patches in path order.
"path": "firefox/bug_2047627_corner_shape_borders.patch"
},
{
"type": "phabricator",
"ids": ["D319577", "D319796", "D319869", "D320211", "D320568", "D321282"],
"name": "Bug 2063169"
},
{
"type": "local",

View File

@@ -1521,7 +1521,7 @@ ${cssSelector} {
shuffleBoost() {
const availFonts = this.fetchFontList();
const commonFonts = this.commonFonts;
let font = commonFonts[Math.round(Math.random() * commonFonts.length)];
let font = commonFonts[Math.floor(Math.random() * commonFonts.length)];
if (availFonts.includes(font)) {
this.currentBoostData.fontFamily = font;
}

View File

@@ -649,9 +649,10 @@ class nsZenBoostsManager {
);
// Close the editor if the tab is switched
const onTabSelect = editor.close.bind(editor);
parentWindow.gBrowser.tabContainer.addEventListener(
"TabSelect",
editor.close.bind(editor),
onTabSelect,
{
once: true,
}
@@ -661,13 +662,24 @@ class nsZenBoostsManager {
onLocationChange: webProgress => {
if (webProgress.isTopLevel) {
editor.close();
parentWindow.gBrowser.removeTabsProgressListener(progressListener);
}
},
};
parentWindow.gBrowser.addProgressListener(progressListener);
editor.addEventListener(
"unload",
() => {
parentWindow.gBrowser.tabContainer.removeEventListener(
"TabSelect",
onTabSelect
);
parentWindow.gBrowser.removeProgressListener(progressListener);
},
{ once: true }
);
// Give the domain
editor.domain = domain;
editor.openerWindow = parentWindow;

View File

@@ -70,8 +70,10 @@ export class ZenBoostsChild extends JSWindowActorChild {
static PREVENTABLE_SET = new Set(ZenBoostsChild.PREVENTABLE_EVENTS);
actorCreated() {
this.#applyBoostForPageIfAvailable();
handleEvent(event) {
if (event.type === "DOMDocElementInserted") {
this.#applyBoostForPageIfAvailable();
}
}
didDestroy() {
@@ -330,6 +332,22 @@ export class ZenBoostsChild extends JSWindowActorChild {
return this.sendQuery("ZenBoost:GetBoostForDomain", domain);
}
/**
* Sets a synced BrowsingContext field only when its value changes. Every
* setter commits a transaction that is broadcast to the whole browsing
* context group, even when the value is unchanged, so this avoids three
* such broadcasts on every page load that has no boost.
*
* @param {BrowsingContext} browsingContext
* @param {string} field - The synced field name.
* @param {number | boolean} value
*/
#setSyncedField(browsingContext, field, value) {
if (browsingContext[field] !== value) {
browsingContext[field] = value;
}
}
/**
* Applies the boost settings for the current page if available.
*
@@ -360,7 +378,11 @@ export class ZenBoostsChild extends JSWindowActorChild {
sizeOverride: boostData.sizeOverride,
});
browsingContext.isZenBoostsInverted = boostData.smartInvert;
this.#setSyncedField(
browsingContext,
"isZenBoostsInverted",
!!boostData.smartInvert
);
if (boostData.enableColorBoost) {
let primaryColor;
if (boostData.autoTheme) {
@@ -396,18 +418,21 @@ export class ZenBoostsChild extends JSWindowActorChild {
boostData
);
}
browsingContext.zenBoostsData = primaryColor;
this.#setSyncedField(browsingContext, "zenBoostsData", primaryColor);
// The complementary accent is derived in the backend by rotating the
// primary accent's hue by this delta (in degrees).
browsingContext.zenBoostsComplementaryRotation =
boostData.secondaryDotAngleDegDelta ?? 0;
this.#setSyncedField(
browsingContext,
"zenBoostsComplementaryRotation",
boostData.secondaryDotAngleDegDelta ?? 0
);
return;
}
} else {
browsingContext.isZenBoostsInverted = false;
this.#setSyncedField(browsingContext, "isZenBoostsInverted", false);
}
browsingContext.zenBoostsData = 0;
browsingContext.zenBoostsComplementaryRotation = 0;
this.#setSyncedField(browsingContext, "zenBoostsData", 0);
this.#setSyncedField(browsingContext, "zenBoostsComplementaryRotation", 0);
}
/**

View File

@@ -28,6 +28,7 @@
"chrome://browser/content/zen-components/ZenEmojiPicker.mjs",
"chrome://browser/content/zen-components/ZenLiveFoldersUI.mjs",
"chrome://browser/content/zen-components/ZenDownloadAnimation.mjs",
"resource:///modules/zen/share/ZenShareManager.mjs",
];
for (let script of scripts) {

View File

@@ -257,7 +257,7 @@ window.gZenUIManager = {
};
},
updateTabsToolbar() {
updateTabsToolbar(fromResizeEvent = false) {
const kUrlbarHeight = 333;
gURLBar.style.setProperty(
"--zen-urlbar-top",
@@ -270,8 +270,8 @@ window.gZenUIManager = {
gZenVerticalTabsManager.actualWindowButtons.removeAttribute(
"zen-has-hover"
);
gZenVerticalTabsManager.recalculateURLBarHeight(true);
if (!this._preventToolbarRebuild) {
gZenVerticalTabsManager.recalculateURLBarHeight(!fromResizeEvent);
if (!this._preventToolbarRebuild && !fromResizeEvent) {
setTimeout(() => {
gZenWorkspaces.updateTabsContainers();
}, 0);
@@ -1278,8 +1278,16 @@ window.gZenVerticalTabsManager = {
if (gZenWorkspaces._processingResize) {
return;
}
this._pendingUrlbarFormatUpdate ||= updateFormat;
if (this._urlbarHeightRecalcScheduled) {
return;
}
this._urlbarHeightRecalcScheduled = true;
requestAnimationFrame(() => {
requestAnimationFrame(() => {
delete this._urlbarHeightRecalcScheduled;
const shouldUpdateFormat = this._pendingUrlbarFormatUpdate;
delete this._pendingUrlbarFormatUpdate;
gURLBar.removeAttribute("--urlbar-height");
let height;
if (!this._hasSetSingleToolbar) {
@@ -1290,7 +1298,7 @@ window.gZenVerticalTabsManager = {
if (typeof height !== "undefined") {
gURLBar.style.setProperty("--urlbar-height", `${height}px`);
}
if (updateFormat) {
if (shouldUpdateFormat) {
gURLBar.zenFormatURLValue();
}
});

View File

@@ -0,0 +1,33 @@
/*
* 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/.
*/
/* Upstream elements that take their radius from --border-radius-circle (9999px).
They are meant to read as circles or pills, so a superellipse only makes them
look lumpy. */
.tab-icon-overlay:not([crashed]):is([soundplaying], [muted], [activemedia-blocked]),
.tab-note-icon-overlay,
.tab-group-overflow-count,
.tab-group-editor-swatch,
.tab-preview-container-indicator,
#tabbrowser-tabs[orient='vertical']:not([expanded]) .tab-close-button,
.all-tabs-item > toolbarbutton.all-tabs-container-indicator::before,
.urlbarView-button,
.downloadButton,
.titlebar-button,
.titlebar-button .toolbarbutton-icon,
#downloads-indicator-progress-inner,
#smartwindow-ask-button-inner,
#PanelUI-fxa-menu-manage-account-button,
.fxa-menu-manage-account-avatar,
[fxastatus='signedin'] #fxa-avatar-image,
#profile-icon-image,
.appMenu-update-banner-pulse-ring,
:is(#appMenu-zoomReduce-button2, #appMenu-zoomEnlarge-button2) > .toolbarbutton-icon,
.color-swatches .userContext-icon,
.reusable-relay-masks > button,
.radio-check {
corner-shape: round;
}

View File

@@ -5,11 +5,17 @@
*/
:root:not([inDOMFullscreen="true"]):not([chromehidden~="location"]):not([chromehidden~="toolbar"]) {
& #tabbrowser-tabbox #tabbrowser-tabpanels .browserSidebarContainer {
--zen-transparent-browser-background: light-dark(rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.1));
overflow: clip;
:root:not([zen-no-padding="true"]) &:not(.zen-glance-overlay) {
border-radius: var(--zen-native-inner-radius);
box-shadow: var(--zen-big-shadow);
@media not (-moz-pref("zen.theme.squircle-browser-view")) {
/* stylelint-disable-next-line property-no-unknown */
corner-shape: round;
border-radius: calc(var(--zen-native-inner-radius) / var(--zen-squircle-value));
}
}
& browser[type="content"] {
@@ -18,7 +24,7 @@
}
&[transparent="true"] {
background: light-dark(rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.1));
background: var(--zen-transparent-browser-background);
}
}

View File

@@ -78,3 +78,52 @@ dialog::part(dialog-button) {
scale: 0.98;
}
}
:root {
--zen-accent-button-color: light-dark(
oklch(from var(--zen-primary-color) clamp(0.35, l, 0.6) c h),
oklch(from var(--zen-primary-color) clamp(0.7, l, 0.85) c h)
);
--zen-accent-button-background: light-dark(
oklch(from var(--zen-primary-color) clamp(0.35, l, 0.6) c h),
oklch(from var(--zen-primary-color) clamp(0.55, l, 0.7) c h)
);
}
.zen-big-accent-button {
appearance: none;
border: none;
border-radius: 12px;
padding: 1.2rem 1rem;
background: transparent;
color: var(--zen-accent-button-color);
font: inherit;
font-size: 1.2rem;
font-weight: 600;
cursor: pointer;
will-change: transform;
transition: scale 0.15s ease-out;
&:hover {
scale: 1.03;
filter: brightness(1.1);
}
&:active:hover {
scale: 1;
color: color-mix(in srgb, var(--zen-accent-button-color) 80%, transparent);
}
&.primary {
background: var(--zen-accent-button-background);
color: white;
&:hover {
filter: brightness(1.1);
}
&:active:hover {
color: rgba(255, 255, 255, 0.8);
}
}
}

View File

@@ -30,6 +30,13 @@ panel[type="arrow"]:not(#feature-callout) {
}
}
@media (-moz-platform: linux) or ((-moz-platform: windows) and (not (-moz-windows-mica-popups))) {
&::part(content) {
corner-shape: round;
border-radius: calc(var(--panel-border-radius) / var(--zen-squircle-value));
}
}
@media (-moz-platform: macos) {
&:is([nonnative], :not([type="arrow"])) {
appearance: auto !important;

View File

@@ -64,6 +64,10 @@ hbox.editBMPanel_folderRow {
margin-right: 10px;
}
#editBMPanel_workspaceSummary .workspace-trigger {
margin-inline-end: 16px;
}
/* Bookmarks in toolbar */
#PersonalToolbar {
background: transparent;

View File

@@ -101,7 +101,7 @@
) !important;
--focus-outline-color: var(--color-accent-primary) !important;
--toolbarbutton-icon-fill-attention: var(--toolbar-color) !important;
--toolbarbutton-icon-fill-attention: var(--toolbar-color, currentColor) !important;
--toolbarbutton-icon-fill: currentColor !important;
--button-background-color-primary: var(--in-content-primary-button-background) !important;
@@ -220,7 +220,7 @@
--zen-squircle-value: 1.3;
@media (-moz-platform: windows) {
--zen-squircle-value: 2.1;
--zen-squircle-value: 2.3;
}
--tab-icon-overlay-stroke: light-dark(white, black) !important;
@@ -361,4 +361,6 @@
.zen-squircle-after::after {
corner-shape: superellipse(var(--zen-squircle-value));
}
%include squircle-exclusions.inc.css
}

View File

@@ -42,7 +42,6 @@ let JSWINDOWACTORS = {
child: {
esModuleURI: "resource:///actors/ZenGlanceChild.sys.mjs",
events: {
DOMContentLoaded: {},
mousedown: {
capture: true,
},
@@ -86,9 +85,7 @@ if (!Services.appinfo.inSafeMode) {
child: {
esModuleURI: "resource:///actors/ZenBoostsChild.sys.mjs",
events: {
// Needed to let the actor be created, please don't remove
// without checking if boosts still work without it, thanks <3
DOMWindowCreated: {},
DOMDocElementInserted: {},
},
},
safeForUntrustedWebProcess: true,

View File

@@ -12,7 +12,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
class nsZenUIMigration {
PREF_NAME = "zen.ui.migration.version";
MIGRATION_VERSION = 7;
MIGRATION_VERSION = 8;
init(isNewProfile) {
if (!isNewProfile) {
@@ -164,6 +164,18 @@ class nsZenUIMigration {
Services.prefs.setBoolPref("zen.widget.macos.window-vibrancy", false);
}
}
_migrateV8() {
// A version mismatch in the downloadable gfx blocklist wrongly blocked
// video overlays for every Windows user. The status pref persists in the
// profile and short-circuits the blocklist evaluation, so clear it (and
// its failure id) and restart, since gfx is already initialized by now.
if (Services.prefs.prefHasUserValue("gfx.blacklist.video-overlay")) {
Services.prefs.clearUserPref("gfx.blacklist.video-overlay");
Services.prefs.clearUserPref("gfx.blacklist.video-overlay.failureid");
this.shouldRestart = true;
}
}
}
export var gZenUIMigration = new nsZenUIMigration();

View File

@@ -41,6 +41,9 @@ document.addEventListener(
case "cmd_zenSplitViewContextMenu":
gZenViewSplitter.contextSplitTabs();
break;
case "cmd_zenCtxShareSplitView":
gZenViewSplitter.contextShareSplitView();
break;
case "cmd_zenCopyCurrentURLMarkdown":
gZenCommonActions.copyCurrentURLAsMarkdownToClipboard();
break;
@@ -90,6 +93,9 @@ document.addEventListener(
case "cmd_zenCtxDeleteWorkspace":
gZenWorkspaces.contextDeleteWorkspace(event);
break;
case "cmd_zenCtxShareWorkspace":
gZenWorkspaces.contextShareWorkspace();
break;
case "cmd_zenChangeWorkspaceName":
gZenVerticalTabsManager.renameTabStart({
target: gZenWorkspaces.activeWorkspaceIndicator.querySelector(

View File

@@ -125,7 +125,7 @@
// Windows defaults to 8px
document.documentElement.style.setProperty(
"--zen-border-radius",
"8px"
"9px"
);
}
} else {

View File

@@ -477,9 +477,9 @@ window.gZenCompactModeManager = {
document.documentElement.setAttribute("zen-compact-animating", "true");
return new Promise(resolve => {
// We need to set the splitter width before hiding it
let splitterWidth = document
.getElementById("zen-sidebar-splitter")
.getBoundingClientRect().width;
let splitterWidth = window.windowUtils.getBoundsWithoutFlushing(
document.getElementById("zen-sidebar-splitter")
).width;
const isCompactMode = this.preference;
const canHideSidebar = this.canHideSidebar;
let canAnimate =

View File

@@ -52,7 +52,7 @@
--zen-compact-float: var(--zen-element-separation);
@media (-moz-mac-tahoe-theme) {
--zen-compact-mode-no-padding-radius-fix: -2px;
--zen-compact-mode-no-padding-radius-fix: -4px;
}
:root[zen-no-padding='true'] & {

View File

@@ -29,7 +29,7 @@
&,
&::before,
&::after {
border-radius: calc((var(--zen-native-inner-radius) - var(--zen-compact-mode-no-padding-radius-fix, 0px)) * var(--zen-squircle-value));
border-radius: calc((var(--zen-native-inner-radius) - var(--zen-compact-mode-no-padding-radius-fix, 0px)));
}
}

View File

@@ -1334,10 +1334,14 @@
dropElement = dragData.dropElement;
dropBefore = dragData.dropBefore;
}
// Essentials should be properly handled by ::animateVerticalPinnedGridDragOver
if (!dropElement || dropElement.hasAttribute("zen-essential")) {
if (!dropElement) {
this.clearDragOverVisuals();
// If dropElement is null or essential, dropElement should be the empty tab
return [undefined, null];
}
// Essentials should be properly handled by ::animateVerticalPinnedGridDragOver
if (dropElement.hasAttribute("zen-essential")) {
this.clearDragOverVisuals();
// If dropElement is essential, dropElement should be the empty tab
return [dropBefore, gZenWorkspaces._emptyTab];
}
if (dropElement.hasAttribute("zen-empty-tab") && dropElement.group) {

View File

@@ -183,6 +183,9 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
case "context_zenFolderChangeIcon":
this.changeFolderUserIcon(this.#lastFolderContextMenu);
break;
case "context_zenShareFolder":
gZenShareManager.shareFolder(this.#lastFolderContextMenu);
break;
}
});
}

View File

@@ -4,4 +4,4 @@
content/browser/zen-components/ZenFolder.mjs (../../zen/folders/ZenFolder.mjs)
content/browser/zen-components/ZenFolders.mjs (../../zen/folders/ZenFolders.mjs)
content/browser/zen-styles/zen-folders.css (../../zen/folders/zen-folders.css)
* content/browser/zen-styles/zen-folders.css (../../zen/folders/zen-folders.css)

View File

@@ -0,0 +1,19 @@
/* 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/. */
& g,
& rect,
& path {
transition:
transform 0.3s cubic-bezier(0.42, 0, 0, 1),
opacity 0.3s cubic-bezier(0.42, 0, 0, 1);
}
&[state="open"] .back {
transform: skewX(16deg) translate(-4px, 2px) scale(0.85);
}
&[state="open"] :is(.front, .dots, .icon) {
transform: skewX(-16deg) translate(8px, 2px) scale(0.85);
}

View File

@@ -8,15 +8,17 @@
display: none !important;
}
:root {
--zen-folder-behind-bgcolor: light-dark(color-mix(in srgb, var(--zen-primary-color) 60%, gray), color-mix(in srgb, var(--zen-primary-color) 60%, #c1c1c1));
--zen-folder-front-bgcolor: light-dark(color-mix(in srgb, var(--zen-primary-color), white 70%), color-mix(in srgb, var(--zen-primary-color), black 40%));
--zen-folder-stroke: light-dark(color-mix(in srgb, var(--zen-primary-color) 50%, black), color-mix(in srgb, var(--zen-colors-primary) 15%, #ebebeb));
}
zen-folder {
display: flex;
flex-direction: column;
visibility: visible;
--zen-folder-behind-bgcolor: light-dark(color-mix(in srgb, var(--zen-primary-color) 60%, gray), color-mix(in srgb, var(--zen-primary-color) 60%, #c1c1c1));
--zen-folder-front-bgcolor: light-dark(color-mix(in srgb, var(--zen-primary-color), white 70%), color-mix(in srgb, var(--zen-primary-color), black 40%));
--zen-folder-stroke: light-dark(color-mix(in srgb, var(--zen-primary-color) 50%, black), color-mix(in srgb, var(--zen-colors-primary) 15%, #ebebeb));
-moz-window-dragging: no-drag;
transition: var(--zen-tabbox-element-indent-transition);
@@ -114,21 +116,7 @@ zen-folder {
transform: translate(8.5px, 10.5px);
}
& g,
& rect,
& path {
transition:
transform 0.3s cubic-bezier(0.42, 0, 0, 1),
opacity 0.3s cubic-bezier(0.42, 0, 0, 1);
}
&[state="open"] .back {
transform: skewX(16deg) translate(-4px, 2px) scale(0.85);
}
&[state="open"] :is(.front, .dots, .icon) {
transform: skewX(-16deg) translate(8px, 2px) scale(0.85);
}
%include zen-folder-icon.inc.css
& .icon {
opacity: 1;

View File

@@ -392,7 +392,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
return Promise.resolve(this.#currentTab);
}
// Existing-tab glances perform no navigation and are exempt; for fresh
// 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)) {

View File

@@ -17,12 +17,18 @@ XPCOMUtils.defineLazyPreferenceGetter(
true
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"activationMethod",
"zen.glance.activation-method",
"ctrl"
);
// A small threshold to allow for minor mouse jitter during a normal click.
// Anything beyond this is likely an intentional drag (like selecting text).
const CLICK_DRAG_THRESHOLD_PX = 4;
export class ZenGlanceChild extends JSWindowActorChild {
#activationMethod;
#mouseDownX = null;
#mouseDownY = null;
@@ -37,12 +43,6 @@ export class ZenGlanceChild extends JSWindowActorChild {
}
}
async #initActivationMethod() {
this.#activationMethod = await this.sendQuery(
"ZenGlance:GetActivationMethod"
);
}
#ensureOnlyKeyModifiers(event) {
return !(event.ctrlKey ^ event.altKey ^ event.shiftKey ^ event.metaKey);
}
@@ -156,7 +156,7 @@ export class ZenGlanceChild extends JSWindowActorChild {
) {
return;
}
const activationMethod = this.#activationMethod;
const activationMethod = lazy.activationMethod;
if (activationMethod === "ctrl" && !event.ctrlKey) {
return;
} else if (activationMethod === "alt" && !event.altKey) {
@@ -184,8 +184,4 @@ export class ZenGlanceChild extends JSWindowActorChild {
this.contentWindow.document.body,
});
}
async on_DOMContentLoaded() {
await this.#initActivationMethod();
}
}

View File

@@ -11,12 +11,6 @@ export class ZenGlanceParent extends JSWindowActorParent {
async receiveMessage(message) {
switch (message.name) {
case "ZenGlance:GetActivationMethod": {
return Services.prefs.getStringPref(
"zen.glance.activation-method",
"ctrl"
);
}
case "ZenGlance:OpenGlance": {
this.openGlance(this.browsingContext.topChromeWindow, message.data);
break;

View File

@@ -105,6 +105,11 @@
.browserSidebarContainer.zen-glance-background,
.browserSidebarContainer.zen-glance-overlay .browserContainer:not([fade-out="true"]) {
border-radius: var(--zen-native-inner-radius);
@media not (-moz-pref("zen.theme.squircle-browser-view")) {
/* stylelint-disable-next-line property-no-unknown */
corner-shape: round;
border-radius: calc(var(--zen-native-inner-radius) / var(--zen-squircle-value));
}
}
.browserSidebarContainer.zen-glance-overlay {
@@ -144,6 +149,11 @@
border-radius: var(--zen-native-inner-radius);
overflow: hidden;
box-shadow: var(--zen-big-shadow);
@media not (-moz-pref("zen.theme.squircle-browser-view")) {
/* stylelint-disable-next-line property-no-unknown */
corner-shape: round;
border-radius: calc(var(--zen-native-inner-radius) / var(--zen-squircle-value));
}
}
& browser {

View File

@@ -524,13 +524,20 @@ class nsZenMods extends nsZenPreloadedFeature {
console.error("[ZenMods]: Error loading Zen Mods:", e);
}
Services.prefs.addObserver(
this.updatePref,
this.#rebuildModsStylesheet.bind(this)
);
Services.prefs.addObserver(
"zen.themes.disable-all",
this.#handleDisableMods.bind(this)
const rebuildObserver = this.#rebuildModsStylesheet.bind(this);
const disableObserver = this.#handleDisableMods.bind(this);
Services.prefs.addObserver(this.updatePref, rebuildObserver);
Services.prefs.addObserver("zen.themes.disable-all", disableObserver);
window.addEventListener(
"unload",
() => {
Services.prefs.removeObserver(this.updatePref, rebuildObserver);
Services.prefs.removeObserver(
"zen.themes.disable-all",
disableObserver
);
},
{ once: true }
);
}

View File

@@ -0,0 +1,238 @@
// 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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
JsonSchema: "resource://gre/modules/JsonSchema.sys.mjs",
clearTimeout: "resource://gre/modules/Timer.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"gBaseUrl",
"zen.share.base-url",
"",
null,
value => value.trim().replace(/\/+$/, "")
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"gSecretKey",
"zen.share.secret-key",
""
);
const SCHEMA_URL = "resource:///modules/zen/share/share.schema.json";
// The server rejects bigger bodies at 25 MiB.
const MAX_SHARE_BYTES = 25 * 1024 * 1024;
const MAX_NAME_LENGTH = 200;
// Uploads can be tens of MiB, reads are small JSON.
const CREATE_TIMEOUT_MS = 120000;
const READ_TIMEOUT_MS = 30000;
// Public share page paths: /{slug}/{XXXX-XXXX-XXXX-XXXX id}.
const SHARE_PATH_RE =
/^\/(space|folder|split-view|split)\/([0-9A-Za-z]{4}-[0-9A-Za-z]{4}-[0-9A-Za-z]{4}-[0-9A-Za-z]{4})\/?$/;
export class ZenShareError extends Error {
/**
* @param {string} code - One of "invalid-document", "auth", "too-large",
* "rate-limited", "not-found", "network", "server".
* @param {string} message
*/
constructor(code, message) {
super(message);
this.name = "ZenShareError";
this.code = code;
}
}
class nsZenShareClient {
#validatorPromise = null;
get #baseUrl() {
return lazy.gBaseUrl;
}
#authHeaders() {
// A secret key authorizes on its own and makes the share permanent. Never
// send both keys.
if (lazy.gSecretKey) {
return { "x-secret-key": lazy.gSecretKey };
}
return { "x-api-key": AppConstants.MOZ_MOZILLA_API_KEY };
}
#getValidator() {
if (!this.#validatorPromise) {
this.#validatorPromise = fetch(SCHEMA_URL, { credentials: "omit" })
.then(response => response.json())
.then(schema => new lazy.JsonSchema.Validator(schema));
}
return this.#validatorPromise;
}
/**
* Validates a share document against the schema.
*
* @param {object} doc
* @returns {Promise<{valid: boolean, errors: object[]}>}
*/
async validateDocument(doc) {
const validator = await this.#getValidator();
return validator.validate(doc);
}
async #assertValidDocument(doc) {
const result = await this.validateDocument(doc);
if (!result.valid) {
console.error("ZenShare: document fails the schema", result.errors);
throw new ZenShareError(
"invalid-document",
"share document fails the schema"
);
}
}
async #request(url, options, timeoutMs = READ_TIMEOUT_MS) {
const controller = new AbortController();
const timer = lazy.setTimeout(() => controller.abort(), timeoutMs);
let response;
try {
response = await fetch(url, {
credentials: "omit",
signal: controller.signal,
...options,
});
} catch (e) {
throw new ZenShareError(
"network",
`could not reach ${url}: ${e.message}`
);
} finally {
lazy.clearTimeout(timer);
}
if (response.ok) {
return response;
}
let message = `${response.status}`;
try {
message = (await response.json()).error || message;
} catch (e) {}
switch (response.status) {
case 401:
case 403:
throw new ZenShareError("auth", message);
case 404:
throw new ZenShareError("not-found", message);
case 413:
throw new ZenShareError("too-large", message);
case 429:
throw new ZenShareError("rate-limited", message);
case 400:
case 422:
throw new ZenShareError("invalid-document", message);
default:
throw new ZenShareError("server", message);
}
}
/**
* Uploads a share document.
*
* @param {object} doc - A document conforming to share.schema.json.
* @param {{name?: string}} options - Optional sharer display name, shown on
* the share page as "A Space from {name}".
* @returns {Promise<object>} The create response, plus `link`: the absolute
* public page URL to hand to the user.
*/
async createShare(doc, { name } = {}) {
const base = this.#baseUrl;
const headers = this.#authHeaders();
await this.#assertValidDocument(doc);
const body = JSON.stringify(doc);
if (new TextEncoder().encode(body).length > MAX_SHARE_BYTES) {
throw new ZenShareError("too-large", "share document is over 25 MiB");
}
let url = `${base}/api/shares`;
if (name) {
url += `?name=${encodeURIComponent(name.slice(0, MAX_NAME_LENGTH))}`;
}
const response = await this.#request(
url,
{
method: "POST",
headers: { ...headers, "content-type": "application/json" },
body,
},
CREATE_TIMEOUT_MS
);
const created = await response.json();
return { ...created, link: base + created.webUrl };
}
/**
* Recognizes public share page URLs on the configured server.
*
* @param {string} spec
* @returns {?{type: string, id: string}} null when the URL is not a share
* page (or no server is configured).
*/
parseShareUrl(spec) {
let uri;
let baseUri;
try {
uri = Services.io.newURI(spec);
baseUri = Services.io.newURI(this.#baseUrl);
} catch (e) {
return null;
}
if (uri.prePath !== baseUri.prePath) {
return null;
}
const match = uri.filePath.match(SHARE_PATH_RE);
if (!match) {
return null;
}
const slug = match[1];
const type = slug === "split" ? "split-view" : slug;
return { type, id: match[2].toUpperCase() };
}
/**
* Fetches everything needed to preview and import a share: the validated
* document, plus the sharer's display name.
*
* @param {{id: string}} share - As returned by parseShareUrl.
* @returns {Promise<{doc: object, name: ?string}>}
*/
async fetchSharePreview(share) {
const meta = await this.getShare(share.id);
await this.#assertValidDocument(meta.data);
return { doc: meta.data, name: meta.name || null };
}
/**
* Fetches a share with its metadata (authed):
* { id, name, createdAt, expiresAt, size, data }.
*
* @param {string} id
*/
async getShare(id) {
const response = await this.#request(`${this.#baseUrl}/api/shares/${id}`, {
headers: this.#authHeaders(),
});
return response.json();
}
}
export const ZenShareClient = new nsZenShareClient();

View File

@@ -0,0 +1,821 @@
// 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 { nsZenDOMOperatedFeature } from "chrome://browser/content/zen-components/ZenCommonUtils.mjs";
import { nsZenThemePicker } from "resource:///modules/zen/ZenGradientGenerator.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs",
ZenShareClient: "resource:///modules/zen/share/ZenShareClient.sys.mjs",
ZenShareError: "resource:///modules/zen/share/ZenShareClient.sys.mjs",
});
const SHARE_DOCUMENT_VERSION = "1";
const SHAREABLE_ICON_RE = /^(data:|chrome:)/;
const FOLDER_ICON_RE = /^chrome:\/\//;
/**
* Shares spaces, folders and split views through the share server, and
* imports them back from share links.
*/
class nsZenShareManager extends nsZenDOMOperatedFeature {
init() {
if (!this.enabled) {
for (const id of [
"context_zenShareWorkspace",
"context_zenShareFolder",
]) {
document.getElementById(id)?.setAttribute("hidden", "true");
}
return;
}
delayedStartupPromise.then(() => {
gBrowser.addTabsProgressListener({
onLocationChange: (browser, webProgress, request, aLocation) => {
this.#onLocationChange(browser, webProgress, aLocation);
},
});
});
}
get enabled() {
return !gZenWorkspaces.privateWindowOrDisabled;
}
// Mark: sharing
async shareSpace(workspaceId) {
const workspace = gZenWorkspaces.getWorkspaceFromId(workspaceId);
const element = gZenWorkspaces.workspaceElement(workspaceId);
if (!workspace || !element) {
return;
}
const item = {
type: "space",
name: workspace.name || "Space",
items: [
...this.#serializeItems(element.pinnedTabsContainer.children, {
pinned: true,
}),
...this.#serializeItems(element.tabsContainer.children, {
pinned: false,
}),
],
};
const theme = this.#serializeTheme(workspace.theme);
if (theme) {
item.theme = theme;
}
await this.#createAndCopyLink(item);
}
async shareFolder(folder) {
if (!folder?.isZenFolder) {
return;
}
await this.#createAndCopyLink(this.#serializeFolder(folder));
}
async shareSplitView(group) {
if (!group?.hasAttribute("split-view-group")) {
return;
}
const item = this.#serializeSplitGroup(group);
if (!item) {
this.#showCreateError(
new lazy.ZenShareError("empty", "nothing to share")
);
return;
}
await this.#createAndCopyLink(item);
}
#serializeSplitGroup(group) {
const tabs = group.tabs
.filter(tab => !tab.hasAttribute("zen-empty-tab"))
.map(tab => this.#serializeTab(tab, { pinned: tab.pinned }))
.filter(Boolean);
return tabs.length >= 2
? { type: "splitView", tabs: tabs.slice(0, gZenViewSplitter.MAX_TABS) }
: null;
}
async #createAndCopyLink(item) {
if (!this.enabled) {
return;
}
if (!(await this.#confirmShare())) {
return;
}
try {
const created = await lazy.ZenShareClient.createShare(
{ version: SHARE_DOCUMENT_VERSION, shared: item },
{ name: this.#displayName() }
);
Cc["@mozilla.org/widget/clipboardhelper;1"]
.getService(Ci.nsIClipboardHelper)
.copyString(created.link);
// The description's l10n args come from the whole options object.
gZenUIManager.showToast("zen-share-link-copied-toast", {
descriptionId: created.expiresAt
? "zen-share-link-expires-description"
: "zen-share-link-permanent-description",
date: created.expiresAt
? new Date(created.expiresAt).toLocaleDateString()
: "",
timeout: 6000,
});
} catch (e) {
this.#showCreateError(e);
}
}
#displayName() {
return (
Services.prefs.getStringPref("zen.share.display-name", "") || undefined
);
}
async #confirmShare() {
if (
Services.prefs.getBoolPref("zen.share.dont-ask-before-sharing", false)
) {
return true;
}
const args = {
accepted: false,
name: Services.prefs.getStringPref("zen.share.display-name", ""),
};
await gDialogBox.open(
"chrome://browser/content/zen-components/windows/zen-share-confirm.xhtml",
args
);
if (args.accepted) {
Services.prefs.setStringPref("zen.share.display-name", args.name);
if (args.dontAsk) {
Services.prefs.setBoolPref("zen.share.dont-ask-before-sharing", true);
}
}
return args.accepted;
}
#showCreateError(error) {
console.error("ZenShare: could not create share:", error);
const descriptions = {
auth: "zen-share-error-auth-description",
"too-large": "zen-share-error-too-large-description",
"rate-limited": "zen-share-error-rate-limited-description",
network: "zen-share-error-network-description",
empty: "zen-share-error-empty-description",
};
gZenUIManager.showToast("zen-share-error-toast", {
descriptionId:
descriptions[error.code] || "zen-share-error-server-description",
timeout: 6000,
});
}
// Mark: serialization
#serializeItems(children, { pinned }) {
const items = [];
for (const child of children) {
if (gBrowser.isTab(child)) {
if (
child.hasAttribute("zen-empty-tab") ||
child.hasAttribute("zen-glance-tab")
) {
continue;
}
const tab = this.#serializeTab(child, { pinned });
if (tab) {
items.push(tab);
}
} else if (child.isZenFolder) {
items.push(this.#serializeFolder(child));
} else if (gBrowser.isTabGroup(child)) {
const split = child.hasAttribute("split-view-group")
? this.#serializeSplitGroup(child)
: null;
if (split) {
items.push(split);
continue;
}
// Plain tab groups (and degenerate splits) keep their member tabs.
for (const tab of child.tabs) {
if (tab.hasAttribute("zen-empty-tab")) {
continue;
}
const serialized = this.#serializeTab(tab, { pinned });
if (serialized) {
items.push(serialized);
}
}
}
}
return items;
}
#serializeFolder(folder) {
const item = {
type: "folder",
name: folder.label || "Folder",
items: this.#serializeItems(folder.allItems, { pinned: false }),
};
const icon = folder.iconURL;
if (icon) {
item.icon = icon;
}
return item;
}
#serializeTab(tab, { pinned = false } = {}) {
const url = tab.linkedBrowser?.currentURI?.spec;
if (!url || !/^https?:\/\//.test(url)) {
return null;
}
const item = { type: "tab", url };
const label = tab.label;
if (label) {
item.label = label;
}
const image = this.#serializeIcon(tab);
if (image) {
item.image = image;
}
if (pinned) {
item.isPinned = true;
}
return item;
}
#serializeIcon(tab) {
let icon = tab.getAttribute("image") || "";
if (icon.startsWith("moz-remote-image:")) {
try {
icon =
new URLSearchParams(Services.io.newURI(icon).query).get("url") || "";
} catch (e) {
return "";
}
}
return SHAREABLE_ICON_RE.test(icon) ? icon : "";
}
#serializeTheme(theme) {
if (theme?.type !== "gradient" || !Array.isArray(theme.gradientColors)) {
return null;
}
const gradientColors = theme.gradientColors
.map(color => this.#serializeGradientColor(color))
.filter(Boolean);
if (!gradientColors.length) {
return null;
}
return { type: "gradient", gradientColors };
}
#serializeGradientColor(color) {
let c = color?.c;
if (typeof c === "string") {
c = this.#parseCssColor(c);
} else if (Array.isArray(c)) {
c = c.slice(0, 3).map(Number);
}
if (
!Array.isArray(c) ||
c.length !== 3 ||
c.some(n => !Number.isFinite(n))
) {
return null;
}
const out = { c, type: typeof color.type === "string" ? color.type : "" };
if (typeof color.isCustom === "boolean") {
out.isCustom = color.isCustom;
}
if (typeof color.isPrimary === "boolean") {
out.isPrimary = color.isPrimary;
}
if (typeof color.algorithm === "string") {
out.algorithm = color.algorithm;
}
if (color.lightness !== undefined && color.lightness !== null) {
out.lightness = String(color.lightness);
}
if (
Number.isFinite(color.position?.x) &&
Number.isFinite(color.position?.y)
) {
out.position = { x: color.position.x, y: color.position.y };
}
return out;
}
#parseCssColor(value) {
const hex = value.trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i)?.[1];
if (hex) {
const full =
hex.length === 3 ? [...hex].map(ch => ch + ch).join("") : hex;
return [0, 2, 4].map(i => parseInt(full.slice(i, i + 2), 16));
}
const numbers = value
.match(/\d+(\.\d+)?/g)
?.slice(0, 3)
.map(Number);
return numbers?.length === 3 ? numbers : null;
}
// Mark: importing
#stylesLoaded = false;
// The overlay styles are only needed once a share link is opened.
#ensureStyles() {
if (this.#stylesLoaded) {
return;
}
this.#stylesLoaded = true;
window.windowUtils.loadSheetUsingURIString(
"chrome://browser/content/zen-styles/zen-share.css",
window.windowUtils.AUTHOR_SHEET
);
}
#setHostTabSubtitle(browser, subtitle) {
const tab = gBrowser.getTabForBrowser(browser);
if (!tab) {
return;
}
tab.setAttribute("zen-show-sublabel", subtitle);
document.l10n.setArgs(tab.querySelector(".zen-tab-sublabel"), {
tabSubtitle: subtitle,
});
}
#clearHostTabSubtitle(browser) {
const tab = gBrowser.getTabForBrowser(browser);
if (!tab?.hasAttribute("zen-show-sublabel")) {
return;
}
tab.removeAttribute("zen-show-sublabel");
document.l10n.setArgs(tab.querySelector(".zen-tab-sublabel"), {
tabSubtitle: "zen-default-pinned",
});
}
#onLocationChange(browser, webProgress, aLocation) {
if (!webProgress.isTopLevel) {
return;
}
if (browser._zenShareOverlay) {
browser._zenShareOverlay.remove();
delete browser._zenShareOverlay;
browser.removeAttribute("zen-share-overlay-showing");
this.#clearHostTabSubtitle(browser);
}
const share = lazy.ZenShareClient.parseShareUrl(aLocation.spec);
if (!share) {
return;
}
if (share.type === "split-view") {
// Split views are light enough to open right away, no preview step.
this.#openSharedSplitView(browser, share);
} else {
this.#showImportOverlay(browser, share);
}
}
async #openSharedSplitView(browser, share) {
let item;
const shareTab = gBrowser.getTabForBrowser(browser);
shareTab.style.display = "none";
const failure = () => {
shareTab.style.removeProperty("display");
};
try {
const { doc } = await lazy.ZenShareClient.fetchSharePreview(share);
item = doc.shared?.type === "splitView" ? doc.shared : null;
} catch (e) {
console.error("ZenShare: could not load shared split view:", e);
const descriptions = {
"not-found": "zen-share-import-error-dead-description",
"invalid-document": "zen-share-import-error-invalid-description",
};
gZenUIManager.showToast("zen-share-import-error-toast", {
descriptionId:
descriptions[e.code] || "zen-share-error-network-description",
timeout: 6000,
});
failure();
return;
}
if (!item) {
failure();
return;
}
const workspaceId = gZenWorkspaces.activeWorkspace;
const pinned = item.tabs.every(child => child.isPinned);
const tabs = item.tabs.map(child => {
// Eager browsers so every pane renders as soon as the split shows.
const tab = this.#importTab(child, workspaceId, { lazyBrowser: false });
if (pinned) {
gBrowser.pinTab(tab);
}
this.#placeTab(tab, workspaceId, pinned);
return tab;
});
gZenViewSplitter.splitTabs(tabs, "grid", 0);
gBrowser.removeTab(shareTab, {
closeWindowWithLastTab: false,
skipSessionStore: true,
});
// Shake the new split group in the sidebar.
const group = tabs[0]?.group;
if (group?.hasAttribute("split-view-group")) {
requestAnimationFrame(() => {
gZenUIManager.motion
.animate(
group,
{ x: [-28, 0] },
{ type: "spring", bounce: 0.9, duration: 1.4 }
)
.then(() => {
group.style.removeProperty("transform");
});
});
}
}
async #showImportOverlay(browser, share) {
const stack = browser.closest(".browserStack");
if (!stack) {
return;
}
this.#ensureStyles();
// Only the status text shows until the preview loads.
const overlay = window.MozXULElement.parseXULToFragment(`
<html:div class="zen-share-overlay">
<html:div class="zen-share-overlay-card">
<html:div class="zen-share-overlay-items"/>
<html:div class="zen-share-overlay-status"
data-l10n-id="zen-share-overlay-loading"/>
<html:div class="zen-share-overlay-footer" hidden="hidden">
<html:div class="zen-share-overlay-badge-stack">
<html:div class="zen-share-overlay-badge-sheet second"/>
<html:div class="zen-share-overlay-badge-sheet first"/>
<html:div class="zen-share-overlay-badge">
<html:img class="zen-share-overlay-badge-icon"
draggable="false"/>
<html:div class="zen-share-overlay-badge-text">
<html:div class="zen-share-overlay-badge-title"/>
<html:div class="zen-share-overlay-badge-subtitle"/>
</html:div>
</html:div>
</html:div>
</html:div>
</html:div>
<html:button class="zen-big-accent-button primary zen-share-overlay-add"
hidden="hidden" disabled="disabled"/>
</html:div>
`).firstElementChild;
const items = overlay.querySelector(".zen-share-overlay-items");
const statusEl = overlay.querySelector(".zen-share-overlay-status");
const footer = overlay.querySelector(".zen-share-overlay-footer");
const badgeIcon = overlay.querySelector(".zen-share-overlay-badge-icon");
const badgeTitle = overlay.querySelector(".zen-share-overlay-badge-title");
const badgeSubtitle = overlay.querySelector(
".zen-share-overlay-badge-subtitle"
);
const button = overlay.querySelector(".zen-share-overlay-add");
stack.appendChild(overlay);
browser._zenShareOverlay = overlay;
browser.setAttribute("zen-share-overlay-showing", "true");
let preview;
try {
preview = await lazy.ZenShareClient.fetchSharePreview(share);
} catch (e) {
console.error("ZenShare: could not load share preview:", e);
const descriptions = {
"not-found": "zen-share-import-error-dead-description",
"invalid-document": "zen-share-import-error-invalid-description",
};
document.l10n.setAttributes(
statusEl,
descriptions[e.code] || "zen-share-error-network-description"
);
return;
}
if (browser._zenShareOverlay !== overlay) {
// Navigated away while loading.
return;
}
const { doc, name: sharerName } = preview;
const first = doc.shared;
const type = first.type;
badgeTitle.textContent = first.name;
this.#renderPreviewRows(items, first.items);
if (type === "folder") {
// The real zen folder icon, with the shared custom icon when set.
const folderIcon = this.#buildFolderIcon(first.icon);
folderIcon.classList.add("zen-share-overlay-badge-icon");
folderIcon.setAttribute("state", "open");
badgeIcon.replaceWith(folderIcon);
} else {
badgeIcon.src = "chrome://browser/skin/zen-icons/duplicate-tab.svg";
}
if (sharerName) {
document.l10n.setAttributes(
badgeSubtitle,
`zen-share-overlay-from-${type}`,
{ name: sharerName }
);
} else {
document.l10n.setAttributes(
badgeSubtitle,
"zen-share-overlay-shared-with-you"
);
}
// Mirror the badge subtitle under the tab's label, like live folders do.
const [tabSubtitle] = await document.l10n.formatValues([
sharerName
? { id: `zen-share-overlay-from-${type}`, args: { name: sharerName } }
: { id: "zen-share-overlay-shared-with-you" },
]);
this.#setHostTabSubtitle(browser, tabSubtitle);
statusEl.remove();
footer.hidden = false;
document.l10n.setAttributes(button, `zen-share-overlay-add-${type}`);
button.hidden = false;
button.disabled = false;
button.addEventListener("click", async () => {
button.disabled = true;
try {
await this.#importDocument(doc);
gZenUIManager.showToast("zen-share-imported-toast");
const tab = gBrowser.getTabForBrowser(browser);
if (tab) {
gBrowser.removeTab(tab, {
closeWindowWithLastTab: false,
skipSessionStore: true,
});
}
} catch (e) {
console.error("ZenShare: could not import share:", e);
gZenUIManager.showToast("zen-share-import-error-toast", {
timeout: 6000,
});
button.disabled = false;
}
});
}
#renderPreviewRows(container, entries) {
for (const entry of entries) {
if (entry.type === "folder") {
container.appendChild(this.#buildFolderRow(entry));
} else if (entry.type === "splitView") {
const split = window.MozXULElement.parseXULToFragment(`
<html:div class="zen-share-overlay-split"/>
`).firstElementChild;
for (const tab of entry.tabs) {
split.appendChild(this.#buildTabRow(tab));
}
container.appendChild(split);
} else {
container.appendChild(this.#buildTabRow(entry));
}
}
}
#buildTabRow(entry) {
const row = window.MozXULElement.parseXULToFragment(`
<html:div class="zen-share-overlay-row">
<html:img class="zen-share-overlay-row-icon" draggable="false"/>
<html:div class="zen-share-overlay-row-label"/>
</html:div>
`).firstElementChild;
row.title = entry.url;
row.querySelector(".zen-share-overlay-row-icon").src =
entry.image && SHAREABLE_ICON_RE.test(entry.image)
? entry.image
: `page-icon:${entry.url}`;
row.querySelector(".zen-share-overlay-row-label").textContent =
entry.label || entry.url;
row.addEventListener("click", () => {
const stackRect = row.closest(".browserStack").getBoundingClientRect();
const data = {
url: entry.url,
clientX: stackRect.width / 2,
clientY: stackRect.height / 2,
width: 0,
height: 0,
triggeringPrincipal:
Services.scriptSecurityManager.getSystemPrincipal(),
};
gZenGlanceManager.lastLinkClickData = data;
gZenGlanceManager.openGlance(data);
});
return row;
}
#buildFolderRow(entry) {
const wrapper = window.MozXULElement.parseXULToFragment(`
<html:div class="zen-share-overlay-folder" collapsed="true">
<html:div class="zen-share-overlay-row">
<html:div class="zen-share-overlay-row-label"/>
</html:div>
<html:div class="zen-share-overlay-children-clip">
<html:div class="zen-share-overlay-children"/>
</html:div>
</html:div>
`).firstElementChild;
const row = wrapper.querySelector(".zen-share-overlay-row");
const label = row.querySelector(".zen-share-overlay-row-label");
label.textContent = entry.name;
const icon = this.#buildFolderIcon(entry.icon);
row.insertBefore(icon, label);
this.#renderPreviewRows(
wrapper.querySelector(".zen-share-overlay-children"),
entry.items
);
row.addEventListener("click", () => {
const collapsed = wrapper.toggleAttribute("collapsed");
if (collapsed) {
icon.removeAttribute("state");
} else {
icon.setAttribute("state", "open");
}
});
return wrapper;
}
#buildFolderIcon(customIcon) {
const { nsZenFolder } = ChromeUtils.importESModule(
"chrome://browser/content/zen-components/ZenFolder.mjs",
{ global: "current" }
);
const svg = nsZenFolder.rawIcon.cloneNode(true);
svg.classList.add("zen-share-overlay-folder-icon");
if (customIcon && FOLDER_ICON_RE.test(customIcon)) {
svg.querySelector(".icon image").setAttribute("href", customIcon);
}
return svg;
}
async #importDocument(doc) {
const item = doc.shared;
switch (item.type) {
case "space":
await this.#importSpace(item);
break;
case "folder":
this.#importFolder(item, gZenWorkspaces.activeWorkspace);
break;
case "splitView":
this.#importSplitView(item);
break;
}
}
async #importSpace(item) {
const workspace = await gZenWorkspaces.createAndSaveWorkspace(
item.name,
undefined,
false,
0,
{
beforeChangeCallback: async newWorkspace => {
for (const child of item.items) {
if (child.type === "tab") {
const tab = this.#importTab(child, newWorkspace.uuid);
if (child.isPinned) {
gBrowser.pinTab(tab);
}
this.#placeTab(tab, newWorkspace.uuid, !!child.isPinned);
} else if (child.type === "folder") {
this.#importFolder(child, newWorkspace.uuid);
} else if (child.type === "splitView") {
this.#importSplitView(child, newWorkspace.uuid);
}
}
},
}
);
if (!workspace) {
return;
}
workspace.name = item.name;
const theme = item.theme;
if (theme) {
workspace.theme = nsZenThemePicker.getTheme(
theme.gradientColors.map(color => this.#importGradientColor(color))
);
}
gZenWorkspaces.saveWorkspace(workspace);
gZenThemePicker.onWorkspaceChange(workspace);
gZenStartup.playWindowSweepAnimation();
}
#importGradientColor(color) {
if (color.isCustom) {
const [r, g, b] = color.c;
return { ...color, c: `rgb(${r}, ${g}, ${b})` };
}
return color;
}
#importFolder(item, workspaceId, parentFolder = null) {
const folder = gZenFolders.createFolder([], {
label: item.name,
collapsed: true,
workspaceId,
insertAfter: parentFolder
? parentFolder.groupContainer.lastElementChild
: null,
});
if (item.icon && FOLDER_ICON_RE.test(item.icon)) {
gZenFolders.setFolderUserIcon(folder, item.icon);
}
for (const child of item.items) {
if (child.type === "tab") {
const tab = this.#importTab(child, workspaceId);
gBrowser.pinTab(tab);
folder.addTabs([tab]);
} else if (child.type === "folder") {
this.#importFolder(child, workspaceId, folder);
} else if (child.type === "splitView") {
this.#importSplitView(child, workspaceId, folder);
}
}
return folder;
}
#importSplitView(item, workspaceId, folder = null) {
workspaceId ??= gZenWorkspaces.activeWorkspace;
const pinned = item.tabs.every(child => child.isPinned);
const tabs = item.tabs.map(child => {
const tab = this.#importTab(child, workspaceId);
if (folder) {
gBrowser.pinTab(tab);
folder.addTabs([tab]);
} else {
if (pinned) {
gBrowser.pinTab(tab);
}
this.#placeTab(tab, workspaceId, pinned);
}
return tab;
});
gZenViewSplitter.splitTabs(tabs, "grid", -1);
}
// Deterministic placement at the end of the right section;
// moveTabToWorkspace's anchor follows the new-tab-button position pref.
#placeTab(tab, workspaceId, pinned) {
const element = gZenWorkspaces.workspaceElement(workspaceId);
const container = pinned
? element.pinnedTabsContainer
: element.tabsContainer;
const anchor = pinned
? container.querySelector(".pinned-tabs-container-separator")
: container.querySelector("#tabbrowser-arrowscrollbox-periphery");
gBrowser.zenHandleTabMove(tab, () => {
tab.setAttribute("zen-workspace-id", workspaceId);
container.insertBefore(tab, anchor);
});
}
#importTab(item, workspaceId, { lazyBrowser = true } = {}) {
const tab = gBrowser.addTrustedTab(item.url, {
createLazyBrowser: lazyBrowser,
inBackground: true,
skipAnimation: true,
skipBackgroundNotify: true,
lazyTabTitle: item.label || undefined,
skipRoute: true,
});
if (workspaceId) {
tab.setAttribute("zen-workspace-id", workspaceId);
}
if (item.image && SHAREABLE_ICON_RE.test(item.image)) {
try {
gBrowser.setIcon(tab, item.image);
lazy.TabStateCache.update(tab.linkedBrowser.permanentKey, {
image: null,
});
} catch (e) {
console.error("ZenShare: could not set imported tab icon:", e);
}
}
return tab;
}
}
window.gZenShareManager = new nsZenShareManager();

7
src/zen/share/jar.inc.mn Normal file
View File

@@ -0,0 +1,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
* content/browser/zen-styles/zen-share.css (../../zen/share/zen-share.css)
* content/browser/zen-components/windows/zen-share-confirm.xhtml (../../zen/share/zen-share-confirm.inc.xhtml)
content/browser/zen-components/windows/zen-share-confirm.js (../../zen/share/zen-share-confirm.js)

View File

@@ -4,4 +4,6 @@
EXTRA_JS_MODULES.zen.share += [
"share.schema.json",
"ZenShareClient.sys.mjs",
"ZenShareManager.mjs",
]

View File

@@ -7,11 +7,7 @@
"type": "string"
},
"shared": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/shareable"
}
"$ref": "#/$defs/shareable"
}
},
"required": ["shared"],
@@ -22,21 +18,10 @@
"oneOf": [
{ "$ref": "#/$defs/space" },
{ "$ref": "#/$defs/folder" },
{ "$ref": "#/$defs/splitView" },
{ "$ref": "#/$defs/gradientPreset" }
{ "$ref": "#/$defs/splitView" }
]
},
"gradientPreset": {
"type": "object",
"properties": {
"type": { "const": "gradientPreset" },
"theme": { "$ref": "#/$defs/theme" }
},
"required": ["type", "theme"],
"additionalProperties": false
},
"space": {
"type": "object",
"properties": {
@@ -144,11 +129,19 @@
},
"spaceItem": {
"oneOf": [{ "$ref": "#/$defs/tab" }, { "$ref": "#/$defs/folder" }]
"oneOf": [
{ "$ref": "#/$defs/tab" },
{ "$ref": "#/$defs/folder" },
{ "$ref": "#/$defs/splitView" }
]
},
"folderItem": {
"oneOf": [{ "$ref": "#/$defs/tab" }, { "$ref": "#/$defs/folder" }]
"oneOf": [
{ "$ref": "#/$defs/tab" },
{ "$ref": "#/$defs/folder" },
{ "$ref": "#/$defs/splitView" }
]
}
}
}

View File

@@ -0,0 +1,60 @@
#filter substitution
<?xml version="1.0"?>
# -*- Mode: HTML -*-
#
# 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/.
<!DOCTYPE window>
<window
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
id="zenShareConfirmDialog"
>
<dialog
buttons="accept,cancel"
data-l10n-id="zen-share-confirm-dialog"
data-l10n-attrs="buttonlabelaccept"
>
<linkset>
<html:link
rel="stylesheet"
href="chrome://global/skin/global.css"
type="text/css"
/>
<html:link
rel="stylesheet"
href="chrome://browser/content/zen-styles/zen-theme.css"
type="text/css"
/>
<html:link rel="localization" href="browser/zen-share.ftl" />
</linkset>
<script src="chrome://browser/content/zen-components/windows/zen-share-confirm.js"></script>
<vbox style="padding-inline: 12px;">
<html:h2
data-l10n-id="zen-share-confirm-title"
></html:h2>
<description data-l10n-id="zen-share-confirm-description" />
<description
style="margin: 12px 0 16px;"
data-l10n-id="zen-share-confirm-anonymous"
/>
<html:input
style="margin-bottom: 12px;"
id="zen-share-confirm-name"
type="text"
maxlength="200"
data-l10n-id="zen-share-confirm-name-input"
/>
<checkbox
id="zen-share-confirm-dont-ask"
data-l10n-id="zen-share-confirm-dont-ask"
/>
</vbox>
</dialog>
</window>

View File

@@ -0,0 +1,27 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
{
const args = window.arguments?.[0] || {};
window.addEventListener(
"load",
() => {
const nameInput = document.getElementById("zen-share-confirm-name");
nameInput.value = args.name || "";
nameInput.focus();
document.addEventListener("dialogaccept", () => {
args.accepted = true;
args.name = nameInput.value.trim();
args.dontAsk = document.getElementById(
"zen-share-confirm-dont-ask"
).checked;
});
},
{ once: true }
);
}

259
src/zen/share/zen-share.css Normal file
View File

@@ -0,0 +1,259 @@
/* 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-share-overlay {
position: absolute;
inset: 0;
z-index: 5;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 28px;
padding: 32px;
background: var(--zen-transparent-browser-background);
}
browser[zen-share-overlay-showing] {
-moz-subtree-hidden-only-visually: 1;
}
.zen-share-overlay-card {
display: flex;
flex-direction: column;
width: 325px;
max-height: 76%;
border-radius: 30px;
background: var(--zen-dialog-background);
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.07),
0 14px 40px -10px rgba(0, 0, 0, 0.28);
overflow: hidden;
animation: zen-share-overlay-enter 0.35s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
.zen-share-overlay-add {
min-width: 260px;
max-width: 100%;
padding: 16px 48px;
border-radius: 18px;
font-size: 1.1em;
animation: zen-share-overlay-enter 0.35s cubic-bezier(0.2, 0.8, 0.2, 1) 0.06s
both;
&[disabled] {
opacity: 0.5;
pointer-events: none;
}
}
@keyframes zen-share-overlay-enter {
from {
opacity: 0;
transform: translateY(14px) scale(0.98);
}
}
.zen-share-overlay-items {
display: flex;
flex-direction: column;
gap: 8px;
overflow-y: auto;
scrollbar-width: none;
padding: 22px 20px 16px;
mask-image: linear-gradient(
transparent,
black 24px,
black calc(100% - 20px),
transparent
);
}
.zen-share-overlay-row {
display: flex;
align-items: center;
gap: 14px;
padding: 10px 14px;
border-radius: 12px;
min-width: 0;
&:hover {
background: color-mix(in srgb, currentColor 7%, transparent);
}
&:active {
background: color-mix(in srgb, currentColor 11%, transparent);
}
}
.zen-share-overlay-row-icon {
width: 18px;
height: 18px;
flex-shrink: 0;
border-radius: 4px;
}
.zen-share-overlay-folder-icon {
width: 24px;
height: 24px;
flex-shrink: 0;
margin: -3px;
%include ../folders/zen-folder-icon.inc.css
/* The dots stand in for the icon on active sidebar folders; the preview
* always shows the plain folder. */
& .dots {
opacity: 0;
}
}
.zen-share-overlay-row-label {
font-size: 1.1em;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.zen-share-overlay-split {
display: flex;
gap: 10px;
min-width: 0;
& .zen-share-overlay-row {
flex: 1 1 0;
padding: 6px 14px;
border-radius: 8px;
}
& .zen-share-overlay-row:not(:first-child) {
position: relative;
&::before {
content: "";
position: absolute;
inset-block: 6px;
inset-inline-start: -5.5px;
width: 1px;
background: var(--zen-colors-border);
}
}
}
.zen-share-overlay-children-clip {
display: grid;
grid-template-rows: 1fr;
transition: grid-template-rows 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.zen-share-overlay-folder[collapsed] > .zen-share-overlay-children-clip {
grid-template-rows: 0fr;
}
.zen-share-overlay-children {
display: flex;
flex-direction: column;
gap: 8px;
margin-inline-start: 28px;
min-height: 0;
overflow: hidden;
}
.zen-share-overlay-status {
padding: 28px;
text-align: center;
opacity: 0.7;
}
.zen-share-overlay-footer {
position: relative;
isolation: isolate;
display: flex;
align-items: center;
margin-top: auto;
padding: 22px 20px 20px;
border-top: 2px dashed
color-mix(in srgb, var(--zen-primary-color) 22%, var(--zen-colors-border));
background: color-mix(
in srgb,
var(--zen-primary-color) 7%,
var(--zen-dialog-background)
);
}
.zen-share-overlay-badge-stack {
position: relative;
flex: 1;
min-width: 0;
}
.zen-share-overlay-badge {
position: relative;
z-index: 2;
display: flex;
align-items: center;
gap: 14px;
min-width: 0;
padding: 14px 18px;
border-radius: 18px;
border: 1px solid
light-dark(
color-mix(in srgb, currentColor 12%, var(--zen-colors-border)),
color-mix(in srgb, currentColor 8%, transparent)
);
background: light-dark(#ffffff, #1c1c1e);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.07);
}
.zen-share-overlay-badge-sheet {
position: absolute;
height: 26px;
border-radius: 14px;
border: 1px solid
light-dark(
color-mix(in srgb, currentColor 12%, var(--zen-colors-border)),
color-mix(in srgb, currentColor 8%, transparent)
);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.07);
&.first {
z-index: 1;
inset-inline: 10px;
top: -7px;
background: color-mix(in srgb, currentColor 2%, light-dark(#ffffff, #1c1c1e));
}
&.second {
z-index: 0;
inset-inline: 22px;
top: -13px;
background: color-mix(in srgb, currentColor 4%, light-dark(#ffffff, #1c1c1e));
}
}
.zen-share-overlay-badge-icon {
width: 26px;
height: 26px;
flex-shrink: 0;
-moz-context-properties: fill;
fill: color-mix(in srgb, var(--zen-accent-button-color) 60%, currentColor);
}
.zen-share-overlay-badge-text {
min-width: 0;
color: color-mix(in srgb, var(--zen-accent-button-color) 30%, currentColor);
}
.zen-share-overlay-badge-title {
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.zen-share-overlay-badge-subtitle {
opacity: 0.6;
font-size: smaller;
}

View File

@@ -110,7 +110,7 @@ class nsZenWorkspaceIcons extends MozXULElement {
button.setAttribute("zen-workspace-id", workspace.uuid);
button.setAttribute("context", "zenWorkspaceMoreActions");
const icon = document.createXULElement("label");
icon.setAttribute("class", "zen-workspace-icon");
icon.setAttribute("class", "zen-workspace-icon no-squircles");
const isSvgIcon = workspace.icon && workspace.icon.endsWith(".svg");
if (gZenWorkspaces.workspaceHasIcon(workspace)) {
if (isSvgIcon) {

View File

@@ -432,8 +432,9 @@ class nsZenWorkspaces {
// Set a hidden state if the essentials section is not supposed
// to be shown on the current workspace, else remove the hidden state
if (
this.containerSpecificEssentials &&
this.getActiveWorkspaceFromCache()?.containerTabId != container
this.activeWorkspace === this.creatingWorkspaceId ||
(this.containerSpecificEssentials &&
this.getActiveWorkspaceFromCache()?.containerTabId + 0 != container)
) {
essentialsContainer.setAttribute("hidden", "true");
} else {
@@ -872,7 +873,7 @@ class nsZenWorkspaces {
) {
this.log(`Found tab to select: ${this._tabToSelect}, ${tabs.length}`);
let tabToUse = gZenGlanceManager.getTabOrGlanceParent(
tabs[this._tabToSelect + 1] || this._emptyTab
tabs[this._tabToSelect] || this._emptyTab
);
gBrowser.selectedTab = tabToUse;
this._removedByStartupPage = true;
@@ -2341,9 +2342,13 @@ class nsZenWorkspaces {
)
) {
tabToSelect = lastSelectedTab;
} else if (!onInit && !tabToSelect) {
// Create new tab if needed and no suitable tab was found
tabToSelect = this._emptyTab;
}
// Find first suitable tab
else {
// If we found a tab to select, select it
if (!tabToSelect || tabToSelect.closing) {
tabToSelect = gBrowser.visibleTabs.find(tab => !tab.pinned);
if (!tabToSelect && gBrowser.visibleTabs.length) {
tabToSelect = gBrowser.visibleTabs[gBrowser.visibleTabs.length - 1];
@@ -2354,12 +2359,6 @@ class nsZenWorkspaces {
}
}
// If we found a tab to select, select it
if (!onInit && !tabToSelect) {
// Create new tab if needed and no suitable tab was found
const newTab = this.selectEmptyTab();
tabToSelect = newTab;
}
if (tabToSelect && !onInit) {
tabToSelect._visuallySelected = true;
}
@@ -2890,6 +2889,12 @@ class nsZenWorkspaces {
});
}
contextShareWorkspace() {
const workspaceId =
this.#contextMenuData?.workspaceId || this.activeWorkspace;
gZenShareManager.shareSpace(workspaceId);
}
async contextDeleteWorkspace() {
const workspaceId =
this.#contextMenuData?.workspaceId || this.activeWorkspace;
@@ -2906,7 +2911,10 @@ class nsZenWorkspaces {
}
findTabToBlur(tab) {
if ((!this._shouldChangeToTab(tab) || !tab) && this._emptyTab) {
if (
(!tab || !this._shouldChangeToTab(tab) || !gBrowser.tabs.includes(tab)) &&
this._emptyTab
) {
return this._emptyTab;
}
return tab;
@@ -3283,7 +3291,7 @@ class nsZenWorkspaces {
if (!(!event || event.target === window)) {
return;
}
gZenUIManager.updateTabsToolbar();
gZenUIManager.updateTabsToolbar(!!event);
// Check if workspace icons overflow the parent container
let parent = this.workspaceIcons;
if (!parent || this._processingResize) {

View File

@@ -30,9 +30,9 @@
& toolbarbutton {
margin: 0;
max-width: 28px;
max-width: 30px;
border-radius: var(--toolbarbutton-border-radius);
height: 28px;
height: 30px;
display: flex;
justify-content: center;
padding: 0 !important;
@@ -58,8 +58,8 @@
}
&[no-icon='true'] {
width: 6px;
height: 6px;
width: 8px;
height: 8px;
background: light-dark(rgba(0, 0, 0, 0.4), rgba(255, 255, 255, 0.4));
border-radius: 50%;
}
@@ -329,7 +329,6 @@ zen-workspace {
color-mix(in srgb, rgba(255, 255, 255, 0.18) 95%, var(--zen-primary-color))
) !important;
--tab-box-shadow-selected: 0 0.8px 1.5px 0px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.05)) !important;
--tab-selected-textcolor: color-mix(in srgb, var(--toolbox-textcolor) 95%, var(--zen-primary-color)) !important;
@media not (prefers-reduced-motion: reduce) {
transition: padding-top 0.1s;
@@ -405,6 +404,7 @@ zen-workspace {
:root:not(:is([animating-background], [swipe-gesture])) #navigator-toolbox:not([movingtab]) &:not([active]) {
-moz-subtree-hidden-only-visually: 1;
content-visibility: hidden;
}
}

View File

@@ -336,7 +336,9 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
if (
!gBrowser.isTab(draggedTab) ||
gBrowser.selectedTab.hasAttribute("zen-empty-tab") ||
draggedTab.documentGlobal !== window
draggedTab.documentGlobal !== window ||
// See gh-15329.
draggedTab.multiselected
) {
return;
}
@@ -1184,6 +1186,8 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
document.l10n.setAttributes(splitTabCommand, "tab-zen-split-tabs", {
tabCount: isExistingSplitView ? -1 : selectedTabs.length,
});
document.getElementById("context_zenShareSplitView").hidden =
!gZenShareManager.enabled || !isExistingSplitView;
if (isExistingSplitView) {
splitTabCommand.removeAttribute("hidden");
return;
@@ -1204,6 +1208,10 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
data-lazy-l10n-id="tab-zen-split-tabs"
data-l10n-args='{"tabCount": 1}'
command="cmd_zenSplitViewContextMenu"/>
<menuitem id="context_zenShareSplitView"
data-lazy-l10n-id="zen-share-split-view"
hidden="true"
command="cmd_zenCtxShareSplitView"/>
`);
document.getElementById("context_moveTabToSplitView").before(element);
}
@@ -1252,6 +1260,16 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
this.splitTabs([currentTab, newTab], undefined, 1);
}
/**
* Shares the split view of the context tab.
*/
contextShareSplitView() {
const group = TabContextMenu.contextTab?.group;
if (group?.hasAttribute("split-view-group")) {
gZenShareManager.shareSplitView(group);
}
}
/**
* Splits the selected tabs.
*

View File

@@ -75,10 +75,9 @@ class ZenSpacesSyncStore extends Store {
}
/**
* Tracker for the Spaces engine. There is no per-event bookkeeping: every
* time fresh sidebar data is collected (or a container changes), the model
* diffs the current projections against the last-uploaded snapshot; the
* score is only bumped when something actually differs.
* Tracker for the Spaces engine. The model diffs the current projections
* against the last-uploaded snapshot. The score is only bumped when
* something actually differs.
*/
class ZenSpacesSyncTracker extends Tracker {
_ignoreAll = false;

View File

@@ -11,10 +11,20 @@ import {
ZenSpacesSyncModel,
} from "resource:///modules/zen/ZenSpacesSyncModel.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"syncNormalTabs",
"zen.spaces-sync.normal-tabs",
false
);
ChromeUtils.defineESModuleGetters(lazy, {
SessionSaver: "resource:///modules/sessionstore/SessionSaver.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs",
ZenWindowSync: "resource:///modules/zen/ZenWindowSync.sys.mjs",
@@ -84,6 +94,11 @@ class nsZenSpacesSyncApplier {
if (!data) {
continue;
}
if (data.pinned === false && !lazy.syncNormalTabs) {
// Normal-tab syncing is off here: leave the record untouched and
// unacknowledged, like an unknown kind.
continue;
}
const entry = { key: record.id, data, record };
switch (record.cleartext.kind) {
case RECORD_KINDS.CONTAINER:
@@ -141,6 +156,7 @@ class nsZenSpacesSyncApplier {
fail(entry.record, noWindow);
}
} else {
await lazy.SessionStore.promiseAllWindowsRestored;
await win.gZenWorkspaces.promiseInitialized;
this.#maybePlayFirstSyncAnimation(win);
// A sync apply is a materialization just like session restore,
@@ -510,9 +526,6 @@ class nsZenSpacesSyncApplier {
if (!folder?.isZenFolder) {
continue;
}
// Members without their own tombstone survive: unpack, then delete
// the (now empty) folder.
await folder.unpackTabs();
await folder.delete();
} catch (e) {
fail(record, e);
@@ -567,7 +580,9 @@ class nsZenSpacesSyncApplier {
if (data.workspaceUuid) {
tab.setAttribute("zen-workspace-id", data.workspaceUuid);
}
win.gBrowser.pinTab(tab);
if (data.pinned !== false) {
win.gBrowser.pinTab(tab);
}
if (data.workspaceUuid) {
win.gZenWorkspaces.moveTabToWorkspace(tab, data.workspaceUuid);
}
@@ -593,7 +608,10 @@ class nsZenSpacesSyncApplier {
const identityChanged =
initial?.entry?.url !== data.url ||
(initial?.entry?.title || "") !== (data.title || "");
if (identityChanged || syncableIconUrl(initial?.image || "") !== icon) {
if (
data.pinned !== false &&
(identityChanged || syncableIconUrl(initial?.image || "") !== icon)
) {
lazy.ZenWindowSync.setPinnedInitialState(
tab,
{ url: data.url, title: data.title || "" },
@@ -633,7 +651,7 @@ class nsZenSpacesSyncApplier {
);
win.gBrowser.setIcon(tab, icon);
lazy.TabStateCache.update(tab.linkedBrowser.permanentKey, {
image: null,
image: icon || null,
});
} catch (e) {
console.error("ZenSpacesSync: failed to set tab icon", e);
@@ -745,6 +763,17 @@ class nsZenSpacesSyncApplier {
// The split record governs placement of its members.
return;
}
const wantPinned = data.pinned !== false;
if (wantPinned !== tab.pinned) {
if (wantPinned) {
win.gBrowser.pinTab(tab);
} else {
win.gBrowser.unpinTab(tab);
// Pin identity would otherwise freeze the projection of what is now
// a normal tab.
delete tab._zenPinnedInitialState;
}
}
this.#applyFolderMembership(win, tab, data.folderId);
if (
!data.folderId &&

View File

@@ -22,6 +22,13 @@ XPCOMUtils.defineLazyPreferenceGetter(
false
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"syncNormalTabs",
"zen.spaces-sync.normal-tabs",
false
);
/**
* Debug logging for the whole Spaces sync pipeline.
*
@@ -258,19 +265,31 @@ class nsZenSpacesSyncModel {
/* Mark: projections */
#isSyncableTab(tabData) {
#isTabRecordMaterial(tabData) {
return !!(
tabData &&
tabData.zenSyncId &&
(tabData.pinned || tabData.zenEssential) &&
!tabData.zenIsEmpty &&
!tabData.zenIsGlance &&
!tabData.zenLiveFolderItemId
);
}
#isSyncableTab(tabData) {
return (
this.#isTabRecordMaterial(tabData) &&
(!!tabData.pinned || !!tabData.zenEssential || lazy.syncNormalTabs)
);
}
#tabIdentity(tabData) {
const initial = tabData._zenPinnedInitialState;
// Pin identity freezes url/title at pin time. A normal tab's identity
// follows its live entry (a leftover initial state from a past pin must
// not shadow it).
const initial =
tabData.pinned || tabData.zenEssential
? tabData._zenPinnedInitialState
: null;
let url = initial?.entry?.url;
let title = initial?.entry?.title;
if (!url || url === "about:blank") {
@@ -473,6 +492,7 @@ class nsZenSpacesSyncModel {
create: true,
}),
essential,
pinned: !!(tab.pinned || tab.zenEssential),
workspaceUuid: essential ? null : tab.zenWorkspace || null,
folderId: ctx.folderOf(tab.groupId || null),
staticLabel:
@@ -494,8 +514,52 @@ class nsZenSpacesSyncModel {
const map = new Map();
const pending = new Set();
const ctx = this.#projectionContext(sidebar);
const { tabs, folders, splits, splitParents, splitWs } = ctx;
const spaces = sidebar.spaces || [];
this.#collectHeldIds(ctx, sidebar, pending);
this.#projectContainers(map);
this.#projectSpaces(map, ctx, spaces);
this.#projectFolders(map, ctx, pending);
this.#projectTabs(map, ctx);
this.#projectSplits(map, ctx);
this.#projectLayout(map, ctx, spaces);
this.#cache = { stamp, map, pending };
return map;
}
/**
* Items excluded only by the normal-tabs option are held back, not
* deleted. Flipping the option off must not tombstone them remotely.
*
* @param {object} ctx - The projection context.
* @param {object} sidebar - The collected sidebar data.
* @param {Set<string>} pending - Receives the held-back ids.
*/
#collectHeldIds(ctx, sidebar, pending) {
if (lazy.syncNormalTabs) {
return;
}
const held = new Set();
for (const tab of ctx.allTabs) {
if (this.#isTabRecordMaterial(tab) && !this.#isSyncableTab(tab)) {
held.add(tab.zenSyncId);
pending.add(tab.zenSyncId);
}
}
for (const split of sidebar.splitViewData || []) {
if (
split?.groupId &&
!ctx.splitIds.has(split.groupId) &&
Array.isArray(split.tabs) &&
split.tabs.some(id => held.has(id))
) {
pending.add(split.groupId);
}
}
}
#projectContainers(map) {
for (const identity of lazy.ContextualIdentityService.getPublicIdentities()) {
if (!identity.name) {
continue;
@@ -516,8 +580,9 @@ class nsZenSpacesSyncModel {
},
});
}
}
const spaces = sidebar.spaces || [];
#projectSpaces(map, ctx, spaces) {
for (const space of spaces) {
if (!space?.uuid) {
continue;
@@ -537,8 +602,10 @@ class nsZenSpacesSyncModel {
},
});
}
}
for (const folder of folders) {
#projectFolders(map, ctx, pending) {
for (const folder of ctx.folders) {
const fid = folder.id;
let live = null;
if (folder.isLiveFolder) {
@@ -565,44 +632,46 @@ class nsZenSpacesSyncModel {
},
});
}
}
this.#projectTabs(map, ctx);
for (const split of splits) {
#projectSplits(map, ctx) {
const tabById = new Map(ctx.tabs.map(t => [t.zenSyncId, t]));
for (const split of ctx.splits) {
const member = tabById.get(split.tabs[0]);
map.set(split.groupId, {
kind: RECORD_KINDS.SPLIT,
data: {
splitId: split.groupId,
gridType: split.gridType || "grid",
pinned: !!(member?.pinned || member?.zenEssential),
tabs: [...split.tabs],
workspaceUuid: splitWs.get(split.groupId) ?? null,
folderId: splitParents.get(split.groupId) || null,
workspaceUuid: ctx.splitWs.get(split.groupId) ?? null,
folderId: ctx.splitParents.get(split.groupId) || null,
},
});
}
}
if (spaces.length) {
const essentials = {};
for (const tab of tabs) {
if (!tab.zenEssential) {
continue;
}
const key =
this.guidForContextId(tab.userContextId, { create: true }) ||
"default";
(essentials[key] ||= []).push(tab.zenSyncId);
#projectLayout(map, ctx, spaces) {
if (!spaces.length) {
return;
}
const essentials = {};
for (const tab of ctx.tabs) {
if (!tab.zenEssential) {
continue;
}
map.set(LAYOUT_RECORD_ID, {
kind: RECORD_KINDS.LAYOUT,
data: {
spaces: spaces.map(s => s.uuid).filter(Boolean),
essentials,
},
});
const key =
this.guidForContextId(tab.userContextId, { create: true }) || "default";
(essentials[key] ||= []).push(tab.zenSyncId);
}
this.#cache = { stamp, map, pending };
return map;
map.set(LAYOUT_RECORD_ID, {
kind: RECORD_KINDS.LAYOUT,
data: {
spaces: spaces.map(s => s.uuid).filter(Boolean),
essentials,
},
});
}
/**

View File

@@ -356,7 +356,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
alwaysUnload &&
["close", "reset", "switch", "reset-switch"].includes(behavior)
) {
behavior = behavior.contains("reset")
behavior = behavior.includes("reset")
? "reset-unload-switch"
: "unload-switch";
}
@@ -680,7 +680,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
gBrowser.setIcon(tab, icon);
lazy.TabStateCache.update(tab.permanentKey, {
image: null,
image: icon || null,
});
},
});

View File

@@ -1059,7 +1059,6 @@
#tabs-newtab-button {
display: none;
font-weight: 500;
-moz-user-focus: ignore !important;
}
@@ -1342,7 +1341,7 @@
}
}
#tabs-newtab-button:not([in-urlbar="true"]) label,
#tabs-newtab-button:not([in-urlbar="true"]),
.zen-current-workspace-indicator-name {
color: color-mix(in srgb, currentColor 70%, transparent);
}

View File

@@ -46,3 +46,38 @@ add_task(async function test_Check_Creation() {
await gZenWorkspaces.removeWorkspace(newWorkspaceUUID);
});
add_task(async function test_Essentials_Hidden_Workspace_Creation() {
const originalWorkspace = gZenWorkspaces.getActiveWorkspace();
const essentialTab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
skipAnimation: true,
userContextId: 0,
});
gZenPinnedTabManager.addToEssentials(essentialTab);
const essentialsContainer = essentialTab.parentNode;
await gZenWorkspaces.createAndSaveWorkspace(
"Empty Container Workspace",
undefined,
false,
1
);
const emptyWorkspace = gZenWorkspaces.getActiveWorkspace();
// The temporary creation workspace uses container 0, which previously
// caused the container 0 Essentials to reappear over the form.
await gZenWorkspaces.openWorkspaceCreation();
const creationForm = document.querySelector("zen-workspace-creation");
ok(creationForm, "Workspace creation form is shown");
ok(
BrowserTestUtils.isHidden(essentialsContainer),
"Essentials remain hidden while creating a workspace"
);
await creationForm.onCancelButtonCommand();
await gZenWorkspaces.changeWorkspace(originalWorkspace);
await gZenWorkspaces.removeWorkspace(emptyWorkspace.uuid);
await BrowserTestUtils.removeTab(essentialTab);
});

View File

@@ -0,0 +1,19 @@
# 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/.
[DEFAULT]
prefs = [
"zen.window-sync.enabled=true",
"zen.urlbar.replace-newtab=false",
"zen.spaces-sync.first-sync-animation-shown=true",
]
support-files = [
"head.js",
]
["browser_folder_delete.js"]
["browser_normal_tabs_apply.js"]
["browser_normal_tabs_projection.js"]

View File

@@ -0,0 +1,100 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
async function applyFolderWithMembers(fid, memberIds) {
const failed = await ZenSpacesSyncApplier.applyBatch([
folderRecord(fid),
...memberIds.map(id => tabRecord(id, { pinned: true, folderId: fid })),
]);
Assert.deepEqual(failed, [], "The folder batch should apply cleanly");
const folder = document.getElementById(fid);
Assert.ok(folder?.isZenFolder, "The folder should materialize");
for (const id of memberIds) {
Assert.equal(
document.getElementById(id)?.group,
folder,
`member ${id} should live in the folder`
);
}
}
add_task(async function test_FolderTombstoneWithMemberTombstones() {
await gZenWorkspaces.promiseInitialized;
const fid = "test-sync-del-a-folder";
const members = ["test-sync-del-a-1", "test-sync-del-a-2"];
await applyFolderWithMembers(fid, members);
const failed = await ZenSpacesSyncApplier.applyBatch([
tombstone(fid),
...members.map(tombstone),
]);
Assert.deepEqual(failed, [], "The tombstone batch should apply cleanly");
await TestUtils.waitForCondition(
() => !document.getElementById(fid),
"waiting for the folder to be removed"
);
for (const id of members) {
Assert.ok(!document.getElementById(id), `member ${id} should be gone`);
}
for (const id of [fid, ...members]) {
ZenSpacesSyncModel.noteApplied(id, null);
}
});
add_task(async function test_FolderTombstoneAloneDeletesMembers() {
await gZenWorkspaces.promiseInitialized;
const fid = "test-sync-del-b-folder";
const members = ["test-sync-del-b-1", "test-sync-del-b-2"];
await applyFolderWithMembers(fid, members);
const failed = await ZenSpacesSyncApplier.applyBatch([tombstone(fid)]);
Assert.deepEqual(failed, [], "The tombstone batch should apply cleanly");
await TestUtils.waitForCondition(
() => !document.getElementById(fid),
"waiting for the folder to be removed"
);
for (const id of members) {
const tab = document.getElementById(id);
Assert.ok(!tab, `member ${id} should be deleted, not orphaned as pinned`);
}
for (const id of [fid, ...members]) {
ZenSpacesSyncModel.noteApplied(id, null);
}
});
add_task(async function test_MemberMovedOutSurvivesFolderTombstone() {
await gZenWorkspaces.promiseInitialized;
const fid = "test-sync-del-c-folder";
const stays = "test-sync-del-c-stays";
const moved = "test-sync-del-c-moved";
await applyFolderWithMembers(fid, [stays, moved]);
const failed = await ZenSpacesSyncApplier.applyBatch([
tabRecord(moved, { pinned: true, folderId: null }),
tombstone(fid),
]);
Assert.deepEqual(failed, [], "The batch should apply cleanly");
await TestUtils.waitForCondition(
() => !document.getElementById(fid),
"waiting for the folder to be removed"
);
const movedTab = document.getElementById(moved);
Assert.ok(movedTab, "the moved-out member should survive");
Assert.ok(!movedTab.group?.isZenFolder, "and no longer be in a folder");
Assert.ok(
!document.getElementById(stays),
"the member with no surviving record should be deleted"
);
ZenSpacesSyncModel.noteApplied(moved, null);
for (const id of [fid, stays]) {
ZenSpacesSyncModel.noteApplied(id, null);
}
if (movedTab) {
BrowserTestUtils.removeTab(movedTab);
}
});

View File

@@ -0,0 +1,129 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function test_ApplySkippedWhileOptionOff() {
await gZenWorkspaces.promiseInitialized;
const id = "test-spaces-sync-normal-off";
const failed = await ZenSpacesSyncApplier.applyBatch([
tabRecord(id, { pinned: false }),
]);
Assert.deepEqual(
failed,
[],
"A skipped normal-tab record should not be reported as failed"
);
Assert.ok(
!document.getElementById(id),
"No tab should materialize while the option is off"
);
});
add_task(async function test_ApplyCreatesUnpinnedTab() {
await SpecialPowers.pushPrefEnv({ set: [[NORMAL_TABS_PREF, true]] });
const id = "test-spaces-sync-normal-on";
const failed = await ZenSpacesSyncApplier.applyBatch([
tabRecord(id, { pinned: false }),
]);
Assert.deepEqual(failed, [], "The record should apply cleanly");
const tab = document.getElementById(id);
Assert.ok(gBrowser.isTab(tab), "The tab should materialize");
Assert.ok(!tab.pinned, "The tab should not be pinned");
Assert.equal(
tab.getAttribute("zen-workspace-id"),
gZenWorkspaces.activeWorkspace,
"The tab should land in the record's workspace"
);
Assert.ok(
!tab._zenPinnedInitialState,
"A normal tab should carry no pin identity"
);
ZenSpacesSyncModel.noteApplied(id, null);
BrowserTestUtils.removeTab(tab);
await SpecialPowers.popPrefEnv();
});
add_task(async function test_UpdateRetargetsUnloadedNormalTab() {
await SpecialPowers.pushPrefEnv({ set: [[NORMAL_TABS_PREF, true]] });
const id = "test-spaces-sync-retarget";
let failed = await ZenSpacesSyncApplier.applyBatch([
tabRecord(id, { pinned: false, url: "https://example.com/first" }),
]);
Assert.deepEqual(failed, [], "The record should apply cleanly");
const tab = document.getElementById(id);
Assert.ok(!tab.linkedPanel, "The synced tab should stay unloaded");
failed = await ZenSpacesSyncApplier.applyBatch([
tabRecord(id, { pinned: false, url: "https://example.com/second" }),
]);
Assert.deepEqual(failed, [], "The navigation record should apply cleanly");
Assert.ok(!tab.linkedPanel, "The tab should stay unloaded after retarget");
const state = JSON.parse(SessionStore.getTabState(tab));
Assert.equal(
state.entries.at(-1)?.url,
"https://example.com/second",
"The unloaded tab's state should point at the new url"
);
ZenSpacesSyncModel.noteApplied(id, null);
BrowserTestUtils.removeTab(tab);
await SpecialPowers.popPrefEnv();
});
add_task(async function test_UpdateRetargetsDiscardedNormalTab() {
await SpecialPowers.pushPrefEnv({ set: [[NORMAL_TABS_PREF, true]] });
const tab = await openSyncableTab("https://example.com/?loadedfirst");
const id = tab.id;
await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[0]);
gBrowser.discardBrowser(tab);
Assert.ok(!tab.linkedPanel, "The tab should be discarded");
const failed = await ZenSpacesSyncApplier.applyBatch([
tabRecord(id, { pinned: false, url: "https://example.com/?remotenav" }),
]);
Assert.deepEqual(failed, [], "The navigation record should apply cleanly");
Assert.ok(!tab.linkedPanel, "The tab should stay unloaded after retarget");
const state = JSON.parse(SessionStore.getTabState(tab));
Assert.equal(
state.entries.at(-1)?.url,
"https://example.com/?remotenav",
"The discarded tab's state should point at the new url"
);
ZenSpacesSyncModel.noteApplied(id, null);
BrowserTestUtils.removeTab(tab);
await SpecialPowers.popPrefEnv();
});
add_task(async function test_ApplyPinStateTransition() {
await SpecialPowers.pushPrefEnv({ set: [[NORMAL_TABS_PREF, true]] });
const id = "test-spaces-sync-transition";
let failed = await ZenSpacesSyncApplier.applyBatch([
tabRecord(id, { pinned: true }),
]);
Assert.deepEqual(failed, [], "The pinned record should apply cleanly");
const tab = document.getElementById(id);
Assert.ok(gBrowser.isTab(tab), "The tab should materialize");
Assert.ok(tab.pinned, "A pinned record should materialize pinned");
await TestUtils.waitForCondition(
() => tab._zenPinnedInitialState,
"waiting for the pin identity to settle"
);
failed = await ZenSpacesSyncApplier.applyBatch([
tabRecord(id, { pinned: false }),
]);
Assert.deepEqual(failed, [], "The demoting record should apply cleanly");
Assert.ok(!tab.pinned, "A record demotion should unpin the tab");
Assert.ok(
!tab._zenPinnedInitialState,
"Demotion should clear the pin identity"
);
ZenSpacesSyncModel.noteApplied(id, null);
BrowserTestUtils.removeTab(tab);
await SpecialPowers.popPrefEnv();
});

View File

@@ -0,0 +1,81 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function test_ProjectionGatedByPref() {
await gZenWorkspaces.promiseInitialized;
const pinnedTab = await openSyncableTab("https://example.com/?pinned", {
pinned: true,
});
const normalTab = await openSyncableTab("https://example.com/?normal");
let projections = await collectProjections();
const pinnedRecord = projections.get(pinnedTab.id);
Assert.ok(pinnedRecord, "Pinned tab should project with the option off");
Assert.equal(
pinnedRecord.data.pinned,
true,
"Pinned tab record should carry pinned: true"
);
Assert.ok(
!projections.get(normalTab.id),
"Normal tab should not project with the option off"
);
await SpecialPowers.pushPrefEnv({ set: [[NORMAL_TABS_PREF, true]] });
ZenSpacesSyncModel.invalidate();
projections = ZenSpacesSyncModel.projections();
const record = projections.get(normalTab.id);
Assert.ok(record, "Normal tab should project with the option on");
Assert.equal(
record.data.pinned,
false,
"Normal tab record should carry pinned: false"
);
Assert.equal(
record.data.workspaceUuid,
gZenWorkspaces.activeWorkspace,
"Normal tab record should name its workspace"
);
const space = projections.get(gZenWorkspaces.activeWorkspace);
Assert.ok(
space?.data?.children?.includes(normalTab.id),
"The space's child order should include the normal tab"
);
await SpecialPowers.popPrefEnv();
BrowserTestUtils.removeTab(normalTab);
BrowserTestUtils.removeTab(pinnedTab);
});
add_task(async function test_DisablingOptionHoldsBackInsteadOfTombstoning() {
await gZenWorkspaces.promiseInitialized;
await SpecialPowers.pushPrefEnv({ set: [[NORMAL_TABS_PREF, true]] });
const normalTab = await openSyncableTab("https://example.com/?heldback");
await collectProjections();
ZenSpacesSyncModel.markUploaded([normalTab.id]);
let changes = ZenSpacesSyncModel.computeChangedIDs();
Assert.ok(
!(normalTab.id in changes),
"An uploaded normal tab should report no pending change"
);
await SpecialPowers.popPrefEnv();
ZenSpacesSyncModel.invalidate();
Assert.ok(
!ZenSpacesSyncModel.projections().get(normalTab.id),
"Normal tab should stop projecting once the option is off"
);
changes = ZenSpacesSyncModel.computeChangedIDs();
Assert.ok(
!(normalTab.id in changes),
"Disabling the option should not tombstone the tab"
);
// Drop the snapshot entry so this task leaves no sync state behind.
ZenSpacesSyncModel.noteApplied(normalTab.id, null);
BrowserTestUtils.removeTab(normalTab);
});

View File

@@ -0,0 +1,108 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { ZenSpacesSyncModel } = ChromeUtils.importESModule(
"resource:///modules/zen/ZenSpacesSyncModel.sys.mjs"
);
const { ZenSpacesSyncApplier } = ChromeUtils.importESModule(
"resource:///modules/zen/ZenSpacesSyncApplier.sys.mjs"
);
const { SessionSaver } = ChromeUtils.importESModule(
"resource:///modules/sessionstore/SessionSaver.sys.mjs"
);
const { TabStateFlusher } = ChromeUtils.importESModule(
"resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
);
const { TestUtils } = ChromeUtils.importESModule(
"resource://testing-common/TestUtils.sys.mjs"
);
const { E10SUtils } = ChromeUtils.importESModule(
"resource://gre/modules/E10SUtils.sys.mjs"
);
const { PromiseTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/PromiseTestUtils.sys.mjs"
);
PromiseTestUtils.allowMatchingRejectionsGlobally(/menu-bookmark-tab/);
const NORMAL_TABS_PREF = "zen.spaces-sync.normal-tabs";
/**
* Runs a full session save (which collects the sidebar data) and returns
* fresh sync projections built from it.
*/
async function collectProjections() {
await SessionSaver.run();
ZenSpacesSyncModel.invalidate();
return ZenSpacesSyncModel.projections();
}
async function openSyncableTab(url, { pinned = false } = {}) {
const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
if (pinned) {
gBrowser.pinTab(tab);
}
await TabStateFlusher.flush(tab.linkedBrowser);
ok(tab.id, "the tab should have a sync id");
return tab;
}
function tabRecord(
id,
{
pinned,
url = "https://example.com/",
current = null,
workspaceUuid,
folderId = null,
} = {}
) {
return {
id,
deleted: false,
cleartext: {
kind: "tab",
data: {
tabId: id,
url,
title: "Synced tab",
current,
icon: null,
containerGuid: null,
essential: false,
pinned,
workspaceUuid: workspaceUuid ?? gZenWorkspaces.activeWorkspace,
folderId,
staticLabel: null,
hasStaticIcon: false,
defaultContainer: false,
},
},
};
}
function folderRecord(id, { name = "Synced folder", workspaceUuid } = {}) {
return {
id,
deleted: false,
cleartext: {
kind: "folder",
data: {
folderId: id,
name,
icon: null,
workspaceUuid: workspaceUuid ?? gZenWorkspaces.activeWorkspace,
parentFolderId: null,
live: null,
children: [],
},
},
};
}
function tombstone(id) {
return { id, deleted: true };
}

View File

@@ -3,7 +3,7 @@
"use strict";
add_task(async function test_Duplicate_Tab_Inside_Folder() {
add_task(async function test_Owned_Tab_Inside_Folder() {
await SpecialPowers.pushPrefEnv({
set: [["zen.folders.owned-tabs-in-folder", true]],
});
@@ -40,7 +40,7 @@ add_task(async function test_Duplicate_Tab_Inside_Folder() {
await SpecialPowers.popPrefEnv();
});
add_task(async function test_Duplicate_Tab_Inside_Folder_Unpinned() {
add_task(async function test_Owned_Tab_Inside_Folder_Unpinned() {
await SpecialPowers.pushPrefEnv({
set: [["zen.folders.owned-tabs-in-folder", false]],
});
@@ -72,3 +72,56 @@ add_task(async function test_Duplicate_Tab_Inside_Folder_Unpinned() {
await removeFolder(folder);
await SpecialPowers.popPrefEnv();
});
add_task(async function test_Duplicate_Tab_Inside_Folder_Pref_Enabled() {
await SpecialPowers.pushPrefEnv({
set: [["zen.folders.owned-tabs-in-folder", true]],
});
const selectedTab = gBrowser.selectedTab;
const tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
const folder = await gZenFolders.createFolder([tab], {
renameFolder: false,
});
gBrowser.selectedTab = tab;
const newTab = gBrowser.duplicateTab(tab);
await BrowserTestUtils.waitForEvent(newTab, "SSTabRestored");
Assert.equal(
folder.tabs.length,
3,
"Folder contains its empty tab, original tab, and duplicated tab"
);
Assert.equal(newTab.group, folder, "Duplicated tab remains in the folder");
gBrowser.selectedTab = selectedTab;
await removeFolder(folder);
await SpecialPowers.popPrefEnv();
});
add_task(async function test_Duplicate_Tab_Inside_Folder_Pref_Disabled() {
await SpecialPowers.pushPrefEnv({
set: [["zen.folders.owned-tabs-in-folder", false]],
});
const selectedTab = gBrowser.selectedTab;
const tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
const folder = await gZenFolders.createFolder([tab], {
renameFolder: false,
});
gBrowser.selectedTab = tab;
const newTab = gBrowser.duplicateTab(tab);
await BrowserTestUtils.waitForEvent(newTab, "SSTabRestored");
Assert.equal(
folder.tabs.length,
2,
"Folder contains its empty tab and original tab"
);
ok(!newTab.group, "Duplicated tab remains outside the folder");
gBrowser.selectedTab = selectedTab;
BrowserTestUtils.removeTab(newTab);
await removeFolder(folder);
await SpecialPowers.popPrefEnv();
});

View File

@@ -6,12 +6,14 @@ BROWSER_CHROME_MANIFESTS += [
"boosts/browser.toml",
"compact_mode/browser.toml",
"container_essentials/browser.toml",
"device_sync/browser.toml",
"folders/browser.toml",
"glance/browser.toml",
"live-folders/browser.toml",
"media/browser.toml",
"pinned/browser.toml",
"popover/browser.toml",
"share/browser.toml",
"site_control/browser.toml",
"space_routing/browser.toml",
"spaces/browser.toml",

View File

@@ -0,0 +1,22 @@
# 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/.
[DEFAULT]
prefs = [
"zen.share.base-url=https://example.com",
"zen.share.dont-ask-before-sharing=true",
]
support-files = [
"head.js",
]
["browser_share_client.js"]
["browser_share_import.js"]
["browser_share_serialize.js"]
["browser_share_split_link.js"]
["browser_share_url_parsing.js"]

View File

@@ -0,0 +1,160 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { HttpServer } = ChromeUtils.importESModule(
"resource://testing-common/httpd.sys.mjs"
);
const VALID_DOC = {
version: "1",
shared: {
type: "space",
name: "Test",
items: [{ type: "tab", url: "https://example.com/", label: "T" }],
},
};
let server;
let base;
let lastRequest = null;
let nextResponse = null;
function readBody(request) {
return NetUtil.readInputStreamToString(
request.bodyInputStream,
request.bodyInputStream.available()
);
}
add_setup(async function () {
server = new HttpServer();
server.registerPathHandler("/api/shares", (request, response) => {
lastRequest = {
method: request.method,
query: request.queryString,
apiKey: request.hasHeader("x-api-key")
? request.getHeader("x-api-key")
: null,
secretKey: request.hasHeader("x-secret-key")
? request.getHeader("x-secret-key")
: null,
body: readBody(request),
};
if (nextResponse) {
response.setStatusLine(request.httpVersion, nextResponse.status, "Err");
response.setHeader("Content-Type", "application/json");
response.write(nextResponse.body);
nextResponse = null;
return;
}
response.setStatusLine(request.httpVersion, 201, "Created");
response.setHeader("Content-Type", "application/json");
response.write(
JSON.stringify({
id: SHARE_ID,
name: null,
createdAt: new Date().toISOString(),
expiresAt: null,
size: 10,
url: `/api/shares/${SHARE_ID}`,
webUrl: `/space/${SHARE_ID}`,
})
);
});
server.registerPrefixHandler("/api/shares/", (request, response) => {
if (request.path.endsWith("/GONE-GONE-GONE-GONE")) {
response.setStatusLine(request.httpVersion, 404, "Not Found");
response.setHeader("Content-Type", "application/json");
response.write(JSON.stringify({ error: "share not found" }));
return;
}
response.setStatusLine(request.httpVersion, 200, "OK");
response.setHeader("Content-Type", "application/json");
response.write(
JSON.stringify({
id: SHARE_ID,
name: "John Zen",
createdAt: new Date().toISOString(),
expiresAt: null,
size: 10,
data: request.path.endsWith("/BADD-BADD-BADD-BADD")
? { shared: [] }
: VALID_DOC,
})
);
});
server.start(-1);
base = `http://localhost:${server.identity.primaryPort}`;
await SpecialPowers.pushPrefEnv({
set: [["zen.share.base-url", base]],
});
registerCleanupFunction(() => new Promise(resolve => server.stop(resolve)));
});
add_task(async function test_create_share() {
const created = await ZenShareClient.createShare(VALID_DOC, {
name: "John Zen",
});
Assert.equal(lastRequest.secretKey, null, "no secret key by default");
Assert.deepEqual(
JSON.parse(lastRequest.body),
VALID_DOC,
"the body is the bare document"
);
Assert.equal(
created.link,
`${base}/space/${SHARE_ID}`,
"link is the absolute public page url"
);
});
add_task(async function test_secret_key_replaces_api_key() {
await SpecialPowers.pushPrefEnv({
set: [["zen.share.secret-key", "shhh"]],
});
await ZenShareClient.createShare(VALID_DOC);
Assert.equal(lastRequest.secretKey, "shhh", "secret key is sent");
Assert.equal(
lastRequest.apiKey,
null,
"the api key is never sent next to a secret key"
);
await SpecialPowers.popPrefEnv();
});
add_task(async function test_invalid_document_never_uploads() {
lastRequest = null;
await Assert.rejects(
ZenShareClient.createShare({ shared: [] }),
e => e.code === "invalid-document",
"an array shared value fails the schema"
);
await Assert.rejects(
ZenShareClient.createShare({
shared: { type: "space", name: "x", items: [], extra: true },
}),
e => e.code === "invalid-document",
"additionalProperties are rejected locally"
);
Assert.equal(lastRequest, null, "invalid documents never hit the server");
});
add_task(async function test_fetch_share_preview() {
const preview = await ZenShareClient.fetchSharePreview({ id: SHARE_ID });
Assert.equal(preview.name, "John Zen", "the sharer name comes back");
Assert.deepEqual(preview.doc, VALID_DOC, "the document comes back");
await Assert.rejects(
ZenShareClient.fetchSharePreview({ id: "GONE-GONE-GONE-GONE" }),
e => e.code === "not-found",
"dead shares map to not-found"
);
await Assert.rejects(
ZenShareClient.fetchSharePreview({ id: "BADD-BADD-BADD-BADD" }),
e => e.code === "invalid-document",
"downloaded documents are validated too"
);
});

View File

@@ -0,0 +1,200 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
let initialWorkspace;
const SPACE_DOC = {
version: "1",
shared: {
type: "space",
name: "Imported Space",
theme: {
type: "gradient",
gradientColors: [
{ c: [217, 105, 24], type: "explicit", isPrimary: true },
{ c: [16, 32, 48], type: "custom", isCustom: true },
],
},
items: [
{
type: "folder",
name: "Research",
icon: "chrome://browser/skin/zen-icons/selectable/star.svg",
items: [
{ type: "tab", url: "https://example.com/sub", label: "Sub tab" },
],
},
{
type: "tab",
url: "https://example.com/pinned",
label: "Pinned tab",
isPinned: true,
},
{
type: "splitView",
tabs: [
{
type: "tab",
url: "https://example.com/left",
label: "Left",
isPinned: true,
},
{
type: "tab",
url: "https://example.com/right",
label: "Right",
isPinned: true,
},
],
},
{ type: "tab", url: "https://example.com/loose", label: "Loose" },
],
},
};
add_setup(async function () {
initialWorkspace = gZenWorkspaces.activeWorkspace;
await SpecialPowers.pushPrefEnv({
set: [["zen.view.show-newtab-button-top", true]],
});
registerCleanupFunction(() => cleanupSpaces(initialWorkspace));
});
add_task(async function test_overlay_preview() {
const restore = stubSharePreview(SPACE_DOC, "John Zen");
const { tab, overlay } = await openShareOverlay(
`${SHARE_BASE}/space/${SHARE_ID}`
);
Assert.ok(
tab.linkedBrowser.hasAttribute("zen-share-overlay-showing"),
"the share page is visually hidden behind the overlay"
);
Assert.equal(
overlay.querySelector(".zen-share-overlay-badge-title").textContent,
"Imported Space"
);
Assert.ok(
tab.hasAttribute("zen-show-sublabel"),
"the host tab gets the shared-by sublabel"
);
// Navigating away tears the overlay down and clears the sublabel.
const loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
BrowserTestUtils.startLoadingURIString(
tab.linkedBrowser,
"https://example.com/"
);
await loaded;
await TestUtils.waitForCondition(
() => !tab.linkedBrowser._zenShareOverlay,
"overlay is removed on navigation"
);
Assert.ok(
!tab.hasAttribute("zen-show-sublabel"),
"the sublabel is cleared on navigation"
);
BrowserTestUtils.removeTab(tab);
restore();
});
add_task(async function test_import_space() {
const restore = stubSharePreview(SPACE_DOC, "John Zen");
let sweepPlayed = false;
const restoreSweep = stubMethod(
gZenStartup,
"playWindowSweepAnimation",
() => (sweepPlayed = true)
);
const { tab, overlay } = await openShareOverlay(
`${SHARE_BASE}/space/${SHARE_ID}`
);
await importFromOverlay(tab, overlay);
await TestUtils.waitForCondition(
() => gZenWorkspaces.activeWorkspace !== initialWorkspace,
"the imported space becomes active"
);
const workspace = gZenWorkspaces.getActiveWorkspaceFromCache();
Assert.equal(workspace.name, "Imported Space", "the shared name wins");
Assert.deepEqual(
workspace.theme.gradientColors[0].c,
[217, 105, 24],
"the shared theme is applied"
);
Assert.equal(
workspace.theme.gradientColors[1].c,
"rgb(16, 32, 48)",
"custom colors go back to css strings locally"
);
Assert.ok(sweepPlayed, "importing a space plays the startup sweep");
const element = gZenWorkspaces.workspaceElement(workspace.uuid);
const pinnedKinds = [...element.pinnedTabsContainer.children]
.map(child => {
if (child.matches?.("zen-folder")) {
return "folder:" + child.label;
}
if (child.matches?.("tab-group[split-view-group]")) {
return "split";
}
return gBrowser.isTab(child) ? "tab:" + child.label : null;
})
.filter(Boolean);
Assert.deepEqual(
pinnedKinds,
["folder:Research", "tab:Pinned tab", "split"],
"pinned items import in document order despite the new-tab-top pref"
);
const folder = element.querySelector("zen-folder");
Assert.ok(folder.collapsed, "imported folders start collapsed");
Assert.equal(
folder.iconURL,
"chrome://browser/skin/zen-icons/selectable/star.svg",
"the folder icon is restored"
);
const split = element.pinnedTabsContainer.querySelector(
"tab-group[split-view-group]"
);
Assert.ok(
split.tabs.every(t => t.pinned),
"an all-pinned shared split imports pinned"
);
const loose = [...element.tabsContainer.children].find(
child => gBrowser.isTab(child) && child.label === "Loose"
);
Assert.ok(loose, "unpinned tabs land in the normal section");
Assert.ok(!loose.pinned, "and stay unpinned");
restoreSweep();
restore();
await cleanupSpaces(initialWorkspace);
});
add_task(async function test_dead_link_shows_error() {
const restore = stubMethod(ZenShareClient, "fetchSharePreview", async () => {
throw new ZenShareError("not-found", "share not found");
});
const tab = gBrowser.addTrustedTab(`${SHARE_BASE}/space/${SHARE_ID}`, {
inBackground: false,
});
gBrowser.selectedTab = tab;
await TestUtils.waitForCondition(() => {
const status = tab.linkedBrowser._zenShareOverlay?.querySelector(
".zen-share-overlay-status"
);
return (
status?.getAttribute("data-l10n-id") ===
"zen-share-import-error-dead-description"
);
}, "the dead-link message shows in the overlay");
BrowserTestUtils.removeTab(tab);
restore();
});

View File

@@ -0,0 +1,129 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
let initialWorkspace;
function captureShare(action) {
return new Promise(resolve => {
const restore = stubMethod(ZenShareClient, "createShare", async doc => {
restore();
resolve(doc);
return {
id: SHARE_ID,
expiresAt: null,
webUrl: `/space/${SHARE_ID}`,
link: `${SHARE_BASE}/space/${SHARE_ID}`,
};
});
action();
});
}
add_setup(async function () {
initialWorkspace = gZenWorkspaces.activeWorkspace;
registerCleanupFunction(() => cleanupSpaces(initialWorkspace));
});
add_task(async function test_space_serialization() {
await gZenWorkspaces.createAndSaveWorkspace("Serialize Me");
const uuid = gZenWorkspaces.activeWorkspace;
const folderTab = makeLazyTab("https://folder.example.com/", "Folder tab");
const folder = gZenFolders.createFolder([folderTab], { label: "Docs" });
gZenFolders.setFolderUserIcon(
folder,
"chrome://browser/skin/zen-icons/selectable/star.svg"
);
const pinnedTab = makeLazyTab("https://pinned.example.com/", "Pinned");
gBrowser.pinTab(pinnedTab);
const splitA = makeLazyTab("https://a.example.com/", "Split A");
const splitB = makeLazyTab("https://b.example.com/", "Split B");
gBrowser.pinTab(splitA);
gBrowser.pinTab(splitB);
gZenViewSplitter.splitTabs([splitA, splitB], "grid", -1);
// Normal section: a plain tab and one that must not be shared.
makeLazyTab("https://normal.example.com/", "Normal");
makeLazyTab("about:preferences", "Internal");
const workspace = gZenWorkspaces.getActiveWorkspaceFromCache();
workspace.theme = {
type: "gradient",
opacity: 0.7,
texture: 0.3,
gradientColors: [
{ c: [10, 20, 30], type: "explicit", isPrimary: true, lightness: "50" },
{ c: "#102030", isCustom: true, type: "custom" },
{ c: "color-mix(in srgb, red, blue)", isCustom: true, type: "custom" },
],
};
const doc = await captureShare(() => gZenShareManager.shareSpace(uuid));
const item = doc.shared;
Assert.equal(item.type, "space");
Assert.equal(item.name, "Serialize Me");
Assert.deepEqual(
item.items.map(i => i.type),
["folder", "tab", "splitView", "tab"],
"pinned items come first, in strip order; internal urls are dropped"
);
const [folderItem, pinnedItem, splitItem, normalItem] = item.items;
Assert.equal(folderItem.name, "Docs");
Assert.equal(
folderItem.icon,
"chrome://browser/skin/zen-icons/selectable/star.svg",
"the custom folder icon is shared"
);
Assert.equal(folderItem.items.length, 1, "folder keeps its tab");
Assert.ok(pinnedItem.isPinned, "loose pinned tabs carry isPinned");
Assert.ok(
splitItem.tabs.every(t => t.isPinned),
"split members carry their real pinned state"
);
Assert.ok(!normalItem.isPinned, "normal tabs carry no pinned flag");
const theme = item.theme;
Assert.ok(!("opacity" in theme), "theme opacity is stripped");
Assert.ok(!("texture" in theme), "theme texture is stripped");
Assert.equal(
theme.gradientColors.length,
2,
"unparseable custom colors are dropped"
);
Assert.deepEqual(
theme.gradientColors[1].c,
[16, 32, 48],
"custom css color strings become [r, g, b]"
);
const validation = await ZenShareClient.validateDocument(doc);
Assert.ok(
validation.valid,
"the serialized document passes the schema: " +
JSON.stringify(validation.errors ?? [])
);
await cleanupSpaces(initialWorkspace);
});
add_task(async function test_share_split_view_requires_two_tabs() {
let created = false;
const restore = stubMethod(ZenShareClient, "createShare", async () => {
created = true;
return { expiresAt: null, webUrl: "/x", link: "x" };
});
const restoreToast = stubMethod(gZenUIManager, "showToast", () => {});
const tab = makeLazyTab("https://only.example.com/", "Only");
const fakeGroup = {
hasAttribute: name => name === "split-view-group",
tabs: [tab],
};
await gZenShareManager.shareSplitView(fakeGroup);
Assert.ok(!created, "a split with fewer than two tabs is not shared");
restore();
restoreToast();
BrowserTestUtils.removeTab(tab);
});

View File

@@ -0,0 +1,112 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
let initialWorkspace;
function splitDoc(pinned) {
return {
shared: {
type: "splitView",
tabs: [
{
type: "tab",
url: "https://example.com/one",
label: "One",
...(pinned && { isPinned: true }),
},
{
type: "tab",
url: "https://example.com/two",
label: "Two",
...(pinned && { isPinned: true }),
},
],
},
};
}
function waitForSplitGroups(count) {
return TestUtils.waitForCondition(
() =>
document.querySelectorAll("tab-group[split-view-group]").length >= count,
`${count} split group(s) should exist`
);
}
add_setup(async function () {
initialWorkspace = gZenWorkspaces.activeWorkspace;
registerCleanupFunction(() => cleanupSpaces(initialWorkspace));
});
add_task(async function test_pinned_split_link_opens_directly() {
const restore = stubSharePreview(splitDoc(true));
const shareTab = gBrowser.addTrustedTab(`${SHARE_BASE}/split/${SHARE_ID}`, {
inBackground: false,
});
gBrowser.selectedTab = shareTab;
await waitForSplitGroups(1);
await TestUtils.waitForCondition(
() => !gBrowser.tabs.includes(shareTab),
"the share tab closes once the split opens"
);
const group = document.querySelector("tab-group[split-view-group]");
Assert.ok(gZenViewSplitter.splitViewActive, "the split view is active");
Assert.ok(
group.tabs.every(t => t.pinned),
"an all-pinned shared split opens pinned"
);
Assert.ok(
group.closest(".zen-workspace-pinned-tabs-section"),
"and lives in the pinned section"
);
restore();
await cleanupSpaces(initialWorkspace);
});
add_task(async function test_unpinned_split_link_stays_normal() {
const restore = stubSharePreview(splitDoc(false));
const shareTab = gBrowser.addTrustedTab(`${SHARE_BASE}/split/${SHARE_ID}`, {
inBackground: false,
});
gBrowser.selectedTab = shareTab;
await waitForSplitGroups(1);
const group = document.querySelector("tab-group[split-view-group]");
Assert.ok(
group.tabs.every(t => !t.pinned),
"an unpinned shared split opens unpinned"
);
Assert.ok(
group.closest(".zen-workspace-normal-tabs-section"),
"and lives in the normal section"
);
restore();
await cleanupSpaces(initialWorkspace);
});
add_task(async function test_failed_split_link_restores_tab() {
const restore = stubMethod(ZenShareClient, "fetchSharePreview", async () => {
throw new ZenShareError("not-found", "share not found");
});
const restoreToast = stubMethod(gZenUIManager, "showToast", () => {});
const shareTab = gBrowser.addTrustedTab(`${SHARE_BASE}/split/${SHARE_ID}`, {
inBackground: false,
});
gBrowser.selectedTab = shareTab;
await TestUtils.waitForCondition(
() => gBrowser.selectedTab === shareTab && !shareTab.hidden,
"the share tab is shown again when the split fails to load"
);
Assert.ok(gBrowser.tabs.includes(shareTab), "the tab is not closed");
restoreToast();
restore();
BrowserTestUtils.removeTab(shareTab);
});

View File

@@ -0,0 +1,56 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function test_slugs_and_types() {
for (const [slug, type] of [
["space", "space"],
["folder", "folder"],
["split-view", "split-view"],
// "split" is the canonical server slug for split views.
["split", "split-view"],
]) {
const share = ZenShareClient.parseShareUrl(
`${SHARE_BASE}/${slug}/${SHARE_ID}`
);
Assert.equal(share?.type, type, `${slug} maps to ${type}`);
Assert.equal(share?.id, SHARE_ID, `${slug} keeps the id`);
}
});
add_task(async function test_id_normalization() {
const share = ZenShareClient.parseShareUrl(
`${SHARE_BASE}/space/aaaa-bbbb-cccc-dddd`
);
Assert.equal(share?.id, SHARE_ID, "ids are matched case-insensitively");
});
add_task(async function test_query_and_fragment_tolerated() {
Assert.ok(
ZenShareClient.parseShareUrl(`${SHARE_BASE}/space/${SHARE_ID}/`),
"trailing slash is accepted"
);
Assert.ok(
ZenShareClient.parseShareUrl(`${SHARE_BASE}/space/${SHARE_ID}?utm=x`),
"query strings are ignored"
);
Assert.ok(
ZenShareClient.parseShareUrl(`${SHARE_BASE}/space/${SHARE_ID}#frag`),
"fragments are ignored"
);
});
add_task(async function test_rejections() {
for (const spec of [
`https://not-the-server.com/space/${SHARE_ID}`,
`${SHARE_BASE}/space/aRO9GBjhTOTZ`, // the old 12-char id format
`${SHARE_BASE}/space/AAAA-BBBB-CCCC`, // too few groups
`${SHARE_BASE}/nonsense/${SHARE_ID}`,
`${SHARE_BASE}/space/${SHARE_ID}/extra`,
`${SHARE_BASE}/space/`,
"not a url",
]) {
Assert.equal(ZenShareClient.parseShareUrl(spec), null, `rejects ${spec}`);
}
});

View File

@@ -0,0 +1,88 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { ZenShareClient, ZenShareError } = ChromeUtils.importESModule(
"resource:///modules/zen/share/ZenShareClient.sys.mjs"
);
const SHARE_BASE = "https://example.com";
const SHARE_ID = "AAAA-BBBB-CCCC-DDDD";
/**
* Replaces a method on an object until the current test file ends.
*
* @returns {Function} Restores the original immediately.
*/
function stubMethod(object, name, replacement) {
const original = object[name];
object[name] = replacement;
const restore = () => (object[name] = original);
registerCleanupFunction(restore);
return restore;
}
function stubSharePreview(doc, name = null) {
return stubMethod(ZenShareClient, "fetchSharePreview", async () => ({
doc,
name,
}));
}
function makeLazyTab(url, label) {
return gBrowser.addTrustedTab(url, {
createLazyBrowser: true,
inBackground: true,
skipAnimation: true,
lazyTabTitle: label,
skipRoute: true,
});
}
/** Opens a share link and waits for its overlay to finish loading. */
async function openShareOverlay(spec) {
const tab = gBrowser.addTrustedTab(spec, { inBackground: false });
gBrowser.selectedTab = tab;
await TestUtils.waitForCondition(() => {
const overlay = tab.linkedBrowser._zenShareOverlay;
return overlay && !overlay.querySelector(".zen-share-overlay-status");
}, "share overlay should finish loading");
return { tab, overlay: tab.linkedBrowser._zenShareOverlay };
}
/** Clicks the overlay's import button and waits for the tab to close. */
async function importFromOverlay(tab, overlay) {
const closed = BrowserTestUtils.waitForTabClosing(tab);
overlay.querySelector(".zen-share-overlay-add").click();
await closed;
}
/** Removes every workspace except the one passed, and all leftover tabs,
* folders and splits of the surviving workspace. */
async function cleanupSpaces(keepUuid) {
for (const workspace of gZenWorkspaces.getWorkspaces()) {
if (workspace.uuid !== keepUuid) {
await gZenWorkspaces.removeWorkspace(workspace.uuid);
}
}
const element = gZenWorkspaces.workspaceElement(keepUuid);
for (const folder of [...element.querySelectorAll("zen-folder")]) {
await folder.delete();
}
// A fresh keeper tab lets every test tab go, including the last pinned
// split member that would otherwise survive and leak into the next task.
const keeper = BrowserTestUtils.addTab(gBrowser, "about:blank", {
skipAnimation: true,
});
for (const tab of [...gBrowser.tabs]) {
if (tab !== keeper && !tab.hasAttribute("zen-empty-tab")) {
BrowserTestUtils.removeTab(tab);
}
}
await TestUtils.waitForCondition(
() => !document.querySelector("tab-group[split-view-group]"),
"no split groups should survive cleanup"
);
}

View File

@@ -95,6 +95,12 @@ add_task(async function test_Welcome_Steps() {
await goNextWelcomePage("zen-generic-next");
ok(true, "Welcome Search Step Test Finished");
await TestUtils.waitForCondition(
() => currentPageContent()?.getAttribute("page") === "essentials",
"the ad blocking page to be skipped when uBlock is unavailable"
);
ok(true, "Welcome Block Ads Step Test Finished");
const essentials = currentPageContent().querySelectorAll(
"#zen-welcome-essentials .zen-welcome-essential"
);

View File

@@ -1,2 +1,2 @@
// Ignore license in this file
https://cdn.jsdelivr.net/npm/motion@latest/dist/motion.js: v12.23.11
https://cdn.jsdelivr.net/npm/motion@latest/dist/motion.js: v13.2.0

File diff suppressed because one or more lines are too long

View File

@@ -6,6 +6,10 @@
let lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
CustomizableUI:
"moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
SearchService: "moz-src:///toolkit/components/search/SearchService.sys.mjs",
});
@@ -26,11 +30,33 @@
{ url: "https://figma.com", icon: "figma", color: "#f24e1e" },
];
const kAdBlockerId = "uBlock0@raymondhill.net";
const gChoices = {
setDefaultBrowser: false,
essentials: new Set(),
blockAds: true,
};
let _adBlocker;
async function fetchAdBlocker() {
if (_adBlocker !== undefined) {
return _adBlocker;
}
try {
const [found, installed] = await Promise.all([
lazy.AddonRepository.getAddonsByIDs([kAdBlockerId]),
lazy.AddonManager.getAddonsByIDs([kAdBlockerId]),
]);
_adBlocker = installed[0] || !found[0]?.sourceURI ? null : found[0];
} catch (ex) {
console.error(ex);
_adBlocker = null;
}
return _adBlocker;
}
function clearBrowserElements() {
for (const element of document.getElementById("browser").children) {
if (kZenElementsToIgnore.includes(element.id)) {
@@ -131,6 +157,44 @@
return label;
}
const _startedInstalls = new Set();
function unpinInstalledAddon(addon) {
const widgetId =
addon.id.toLowerCase().replace(/[^a-z0-9_-]/g, "_") + "-browser-action";
try {
if (lazy.CustomizableUI.getPlacementOfWidget(widgetId)) {
lazy.CustomizableUI.addWidgetToArea(
widgetId,
lazy.CustomizableUI.AREA_ADDONS
);
}
} catch (ex) {
console.error(ex);
}
}
function installAddons(addons) {
for (const addon of addons) {
if (_startedInstalls.has(addon.id)) {
continue;
}
_startedInstalls.add(addon.id);
(async () => {
try {
const install = await lazy.AddonManager.getInstallForURL(
addon.sourceURI.spec,
{ name: addon.name, icons: addon.icons }
);
await install.install();
unpinInstalledAddon(addon);
} catch (ex) {
console.error(`Failed to install ${addon.id}`, ex);
}
})();
}
}
function removeVideoBackground() {
const video = document.getElementById("zen-welcome-video");
if (!video) {
@@ -207,6 +271,12 @@
#show(index, direction) {
const previous = this.currentPage;
while (this.#pages[index]?.skip?.()) {
index += direction;
}
if (index < 0) {
return;
}
this.#index = index;
const page = this.currentPage;
if (!page) {
@@ -269,7 +339,7 @@
if (page.id) {
content.setAttribute("page", page.id);
}
page.render(content);
page.render(content, this);
this.contentContainer.appendChild(content);
this.#content = content;
animate(content, { opacity: [0, 1] }, kFade);
@@ -281,8 +351,8 @@
for (const button of this.currentPage.buttons) {
const element = document.createElement("button");
element.className = button.primary
? "zen-welcome-button primary"
: "zen-welcome-button";
? "zen-big-accent-button primary"
: "zen-big-accent-button";
document.l10n.setAttributes(element, button.l10n);
element.addEventListener("click", () => {
if (button.onclick?.(this) !== false) {
@@ -324,6 +394,8 @@
await animate(`#browser > *:not(${elementsToIgnore})`, {
opacity: [0, 1],
});
_adBlocker = undefined;
_startedInstalls.clear();
}
async #applyChoices() {
@@ -558,6 +630,46 @@
}
},
},
{
id: "block-ads",
title: "zen-welcome-block-ads-title",
descriptions: ["zen-welcome-block-ads-description"],
buttons: [kNextButton],
// Nothing to offer once we know the add-on can't be installed.
skip() {
return _adBlocker === null;
},
async render(content, pages) {
content.appendChild(
createOption({
id: "zen-welcome-block-ads-yes",
group: "zen-welcome-block-ads",
l10n: "zen-welcome-block-ads-yes",
checked: gChoices.blockAds,
})
);
content.appendChild(
createOption({
id: "zen-welcome-block-ads-no",
group: "zen-welcome-block-ads",
l10n: "zen-welcome-block-ads-no",
checked: !gChoices.blockAds,
})
);
// The lookup is warmed at startup, so this usually settled long ago.
if ((await fetchAdBlocker()) === null && content.isConnected) {
pages.next();
}
},
commit(content) {
gChoices.blockAds = content.querySelector(
"#zen-welcome-block-ads-yes"
).checked;
if (gChoices.blockAds && _adBlocker) {
installAddons([_adBlocker]);
}
},
},
{
id: "essentials",
title: "zen-welcome-essentials-title",
@@ -768,6 +880,7 @@
}
function startZenWelcome() {
fetchAdBlocker();
clearBrowserElements();
centerWindowOnScreen();
initializeZenWelcome();

View File

@@ -40,14 +40,7 @@
}
#zen-welcome {
--zen-welcome-accent: light-dark(
oklch(from var(--zen-primary-color) clamp(0.35, l, 0.6) c h),
oklch(from var(--zen-primary-color) clamp(0.7, l, 0.85) c h)
);
--zen-welcome-accent-button: light-dark(
oklch(from var(--zen-primary-color) clamp(0.35, l, 0.6) c h),
oklch(from var(--zen-primary-color) clamp(0.55, l, 0.7) c h)
);
--zen-welcome-accent: var(--zen-accent-button-color);
--zen-welcome-sidebar-bg: light-dark(#ffffff, #1c1c1e);
--zen-welcome-content-bg: light-dark(#f6f6f8f5, #2b2b30ed);
--zen-welcome-surface-bg: light-dark(#ffffff, #1c1c1e);
@@ -62,39 +55,6 @@
);
}
.zen-welcome-button {
appearance: none;
border: none;
border-radius: 12px;
padding: 1.2rem 1rem;
background: transparent;
color: var(--zen-welcome-accent);
font: inherit;
font-size: 1.2rem;
font-weight: 600;
cursor: pointer;
will-change: transform;
transition: scale 0.15s ease-out;
&:hover {
scale: 1.03;
filter: brightness(1.1);
}
&:active {
scale: 1;
}
&.primary {
background: var(--zen-welcome-accent-button);
color: white;
&:hover {
filter: brightness(1.1);
}
}
}
#zen-welcome-start {
flex-direction: column;
-moz-window-dragging: drag;

View File

@@ -40,6 +40,8 @@ export default [
"gZenViewSplitter",
"gZenShareManager",
"gZenSpaceRoutingManager",
"Ci",

Some files were not shown because too many files have changed in this diff Show More