mirror of
https://github.com/zen-browser/desktop.git
synced 2026-09-22 20:37:38 +00:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22961e97d8 | ||
|
|
cb6a607c67 | ||
|
|
267c9aabe1 | ||
|
|
d54e12359c | ||
|
|
c89645a787 | ||
|
|
0a548c7572 | ||
|
|
6d158223b5 | ||
|
|
76fcd5c822 | ||
|
|
ed6900d762 | ||
|
|
2e9aecd41b | ||
|
|
da995e8653 | ||
|
|
64d73ab3e3 | ||
|
|
9551444c61 | ||
|
|
fdf9588b7b | ||
|
|
b07b17c87e | ||
|
|
c9d3cb3dc9 | ||
|
|
2c030369c5 | ||
|
|
88a90206ce | ||
|
|
6d398dec63 | ||
|
|
243a8791a0 | ||
|
|
412731f37e | ||
|
|
7a78848177 | ||
|
|
a323036995 | ||
|
|
abc69a1fcb | ||
|
|
8df45e5c9a | ||
|
|
58e83c137f |
1
.github/workflows/macos-profile-build.yml
vendored
1
.github/workflows/macos-profile-build.yml
vendored
@@ -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:
|
||||
|
||||
1
.github/workflows/windows-profile-build.yml
vendored
1
.github/workflows/windows-profile-build.yml
vendored
@@ -27,7 +27,6 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch: [x86_64, aarch64]
|
||||
|
||||
runs-on: ${{ inputs.release-branch == 'release' && 'windows-latest' || 'windows-latest' }}
|
||||
|
||||
steps:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
43
locales/en-US/browser/browser/zen-share.ftl
Normal file
43
locales/en-US/browser/browser/zen-share.ftl
Normal 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
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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",
|
||||
+ },
|
||||
+ ],
|
||||
+ },
|
||||
+ ],
|
||||
+ },
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
+ }
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
1089
src/external-patches/firefox/bug_2047627_corner_shape_borders.patch
Normal file
1089
src/external-patches/firefox/bug_2047627_corner_shape_borders.patch
Normal file
File diff suppressed because it is too large
Load Diff
558
src/external-patches/firefox/bug_2063169/D319577.patch
Normal file
558
src/external-patches/firefox/bug_2063169/D319577.patch
Normal 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,
|
||||
®ion_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",
|
||||
|
||||
307
src/external-patches/firefox/bug_2063169/D319796.patch
Normal file
307
src/external-patches/firefox/bug_2063169/D319796.patch
Normal 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) {
|
||||
|
||||
560
src/external-patches/firefox/bug_2063169/D319869.patch
Normal file
560
src/external-patches/firefox/bug_2063169/D319869.patch
Normal 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");
|
||||
|
||||
15
src/external-patches/firefox/bug_2063169/D320211.patch
Normal file
15
src/external-patches/firefox/bug_2063169/D320211.patch
Normal 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"]
|
||||
|
||||
34
src/external-patches/firefox/bug_2063169/D320568.patch
Normal file
34
src/external-patches/firefox/bug_2063169/D320568.patch
Normal 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(
|
||||
|
||||
53
src/external-patches/firefox/bug_2063169/D321282.patch
Normal file
53
src/external-patches/firefox/bug_2063169/D321282.patch
Normal 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
154
src/external-patches/firefox/issue_15123.patch
Normal file
154
src/external-patches/firefox/issue_15123.patch
Normal 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"]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
33
src/zen/common/styles/squircle-exclusions.inc.css
Normal file
33
src/zen/common/styles/squircle-exclusions.inc.css
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -64,6 +64,10 @@ hbox.editBMPanel_folderRow {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#editBMPanel_workspaceSummary .workspace-trigger {
|
||||
margin-inline-end: 16px;
|
||||
}
|
||||
|
||||
/* Bookmarks in toolbar */
|
||||
#PersonalToolbar {
|
||||
background: transparent;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
// Windows defaults to 8px
|
||||
document.documentElement.style.setProperty(
|
||||
"--zen-border-radius",
|
||||
"8px"
|
||||
"9px"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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'] & {
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -183,6 +183,9 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
|
||||
case "context_zenFolderChangeIcon":
|
||||
this.changeFolderUserIcon(this.#lastFolderContextMenu);
|
||||
break;
|
||||
case "context_zenShareFolder":
|
||||
gZenShareManager.shareFolder(this.#lastFolderContextMenu);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
19
src/zen/folders/zen-folder-icon.inc.css
Normal file
19
src/zen/folders/zen-folder-icon.inc.css
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
238
src/zen/share/ZenShareClient.sys.mjs
Normal file
238
src/zen/share/ZenShareClient.sys.mjs
Normal 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();
|
||||
821
src/zen/share/ZenShareManager.mjs
Normal file
821
src/zen/share/ZenShareManager.mjs
Normal 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
7
src/zen/share/jar.inc.mn
Normal 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)
|
||||
@@ -4,4 +4,6 @@
|
||||
|
||||
EXTRA_JS_MODULES.zen.share += [
|
||||
"share.schema.json",
|
||||
"ZenShareClient.sys.mjs",
|
||||
"ZenShareManager.mjs",
|
||||
]
|
||||
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
60
src/zen/share/zen-share-confirm.inc.xhtml
Normal file
60
src/zen/share/zen-share-confirm.inc.xhtml
Normal 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>
|
||||
27
src/zen/share/zen-share-confirm.js
Normal file
27
src/zen/share/zen-share-confirm.js
Normal 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
259
src/zen/share/zen-share.css
Normal 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;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
19
src/zen/tests/device_sync/browser.toml
Normal file
19
src/zen/tests/device_sync/browser.toml
Normal 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"]
|
||||
100
src/zen/tests/device_sync/browser_folder_delete.js
Normal file
100
src/zen/tests/device_sync/browser_folder_delete.js
Normal 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);
|
||||
}
|
||||
});
|
||||
129
src/zen/tests/device_sync/browser_normal_tabs_apply.js
Normal file
129
src/zen/tests/device_sync/browser_normal_tabs_apply.js
Normal 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();
|
||||
});
|
||||
81
src/zen/tests/device_sync/browser_normal_tabs_projection.js
Normal file
81
src/zen/tests/device_sync/browser_normal_tabs_projection.js
Normal 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);
|
||||
});
|
||||
108
src/zen/tests/device_sync/head.js
Normal file
108
src/zen/tests/device_sync/head.js
Normal 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 };
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
22
src/zen/tests/share/browser.toml
Normal file
22
src/zen/tests/share/browser.toml
Normal 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"]
|
||||
160
src/zen/tests/share/browser_share_client.js
Normal file
160
src/zen/tests/share/browser_share_client.js
Normal 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"
|
||||
);
|
||||
});
|
||||
200
src/zen/tests/share/browser_share_import.js
Normal file
200
src/zen/tests/share/browser_share_import.js
Normal 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();
|
||||
});
|
||||
129
src/zen/tests/share/browser_share_serialize.js
Normal file
129
src/zen/tests/share/browser_share_serialize.js
Normal 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);
|
||||
});
|
||||
112
src/zen/tests/share/browser_share_split_link.js
Normal file
112
src/zen/tests/share/browser_share_split_link.js
Normal 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);
|
||||
});
|
||||
56
src/zen/tests/share/browser_share_url_parsing.js
Normal file
56
src/zen/tests/share/browser_share_url_parsing.js
Normal 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}`);
|
||||
}
|
||||
});
|
||||
88
src/zen/tests/share/head.js
Normal file
88
src/zen/tests/share/head.js
Normal 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"
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
2
src/zen/vendor/motion.dep
vendored
2
src/zen/vendor/motion.dep
vendored
@@ -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
|
||||
2
src/zen/vendor/motion.min.mjs
vendored
2
src/zen/vendor/motion.min.mjs
vendored
File diff suppressed because one or more lines are too long
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user