diff --git a/prefs/zen/view.yaml b/prefs/zen/view.yaml index d990c13fa..1f36da3f0 100644 --- a/prefs/zen/view.yaml +++ b/prefs/zen/view.yaml @@ -57,3 +57,6 @@ - name: zen.view.overflow-webext-toolbar value: "@IS_TWILIGHT@" + +- name: zen.view.enable-loading-indicator + value: true diff --git a/src/browser/base/content/browser-js.patch b/src/browser/base/content/browser-js.patch index 15dfe2198..a36248923 100644 --- a/src/browser/base/content/browser-js.patch +++ b/src/browser/base/content/browser-js.patch @@ -1,5 +1,5 @@ diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js -index 0ea3d82b88819c41ffd866ae9533ebb5a7bff957..37db181d7e71fb6250df5bae363e9cf984b44f79 100644 +index 0ea3d82b88819c41ffd866ae9533ebb5a7bff957..3ed2fc4d08b20883e0587e4435daacd86ad603de 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -33,6 +33,7 @@ ChromeUtils.defineESModuleGetters(this, { @@ -24,16 +24,7 @@ index 0ea3d82b88819c41ffd866ae9533ebb5a7bff957..37db181d7e71fb6250df5bae363e9cf9 if (backDisabled) { backCommand.removeAttribute("disabled"); } else { -@@ -2305,6 +2311,8 @@ var XULBrowserWindow = { - AboutReaderParent.updateReaderButton(gBrowser.selectedBrowser); - TranslationsParent.onLocationChange(gBrowser.selectedBrowser); - -+ gZenPinnedTabManager.onLocationChange(gBrowser.selectedBrowser, location); -+ - PictureInPicture.updateUrlbarToggle(gBrowser.selectedBrowser); - - if (!gMultiProcessBrowser) { -@@ -3820,7 +3828,7 @@ function warnAboutClosingWindow() { +@@ -3820,7 +3826,7 @@ function warnAboutClosingWindow() { if (!isPBWindow && !toolbar.visible) { return gBrowser.warnAboutClosingTabs( @@ -42,7 +33,7 @@ index 0ea3d82b88819c41ffd866ae9533ebb5a7bff957..37db181d7e71fb6250df5bae363e9cf9 gBrowser.closingTabsEnum.ALL ); } -@@ -3860,7 +3868,7 @@ function warnAboutClosingWindow() { +@@ -3860,7 +3866,7 @@ function warnAboutClosingWindow() { return ( isPBWindow || gBrowser.warnAboutClosingTabs( @@ -51,7 +42,7 @@ index 0ea3d82b88819c41ffd866ae9533ebb5a7bff957..37db181d7e71fb6250df5bae363e9cf9 gBrowser.closingTabsEnum.ALL ) ); -@@ -3885,7 +3893,7 @@ function warnAboutClosingWindow() { +@@ -3885,7 +3891,7 @@ function warnAboutClosingWindow() { AppConstants.platform != "macosx" || isPBWindow || gBrowser.warnAboutClosingTabs( @@ -60,7 +51,7 @@ index 0ea3d82b88819c41ffd866ae9533ebb5a7bff957..37db181d7e71fb6250df5bae363e9cf9 gBrowser.closingTabsEnum.ALL ) ); -@@ -4825,6 +4833,9 @@ var ConfirmationHint = { +@@ -4825,6 +4831,9 @@ var ConfirmationHint = { } document.l10n.setAttributes(this._message, messageId, options.l10nArgs); diff --git a/src/zen/common/modules/ZenStartup.mjs b/src/zen/common/modules/ZenStartup.mjs index 664d4bc96..a0611a8df 100644 --- a/src/zen/common/modules/ZenStartup.mjs +++ b/src/zen/common/modules/ZenStartup.mjs @@ -57,6 +57,7 @@ class ZenStartup { gZenWorkspaces.init(); setTimeout(() => { gZenUIManager.init(); + this.#initUIComponents(); this.#checkForWelcomePage(); }, 0); } catch (e) { @@ -161,6 +162,16 @@ class ZenStartup { } } + #initUIComponents() { + const kUIComponents = ["ZenProgressBar"]; + for (let component of kUIComponents) { + const module = ChromeUtils.importESModule( + "resource:///modules/zen/ui/" + component + ".sys.mjs" + ); + new module[component](window); + } + } + #checkForWelcomePage() { const kWelcomeScreenSeenPref = "zen.welcome-screen.seen"; if (Services.env.get("MOZ_HEADLESS")) { diff --git a/src/zen/common/moz.build b/src/zen/common/moz.build index 8dc973b44..115234dad 100644 --- a/src/zen/common/moz.build +++ b/src/zen/common/moz.build @@ -7,3 +7,8 @@ EXTRA_JS_MODULES += [ "sys/ZenCustomizableUI.sys.mjs", "sys/ZenUIMigration.sys.mjs", ] + +EXTRA_JS_MODULES.zen.ui += [ + "sys/ui/ZenProgressBar.sys.mjs", + "sys/ui/ZenUIComponent.sys.mjs", +] diff --git a/src/zen/common/styles/zen-animations.css b/src/zen/common/styles/zen-animations.css index 9756f8894..9b0185ada 100644 --- a/src/zen/common/styles/zen-animations.css +++ b/src/zen/common/styles/zen-animations.css @@ -56,3 +56,38 @@ background-position: -400% 50%; } } + +@keyframes zen-progress-bar-pulse { + 0% { + transform: scale(0.8) translate(-50%, -50%); + opacity: 0.6; + } + 50% { + transform: scale(0.95) translate(-50%, -50%); + opacity: 1; + } + 100% { + transform: scale(0.8) translate(-50%, -50%); + opacity: 0.6; + } +} + +@keyframes zen-progress-bar-long-load { + 0% { + left: -100%; + opacity: 1; + } + + 100% { + left: 100%; + opacity: 1; + } +} + +@keyframes zen-progress-bar-settle { + to { + transform: translate(-50%, -50%) scale(1); + opacity: 1; + width: 10rem; + } +} diff --git a/src/zen/common/styles/zen-omnibox.css b/src/zen/common/styles/zen-omnibox.css index a334a3d04..0a29c1936 100644 --- a/src/zen/common/styles/zen-omnibox.css +++ b/src/zen/common/styles/zen-omnibox.css @@ -486,11 +486,12 @@ margin-block: -1px !important; } } +} - & #identity-icon-box { - --urlbar-box-hover-bgcolor: transparent; - margin-inline: 2px 8px; - } +#urlbar[open][zen-floating-urlbar="true"] #identity-icon-box, +:root[zen-single-toolbar="true"] #urlbar[breakout-extend="true"] #identity-icon-box { + --urlbar-box-hover-bgcolor: transparent; + margin-inline: 2px 8px; } /* stylelint-disable-next-line media-query-no-invalid */ diff --git a/src/zen/common/styles/zen-single-components.css b/src/zen/common/styles/zen-single-components.css index f40e64210..f4a6d582a 100644 --- a/src/zen/common/styles/zen-single-components.css +++ b/src/zen/common/styles/zen-single-components.css @@ -683,3 +683,47 @@ } } } + +/* Loading progress bar */ +#zen-loading-progress-bar { + position: fixed; + top: max(calc(var(--zen-element-separation) / -2), -4px); + left: 50%; + transform: translate(-50%, -50%) scale(0); + background: light-dark(rgba(0, 0, 0, 0.7), rgba(255, 255, 255, 0.7)); + height: .4rem; + width: 5rem; + z-index: 9; + border-radius: 100px; + transition: opacity .3s ease-in-out, + background-color .3s ease-in-out, + transform .3s ease-in-out; + overflow: clip; + pointer-events: none; + animation: zen-progress-bar-pulse 1.5s linear infinite forwards; + transform-origin: 0 0; + + &[long-load="true"] { + opacity: 1; + animation: zen-progress-bar-settle .3s ease-out forwards; + background: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.1)); + + &::before { + content: ""; + position: absolute; + top: 0; + height: 100%; + width: 75%; + opacity: 0; + animation: zen-progress-bar-long-load 1s ease-in-out infinite; + animation-delay: 0.3s; + background: light-dark(rgba(0, 0, 0, 0.7), rgba(255, 255, 255, 0.7)); + border-radius: inherit; + } + } + + /* stylelint-disable-next-line media-query-no-invalid */ + @media not -moz-pref("zen.view.enable-loading-indicator") { + display: none; + } +} diff --git a/src/zen/common/sys/ui/ZenProgressBar.sys.mjs b/src/zen/common/sys/ui/ZenProgressBar.sys.mjs new file mode 100644 index 000000000..e8fa8157e --- /dev/null +++ b/src/zen/common/sys/ui/ZenProgressBar.sys.mjs @@ -0,0 +1,127 @@ +// 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 { ZenUIComponent } from "resource:///modules/zen/ui/ZenUIComponent.sys.mjs"; + +const WAIT_BEFORE_SHOWING_LONG_LOAD = 3000; + +export class ZenProgressBar extends ZenUIComponent { + #element = null; + #loadingTab = null; + #longLoadTimer = null; + + init() { + this.listenBrowserTabsProgress(); + this.addEventListener("TabSelect"); + } + + onStateChange(aWebProgress) { + this.#checkBrowserProgress(aWebProgress); + } + + onLocationChange(webProgress) { + this.#checkBrowserProgress(webProgress); + } + + on_TabSelect() { + const gBrowser = this.window.gBrowser; + const selectedTab = gBrowser.selectedTab; + this.onLocationChange(gBrowser.getBrowserForTab(selectedTab)); + } + + get #progressBar() { + if (!this.#loadingTab) { + return null; + } + if (!this.#element) { + this.#element = this.window.document.createXULElement("hbox"); + this.#element.id = "zen-loading-progress-bar"; + } + if ( + this.#element._loadingTab?.deref() !== this.#loadingTab && + this.#loadingTab + ) { + this.#element._loadingTab = new WeakRef(this.#loadingTab); + const container = this.window.document.getElementById( + this.#loadingTab.linkedPanel + ); + container.firstChild.before(this.#element); + this.window.gZenUIManager.elementAnimate( + this.#element, + { + opacity: [0, 0.6], + }, + { + duration: 400, + } + ); + } + return this.#element; + } + + #checkBrowserProgress(webProgress) { + const window = this.window; + const gBrowser = window.gBrowser; + const tab = gBrowser.getTabForBrowser(webProgress); + const isLoading = + tab?.selected && + (tab.hasAttribute("busy") || tab.hasAttribute("progress")); + if (isLoading) { + this.#showProgressBar(tab); + } else { + this.#hideProgressBar(); + } + } + + #hideProgressBar() { + const progressBar = this.#element; + const window = this.window; + if (this.#longLoadTimer) { + window.clearTimeout(this.#longLoadTimer); + this.#longLoadTimer = null; + } + + this.#loadingTab = null; + if (!progressBar) { + return; + } + const callback = () => { + delete progressBar._loadingTab; + progressBar.remove(); + this.#element = null; + }; + if (this.window.gReduceMotion) { + callback(); + return; + } + this.window.gZenUIManager + .elementAnimate( + progressBar, + { + transform: ["scaleX(0.8) translate(-50%, -50%)"], + opacity: [0], + }, + { + duration: 300, + } + ) + .then(callback); + } + + #showProgressBar(aTab) { + if (this.#loadingTab === aTab) { + return; + } + this.#loadingTab = aTab; + const progressBar = this.#progressBar; + progressBar.removeAttribute("fade-out"); + progressBar.removeAttribute("long-load"); + this.#longLoadTimer = this.window.setTimeout(() => { + if (this.#loadingTab === aTab) { + progressBar.setAttribute("long-load", "true"); + } + this.#longLoadTimer = null; + }, WAIT_BEFORE_SHOWING_LONG_LOAD); + } +} diff --git a/src/zen/common/sys/ui/ZenUIComponent.sys.mjs b/src/zen/common/sys/ui/ZenUIComponent.sys.mjs new file mode 100644 index 000000000..7a26773ea --- /dev/null +++ b/src/zen/common/sys/ui/ZenUIComponent.sys.mjs @@ -0,0 +1,61 @@ +// 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/. + +/** + * Base class for UI components in Zen. + * UI components are responsible for managing their own event listeners + * and providing a consistent interface for handling events. + */ +export class ZenUIComponent { + #window = null; + #eventListeners = new Set(); + + constructor(aWindow) { + this.#window = aWindow; + this.init(); + this.#window.addEventListener("unload", () => { + if (typeof this.uninit === "function") { + this.uninit(); + } + for (const { type, options } of this.#eventListeners) { + this.#window.removeEventListener(type, this, options); + } + this.#eventListeners.clear(); + }); + } + + get window() { + return this.#window; + } + + /** + * Adds an event listener to the component that will automatically be removed when the window unloads. + * + * @param {string} type - The event type to listen for. + * @param {object} options - The event listener function or an object containing options. + * @returns {void} + */ + addEventListener(type, options = {}) { + this.#window.addEventListener(type, this, options); + if (options?.once) { + return; + } + this.#eventListeners.add({ type, options }); + } + + listenBrowserTabsProgress() { + this.#window.gBrowser.addTabsProgressListener(this); + } + + listenBrowserProgress() { + this.#window.gBrowser.addProgressListener(this); + } + + handleEvent(event) { + const handlerName = "on_" + event.type; + if (typeof this[handlerName] === "function") { + this[handlerName](event); + } + } +} diff --git a/src/zen/glance/zen-glance.css b/src/zen/glance/zen-glance.css index 807aea8c2..bca099ab7 100644 --- a/src/zen/glance/zen-glance.css +++ b/src/zen/glance/zen-glance.css @@ -96,12 +96,6 @@ .browserSidebarContainer.zen-glance-background { box-shadow: var(--zen-big-shadow); - - & .browserContainer { - /* For rounding the corners of the content to work while - * applying a transformation to the container. */ - will-change: transform; - } } .browserSidebarContainer.zen-glance-background, diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index 8a8e4e832..2c048c8cc 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -79,8 +79,9 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { this._zenClickEventListener = this._onTabClick.bind(this); gZenWorkspaces._resolvePinnedInitialized(); - if (lazy.zenPinnedTabRestorePinnedTabsToPinnedUrl) { - gZenWorkspaces.promiseInitialized.then(() => { + gZenWorkspaces.promiseInitialized.then(() => { + gBrowser.addTabsProgressListener(this); + if (lazy.zenPinnedTabRestorePinnedTabsToPinnedUrl) { for (const tab of gZenWorkspaces.allStoredTabs) { try { this.resetPinnedTab(tab); @@ -88,8 +89,8 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { console.error("Error restoring pinned tab:", ex); } } - }); - } + } + }); } log(message) { @@ -833,11 +834,13 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } } - onLocationChange(aBrowser, aLocation) { + onLocationChange(aBrowser, aWebProgress, aRequest, aLocationURI) { + // eslint-disable-next-line no-shadow + let location = aLocationURI ? aLocationURI.spec : ""; if ( - (aLocation == "about:blank" && + (location == "about:blank" && BrowserUIUtils.checkEmptyPageOrigin(aBrowser)) || - aLocation == "" + location == "" ) { return; } @@ -852,7 +855,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } // Remove # and ? from the URL const pinUrl = tab._zenPinnedInitialState.entry.url.split("#")[0]; - const currentUrl = aLocation.split("#")[0]; + const currentUrl = location.split("#")[0]; // Add an indicator that the pin has been changed if (pinUrl === currentUrl) { this.resetPinChangedUrl(tab);