gh-13258: Implement new loading indicator (gh-13259)

This commit is contained in:
mr. m
2026-04-15 16:49:52 +02:00
committed by GitHub
parent 767dfce556
commit c128b79723
11 changed files with 307 additions and 32 deletions

View File

@@ -57,3 +57,6 @@
- name: zen.view.overflow-webext-toolbar - name: zen.view.overflow-webext-toolbar
value: "@IS_TWILIGHT@" value: "@IS_TWILIGHT@"
- name: zen.view.enable-loading-indicator
value: true

View File

@@ -1,5 +1,5 @@
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js 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 --- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js +++ b/browser/base/content/browser.js
@@ -33,6 +33,7 @@ ChromeUtils.defineESModuleGetters(this, { @@ -33,6 +33,7 @@ ChromeUtils.defineESModuleGetters(this, {
@@ -24,16 +24,7 @@ index 0ea3d82b88819c41ffd866ae9533ebb5a7bff957..37db181d7e71fb6250df5bae363e9cf9
if (backDisabled) { if (backDisabled) {
backCommand.removeAttribute("disabled"); backCommand.removeAttribute("disabled");
} else { } else {
@@ -2305,6 +2311,8 @@ var XULBrowserWindow = { @@ -3820,7 +3826,7 @@ function warnAboutClosingWindow() {
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() {
if (!isPBWindow && !toolbar.visible) { if (!isPBWindow && !toolbar.visible) {
return gBrowser.warnAboutClosingTabs( return gBrowser.warnAboutClosingTabs(
@@ -42,7 +33,7 @@ index 0ea3d82b88819c41ffd866ae9533ebb5a7bff957..37db181d7e71fb6250df5bae363e9cf9
gBrowser.closingTabsEnum.ALL gBrowser.closingTabsEnum.ALL
); );
} }
@@ -3860,7 +3868,7 @@ function warnAboutClosingWindow() { @@ -3860,7 +3866,7 @@ function warnAboutClosingWindow() {
return ( return (
isPBWindow || isPBWindow ||
gBrowser.warnAboutClosingTabs( gBrowser.warnAboutClosingTabs(
@@ -51,7 +42,7 @@ index 0ea3d82b88819c41ffd866ae9533ebb5a7bff957..37db181d7e71fb6250df5bae363e9cf9
gBrowser.closingTabsEnum.ALL gBrowser.closingTabsEnum.ALL
) )
); );
@@ -3885,7 +3893,7 @@ function warnAboutClosingWindow() { @@ -3885,7 +3891,7 @@ function warnAboutClosingWindow() {
AppConstants.platform != "macosx" || AppConstants.platform != "macosx" ||
isPBWindow || isPBWindow ||
gBrowser.warnAboutClosingTabs( gBrowser.warnAboutClosingTabs(
@@ -60,7 +51,7 @@ index 0ea3d82b88819c41ffd866ae9533ebb5a7bff957..37db181d7e71fb6250df5bae363e9cf9
gBrowser.closingTabsEnum.ALL gBrowser.closingTabsEnum.ALL
) )
); );
@@ -4825,6 +4833,9 @@ var ConfirmationHint = { @@ -4825,6 +4831,9 @@ var ConfirmationHint = {
} }
document.l10n.setAttributes(this._message, messageId, options.l10nArgs); document.l10n.setAttributes(this._message, messageId, options.l10nArgs);

View File

@@ -57,6 +57,7 @@ class ZenStartup {
gZenWorkspaces.init(); gZenWorkspaces.init();
setTimeout(() => { setTimeout(() => {
gZenUIManager.init(); gZenUIManager.init();
this.#initUIComponents();
this.#checkForWelcomePage(); this.#checkForWelcomePage();
}, 0); }, 0);
} catch (e) { } 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() { #checkForWelcomePage() {
const kWelcomeScreenSeenPref = "zen.welcome-screen.seen"; const kWelcomeScreenSeenPref = "zen.welcome-screen.seen";
if (Services.env.get("MOZ_HEADLESS")) { if (Services.env.get("MOZ_HEADLESS")) {

View File

@@ -7,3 +7,8 @@ EXTRA_JS_MODULES += [
"sys/ZenCustomizableUI.sys.mjs", "sys/ZenCustomizableUI.sys.mjs",
"sys/ZenUIMigration.sys.mjs", "sys/ZenUIMigration.sys.mjs",
] ]
EXTRA_JS_MODULES.zen.ui += [
"sys/ui/ZenProgressBar.sys.mjs",
"sys/ui/ZenUIComponent.sys.mjs",
]

View File

@@ -56,3 +56,38 @@
background-position: -400% 50%; 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;
}
}

View File

@@ -486,11 +486,12 @@
margin-block: -1px !important; margin-block: -1px !important;
} }
} }
}
& #identity-icon-box { #urlbar[open][zen-floating-urlbar="true"] #identity-icon-box,
--urlbar-box-hover-bgcolor: transparent; :root[zen-single-toolbar="true"] #urlbar[breakout-extend="true"] #identity-icon-box {
margin-inline: 2px 8px; --urlbar-box-hover-bgcolor: transparent;
} margin-inline: 2px 8px;
} }
/* stylelint-disable-next-line media-query-no-invalid */ /* stylelint-disable-next-line media-query-no-invalid */

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -96,12 +96,6 @@
.browserSidebarContainer.zen-glance-background { .browserSidebarContainer.zen-glance-background {
box-shadow: var(--zen-big-shadow); 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, .browserSidebarContainer.zen-glance-background,

View File

@@ -79,8 +79,9 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
this._zenClickEventListener = this._onTabClick.bind(this); this._zenClickEventListener = this._onTabClick.bind(this);
gZenWorkspaces._resolvePinnedInitialized(); gZenWorkspaces._resolvePinnedInitialized();
if (lazy.zenPinnedTabRestorePinnedTabsToPinnedUrl) { gZenWorkspaces.promiseInitialized.then(() => {
gZenWorkspaces.promiseInitialized.then(() => { gBrowser.addTabsProgressListener(this);
if (lazy.zenPinnedTabRestorePinnedTabsToPinnedUrl) {
for (const tab of gZenWorkspaces.allStoredTabs) { for (const tab of gZenWorkspaces.allStoredTabs) {
try { try {
this.resetPinnedTab(tab); this.resetPinnedTab(tab);
@@ -88,8 +89,8 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
console.error("Error restoring pinned tab:", ex); console.error("Error restoring pinned tab:", ex);
} }
} }
}); }
} });
} }
log(message) { 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 ( if (
(aLocation == "about:blank" && (location == "about:blank" &&
BrowserUIUtils.checkEmptyPageOrigin(aBrowser)) || BrowserUIUtils.checkEmptyPageOrigin(aBrowser)) ||
aLocation == "" location == ""
) { ) {
return; return;
} }
@@ -852,7 +855,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
} }
// Remove # and ? from the URL // Remove # and ? from the URL
const pinUrl = tab._zenPinnedInitialState.entry.url.split("#")[0]; 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 // Add an indicator that the pin has been changed
if (pinUrl === currentUrl) { if (pinUrl === currentUrl) {
this.resetPinChangedUrl(tab); this.resetPinChangedUrl(tab);