From d76fa13eddf127eaea33b624e82981086987b138 Mon Sep 17 00:00:00 2001 From: Ashvin Jangid <142579833+ashvwinn@users.noreply.github.com> Date: Thu, 28 May 2026 22:03:29 +0530 Subject: [PATCH 01/53] gh-13923: fix empty keyboard shortcut label caused by `key_duplicateTab` (gh-13924) --- src/browser/components/preferences/zen-settings.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/browser/components/preferences/zen-settings.js b/src/browser/components/preferences/zen-settings.js index 2d6bef1a8..288b342d6 100644 --- a/src/browser/components/preferences/zen-settings.js +++ b/src/browser/components/preferences/zen-settings.js @@ -825,6 +825,7 @@ var zenIgnoreKeyboardShortcutIDs = [ "key_enterFullScreen_compat", "key_exitFullScreen_old", "key_exitFullScreen_compat", + "key_duplicateTab", ]; var zenIgnoreKeyboardShortcutL10n = [ From 7aa0ca05ec289ba5ebb441e93cb94c88501033ac Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Sat, 30 May 2026 11:55:36 +0200 Subject: [PATCH 02/53] gh-13947: Fixed loading bar appearing on dom fullscreen (gh-13954) --- src/zen/common/styles/zen-single-components.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/zen/common/styles/zen-single-components.css b/src/zen/common/styles/zen-single-components.css index 2544b8b11..d34677a83 100644 --- a/src/zen/common/styles/zen-single-components.css +++ b/src/zen/common/styles/zen-single-components.css @@ -786,4 +786,8 @@ @media not -moz-pref("zen.view.enable-loading-indicator") { display: none; } + + :root[inDOMFullscreen="true"] & { + display: none; + } } From 072d874f7832f1383500b4bf6fd271ae7a1047d8 Mon Sep 17 00:00:00 2001 From: sporocyst Date: Sat, 30 May 2026 18:26:20 +0800 Subject: [PATCH 03/53] gh-13941: Fixed boost button viable when non-effective (gh-13953) --- src/zen/urlbar/ZenSiteDataPanel.sys.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/zen/urlbar/ZenSiteDataPanel.sys.mjs b/src/zen/urlbar/ZenSiteDataPanel.sys.mjs index b8d7e79f5..90e037b09 100644 --- a/src/zen/urlbar/ZenSiteDataPanel.sys.mjs +++ b/src/zen/urlbar/ZenSiteDataPanel.sys.mjs @@ -250,12 +250,12 @@ export class nsZenSiteDataPanel { const boostButton = this.document.getElementById("zen-site-data-boost"); if (!canBoostSite) { boostButton.removeAttribute("boosting"); - } - - if (!canBoostSite) { + boostButton.setAttribute("disabled", "true"); return; } + boostButton.removeAttribute("disabled"); + if (lazy.gZenBoostsManager.registeredBoostForDomain(domain)) { boostButton.setAttribute("boosting", "true"); } else { From 193d004a37bbe6cca5c27ad1f39dd529d4edc89d Mon Sep 17 00:00:00 2001 From: NHClaessens <97399433+NHClaessens@users.noreply.github.com> Date: Sat, 30 May 2026 17:35:40 +0200 Subject: [PATCH 04/53] gh-10594: Change conditional for triggering padding bump (gh-13959) --- src/zen/common/zenThemeModifier.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/zen/common/zenThemeModifier.js b/src/zen/common/zenThemeModifier.js index 30f2a42ce..314e5fc6e 100644 --- a/src/zen/common/zenThemeModifier.js +++ b/src/zen/common/zenThemeModifier.js @@ -168,17 +168,17 @@ document.documentElement.setAttribute("zen-no-padding", true); } else { document.documentElement.removeAttribute("zen-no-padding"); - if (domFullscreen) { - const selectedBrowser = gBrowser.selectedBrowser; - selectedBrowser.style.paddingRight = "0.5px"; - window.addEventListener( - "MozAfterPaint", - () => { - selectedBrowser.style.paddingRight = ""; - }, - { once: true } - ); - } + } + if (domFullscreen) { + const selectedBrowser = gBrowser.selectedBrowser; + selectedBrowser.style.paddingRight = "0.5px"; + window.addEventListener( + "MozAfterPaint", + () => { + selectedBrowser.style.paddingRight = ""; + }, + { once: true } + ); } }, From 2c59265249173b948224627cef5735f7d248b29a Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Sat, 30 May 2026 20:54:55 +0200 Subject: [PATCH 05/53] gh-13745: Fixed nightly being installed accidentally (gh-13964) --- .../installer/windows/nsis/defines-nsi-in.patch | 12 ++++++++++-- surfer.json | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/browser/installer/windows/nsis/defines-nsi-in.patch b/src/browser/installer/windows/nsis/defines-nsi-in.patch index 862ac618b..f423ee10d 100644 --- a/src/browser/installer/windows/nsis/defines-nsi-in.patch +++ b/src/browser/installer/windows/nsis/defines-nsi-in.patch @@ -1,8 +1,16 @@ diff --git a/browser/installer/windows/nsis/defines.nsi.in b/browser/installer/windows/nsis/defines.nsi.in -index a9bf2d67ce522054442be664bda1434c63609ea1..16e03732d78207ce602066a4bccc0d2c2385ca91 100644 +index a9bf2d67ce522054442be664bda1434c63609ea1..54e2c97f79b1e526eca66f4ca611e26b9c3f89d4 100644 --- a/browser/installer/windows/nsis/defines.nsi.in +++ b/browser/installer/windows/nsis/defines.nsi.in -@@ -147,7 +147,7 @@ VIAddVersionKey "ProductVersion" "${AppVersion}" +@@ -110,7 +110,6 @@ + #endif + + #ifdef DESKTOP_LAUNCHER_ENABLED +-!define DESKTOP_LAUNCHER_ENABLED + #endif + + #ifdef MOZ_BITS_DOWNLOAD +@@ -147,7 +146,7 @@ VIAddVersionKey "ProductVersion" "${AppVersion}" !define APPROXIMATE_REQUIRED_SPACE_MB "145" # Constants for parts of the telemetry submission URL diff --git a/surfer.json b/surfer.json index a53e9df9a..fefc53dd4 100644 --- a/surfer.json +++ b/surfer.json @@ -20,7 +20,7 @@ "brandShortName": "Zen", "brandFullName": "Zen Browser", "release": { - "displayVersion": "1.20.1b", + "displayVersion": "1.20.2b", "github": { "repo": "zen-browser/desktop" }, From c7818fe35554b76932754f30e26a7b8e2c212139 Mon Sep 17 00:00:00 2001 From: fen4flo <75260616+FlorianButz@users.noreply.github.com> Date: Sat, 30 May 2026 21:10:37 +0200 Subject: [PATCH 06/53] gh-13839: Fix icons when using custom fonts (gh-13956) Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com> Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com> --- src/zen/boosts/ZenBoostStyles.sys.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zen/boosts/ZenBoostStyles.sys.mjs b/src/zen/boosts/ZenBoostStyles.sys.mjs index 40c230f6f..ac2a022f8 100644 --- a/src/zen/boosts/ZenBoostStyles.sys.mjs +++ b/src/zen/boosts/ZenBoostStyles.sys.mjs @@ -82,7 +82,7 @@ export class nsZenBoostStyles { if (fontCase != "" || fontFamily != "") { style += `/* Text Format */\n`; - style += `body * {\n`; + style += `body *:not(.google-symbols, gf-load-icon-font, mat-icon, .google-material-icons) {\n`; style += `${fontFamily}\n`; style += `${fontCase}\n`; style += `}\n`; From feca914821a6ea510691ee3f2120b62ed44830c2 Mon Sep 17 00:00:00 2001 From: fen4flo <75260616+FlorianButz@users.noreply.github.com> Date: Sun, 31 May 2026 15:53:33 +0200 Subject: [PATCH 07/53] gh-13949: Secondary dot snapping to center in a new Boost (gh-13965) Co-authored-by: mr. m Signed-off-by: fen4flo <75260616+FlorianButz@users.noreply.github.com> --- .../urlbar/content/UrlbarInput-mjs.patch | 44 ++++++++++--------- src/zen/boosts/ZenBoostsEditor.mjs | 36 ++++++++++----- src/zen/boosts/ZenBoostsManager.sys.mjs | 7 ++- 3 files changed, 52 insertions(+), 35 deletions(-) diff --git a/src/browser/components/urlbar/content/UrlbarInput-mjs.patch b/src/browser/components/urlbar/content/UrlbarInput-mjs.patch index bb291a8b8..f4e335a6d 100644 --- a/src/browser/components/urlbar/content/UrlbarInput-mjs.patch +++ b/src/browser/components/urlbar/content/UrlbarInput-mjs.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/urlbar/content/UrlbarInput.mjs b/browser/components/urlbar/content/UrlbarInput.mjs -index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e6463b45191 100644 +index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..c166b7de23c35716bf8c51b6b9c72f771f0a75a8 100644 --- a/browser/components/urlbar/content/UrlbarInput.mjs +++ b/browser/components/urlbar/content/UrlbarInput.mjs @@ -98,6 +98,13 @@ const lazy = XPCOMUtils.declareLazy({ @@ -132,7 +132,7 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e64 // 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")) { -@@ -2891,6 +2966,27 @@ ${ +@@ -2891,6 +2966,29 @@ ${ return; } @@ -140,7 +140,9 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e64 + this._zenHandleUrlbarClose(); + } else if (!this._untrimmedValue || this.searchMode) { + // Restore the current page URL when the urlbar is empty on blur -+ this.handleRevert(); ++ this.window.setTimeout(() => { ++ this.handleRevert(); ++ }, 0); + } + + // Arc like URLbar: Blur the input on exit @@ -160,7 +162,7 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e64 this.removeAttribute("breakout-extend"); this.#updateTextboxPosition(); } -@@ -2921,7 +3017,7 @@ ${ +@@ -2921,7 +3019,7 @@ ${ forceUnifiedSearchButtonAvailable = false ) { let prevState = this.getAttribute("pageproxystate"); @@ -169,7 +171,7 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e64 this.setAttribute("pageproxystate", state); this._inputContainer.setAttribute("pageproxystate", state); this._identityBox?.setAttribute("pageproxystate", state); -@@ -3198,10 +3294,12 @@ ${ +@@ -3198,10 +3296,12 @@ ${ return; } this.style.top = px( @@ -182,7 +184,7 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e64 ); } -@@ -3260,9 +3358,10 @@ ${ +@@ -3260,9 +3360,10 @@ ${ return; } @@ -194,7 +196,7 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e64 ); this.style.setProperty( "--urlbar-height", -@@ -3768,6 +3867,7 @@ ${ +@@ -3768,6 +3869,7 @@ ${ } _toggleActionOverride(event) { @@ -202,7 +204,7 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e64 if ( event.keyCode == KeyEvent.DOM_VK_SHIFT || event.keyCode == KeyEvent.DOM_VK_ALT || -@@ -3880,8 +3980,8 @@ ${ +@@ -3880,8 +3982,8 @@ ${ if (!this.#isAddressbar) { return val; } @@ -213,7 +215,7 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e64 : val; // Only trim value if the directionality doesn't change to RTL and we're not // showing a strikeout https protocol. -@@ -4180,6 +4280,7 @@ ${ +@@ -4180,6 +4282,7 @@ ${ resultDetails = null, browser = this.window.gBrowser.selectedBrowser ) { @@ -221,7 +223,7 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e64 if (this.#isAddressbar) { this.#prepareAddressbarLoad( url, -@@ -4291,6 +4392,10 @@ ${ +@@ -4291,6 +4394,10 @@ ${ } reuseEmpty = true; } @@ -232,7 +234,7 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e64 if ( where == "tab" && reuseEmpty && -@@ -4298,6 +4403,9 @@ ${ +@@ -4298,6 +4405,9 @@ ${ ) { where = "current"; } @@ -242,7 +244,7 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e64 return where; } -@@ -4552,6 +4660,7 @@ ${ +@@ -4552,6 +4662,7 @@ ${ this.setResultForCurrentValue(null); this.handleCommand(); this.controller.clearLastQueryContextCache(); @@ -250,7 +252,7 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e64 this._suppressStartQuery = false; }); -@@ -4559,7 +4668,6 @@ ${ +@@ -4559,7 +4670,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. @@ -258,7 +260,7 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e64 let controller = this.document.commandDispatcher.getControllerForCommand("cmd_paste"); -@@ -4715,7 +4823,11 @@ ${ +@@ -4715,7 +4825,11 @@ ${ if (!engineName && !source && !this.hasAttribute("searchmode")) { return; } @@ -271,7 +273,7 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e64 if (this._searchModeIndicatorTitle) { this._searchModeIndicatorTitle.textContent = ""; this._searchModeIndicatorTitle.removeAttribute("data-l10n-id"); -@@ -5031,6 +5143,7 @@ ${ +@@ -5031,6 +5145,7 @@ ${ this.document.l10n.setAttributes( this.inputField, @@ -279,7 +281,7 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e64 l10nId, l10nId == "urlbar-placeholder-with-name" ? { name: engineName } -@@ -5156,6 +5269,11 @@ ${ +@@ -5156,6 +5271,11 @@ ${ } _on_click(event) { @@ -291,7 +293,7 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e64 switch (event.target) { case this.inputField: case this._inputContainer: -@@ -5242,7 +5360,7 @@ ${ +@@ -5242,7 +5362,7 @@ ${ } } @@ -300,7 +302,7 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e64 this.view.autoOpen({ event }); } else { if (this._untrimOnFocusAfterKeydown) { -@@ -5282,9 +5400,16 @@ ${ +@@ -5282,9 +5402,16 @@ ${ } _on_mousedown(event) { @@ -318,7 +320,7 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e64 if ( event.composedTarget != this.inputField && event.composedTarget != this._inputContainer -@@ -5294,6 +5419,10 @@ ${ +@@ -5294,6 +5421,10 @@ ${ this.focusedViaMousedown = !this.focused; this.#preventClickSelectsAll = this.focused; @@ -329,7 +331,7 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e64 // Keep the focus status, since the attribute may be changed // upon calling this.focus(). -@@ -5329,7 +5458,7 @@ ${ +@@ -5329,7 +5460,7 @@ ${ } // Don't close the view when clicking on a tab; we may want to keep the // view open on tab switch, and the TabSelect event arrived earlier. @@ -338,7 +340,7 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..e1128b0d7f5accfd48af27f99e3b2e64 break; } -@@ -5636,7 +5765,7 @@ ${ +@@ -5636,7 +5767,7 @@ ${ // When we are in actions search mode we can show more results so // increase the limit. let maxResults = diff --git a/src/zen/boosts/ZenBoostsEditor.mjs b/src/zen/boosts/ZenBoostsEditor.mjs index 723229743..6956c124a 100644 --- a/src/zen/boosts/ZenBoostsEditor.mjs +++ b/src/zen/boosts/ZenBoostsEditor.mjs @@ -824,13 +824,19 @@ ${cssSelector} { const dotSec = this.doc.querySelector( "#zen-boost-color-picker-dot-secondary" ); + + const dotDistance = this.currentBoostData.dotDistance; + const dotAngleDeg = this.currentBoostData.dotAngleDeg; + const secondaryDotAngleDelta = + this.currentBoostData.secondaryDotAngleDegDelta ?? 0; + dot.style.setProperty( "--zen-theme-picker-dot-color", - `hsl(${this.currentBoostData.dotAngleDeg}deg, ${this.currentBoostData.dotDistance * 100}%, 55%)` + `hsl(${dotAngleDeg}deg, ${dotDistance * 100}%, 55%)` ); dotSec.style.setProperty( "--zen-theme-picker-dot-color", - `hsl(${this.currentBoostData.dotAngleDeg + this.currentBoostData.secondaryDotAngleDegDelta}deg, ${this.currentBoostData.dotDistance * 100}%, 20%)` + `hsl(${dotAngleDeg + secondaryDotAngleDelta}deg, ${dotDistance * 100}%, 20%)` ); } @@ -854,22 +860,23 @@ ${cssSelector} { const centerY = rect.top + rect.height / 2; const radius = (rect.width - padding) / 2; + const dotDistance = this.currentBoostData.dotDistance; + const primaryDotAngleDeg = this.currentBoostData.dotAngleDeg; + let angle = null; - if (!pixelX || !pixelY) { + if (pixelX == null || pixelY == null) { pixelX = centerX; pixelY = centerY; angle = this.currentBoostData.secondaryDotAngleDegDelta; } else { angle = Math.atan2(pixelY - centerY, pixelX - centerX); - pixelX = - centerX + Math.cos(angle) * this.currentBoostData.dotDistance * radius; - pixelY = - centerY + Math.sin(angle) * this.currentBoostData.dotDistance * radius; + pixelX = centerX + Math.cos(angle) * dotDistance * radius; + pixelY = centerY + Math.sin(angle) * dotDistance * radius; } // Rad to degree this.currentBoostData.secondaryDotAngleDegDelta = - ((angle * 180) / Math.PI + 100 - this.currentBoostData.dotAngleDeg) % 360; + ((angle * 180) / Math.PI + 100 - primaryDotAngleDeg) % 360; if (this.currentBoostData.secondaryDotAngleDegDelta < 0) { this.currentBoostData.secondaryDotAngleDegDelta += 360; } @@ -902,14 +909,19 @@ ${cssSelector} { const cx = rect.width / 2; const cy = rect.height / 2; + const dotDistance = this.currentBoostData.dotDistance; + const dotAngleDeg = this.currentBoostData.dotAngleDeg; + const secondaryDotAngleDelta = + this.currentBoostData.secondaryDotAngleDegDelta ?? 0; + // Updating the circle size to match the distance of the point const circle = this.doc.querySelector(".zen-boost-color-picker-circle"); circle.setAttribute("animated", "false"); - circle.style.width = `${this.currentBoostData.dotDistance * radius * 2}px`; - circle.style.height = `${this.currentBoostData.dotDistance * radius * 2}px`; + circle.style.width = `${dotDistance * radius * 2}px`; + circle.style.height = `${dotDistance * radius * 2}px`; - const dotColor = `hsl(${this.currentBoostData.dotAngleDeg}deg, ${this.currentBoostData.dotDistance * 100}%, 55%)`; - const dotColorSec = `hsl(${this.currentBoostData.dotAngleDeg + this.currentBoostData.secondaryDotAngleDegDelta}deg, ${this.currentBoostData.dotDistance * 100}%, 20%)`; + const dotColor = `hsl(${dotAngleDeg}deg, ${dotDistance * 100}%, 55%)`; + const dotColorSec = `hsl(${dotAngleDeg + secondaryDotAngleDelta}deg, ${dotDistance * 100}%, 20%)`; this.updateArcFill(cx, cy, radius, dotColor, dotColorSec); } diff --git a/src/zen/boosts/ZenBoostsManager.sys.mjs b/src/zen/boosts/ZenBoostsManager.sys.mjs index a93492da5..0a84833d1 100644 --- a/src/zen/boosts/ZenBoostsManager.sys.mjs +++ b/src/zen/boosts/ZenBoostsManager.sys.mjs @@ -114,9 +114,12 @@ class nsZenBoostsManager { boostData: { boostName: "My Boost", - dotAngleDeg: 0, + /* These initial values depend on + each other. Changing one means having to + recalculate all of them manually. */ + dotAngleDeg: 131.61, dotPos: { x: 0.76, y: 0.66 }, - dotDistance: 0, + dotDistance: 0.91, secondaryDotAngleDegDelta: 55, secondaryDotPos: { x: 0.5, y: 0.81 }, From 64e2e49a00c7b22cbaa90680116452615f5bef2b Mon Sep 17 00:00:00 2001 From: sporocyst Date: Mon, 1 Jun 2026 16:59:41 +0800 Subject: [PATCH 08/53] gh-13941: Fixed boost button viable when non-effective, CSS part (gh-13975) Signed-off-by: sporocyst --- src/zen/common/styles/zen-single-components.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/zen/common/styles/zen-single-components.css b/src/zen/common/styles/zen-single-components.css index d34677a83..02a2d64d2 100644 --- a/src/zen/common/styles/zen-single-components.css +++ b/src/zen/common/styles/zen-single-components.css @@ -540,6 +540,10 @@ & toolbarbutton { margin: 0; + + &[disabled] { + visibility: hidden; + } } } From 45075e2fbc3ee94c10522b62e6e8bc9afd5062ca Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Mon, 1 Jun 2026 11:24:45 +0200 Subject: [PATCH 09/53] gh-13439: Add tests coverage for boosts (gh-13977) --- package.json | 2 +- .../boosts/gtest/TestZenBoostsAccentCache.cpp | 104 +++++++++++++++++ .../gtest/TestZenBoostsResolveStyleColor.cpp | 42 +++++++ src/zen/boosts/gtest/moz.build | 2 + src/zen/boosts/nsZenBoostsBackend.cpp | 30 ++++- src/zen/boosts/nsZenBoostsBackend.h | 11 +- src/zen/tests/boosts/browser.toml | 35 ++++++ .../tests/boosts/browser_boosts_animation.js | 52 +++++++++ .../tests/boosts/browser_boosts_background.js | 50 +++++++++ src/zen/tests/boosts/browser_boosts_border.js | 53 +++++++++ .../tests/boosts/browser_boosts_gradient.js | 83 ++++++++++++++ .../tests/boosts/browser_boosts_inline_svg.js | 106 ++++++++++++++++++ .../tests/boosts/browser_boosts_input_text.js | 59 ++++++++++ src/zen/tests/boosts/browser_boosts_invert.js | 46 ++++++++ .../tests/boosts/browser_boosts_outline.js | 54 +++++++++ .../boosts/browser_boosts_placeholder.js | 57 ++++++++++ .../boosts/browser_boosts_pseudo_before.js | 59 ++++++++++ src/zen/tests/boosts/browser_boosts_shadow.js | 62 ++++++++++ .../browser_boosts_svg_background_image.js | 62 ++++++++++ .../tests/boosts/browser_boosts_svg_image.js | 63 +++++++++++ .../browser_boosts_svg_linear_gradient.js | 71 ++++++++++++ .../boosts/browser_boosts_svg_use_sprite.js | 61 ++++++++++ .../tests/boosts/browser_boosts_text_color.js | 50 +++++++++ src/zen/tests/boosts/head.js | 105 +++++++++++++++++ 24 files changed, 1312 insertions(+), 7 deletions(-) create mode 100644 src/zen/boosts/gtest/TestZenBoostsAccentCache.cpp create mode 100644 src/zen/boosts/gtest/TestZenBoostsResolveStyleColor.cpp create mode 100644 src/zen/tests/boosts/browser_boosts_animation.js create mode 100644 src/zen/tests/boosts/browser_boosts_background.js create mode 100644 src/zen/tests/boosts/browser_boosts_border.js create mode 100644 src/zen/tests/boosts/browser_boosts_gradient.js create mode 100644 src/zen/tests/boosts/browser_boosts_inline_svg.js create mode 100644 src/zen/tests/boosts/browser_boosts_input_text.js create mode 100644 src/zen/tests/boosts/browser_boosts_invert.js create mode 100644 src/zen/tests/boosts/browser_boosts_outline.js create mode 100644 src/zen/tests/boosts/browser_boosts_placeholder.js create mode 100644 src/zen/tests/boosts/browser_boosts_pseudo_before.js create mode 100644 src/zen/tests/boosts/browser_boosts_shadow.js create mode 100644 src/zen/tests/boosts/browser_boosts_svg_background_image.js create mode 100644 src/zen/tests/boosts/browser_boosts_svg_image.js create mode 100644 src/zen/tests/boosts/browser_boosts_svg_linear_gradient.js create mode 100644 src/zen/tests/boosts/browser_boosts_svg_use_sprite.js create mode 100644 src/zen/tests/boosts/browser_boosts_text_color.js diff --git a/package.json b/package.json index 6e7fd8fd0..c62853c08 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "surfer": "surfer", "test": "python3 scripts/run_tests.py", "test:dbg": "python3 scripts/run_tests.py --jsdebugger --debug-on-failure", - "test:gtest": "cd engine && ./mach gtest", + "test:gtest": "cd engine && ./mach gtest Zen*", "ffprefs": "cd tools/ffprefs && cargo run --bin ffprefs -- ../../", "lc": "surfer license-check", "lc:fix": "surfer license-check --fix", diff --git a/src/zen/boosts/gtest/TestZenBoostsAccentCache.cpp b/src/zen/boosts/gtest/TestZenBoostsAccentCache.cpp new file mode 100644 index 000000000..39896f863 --- /dev/null +++ b/src/zen/boosts/gtest/TestZenBoostsAccentCache.cpp @@ -0,0 +1,104 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "mozilla/nsZenBoostsBackend.h" + +using zen::detail::AccentCacheSize; +using zen::detail::EnsureCachedAccent; +using zen::detail::IsAccentCached; +using zen::detail::ResetAccentCache; + +namespace { + +class ZenBoostsAccentCache : public ::testing::Test { + protected: + void SetUp() override { ResetAccentCache(); } + void TearDown() override { ResetAccentCache(); } +}; + +constexpr nscolor kAccentA = NS_RGBA(80, 120, 200, 200); +constexpr nscolor kAccentB = NS_RGBA(200, 80, 80, 200); +constexpr nscolor kAccentC = NS_RGBA(80, 200, 120, 200); +constexpr nscolor kAccentD = NS_RGBA(200, 200, 80, 200); +constexpr nscolor kAccentE = NS_RGBA(120, 80, 200, 200); + +} // namespace + +TEST_F(ZenBoostsAccentCache, SizeIsAtLeastFour) { + EXPECT_GE(AccentCacheSize(), 4u); +} + +TEST_F(ZenBoostsAccentCache, EmptyAfterReset) { + EnsureCachedAccent(kAccentA, 0.0f); + ResetAccentCache(); + EXPECT_FALSE(IsAccentCached(kAccentA, 0.0f)); +} + +TEST_F(ZenBoostsAccentCache, SameKeyIsCachedAfterEnsure) { + EXPECT_FALSE(IsAccentCached(kAccentA, 0.0f)); + EnsureCachedAccent(kAccentA, 0.0f); + EXPECT_TRUE(IsAccentCached(kAccentA, 0.0f)); +} + +// Keying on accent alone would silently serve a stale complementary accent +// when the rotation changes. +TEST_F(ZenBoostsAccentCache, DifferentRotationOccupiesDistinctEntry) { + EnsureCachedAccent(kAccentA, 0.0f); + EnsureCachedAccent(kAccentA, 90.0f); + EXPECT_TRUE(IsAccentCached(kAccentA, 0.0f)); + EXPECT_TRUE(IsAccentCached(kAccentA, 90.0f)); +} + +TEST_F(ZenBoostsAccentCache, DifferentAccentOccupiesDistinctEntry) { + EnsureCachedAccent(kAccentA, 30.0f); + EnsureCachedAccent(kAccentB, 30.0f); + EXPECT_TRUE(IsAccentCached(kAccentA, 30.0f)); + EXPECT_TRUE(IsAccentCached(kAccentB, 30.0f)); +} + +TEST_F(ZenBoostsAccentCache, RoundRobinEvictsOldestEntry) { + ASSERT_EQ(AccentCacheSize(), 4u); + + EnsureCachedAccent(kAccentA, 0.0f); + EnsureCachedAccent(kAccentB, 0.0f); + EnsureCachedAccent(kAccentC, 0.0f); + EnsureCachedAccent(kAccentD, 0.0f); + + EXPECT_TRUE(IsAccentCached(kAccentA, 0.0f)); + EXPECT_TRUE(IsAccentCached(kAccentB, 0.0f)); + EXPECT_TRUE(IsAccentCached(kAccentC, 0.0f)); + EXPECT_TRUE(IsAccentCached(kAccentD, 0.0f)); + + EnsureCachedAccent(kAccentE, 0.0f); + EXPECT_FALSE(IsAccentCached(kAccentA, 0.0f)); + EXPECT_TRUE(IsAccentCached(kAccentB, 0.0f)); + EXPECT_TRUE(IsAccentCached(kAccentC, 0.0f)); + EXPECT_TRUE(IsAccentCached(kAccentD, 0.0f)); + EXPECT_TRUE(IsAccentCached(kAccentE, 0.0f)); +} + +// A cache hit must not consume a fresh slot, otherwise repeated paints with +// the same accent would evict their own neighbours. +TEST_F(ZenBoostsAccentCache, RepeatEnsureDoesNotChurnTheCache) { + ASSERT_EQ(AccentCacheSize(), 4u); + + EnsureCachedAccent(kAccentA, 0.0f); + EnsureCachedAccent(kAccentB, 0.0f); + EnsureCachedAccent(kAccentC, 0.0f); + + for (int i = 0; i < 16; ++i) { + EnsureCachedAccent(kAccentA, 0.0f); + } + + EXPECT_TRUE(IsAccentCached(kAccentA, 0.0f)); + EXPECT_TRUE(IsAccentCached(kAccentB, 0.0f)); + EXPECT_TRUE(IsAccentCached(kAccentC, 0.0f)); + + EnsureCachedAccent(kAccentD, 0.0f); + EnsureCachedAccent(kAccentE, 0.0f); + + EXPECT_FALSE(IsAccentCached(kAccentA, 0.0f)); +} diff --git a/src/zen/boosts/gtest/TestZenBoostsResolveStyleColor.cpp b/src/zen/boosts/gtest/TestZenBoostsResolveStyleColor.cpp new file mode 100644 index 000000000..625bda981 --- /dev/null +++ b/src/zen/boosts/gtest/TestZenBoostsResolveStyleColor.cpp @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "mozilla/nsZenBoostsBackend.h" + +using zen::nsZenBoostsBackend; + +namespace { + +const nscolor kResolveColors[] = { + NS_RGBA(0, 0, 0, 255), NS_RGBA(255, 255, 255, 255), + NS_RGBA(128, 128, 128, 255), NS_RGBA(255, 0, 0, 255), + NS_RGBA(0, 255, 0, 255), NS_RGBA(0, 0, 255, 255), + NS_RGBA(40, 44, 52, 255), NS_RGBA(248, 248, 248, 255), + NS_RGBA(20, 22, 28, 255), NS_RGBA(80, 80, 80, 200), + NS_RGBA(240, 17, 99, 1), NS_RGBA(0, 0, 0, 0), +}; + +} // namespace + +// Removing the null-frame guard would crash chrome-process callers that +// legitimately pass nullptr (canvas getComputedStyle, font-palette binding, +// the StyleColor(nscolor)/StyleColor(StyleAbsoluteColor) overloads). +TEST(ZenBoostsResolveStyleColor, NullFrameIsIdentity) +{ + for (nscolor c : kResolveColors) { + EXPECT_EQ(nsZenBoostsBackend::ResolveStyleColor(c, nullptr), c); + } +} + +TEST(ZenBoostsResolveStyleColor, NullFrameIsIdempotent) +{ + for (nscolor c : kResolveColors) { + nscolor once = nsZenBoostsBackend::ResolveStyleColor(c, nullptr); + nscolor twice = nsZenBoostsBackend::ResolveStyleColor(once, nullptr); + EXPECT_EQ(once, c); + EXPECT_EQ(twice, c); + } +} diff --git a/src/zen/boosts/gtest/moz.build b/src/zen/boosts/gtest/moz.build index cc14100b7..0d375c734 100644 --- a/src/zen/boosts/gtest/moz.build +++ b/src/zen/boosts/gtest/moz.build @@ -3,7 +3,9 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. UNIFIED_SOURCES += [ + "TestZenBoostsAccentCache.cpp", "TestZenBoostsColorFilter.cpp", + "TestZenBoostsResolveStyleColor.cpp", ] FINAL_LIBRARY = "xul-gtest" diff --git a/src/zen/boosts/nsZenBoostsBackend.cpp b/src/zen/boosts/nsZenBoostsBackend.cpp index 6a64cd724..9efc6acef 100644 --- a/src/zen/boosts/nsZenBoostsBackend.cpp +++ b/src/zen/boosts/nsZenBoostsBackend.cpp @@ -450,11 +450,9 @@ inline static void GetZenBoostsDataForFrame(const nsIFrame* aFrame, } // namespace +#ifdef ENABLE_TESTS namespace detail { -// Thin forwarders that give unit tests access to the pure color math without -// pulling in the singleton / BrowsingContext. They are defined here, after the -// anonymous namespace, so they can reach those file-local implementations. nsZenAccentOklab PrecomputeAccent(nscolor aAccentColor) { return zenPrecomputeAccent(aAccentColor); } @@ -474,7 +472,33 @@ nscolor InvertColorChannel(nscolor aColor) { return zenInvertColorChannel(aColor); } +size_t AccentCacheSize() { return kAccentCacheSize; } + +void ResetAccentCache() { + for (auto& entry : sAccentCache) { + entry.valid = false; + entry.accentNS = 0; + entry.rotationDeg = 0.0f; + } + sAccentCacheNext = 0; +} + +bool IsAccentCached(nscolor aAccentNS, float aRotationDeg) { + for (const auto& entry : sAccentCache) { + if (entry.valid && entry.accentNS == aAccentNS && + entry.rotationDeg == aRotationDeg) { + return true; + } + } + return false; +} + +void EnsureCachedAccent(nscolor aAccentNS, float aRotationDeg) { + (void)GetCachedAccent(aAccentNS, aRotationDeg); +} + } // namespace detail +#endif // ENABLE_TESTS static mozilla::StaticRefPtr sZenBoostsBackend; diff --git a/src/zen/boosts/nsZenBoostsBackend.h b/src/zen/boosts/nsZenBoostsBackend.h index 1d2073372..2905dae5e 100644 --- a/src/zen/boosts/nsZenBoostsBackend.h +++ b/src/zen/boosts/nsZenBoostsBackend.h @@ -27,10 +27,9 @@ struct nsZenAccentOklab { float contrastFactor; }; +#ifdef ENABLE_TESTS +// Test-only forwarders into the file-local color math and accent cache. namespace detail { -// Pure color-math primitives, exposed for unit testing. These have no -// dependency on the singleton, the BrowsingContext, or the process type, so -// they can be exercised directly from gtest. nsZenAccentOklab PrecomputeAccent(nscolor aAccentColor); nsZenAccentOklab RotateAccent(const nsZenAccentOklab& aBase, float aRotationDeg); @@ -38,7 +37,13 @@ nscolor FilterColorChannel(nscolor aOriginalColor, const nsZenAccentOklab& aAccent, const nsZenAccentOklab& aComplementary); nscolor InvertColorChannel(nscolor aColor); + +size_t AccentCacheSize(); +void ResetAccentCache(); +bool IsAccentCached(nscolor aAccentNS, float aRotationDeg); +void EnsureCachedAccent(nscolor aAccentNS, float aRotationDeg); } // namespace detail +#endif // ENABLE_TESTS class nsZenBoostsBackend final : public nsISupports { public: diff --git a/src/zen/tests/boosts/browser.toml b/src/zen/tests/boosts/browser.toml index 9cacfe47f..a514586e6 100644 --- a/src/zen/tests/boosts/browser.toml +++ b/src/zen/tests/boosts/browser.toml @@ -8,6 +8,41 @@ support-files = [ ] ["browser_boost_selector_basic.js"] + ["browser_boost_selector_escaping.js"] + ["browser_boost_selector_invalid.js"] + ["browser_boost_selector_nthchild.js"] + +["browser_boosts_animation.js"] + +["browser_boosts_background.js"] + +["browser_boosts_border.js"] + +["browser_boosts_gradient.js"] + +["browser_boosts_inline_svg.js"] + +["browser_boosts_input_text.js"] + +["browser_boosts_invert.js"] + +["browser_boosts_outline.js"] + +["browser_boosts_placeholder.js"] + +["browser_boosts_pseudo_before.js"] + +["browser_boosts_shadow.js"] + +["browser_boosts_svg_background_image.js"] + +["browser_boosts_svg_image.js"] + +["browser_boosts_svg_linear_gradient.js"] + +["browser_boosts_svg_use_sprite.js"] + +["browser_boosts_text_color.js"] diff --git a/src/zen/tests/boosts/browser_boosts_animation.js b/src/zen/tests/boosts/browser_boosts_animation.js new file mode 100644 index 000000000..f723e1094 --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_animation.js @@ -0,0 +1,52 @@ +/* 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"; + +// The compositor animation path in AnimationInfo.cpp resolves a colour with +// the host frame, then either takes it as-is (currentColor keyframe) or +// passes it through ResolveStyleColor (absolute keyframe). Both paths must +// end up at the same boosted colour as the static equivalent. We pin a +// background-color animation at 50% via animation-delay and compare against +// a static element that holds the interpolated colour. +add_task(async function animated_background_is_boosted() { + // Paused-at-50% animation between #000 and #fff → mid grey at the sampled + // time. We compare against a static rgb(128,128,128) swatch and require + // both to land at the same colour after boost. + const html = ` + +
+
`; + + await BrowserTestUtils.withNewTab(dataUrl(html), async browser => { + await waitForRepaint(browser); + + await setBoost(browser, { + accent: PAGE_ACCENT, + complementaryRotation: PAGE_COMPLEMENTARY_ROTATION, + }); + const staticBoosted = await pixelInElement(browser, "#static"); + const animBoosted = await pixelInElement(browser, "#animated"); + + Assert.ok( + pixelsClose(staticBoosted, animBoosted, 6), + `animated and static mid-grey must land at the same boosted colour; ` + + `static=${JSON.stringify(staticBoosted)} animated=${JSON.stringify( + animBoosted + )}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_background.js b/src/zen/tests/boosts/browser_boosts_background.js new file mode 100644 index 000000000..9f62f3d96 --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_background.js @@ -0,0 +1,50 @@ +/* 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"; + +// Verifies that activating a boost on a tab moves the painted colour of a +// plain CSS `background-color` block. Catches regressions where the per-color +// boost in StyleAbsoluteColor::ToColor or CalcColor is bypassed for +// backgrounds, and the gap that would surface if nsCSSRendering ever stopped +// passing the frame through GetVisitedDependentColor. +add_task(async function bg_color_is_tinted() { + const html = ` + +
`; + + await BrowserTestUtils.withNewTab(dataUrl(html), async browser => { + await waitForRepaint(browser); + + await setBoost(browser, { accent: 0 }); + const baseline = await pixelInElement(browser, "#bg"); + Assert.equal(baseline.r, 120, "baseline R is the literal background"); + Assert.equal(baseline.g, 120, "baseline G is the literal background"); + Assert.equal(baseline.b, 120, "baseline B is the literal background"); + + await setBoost(browser, { + accent: PAGE_ACCENT, + complementaryRotation: PAGE_COMPLEMENTARY_ROTATION, + }); + const boosted = await pixelInElement(browser, "#bg"); + + Assert.ok( + pixelsDiffer(baseline, boosted, 3), + `boost should tint the background; got baseline=${JSON.stringify( + baseline + )} boosted=${JSON.stringify(boosted)}` + ); + + // Sanity: clear the boost and the painted colour returns home. + await setBoost(browser, { accent: 0 }); + const cleared = await pixelInElement(browser, "#bg"); + Assert.ok( + pixelsClose(cleared, baseline, 2), + `clearing boost should restore original; got ${JSON.stringify(cleared)}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_border.js b/src/zen/tests/boosts/browser_boosts_border.js new file mode 100644 index 000000000..ffe4ba1ed --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_border.js @@ -0,0 +1,53 @@ +/* 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"; + +// Borders go through nsCSSRendering::ComputeBorderColors → CalcColor with the +// frame, so the boost should reach them. Use a thick solid border and sample +// inside the border band (which is fully the border colour) rather than the +// element interior. +add_task(async function border_color_is_tinted() { + const html = ` + +
`; + + await BrowserTestUtils.withNewTab(dataUrl(html), async browser => { + await waitForRepaint(browser); + + // Sample a point inside the top border band: x = centre of element, y just + // below the top edge (well inside the 30px-wide border). + const point = await SpecialPowers.spawn(browser, [], () => { + const r = content.document.querySelector("#b").getBoundingClientRect(); + return { + x: Math.round(r.left + r.width / 2), + y: Math.round(r.top + 15), + }; + }); + + await setBoost(browser, { accent: 0 }); + const baseline = await pixelAt(browser, point.x, point.y); + Assert.ok( + pixelsClose(baseline, { r: 120, g: 120, b: 120 }, 5), + `baseline border colour ≈ rgb(120,120,120); got ${JSON.stringify(baseline)}` + ); + + await setBoost(browser, { + accent: PAGE_ACCENT, + complementaryRotation: PAGE_COMPLEMENTARY_ROTATION, + }); + const boosted = await pixelAt(browser, point.x, point.y); + + Assert.ok( + pixelsDiffer(baseline, boosted, 3), + `border colour should be tinted; baseline=${JSON.stringify(baseline)} ` + + `boosted=${JSON.stringify(boosted)}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_gradient.js b/src/zen/tests/boosts/browser_boosts_gradient.js new file mode 100644 index 000000000..b2167b14b --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_gradient.js @@ -0,0 +1,83 @@ +/* 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"; + +// CSS linear-gradient stops are resolved via the nsCSSGradientRenderer + +// ColorStopInterpolator path, which threads the frame down into +// gfxUtils::ToDeviceColor(StyleAbsoluteColor, frame). If that threading +// regresses, gradient stops paint without the boost while everything else +// around them gets tinted — a particularly visible regression. +add_task(async function linear_gradient_stops_are_boosted() { + // Use a two-stop horizontal gradient so a sample near the left edge is + // dominated by the first stop and a sample near the right edge by the + // second. The element is sized 400×200 so we have generous sample regions. + const html = ` + +
`; + + await BrowserTestUtils.withNewTab(dataUrl(html), async browser => { + await waitForRepaint(browser); + + // Sample 5% in (mostly first stop) and 95% in (mostly last stop). + const points = await SpecialPowers.spawn(browser, [], () => { + const r = content.document.querySelector("#g").getBoundingClientRect(); + return { + left: { + x: Math.round(r.left + r.width * 0.05), + y: Math.round(r.top + r.height / 2), + }, + right: { + x: Math.round(r.left + r.width * 0.95), + y: Math.round(r.top + r.height / 2), + }, + }; + }); + + await setBoost(browser, { accent: 0 }); + const leftBaseline = await pixelAt(browser, points.left.x, points.left.y); + const rightBaseline = await pixelAt( + browser, + points.right.x, + points.right.y + ); + + await setBoost(browser, { + accent: PAGE_ACCENT, + complementaryRotation: PAGE_COMPLEMENTARY_ROTATION, + }); + const leftBoosted = await pixelAt(browser, points.left.x, points.left.y); + const rightBoosted = await pixelAt(browser, points.right.x, points.right.y); + + Assert.ok( + pixelsDiffer(leftBaseline, leftBoosted, 3), + `left gradient stop must tint; baseline=${JSON.stringify( + leftBaseline + )} boosted=${JSON.stringify(leftBoosted)}` + ); + Assert.ok( + pixelsDiffer(rightBaseline, rightBoosted, 3), + `right gradient stop must tint; baseline=${JSON.stringify( + rightBaseline + )} boosted=${JSON.stringify(rightBoosted)}` + ); + + // The two stops must remain distinguishable after boost — otherwise the + // gradient has flattened, which would be a separate regression (e.g., + // ToDeviceColor losing per-stop frame context and collapsing to a single + // tinted value). + Assert.ok( + pixelsDiffer(leftBoosted, rightBoosted, 8), + `boosted gradient endpoints collapsed; left=${JSON.stringify( + leftBoosted + )} right=${JSON.stringify(rightBoosted)}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_inline_svg.js b/src/zen/tests/boosts/browser_boosts_inline_svg.js new file mode 100644 index 000000000..afe3ae655 --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_inline_svg.js @@ -0,0 +1,106 @@ +/* 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"; + +// The "twice" diagnostic: paint an inline SVG fill and a CSS background-color +// with the *same* source colour, side by side. Under boost they must end up +// painted with the same boosted colour. If the SVG sample comes out +// noticeably more saturated / further from the baseline than the CSS one, the +// SVG paint path is applying the boost twice (the symptom you reported). +add_task(async function inline_svg_fill_matches_css_bg_under_boost() { + const html = ` + +
+ + + `; + + await BrowserTestUtils.withNewTab(dataUrl(html), async browser => { + await waitForRepaint(browser); + + await setBoost(browser, { accent: 0 }); + const divBaseline = await pixelInElement(browser, "#div"); + const svgBaseline = await pixelInElement(browser, "#svg"); + // Sanity: both paint the same source colour before any boost. + Assert.ok( + pixelsClose(divBaseline, svgBaseline, 2), + `pre-boost div/svg should already match; div=${JSON.stringify( + divBaseline + )} svg=${JSON.stringify(svgBaseline)}` + ); + + await setBoost(browser, { + accent: PAGE_ACCENT, + complementaryRotation: PAGE_COMPLEMENTARY_ROTATION, + }); + const divBoosted = await pixelInElement(browser, "#div"); + const svgBoosted = await pixelInElement(browser, "#svg"); + + Assert.ok( + pixelsDiffer(divBaseline, divBoosted, 3), + "div background must be tinted under boost (sanity)" + ); + Assert.ok( + pixelsDiffer(svgBaseline, svgBoosted, 3), + "SVG fill must be tinted under boost (sanity)" + ); + + // Headline assertion: the SVG fill and the CSS background, both starting + // from rgb(51, 54, 57) on the same page, must land at the same colour + // after the boost. A larger gap is the "filtered twice" symptom. + Assert.ok( + pixelsClose(divBoosted, svgBoosted, 4), + `SVG fill drifted from CSS background under boost — likely double-` + + `applied boost on the SVG path. div=${JSON.stringify( + divBoosted + )} svg=${JSON.stringify(svgBoosted)}` + ); + }); +}); + +// Same comparison but with `fill="currentColor"` — your reported case. The SVG +// inherits `color` and resolves it via the path frame; the CSS swatch resolves +// via its own frame. Both must land in the same place after one boost pass. +add_task(async function inline_svg_currentcolor_matches_css_under_boost() { + const html = ` + +
+
+ + + +
`; + + await BrowserTestUtils.withNewTab(dataUrl(html), async browser => { + await waitForRepaint(browser); + + await setBoost(browser, { + accent: PAGE_ACCENT, + complementaryRotation: PAGE_COMPLEMENTARY_ROTATION, + }); + const divBoosted = await pixelInElement(browser, "#div"); + const svgBoosted = await pixelInElement(browser, "#svg"); + + Assert.ok( + pixelsClose(divBoosted, svgBoosted, 4), + `SVG currentColor fill must match the same-colour CSS swatch after ` + + `boost. div=${JSON.stringify(divBoosted)} svg=${JSON.stringify( + svgBoosted + )}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_input_text.js b/src/zen/tests/boosts/browser_boosts_input_text.js new file mode 100644 index 000000000..454e0722b --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_input_text.js @@ -0,0 +1,59 @@ +/* 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"; + +// The editor content of / `; + // Full-block U+2588 again — the centre pixel is the solid foreground colour. + + await BrowserTestUtils.withNewTab(dataUrl(html), async browser => { + await waitForRepaint(browser); + + // The block character sits near the start of the textarea content box. + const point = await SpecialPowers.spawn(browser, [], () => { + const r = content.document.querySelector("#t").getBoundingClientRect(); + // Estimate the block's centre: x ≈ left padding + half a glyph width; + // y ≈ vertical centre of the line box. + return { + x: Math.round(r.left + 120), + y: Math.round(r.top + 120), + }; + }); + + await setBoost(browser, { accent: 0 }); + const baseline = await pixelAt(browser, point.x, point.y); + + await setBoost(browser, { + accent: PAGE_ACCENT, + complementaryRotation: PAGE_COMPLEMENTARY_ROTATION, + }); + const boosted = await pixelAt(browser, point.x, point.y); + + Assert.ok( + pixelsDiffer(baseline, boosted, 3), + `editor text must tint with boost; baseline=${JSON.stringify( + baseline + )} boosted=${JSON.stringify(boosted)}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_invert.js b/src/zen/tests/boosts/browser_boosts_invert.js new file mode 100644 index 000000000..ae74bd99c --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_invert.js @@ -0,0 +1,46 @@ +/* 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"; + +// Invert mode: white must come out dark, black must come out light. A double- +// invert regression (invert applied twice somewhere in the paint pipeline) +// looks like "white stays white" / "black stays black" — which is exactly +// what this test guards. +add_task(async function invert_flips_lightness() { + const html = ` + +
+
`; + + await BrowserTestUtils.withNewTab(dataUrl(html), async browser => { + await waitForRepaint(browser); + + await setBoost(browser, { accent: 0, inverted: false }); + const whiteOff = await pixelInElement(browser, "#white"); + const blackOff = await pixelInElement(browser, "#black"); + Assert.greater(pxLuma(whiteOff), 240, "baseline white is bright"); + Assert.less(pxLuma(blackOff), 16, "baseline black is dark"); + + await setBoost(browser, { accent: 0, inverted: true }); + const whiteOn = await pixelInElement(browser, "#white"); + const blackOn = await pixelInElement(browser, "#black"); + + Assert.less( + pxLuma(whiteOn), + pxLuma(whiteOff), + "white must darken under invert; double-invert would leave it bright" + ); + Assert.greater( + pxLuma(blackOn), + pxLuma(blackOff), + "black must lighten under invert (and stay off pure black via the floor)" + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_outline.js b/src/zen/tests/boosts/browser_boosts_outline.js new file mode 100644 index 000000000..3e8710c34 --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_outline.js @@ -0,0 +1,54 @@ +/* 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"; + +// CSS `outline` is painted by nsCSSRendering with its own color path. Pin +// that it gets the boost: a thick solid outline must tint when the page is +// boosted (just like a border does), and the sample point inside the outline +// band must move noticeably. +add_task(async function outline_color_is_tinted() { + const html = ` + +
`; + + await BrowserTestUtils.withNewTab(dataUrl(html), async browser => { + await waitForRepaint(browser); + + // Sample inside the outline band on the left side of the element. + const point = await SpecialPowers.spawn(browser, [], () => { + const r = content.document.querySelector("#o").getBoundingClientRect(); + return { + x: Math.round(r.left - 15), + y: Math.round(r.top + r.height / 2), + }; + }); + + await setBoost(browser, { accent: 0 }); + const baseline = await pixelAt(browser, point.x, point.y); + Assert.ok( + pixelsClose(baseline, { r: 120, g: 120, b: 120 }, 5), + `baseline outline ≈ rgb(120,120,120); got ${JSON.stringify(baseline)}` + ); + + await setBoost(browser, { + accent: PAGE_ACCENT, + complementaryRotation: PAGE_COMPLEMENTARY_ROTATION, + }); + const boosted = await pixelAt(browser, point.x, point.y); + + Assert.ok( + pixelsDiffer(baseline, boosted, 3), + `outline must tint; baseline=${JSON.stringify( + baseline + )} boosted=${JSON.stringify(boosted)}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_placeholder.js b/src/zen/tests/boosts/browser_boosts_placeholder.js new file mode 100644 index 000000000..39a5b0a1e --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_placeholder.js @@ -0,0 +1,57 @@ +/* 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"; + +// `::placeholder` is an element-backed pseudo inside a text-control's UA +// widget shadow tree. The boost-exemption logic must un-exempt it the same +// way it does the editor's typed text. Use a solid-block character as the +// placeholder so we get a clean foreground sample. +add_task(async function placeholder_is_boosted() { + const html = ` + + `; + + await BrowserTestUtils.withNewTab(dataUrl(html), async browser => { + await waitForRepaint(browser); + + const point = await SpecialPowers.spawn(browser, [], () => { + const r = content.document.querySelector("#i").getBoundingClientRect(); + // The placeholder block sits near the left of the input. + return { + x: Math.round(r.left + 120), + y: Math.round(r.top + 130), + }; + }); + + await setBoost(browser, { accent: 0 }); + const baseline = await pixelAt(browser, point.x, point.y); + + await setBoost(browser, { + accent: PAGE_ACCENT, + complementaryRotation: PAGE_COMPLEMENTARY_ROTATION, + }); + const boosted = await pixelAt(browser, point.x, point.y); + + Assert.ok( + pixelsDiffer(baseline, boosted, 3), + `::placeholder must tint with boost; baseline=${JSON.stringify( + baseline + )} boosted=${JSON.stringify(boosted)}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_pseudo_before.js b/src/zen/tests/boosts/browser_boosts_pseudo_before.js new file mode 100644 index 000000000..fa67c95de --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_pseudo_before.js @@ -0,0 +1,59 @@ +/* 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"; + +// Generated content (::before/::after) lives in a native-anonymous subtree, +// so IsBoostExemptFrame has historically over-exempted it. This test pins the +// fix: an inline-block ::before with a solid background-color must take the +// page's tint just like a regular element. +add_task(async function pseudo_before_is_boosted() { + const html = ` + +
`; + + await BrowserTestUtils.withNewTab(dataUrl(html), async browser => { + await waitForRepaint(browser); + + // ::before sits inside the host: sample inside its area (start of host). + const point = await SpecialPowers.spawn(browser, [], () => { + const r = content.document.querySelector("#host").getBoundingClientRect(); + return { + x: Math.round(r.left + 100), + y: Math.round(r.top + 100), + }; + }); + + await setBoost(browser, { accent: 0 }); + const baseline = await pixelAt(browser, point.x, point.y); + Assert.ok( + pixelsClose(baseline, { r: 120, g: 120, b: 120 }, 4), + `baseline ::before colour ≈ rgb(120,120,120); got ${JSON.stringify( + baseline + )}` + ); + + await setBoost(browser, { + accent: PAGE_ACCENT, + complementaryRotation: PAGE_COMPLEMENTARY_ROTATION, + }); + const boosted = await pixelAt(browser, point.x, point.y); + + Assert.ok( + pixelsDiffer(baseline, boosted, 3), + `::before background must tint with the boost; baseline=${JSON.stringify( + baseline + )} boosted=${JSON.stringify(boosted)}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_shadow.js b/src/zen/tests/boosts/browser_boosts_shadow.js new file mode 100644 index 000000000..e7fdc0246 --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_shadow.js @@ -0,0 +1,62 @@ +/* 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"; + +// box-shadow is the property where the "alpha byte is contrast for accents but +// must stay opacity for content colours" invariant is most visible. We render +// a thick, fully-opaque box-shadow on a white background and verify (a) it's +// tinted by the boost and (b) the sampled pixel's alpha — after compositing — +// is not the accent's contrast byte bleeding through. +add_task(async function box_shadow_is_tinted_alpha_preserved() { + // Use a solid (alpha = 1.0) shadow colour so we can sample inside the shadow + // band on a white background without dealing with partial transparency. + const html = ` + +
`; + + await BrowserTestUtils.withNewTab(dataUrl(html), async browser => { + await waitForRepaint(browser); + + const point = await SpecialPowers.spawn(browser, [], () => { + const r = content.document.querySelector("#s").getBoundingClientRect(); + // 40px inside the 80px tall shadow band that lives below the box. + return { + x: Math.round(r.left + r.width / 2), + y: Math.round(r.bottom + 40), + }; + }); + + await setBoost(browser, { accent: 0 }); + const baseline = await pixelAt(browser, point.x, point.y); + Assert.ok( + pixelsClose(baseline, { r: 80, g: 80, b: 80 }, 5), + `baseline shadow ≈ rgb(80,80,80); got ${JSON.stringify(baseline)}` + ); + + await setBoost(browser, { + accent: PAGE_ACCENT, + complementaryRotation: PAGE_COMPLEMENTARY_ROTATION, + }); + const boosted = await pixelAt(browser, point.x, point.y); + + Assert.ok( + pixelsDiffer(baseline, boosted, 3), + `box-shadow colour should be tinted; baseline=${JSON.stringify( + baseline + )} boosted=${JSON.stringify(boosted)}` + ); + + // The compositor combines RGB only; the rendered pixel from drawWindow is + // always alpha=255 because the canvas backing is opaque. The real + // alpha-preservation invariant is enforced as a gtest on the filter + // primitive (TestZenBoostsColorFilter.ShadowAlphaPreserved). Here we just + // assert the visible pixel isn't pathological (e.g., turned transparent). + Assert.equal(boosted.a, 255, "composited shadow pixel has full alpha"); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_svg_background_image.js b/src/zen/tests/boosts/browser_boosts_svg_background_image.js new file mode 100644 index 000000000..743674508 --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_svg_background_image.js @@ -0,0 +1,62 @@ +/* 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"; + +// `background-image: url(*.svg)` goes through nsImageRenderer's WebRender +// blob path, which is a separate code path from the case. Verify that +// path also has the boost propagated: a div whose background is an SVG image +// must tint just like a div with a CSS background-color of the same value. +add_task(async function svg_background_image_is_boosted() { + const svgSrc = encodeURIComponent( + `` + + `` + + `` + ); + const html = ` + +
+
`; + + await BrowserTestUtils.withNewTab(dataUrl(html), async browser => { + await waitForRepaint(browser); + for (let i = 0; i < 4; i++) { + await waitForRepaint(browser); + } + + await setBoost(browser, { accent: 0 }); + const cssBaseline = await pixelInElement(browser, "#css"); + const bgBaseline = await pixelInElement(browser, "#bg"); + Assert.ok( + pixelsClose(cssBaseline, bgBaseline, 3), + `baseline mismatch: css=${JSON.stringify( + cssBaseline + )} bg-image=${JSON.stringify(bgBaseline)}` + ); + + await setBoost(browser, { + accent: PAGE_ACCENT, + complementaryRotation: PAGE_COMPLEMENTARY_ROTATION, + }); + const cssBoosted = await pixelInElement(browser, "#css"); + const bgBoosted = await pixelInElement(browser, "#bg"); + + Assert.ok( + pixelsDiffer(bgBaseline, bgBoosted, 3), + "background-image SVG must tint under boost" + ); + Assert.ok( + pixelsClose(cssBoosted, bgBoosted, 4), + `SVG background-image must match CSS background-color after boost. ` + + `css=${JSON.stringify(cssBoosted)} bg=${JSON.stringify(bgBoosted)}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_svg_image.js b/src/zen/tests/boosts/browser_boosts_svg_image.js new file mode 100644 index 000000000..09516b4d0 --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_svg_image.js @@ -0,0 +1,63 @@ +/* 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"; + +// SVG used as an image () renders inside its own image document +// with no BrowsingContext, so the boost must be propagated through the +// SVGImageContext + AutoRestoreSVGState plumbing onto the image document's +// PresContext. Compare the painted colour of an -rendered SVG to an +// inline with the same fill — both should land at the same boosted +// colour after one pass. +add_task(async function svg_as_img_matches_inline_under_boost() { + const svgSrc = encodeURIComponent( + `` + + `` + + `` + ); + const html = ` + + + + + `; + + await BrowserTestUtils.withNewTab(dataUrl(html), async browser => { + await waitForRepaint(browser); + // SVG images are loaded async; give the a few frames to paint. + for (let i = 0; i < 4; i++) { + await waitForRepaint(browser); + } + + await setBoost(browser, { accent: 0 }); + const imgBaseline = await pixelInElement(browser, "#img"); + const inlineBaseline = await pixelInElement(browser, "#inline"); + Assert.ok( + pixelsClose(imgBaseline, inlineBaseline, 3), + `baseline mismatch between -svg and inline svg: img=` + + `${JSON.stringify(imgBaseline)} inline=${JSON.stringify(inlineBaseline)}` + ); + + await setBoost(browser, { + accent: PAGE_ACCENT, + complementaryRotation: PAGE_COMPLEMENTARY_ROTATION, + }); + const imgBoosted = await pixelInElement(browser, "#img"); + const inlineBoosted = await pixelInElement(browser, "#inline"); + + Assert.ok( + pixelsDiffer(imgBaseline, imgBoosted, 3), + "-rendered SVG must tint under boost" + ); + Assert.ok( + pixelsClose(imgBoosted, inlineBoosted, 4), + `-rendered SVG must match inline SVG after boost. img=` + + `${JSON.stringify(imgBoosted)} inline=${JSON.stringify(inlineBoosted)}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_svg_linear_gradient.js b/src/zen/tests/boosts/browser_boosts_svg_linear_gradient.js new file mode 100644 index 000000000..dea6b29fe --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_svg_linear_gradient.js @@ -0,0 +1,71 @@ +/* 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"; + +// SVG paint-server gradients () go through SVGGradientFrame +// which threads the host frame into ToDeviceColor for each stop. Coverage +// here pins that threading: a paint-server gradient must tint stops the same +// way a CSS gradient does. +add_task(async function svg_linear_gradient_stops_are_boosted() { + const html = ` + + + + + + + + + + `; + + await BrowserTestUtils.withNewTab(dataUrl(html), async browser => { + await waitForRepaint(browser); + + const points = await SpecialPowers.spawn(browser, [], () => { + const r = content.document.querySelector("#g").getBoundingClientRect(); + return { + left: { + x: Math.round(r.left + r.width * 0.05), + y: Math.round(r.top + r.height / 2), + }, + right: { + x: Math.round(r.left + r.width * 0.95), + y: Math.round(r.top + r.height / 2), + }, + }; + }); + + await setBoost(browser, { accent: 0 }); + const leftBaseline = await pixelAt(browser, points.left.x, points.left.y); + const rightBaseline = await pixelAt( + browser, + points.right.x, + points.right.y + ); + + await setBoost(browser, { + accent: PAGE_ACCENT, + complementaryRotation: PAGE_COMPLEMENTARY_ROTATION, + }); + const leftBoosted = await pixelAt(browser, points.left.x, points.left.y); + const rightBoosted = await pixelAt(browser, points.right.x, points.right.y); + + Assert.ok( + pixelsDiffer(leftBaseline, leftBoosted, 3), + "SVG first stop must tint" + ); + Assert.ok( + pixelsDiffer(rightBaseline, rightBoosted, 3), + "SVG last stop must tint" + ); + Assert.ok( + pixelsDiffer(leftBoosted, rightBoosted, 8), + "SVG stops must stay distinguishable after boost" + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_svg_use_sprite.js b/src/zen/tests/boosts/browser_boosts_svg_use_sprite.js new file mode 100644 index 000000000..3e7fb9e4a --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_svg_use_sprite.js @@ -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/. */ + +"use strict"; + +// Inline clones the symbol's content into a shadow tree. +// The clone is native-anonymous, so IsBoostExemptFrame must NOT exempt it +// (that's what the GetContainingShadow carve-out is for). Compare against a +// direct inline rect with the same fill — both must land at the same boosted +// colour, demonstrating the use-clone isn't being skipped. +add_task(async function svg_use_clone_is_boosted_like_direct_inline() { + const html = ` + + + + + + + + + + + + `; + + await BrowserTestUtils.withNewTab(dataUrl(html), async browser => { + await waitForRepaint(browser); + + await setBoost(browser, { accent: 0 }); + const directBaseline = await pixelInElement(browser, "#direct"); + const usedBaseline = await pixelInElement(browser, "#used"); + Assert.ok( + pixelsClose(directBaseline, usedBaseline, 3), + `baseline mismatch between direct and used; direct=${JSON.stringify( + directBaseline + )} used=${JSON.stringify(usedBaseline)}` + ); + + await setBoost(browser, { + accent: PAGE_ACCENT, + complementaryRotation: PAGE_COMPLEMENTARY_ROTATION, + }); + const directBoosted = await pixelInElement(browser, "#direct"); + const usedBoosted = await pixelInElement(browser, "#used"); + + Assert.ok( + pixelsDiffer(usedBaseline, usedBoosted, 3), + "-cloned content must tint under boost (use-shadow not exempt)" + ); + Assert.ok( + pixelsClose(directBoosted, usedBoosted, 4), + ` clone must match direct inline rect after boost. direct=` + + `${JSON.stringify(directBoosted)} used=${JSON.stringify(usedBoosted)}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_text_color.js b/src/zen/tests/boosts/browser_boosts_text_color.js new file mode 100644 index 000000000..3c1de02bd --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_text_color.js @@ -0,0 +1,50 @@ +/* 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"; + +// Verifies that text colour is tinted under a boost. Uses a large solid-colour +// glyph and samples at its geometric centre, where the rendered pixel is fully +// the foreground colour (no anti-aliased blend with the background). +add_task(async function text_color_is_tinted() { + const html = ` + + `; + // Full-block U+2588 fills its glyph cell with the foreground colour, so the + // centre pixel is a clean sample of the painted text colour. + + await BrowserTestUtils.withNewTab(dataUrl(html), async browser => { + await waitForRepaint(browser); + + await setBoost(browser, { accent: 0 }); + const baseline = await pixelInElement(browser, "#t"); + + await setBoost(browser, { + accent: PAGE_ACCENT, + complementaryRotation: PAGE_COMPLEMENTARY_ROTATION, + }); + const boosted = await pixelInElement(browser, "#t"); + + Assert.ok( + pixelsDiffer(baseline, boosted, 3), + `text colour should be tinted; baseline=${JSON.stringify(baseline)} ` + + `boosted=${JSON.stringify(boosted)}` + ); + + // The text was clearly darker than white before the boost, and the boost + // preserves perceived luminance roughly, so it must stay darker than its + // (white) background afterwards. A broken filter that inverts the + // luminance direction would flip this. + const bg = await pixelAt(browser, 5, 5); + Assert.greater( + pxLuma(bg), + pxLuma(boosted), + "boosted text must remain darker than its boosted white background" + ); + }); +}); diff --git a/src/zen/tests/boosts/head.js b/src/zen/tests/boosts/head.js index 7514c6dea..011447b43 100644 --- a/src/zen/tests/boosts/head.js +++ b/src/zen/tests/boosts/head.js @@ -5,3 +5,108 @@ const { SelectorComponent } = ChromeUtils.importESModule( "resource:///modules/zen/boosts/ZenSelectorComponent.sys.mjs" ); + +// --- Boost pixel-level test helpers -------------------------------------- +// +// Used by browser_boosts_*.js. Each helper documents what regression in the +// boost paint paths it's meant to catch. + +// Construct an nscolor in Firefox's ABGR encoding from RGB + the alpha byte. +// The boost backend reuses the alpha byte as the accent's contrast/strength +// (see NS_GET_CONTRAST in nsZenBoostsBackend.cpp), so for boost activation +// use `contrast` as the fourth arg with a typical value of 200. +function nsRGBA(r, g, b, a = 255) { + return (((a >>> 0) << 24) | (b << 16) | (g << 8) | r) >>> 0; +} + +// Two animation frames is enough for a BC-field-triggered restyle + repaint +// to settle in our tests; the DidSet handlers in nsZenBCOverrides.cpp +// dispatch a RecascadeSubtree + visual hint that's processed by the next +// refresh tick. +async function waitForRepaint(browser) { + await SpecialPowers.spawn(browser, [], async () => { + await new Promise(r => content.requestAnimationFrame(r)); + await new Promise(r => content.requestAnimationFrame(r)); + }); +} + +// Apply (or clear) a boost on the tab's top-level content BrowsingContext. +// Passing accent = 0 clears the boost so a test can sample a no-boost +// baseline and a boosted state on the same loaded page. +async function setBoost( + browser, + { accent = 0, complementaryRotation = 0, inverted = false } = {} +) { + const bc = browser.browsingContext; + bc.zenBoostsData = accent; + bc.zenBoostsComplementaryRotation = complementaryRotation; + bc.isZenBoostsInverted = inverted; + await waitForRepaint(browser); +} + +// Read the RGBA pixel at content coordinates (x, y). Runs in the content +// process so drawWindow targets the real painted output of the tab. +async function pixelAt(browser, x, y) { + return SpecialPowers.spawn(browser, [x, y], async (px, py) => { + const w = content.innerWidth; + const h = content.innerHeight; + const canvas = content.document.createElementNS( + "http://www.w3.org/1999/xhtml", + "canvas" + ); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext("2d"); + ctx.drawWindow(content, 0, 0, w, h, "rgba(0,0,0,0)"); + const data = ctx.getImageData(px, py, 1, 1).data; + return { r: data[0], g: data[1], b: data[2], a: data[3] }; + }); +} + +// Sample the centre of the element matching |selector|. +async function pixelInElement(browser, selector) { + const point = await SpecialPowers.spawn(browser, [selector], sel => { + const el = content.document.querySelector(sel); + if (!el) { + throw new Error(`No element matches selector: ${sel}`); + } + const r = el.getBoundingClientRect(); + return { + x: Math.round(r.left + r.width / 2), + y: Math.round(r.top + r.height / 2), + }; + }); + return pixelAt(browser, point.x, point.y); +} + +// Coarse RGB-distance threshold for "the colour clearly changed". The boost's +// duotone moves channels by tens of units even for a modest accent; tolerance +// 3 is comfortably below that while ignoring sub-pixel/anti-aliasing noise. +function pixelsDiffer(a, b, tol = 3) { + return ( + Math.abs(a.r - b.r) > tol || + Math.abs(a.g - b.g) > tol || + Math.abs(a.b - b.b) > tol + ); +} + +function pixelsClose(a, b, tol = 3) { + return !pixelsDiffer(a, b, tol); +} + +// BT.601-ish perceived luminance, integer-valued. Matches the coefficients +// used by InvertColorChannel in the backend, so a test expressing "X stays +// darker than Y after boost" maps to what the user actually perceives. +function pxLuma({ r, g, b }) { + return (r * 54 + g * 183 + b * 19) >> 8; +} + +function dataUrl(html) { + return "data:text/html;charset=utf-8," + encodeURIComponent(html); +} + +// A "page accent" colour used across the property tests. Strong enough to +// move a mid-grey by tens of units per channel; rotation kept small so the +// duotone stays cohesive. +const PAGE_ACCENT = nsRGBA(80, 120, 200, /*contrast*/ 200); +const PAGE_COMPLEMENTARY_ROTATION = 30; From 8ea65cba48263a7d655cb0aceeaea1b28648a7c5 Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:24:16 +0200 Subject: [PATCH 10/53] gh-12112: Fixed auto focus for new windows not working (gh-13976) --- .github/workflows/sync-upstream.yml | 2 +- .../urlbar/content/UrlbarInput-mjs.patch | 2 +- src/browser/themes/shared/zen-icons/icons.css | 3 +- .../common/styles/zen-single-components.css | 4 +- src/zen/spaces/ZenSpaceManager.mjs | 1 + src/zen/tests/spaces/browser.toml | 2 + src/zen/tests/spaces/browser_issue_10455.js | 27 ++++-- src/zen/tests/spaces/browser_issue_12112.js | 84 +++++++++++++++++++ src/zen/tests/spaces/browser_private_mode.js | 10 ++- src/zen/tests/urlbar/browser.toml | 1 + src/zen/tests/urlbar/browser_issue_7385.js | 31 ++++--- .../browser_single_toolbar_blur_revert.js | 82 ++++++++++++++++++ 12 files changed, 221 insertions(+), 28 deletions(-) create mode 100644 src/zen/tests/spaces/browser_issue_12112.js create mode 100644 src/zen/tests/urlbar/browser_single_toolbar_blur_revert.js diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml index e4bca9156..e2662de21 100644 --- a/.github/workflows/sync-upstream.yml +++ b/.github/workflows/sync-upstream.yml @@ -129,7 +129,7 @@ jobs: token: ${{ secrets.DEPLOY_KEY }} commit-message: "chore: Sync upstream to `Firefox ${{ steps.build-data.outputs.version }}`" branch: "chore/upstream-sync" - title: "no-bug: Sync upstream Firefox to version ${{ steps.build-data.outputs.version }}" + title: "no-bug: Sync upstream Firefox to version `${{ steps.build-data.outputs.version }}`" body: | This PR syncs the upstream Firefox to version ${{ steps.build-data.outputs.version }}. diff --git a/src/browser/components/urlbar/content/UrlbarInput-mjs.patch b/src/browser/components/urlbar/content/UrlbarInput-mjs.patch index f4e335a6d..f33a99404 100644 --- a/src/browser/components/urlbar/content/UrlbarInput-mjs.patch +++ b/src/browser/components/urlbar/content/UrlbarInput-mjs.patch @@ -138,7 +138,7 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..c166b7de23c35716bf8c51b6b9c72f77 + if (this._zenHandleUrlbarClose) { + this._zenHandleUrlbarClose(); -+ } else if (!this._untrimmedValue || this.searchMode) { ++ } else if (!this._untrimmedValue || this.searchMode || this.window.gZenVerticalTabsManager._hasSetSingleToolbar) { + // Restore the current page URL when the urlbar is empty on blur + this.window.setTimeout(() => { + this.handleRevert(); diff --git a/src/browser/themes/shared/zen-icons/icons.css b/src/browser/themes/shared/zen-icons/icons.css index ed143a42b..bf563df95 100644 --- a/src/browser/themes/shared/zen-icons/icons.css +++ b/src/browser/themes/shared/zen-icons/icons.css @@ -520,7 +520,8 @@ list-style-image: url("permissions-fill.svg"); } &[boosting] image { - color: var(--color-accent-primary); + fill-opacity: 1 !important; + color: var(--zen-sidebar-themed-icon-fill); list-style-image: url("permissions-fill.svg"); } diff --git a/src/zen/common/styles/zen-single-components.css b/src/zen/common/styles/zen-single-components.css index 02a2d64d2..d9687232d 100644 --- a/src/zen/common/styles/zen-single-components.css +++ b/src/zen/common/styles/zen-single-components.css @@ -495,7 +495,7 @@ border-radius: 99px; width: var(--size-item-large); height: var(--size-item-large); - background: var(--button-background-color-primary); + background: var(--zen-sidebar-themed-icon-fill); opacity: 0.6; transition: transform 0.12s ease-in-out, @@ -625,7 +625,7 @@ color: var(--button-primary-color); &::before { - background: var(--button-background-color-primary); + background: var(--zen-sidebar-themed-icon-fill); } } diff --git a/src/zen/spaces/ZenSpaceManager.mjs b/src/zen/spaces/ZenSpaceManager.mjs index 36ca9c50e..bad548890 100644 --- a/src/zen/spaces/ZenSpaceManager.mjs +++ b/src/zen/spaces/ZenSpaceManager.mjs @@ -861,6 +861,7 @@ class nsZenWorkspaces { "Selecting empty tab because startup page didnt select a valid tab" ); this.selectEmptyTab(); + initialTabWasEmpty = true; } this.log("Removing empty tab added by startup page"); this._removedByStartupPage = true; diff --git a/src/zen/tests/spaces/browser.toml b/src/zen/tests/spaces/browser.toml index deacec1f7..99c7db003 100644 --- a/src/zen/tests/spaces/browser.toml +++ b/src/zen/tests/spaces/browser.toml @@ -14,6 +14,8 @@ support-files = [ ["browser_issue_10455.js"] +["browser_issue_12112.js"] + ["browser_issue_8699.js"] ["browser_issue_9900.js"] diff --git a/src/zen/tests/spaces/browser_issue_10455.js b/src/zen/tests/spaces/browser_issue_10455.js index ed7644d39..962d99dfa 100644 --- a/src/zen/tests/spaces/browser_issue_10455.js +++ b/src/zen/tests/spaces/browser_issue_10455.js @@ -4,16 +4,24 @@ "use strict"; add_task(async function test_Issue_10455() { - debugger; await SpecialPowers.pushPrefEnv({ - set: [["browser.tabs.closeWindowWithLastTab", true]], + set: [ + ["browser.tabs.closeWindowWithLastTab", true], + ["zen.testing.enabled", false], + ["zen.window-sync.enabled", false], + ], }); - debugger; let newWindow = await BrowserTestUtils.openNewBrowserWindow(); await newWindow.gZenWorkspaces.promiseInitialized; const unloadEvent = BrowserTestUtils.waitForEvent(newWindow, "unload"); + Assert.equal( + newWindow.gBrowser.tabs.length, + 3, + "New window should have three tabs" + ); + newWindow.BrowserCommands.closeTabOrWindow(); newWindow.BrowserCommands.closeTabOrWindow(); await unloadEvent; @@ -22,14 +30,23 @@ add_task(async function test_Issue_10455() { }); add_task(async function test_Issue_10455_Dont_Close() { - debugger; await SpecialPowers.pushPrefEnv({ - set: [["browser.tabs.closeWindowWithLastTab", false]], + set: [ + ["browser.tabs.closeWindowWithLastTab", false], + ["zen.testing.enabled", false], + ["zen.window-sync.enabled", false], + ], }); let newWindow = await BrowserTestUtils.openNewBrowserWindow(); await newWindow.gZenWorkspaces.promiseInitialized; + Assert.equal( + newWindow.gBrowser.tabs.length, + 3, + "New window should have three tabs" + ); + newWindow.BrowserCommands.closeTabOrWindow(); newWindow.BrowserCommands.closeTabOrWindow(); Assert.strictEqual( newWindow.gBrowser.tabs.length, diff --git a/src/zen/tests/spaces/browser_issue_12112.js b/src/zen/tests/spaces/browser_issue_12112.js new file mode 100644 index 000000000..c17218065 --- /dev/null +++ b/src/zen/tests/spaces/browser_issue_12112.js @@ -0,0 +1,84 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["zen.urlbar.replace-newtab", false]], + }); + registerCleanupFunction(async () => { + await SpecialPowers.popPrefEnv(); + }); +}); + +add_task( + async function test_focuses_urlbar_on_startup_without_replace_newtab() { + await gZenWorkspaces.promiseInitialized; + Assert.ok( + !gZenVerticalTabsManager._canReplaceNewTab, + "Precondition: zen.urlbar.replace-newtab is disabled" + ); + + const originalTab = gBrowser.selectedTab; + const originalOpenLocation = window.openLocation; + const originalTestingEnabled = gZenUIManager.testingEnabled; + + let openLocationCalls = 0; + window.openLocation = () => { + openLocationCalls++; + }; + + // selectStartPage() and selectEmptyTab() are both no-ops while testing mode + // is enabled; temporarily disable it to exercise the real startup path. + gZenUIManager.testingEnabled = false; + + // The tab the startup page leaves selected, which Zen wants to replace. + const tabToRemove = BrowserTestUtils.addTab(gBrowser, "about:blank", { + skipAnimation: true, + }); + gBrowser.selectedTab = tabToRemove; + gZenWorkspaces._tabToRemoveForEmpty = tabToRemove; + delete gZenWorkspaces._tabToSelect; + delete gZenWorkspaces._shouldOverrideTabs; + delete gZenWorkspaces._initialTab; + + try { + await gZenWorkspaces.selectStartPage(); + + await TestUtils.waitForCondition( + () => openLocationCalls > 0, + "openLocation() should be called to focus the address bar" + ); + + Assert.equal( + openLocationCalls, + 1, + "The address bar was focused via openLocation()" + ); + Assert.ok( + !gBrowser.selectedTab.hasAttribute("zen-empty-tab"), + "A fallback homepage tab is selected (no zen-empty-tab attribute), so " + + "the focus decision came from initialTabWasEmpty, not shownEmptyTab" + ); + Assert.ok( + !gBrowser.tabs.includes(tabToRemove), + "The empty tab added by the startup page was removed" + ); + } finally { + window.openLocation = originalOpenLocation; + gZenUIManager.testingEnabled = originalTestingEnabled; + delete gZenWorkspaces._tabToRemoveForEmpty; + + // Remove any tab created by the startup path, then restore the original. + for (const tab of [...gBrowser.tabs]) { + if (tab !== originalTab && !tab.hasAttribute("zen-empty-tab")) { + BrowserTestUtils.removeTab(tab); + } + } + if (!originalTab.closing) { + gBrowser.selectedTab = originalTab; + } + } + } +); diff --git a/src/zen/tests/spaces/browser_private_mode.js b/src/zen/tests/spaces/browser_private_mode.js index e2c0e19fd..b37677d6d 100644 --- a/src/zen/tests/spaces/browser_private_mode.js +++ b/src/zen/tests/spaces/browser_private_mode.js @@ -5,7 +5,10 @@ add_task(async function test_Private_Mode() { await SpecialPowers.pushPrefEnv({ - set: [["privacy.userContext.enabled", true]], + set: [ + ["privacy.userContext.enabled", true], + ["zen.testing.enabled", false], + ], }); let privateWindow = await BrowserTestUtils.openNewBrowserWindow({ @@ -13,6 +16,11 @@ add_task(async function test_Private_Mode() { }); await privateWindow.gZenWorkspaces.promiseInitialized; + Assert.ok( + privateWindow.gBrowser.selectedTab.hasAttribute("zen-empty-tab"), + "Private window should start with a zen empty tab" + ); + await BrowserTestUtils.closeWindow(privateWindow); await SpecialPowers.popPrefEnv(); }); diff --git a/src/zen/tests/urlbar/browser.toml b/src/zen/tests/urlbar/browser.toml index 4f7b7194e..e2c4bbb40 100644 --- a/src/zen/tests/urlbar/browser.toml +++ b/src/zen/tests/urlbar/browser.toml @@ -3,6 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. [DEFAULT] +prefs = ["browser.urlbar.closeOnWindowBlur=false"] support-files = [ "head.js", "!/browser/components/urlbar/tests/browser/head.js", diff --git a/src/zen/tests/urlbar/browser_issue_7385.js b/src/zen/tests/urlbar/browser_issue_7385.js index b77187e9d..37c3c7f7c 100644 --- a/src/zen/tests/urlbar/browser_issue_7385.js +++ b/src/zen/tests/urlbar/browser_issue_7385.js @@ -9,27 +9,24 @@ ChromeUtils.defineESModuleGetters(this, { add_task(async function test_Selection_Remains_Double_Toolbar() { await goToMultipleLayouts(async () => { - const untrimmedValue = "https://example.com"; + const untrimmedValue = "https://example.com/"; let trimmedValue = UrlbarTestUtils.trimURL(untrimmedValue); - gURLBar._setValue(untrimmedValue, { - allowTrim: true, - valueIsTyped: false, - }); - gURLBar.blur(); await SimpleTest.promiseFocus(window); - Assert.equal(gURLBar.value, trimmedValue, "Value has been trimmed"); - await selectWithMouseDrag(100, 200); + await BrowserTestUtils.withNewTab(untrimmedValue, async () => { + Assert.equal(gURLBar.value, trimmedValue, "Value has been trimmed"); + await selectWithMouseDrag(10, 20); - Assert.greater(gURLBar.selectionStart, 0, "Selection start is positive."); - Assert.greater( - gURLBar.selectionEnd, - gURLBar.selectionStart, - "Selection is not empty." - ); + Assert.greater(gURLBar.selectionStart, 0, "Selection start is positive."); + Assert.greater( + gURLBar.selectionEnd, + gURLBar.selectionStart, + "Selection is not empty." + ); - Assert.equal(gURLBar.value, untrimmedValue, `Value should be untrimmed`); + Assert.equal(gURLBar.value, untrimmedValue, `Value should be untrimmed`); - gURLBar.handleRevert(); - gURLBar.view.close(); + gURLBar.handleRevert(); + gURLBar.view.close(); + }); }); }); diff --git a/src/zen/tests/urlbar/browser_single_toolbar_blur_revert.js b/src/zen/tests/urlbar/browser_single_toolbar_blur_revert.js new file mode 100644 index 000000000..f35dadf64 --- /dev/null +++ b/src/zen/tests/urlbar/browser_single_toolbar_blur_revert.js @@ -0,0 +1,82 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs", +}); + +const PAGE_URL = "https://example.com/"; +const TYPED_VALUE = "zen blur revert test"; + +async function typeIntoUrlbar() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TYPED_VALUE, + fireInputEvent: true, + }); + Assert.equal( + gURLBar.value, + TYPED_VALUE, + "The typed value is present while the address bar is focused" + ); +} + +add_task(async function test_single_toolbar_reverts_typed_value_on_blur() { + await TestUtils.waitForCondition( + () => gZenVerticalTabsManager._hasSetSingleToolbar, + "The default layout should be single-toolbar" + ); + + await BrowserTestUtils.withNewTab(PAGE_URL, async () => { + await typeIntoUrlbar(); + + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + await SimpleTest.promiseFocus(window); + await new Promise(resolve => setTimeout(resolve)); + + await TestUtils.waitForCondition( + () => gURLBar.value !== TYPED_VALUE, + "The address bar should revert away from the typed value on blur" + ); + + Assert.ok( + gURLBar.value.includes("example.com"), + `Reverted to the page URL (got "${gURLBar.value}")` + ); + Assert.notEqual( + gURLBar.value, + TYPED_VALUE, + "Single-toolbar blur did not retain the typed value" + ); + }); + + gURLBar.handleRevert(); +}); + +add_task(async function test_double_toolbar_keeps_typed_value_on_blur() { + await SpecialPowers.pushPrefEnv({ + set: [["zen.view.use-single-toolbar", false]], + }); + await TestUtils.waitForCondition( + () => !gZenVerticalTabsManager._hasSetSingleToolbar, + "The layout should switch to double-toolbar" + ); + + await BrowserTestUtils.withNewTab(PAGE_URL, async () => { + await typeIntoUrlbar(); + + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + await SimpleTest.promiseFocus(window); + + Assert.equal( + gURLBar.value, + TYPED_VALUE, + "Double-toolbar blur keeps the typed value (no forced revert)" + ); + }); + + gURLBar.handleRevert(); + await SpecialPowers.popPrefEnv(); +}); From 2311e183f1d1dccc749c82fde2d0795fc35e1724 Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:16:29 +0200 Subject: [PATCH 11/53] gh-13745: Disable desktop launcher for windows (gh-13987) --- .../urlbar/content/UrlbarInput-mjs.patch | 4 ++-- .../windows/nsis/defines-nsi-in.patch | 12 ++-------- src/toolkit/moz-configure.patch | 23 +++++++++++++------ 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/browser/components/urlbar/content/UrlbarInput-mjs.patch b/src/browser/components/urlbar/content/UrlbarInput-mjs.patch index f33a99404..43314bcc0 100644 --- a/src/browser/components/urlbar/content/UrlbarInput-mjs.patch +++ b/src/browser/components/urlbar/content/UrlbarInput-mjs.patch @@ -140,9 +140,9 @@ index d6615ec5a29f3e3327ac4171f3fc5d9a69bd09fe..c166b7de23c35716bf8c51b6b9c72f77 + this._zenHandleUrlbarClose(); + } else if (!this._untrimmedValue || this.searchMode || this.window.gZenVerticalTabsManager._hasSetSingleToolbar) { + // Restore the current page URL when the urlbar is empty on blur -+ this.window.setTimeout(() => { ++ this.window.requestAnimationFrame(() => { + this.handleRevert(); -+ }, 0); ++ }); + } + + // Arc like URLbar: Blur the input on exit diff --git a/src/browser/installer/windows/nsis/defines-nsi-in.patch b/src/browser/installer/windows/nsis/defines-nsi-in.patch index f423ee10d..862ac618b 100644 --- a/src/browser/installer/windows/nsis/defines-nsi-in.patch +++ b/src/browser/installer/windows/nsis/defines-nsi-in.patch @@ -1,16 +1,8 @@ diff --git a/browser/installer/windows/nsis/defines.nsi.in b/browser/installer/windows/nsis/defines.nsi.in -index a9bf2d67ce522054442be664bda1434c63609ea1..54e2c97f79b1e526eca66f4ca611e26b9c3f89d4 100644 +index a9bf2d67ce522054442be664bda1434c63609ea1..16e03732d78207ce602066a4bccc0d2c2385ca91 100644 --- a/browser/installer/windows/nsis/defines.nsi.in +++ b/browser/installer/windows/nsis/defines.nsi.in -@@ -110,7 +110,6 @@ - #endif - - #ifdef DESKTOP_LAUNCHER_ENABLED --!define DESKTOP_LAUNCHER_ENABLED - #endif - - #ifdef MOZ_BITS_DOWNLOAD -@@ -147,7 +146,7 @@ VIAddVersionKey "ProductVersion" "${AppVersion}" +@@ -147,7 +147,7 @@ VIAddVersionKey "ProductVersion" "${AppVersion}" !define APPROXIMATE_REQUIRED_SPACE_MB "145" # Constants for parts of the telemetry submission URL diff --git a/src/toolkit/moz-configure.patch b/src/toolkit/moz-configure.patch index cacaabb60..48f198e0c 100644 --- a/src/toolkit/moz-configure.patch +++ b/src/toolkit/moz-configure.patch @@ -1,8 +1,8 @@ diff --git a/toolkit/moz.configure b/toolkit/moz.configure -index 226d0c5a93a9a2404e1974001da4e34b7b670067..b73277448f7d2706d316df2505e17d232f392d47 100644 +index 0f5dab192533aa42df97ee3bd4176a9f44d4d568..d45ad2409f6d36106fce5a380545aaae9397ae84 100644 --- a/toolkit/moz.configure +++ b/toolkit/moz.configure -@@ -22,6 +22,7 @@ def check_moz_app_id(moz_app_id, build_project): +@@ -20,6 +20,7 @@ def check_moz_app_id(moz_app_id, build_project): project_flag( env="MOZ_APP_VENDOR", @@ -10,7 +10,7 @@ index 226d0c5a93a9a2404e1974001da4e34b7b670067..b73277448f7d2706d316df2505e17d23 nargs=1, help='Used for application.ini\'s "Vendor" field, which also impacts profile location and user-visible fields', ) -@@ -35,6 +36,7 @@ project_flag( +@@ -33,6 +34,7 @@ project_flag( project_flag( "MOZ_APP_PROFILE", @@ -18,7 +18,7 @@ index 226d0c5a93a9a2404e1974001da4e34b7b670067..b73277448f7d2706d316df2505e17d23 nargs=1, help='Used for application.ini\'s "Profile" field, which controls profile location', ) -@@ -86,10 +88,13 @@ option( +@@ -84,10 +86,13 @@ option( ) set_config("MOZ_INCLUDE_SOURCE_INFO", True, when="MOZ_INCLUDE_SOURCE_INFO") @@ -33,7 +33,7 @@ index 226d0c5a93a9a2404e1974001da4e34b7b670067..b73277448f7d2706d316df2505e17d23 help="Set distribution-specific id", ) set_config("MOZ_DISTRIBUTION_ID", depends("--with-distribution-id")(lambda v: v[0])) -@@ -931,9 +936,9 @@ set_config("MOZ_SYSTEM_AV1", True, when="--with-system-av1") +@@ -874,9 +879,9 @@ set_config("MOZ_SYSTEM_AV1", True, when="--with-system-av1") option("--disable-jxl", help="Disable jxl image support") @@ -46,7 +46,7 @@ index 226d0c5a93a9a2404e1974001da4e34b7b670067..b73277448f7d2706d316df2505e17d23 return True -@@ -2070,7 +2075,7 @@ set_define("A11Y_LOG", True, when=a11y_log) +@@ -2028,7 +2033,7 @@ set_define("A11Y_LOG", True, when=a11y_log) # ============================================================== @depends(milestone) def require_signing(milestone): @@ -55,7 +55,7 @@ index 226d0c5a93a9a2404e1974001da4e34b7b670067..b73277448f7d2706d316df2505e17d23 option( -@@ -3903,7 +3908,7 @@ with only_when(compile_environment): +@@ -3912,7 +3917,7 @@ with only_when(compile_environment): return "Mozilla" elif target.os == "Android": return ".mozilla" @@ -64,3 +64,12 @@ index 226d0c5a93a9a2404e1974001da4e34b7b670067..b73277448f7d2706d316df2505e17d23 option( "--with-user-appdir", +@@ -4325,7 +4330,7 @@ with only_when(target_is_windows): + + @depends(target.abi) + def desktop_launcher_enabled(abi): +- return abi == "msvc" ++ return False # See gh-13745 + + set_config("DESKTOP_LAUNCHER_ENABLED", True, when=desktop_launcher_enabled) + set_define("DESKTOP_LAUNCHER_ENABLED", True, when=desktop_launcher_enabled) From 0a45b82a6cb5ab3f32fa97e464395e99159ddda5 Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:47:19 +0200 Subject: [PATCH 12/53] no-bug: Sync upstream Firefox to version `151.0.3` (gh-13991) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR syncs the upstream Firefox to version 151.0.3. * ✅ All patches applied cleanly. @mr-cheffy please review and merge this PR. --------- Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com> --- README.md | 4 ++-- build/firefox-cache/l10n-last-commit-hash | 2 +- src/zen/tests/mochitests/sandbox/browser.toml | 5 +++-- surfer.json | 6 +++--- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cb51a0f61..f2655b75a 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ Zen is a firefox-based browser with the aim of pushing your productivity to a ne ### Firefox Versions -- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `151.0.2`! 🚀 -- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 151.0.2`! +- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `151.0.3`! 🚀 +- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 151.0.3`! ### Contributing diff --git a/build/firefox-cache/l10n-last-commit-hash b/build/firefox-cache/l10n-last-commit-hash index 7bcc95dc3..dce1749c7 100644 --- a/build/firefox-cache/l10n-last-commit-hash +++ b/build/firefox-cache/l10n-last-commit-hash @@ -1 +1 @@ -05272df13c2e4f435b4e0a706715f302b09ef829 \ No newline at end of file +5c4d14a559bf26eb4ab3e136d2084310ebe51ac0 \ No newline at end of file diff --git a/src/zen/tests/mochitests/sandbox/browser.toml b/src/zen/tests/mochitests/sandbox/browser.toml index f12d053b9..8e5eb4723 100644 --- a/src/zen/tests/mochitests/sandbox/browser.toml +++ b/src/zen/tests/mochitests/sandbox/browser.toml @@ -25,11 +25,11 @@ tags = "os_integration" ["browser_content_sandbox_fs.js"] skip-if = [ - "os == 'mac' && os_version == '15.30' && arch == 'aarch64'", # Bug 2023967 "os == 'win' && os_version == '11.26100' && arch == 'x86' && debug", # bug 1379635 - "os == 'win' && os_version == '11.26100' && arch == 'x86_64' && debug", # bug 1379635 "os == 'win' && os_version == '11.26200' && arch == 'x86' && debug", # bug 1379635 + "os == 'win' && os_version == '11.26100' && arch == 'x86_64' && debug", # bug 1379635 "os == 'win' && os_version == '11.26200' && arch == 'x86_64' && debug", # bug 1379635 + "os == 'mac' && os_version == '15.30' && arch == 'aarch64'", # Bug 2023967 ] ["browser_content_sandbox_syscalls.js"] @@ -38,6 +38,7 @@ skip-if = [ skip-if = [ "os == 'win' && os_version == '11.26200' && arch == 'x86' && debug", # bug 2028636 "os == 'win' && os_version == '11.26200' && arch == 'x86_64' && debug", # bug 2028636 + "os == 'linux' && debug && artifact", # bug 1945658 ] run-if = [ "debug", diff --git a/surfer.json b/surfer.json index fefc53dd4..479ccbe91 100644 --- a/surfer.json +++ b/surfer.json @@ -5,8 +5,8 @@ "binaryName": "zen", "version": { "product": "firefox", - "version": "151.0.2", - "candidate": "151.0.2", + "version": "151.0.3", + "candidate": "151.0.3", "candidateBuild": 1 }, "buildOptions": { @@ -54,4 +54,4 @@ "licenseType": "MPL-2.0" }, "updateHostname": "updates.zen-browser.app" -} \ No newline at end of file +} From bf365f804355a7db4ee0369289bc48530626c644 Mon Sep 17 00:00:00 2001 From: Ashvin Jangid <142579833+ashvwinn@users.noreply.github.com> Date: Tue, 2 Jun 2026 19:17:37 +0530 Subject: [PATCH 13/53] gh-13439: Implement dark mode boosts panel (gh-13979) --- locales/ar/browser/browser/zen-boosts.ftl | 6 +- locales/bg/browser/browser/zen-boosts.ftl | 6 +- locales/bs/browser/browser/zen-boosts.ftl | 6 +- locales/ca/browser/browser/zen-boosts.ftl | 6 +- locales/cs/browser/browser/zen-boosts.ftl | 6 +- locales/cy/browser/browser/zen-boosts.ftl | 6 +- locales/da/browser/browser/zen-boosts.ftl | 6 +- locales/de/browser/browser/zen-boosts.ftl | 6 +- locales/el/browser/browser/zen-boosts.ftl | 6 +- locales/en-GB/browser/browser/zen-boosts.ftl | 6 +- locales/en-US/browser/browser/zen-boosts.ftl | 6 +- locales/es-ES/browser/browser/zen-boosts.ftl | 6 +- locales/et/browser/browser/zen-boosts.ftl | 6 +- locales/eu/browser/browser/zen-boosts.ftl | 6 +- locales/fa/browser/browser/zen-boosts.ftl | 6 +- locales/fi/browser/browser/zen-boosts.ftl | 6 +- locales/fr/browser/browser/zen-boosts.ftl | 6 +- locales/ga-IE/browser/browser/zen-boosts.ftl | 6 +- locales/he/browser/browser/zen-boosts.ftl | 6 +- locales/hu/browser/browser/zen-boosts.ftl | 6 +- locales/id/browser/browser/zen-boosts.ftl | 6 +- locales/is/browser/browser/zen-boosts.ftl | 6 +- locales/it/browser/browser/zen-boosts.ftl | 6 +- locales/ja/browser/browser/zen-boosts.ftl | 6 +- locales/ko/browser/browser/zen-boosts.ftl | 6 +- locales/lt/browser/browser/zen-boosts.ftl | 6 +- locales/nb/browser/browser/zen-boosts.ftl | 6 +- locales/nl/browser/browser/zen-boosts.ftl | 6 +- locales/nn-NO/browser/browser/zen-boosts.ftl | 6 +- locales/pl/browser/browser/zen-boosts.ftl | 6 +- locales/pt-BR/browser/browser/zen-boosts.ftl | 6 +- locales/pt-PT/browser/browser/zen-boosts.ftl | 6 +- locales/ro/browser/browser/zen-boosts.ftl | 6 +- locales/ru/browser/browser/zen-boosts.ftl | 6 +- locales/sk/browser/browser/zen-boosts.ftl | 6 +- locales/sv-SE/browser/browser/zen-boosts.ftl | 6 +- locales/th/browser/browser/zen-boosts.ftl | 6 +- locales/tr/browser/browser/zen-boosts.ftl | 6 +- locales/uk/browser/browser/zen-boosts.ftl | 6 +- locales/vi/browser/browser/zen-boosts.ftl | 6 +- locales/zh-CN/browser/browser/zen-boosts.ftl | 6 +- locales/zh-TW/browser/browser/zen-boosts.ftl | 6 +- src/browser/themes/shared/zen-icons/icons.css | 4 +- src/zen/boosts/ZenBoostsEditor.mjs | 21 ++-- src/zen/boosts/zen-advanced-color-options.css | 3 +- src/zen/boosts/zen-boost-editor.inc.xhtml | 19 ++- src/zen/boosts/zen-boosts.css | 116 +++++++++++------- src/zen/boosts/zen-selector.css | 54 +++++--- 48 files changed, 264 insertions(+), 205 deletions(-) diff --git a/locales/ar/browser/browser/zen-boosts.ftl b/locales/ar/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/ar/browser/browser/zen-boosts.ftl +++ b/locales/ar/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/bg/browser/browser/zen-boosts.ftl b/locales/bg/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/bg/browser/browser/zen-boosts.ftl +++ b/locales/bg/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/bs/browser/browser/zen-boosts.ftl b/locales/bs/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/bs/browser/browser/zen-boosts.ftl +++ b/locales/bs/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/ca/browser/browser/zen-boosts.ftl b/locales/ca/browser/browser/zen-boosts.ftl index 588c417b0..80e72030a 100644 --- a/locales/ca/browser/browser/zen-boosts.ftl +++ b/locales/ca/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Trieu el selector zen-boost-css-inspector = .tooltiptext = Obre l'inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brillantor -zen-bootst-color-original-saturation = Saturació original +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brillantor +zen-boost-color-original-saturation = Saturació original zen-add-zap-helper = Feu clic als elements de la pàgina per amagar-los zen-remove-zap-helper = ← Feu clic per mostrar zen-select-this = Insereix un selector per a això diff --git a/locales/cs/browser/browser/zen-boosts.ftl b/locales/cs/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/cs/browser/browser/zen-boosts.ftl +++ b/locales/cs/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/cy/browser/browser/zen-boosts.ftl b/locales/cy/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/cy/browser/browser/zen-boosts.ftl +++ b/locales/cy/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/da/browser/browser/zen-boosts.ftl b/locales/da/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/da/browser/browser/zen-boosts.ftl +++ b/locales/da/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/de/browser/browser/zen-boosts.ftl b/locales/de/browser/browser/zen-boosts.ftl index 21d9907ab..3dc0465ed 100644 --- a/locales/de/browser/browser/zen-boosts.ftl +++ b/locales/de/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Selektor auswählen zen-boost-css-inspector = .tooltiptext = Inspektor öffnen -zen-bootst-color-contrast = Kontrast -zen-bootst-color-brightness = Helligkeit -zen-bootst-color-original-saturation = Ausgangssättigung +zen-boost-color-contrast = Kontrast +zen-boost-color-brightness = Helligkeit +zen-boost-color-original-saturation = Ausgangssättigung zen-add-zap-helper = Klicke auf Elemente auf der Seite, um sie mit Zap zu markieren zen-remove-zap-helper = ← Erneut klicken zum Wiederherstellen zen-select-this = Selektor für dieses Element einfügen diff --git a/locales/el/browser/browser/zen-boosts.ftl b/locales/el/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/el/browser/browser/zen-boosts.ftl +++ b/locales/el/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/en-GB/browser/browser/zen-boosts.ftl b/locales/en-GB/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/en-GB/browser/browser/zen-boosts.ftl +++ b/locales/en-GB/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/en-US/browser/browser/zen-boosts.ftl b/locales/en-US/browser/browser/zen-boosts.ftl index 07e32431c..55da47677 100644 --- a/locales/en-US/browser/browser/zen-boosts.ftl +++ b/locales/en-US/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/es-ES/browser/browser/zen-boosts.ftl b/locales/es-ES/browser/browser/zen-boosts.ftl index 3af79691b..1542ac34f 100644 --- a/locales/es-ES/browser/browser/zen-boosts.ftl +++ b/locales/es-ES/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Escoger selector zen-boost-css-inspector = .tooltiptext = Abrir inspector -zen-bootst-color-contrast = Contraste -zen-bootst-color-brightness = Brillo -zen-bootst-color-original-saturation = Saturación original +zen-boost-color-contrast = Contraste +zen-boost-color-brightness = Brillo +zen-boost-color-original-saturation = Saturación original zen-add-zap-helper = Haga clic en los elementos de la página para borrarlos zen-remove-zap-helper = ← Clic para deshacer zen-select-this = Insertar selector para esto diff --git a/locales/et/browser/browser/zen-boosts.ftl b/locales/et/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/et/browser/browser/zen-boosts.ftl +++ b/locales/et/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/eu/browser/browser/zen-boosts.ftl b/locales/eu/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/eu/browser/browser/zen-boosts.ftl +++ b/locales/eu/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/fa/browser/browser/zen-boosts.ftl b/locales/fa/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/fa/browser/browser/zen-boosts.ftl +++ b/locales/fa/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/fi/browser/browser/zen-boosts.ftl b/locales/fi/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/fi/browser/browser/zen-boosts.ftl +++ b/locales/fi/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/fr/browser/browser/zen-boosts.ftl b/locales/fr/browser/browser/zen-boosts.ftl index d06ad7610..0cae8d22a 100644 --- a/locales/fr/browser/browser/zen-boosts.ftl +++ b/locales/fr/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Choisir le sélecteur zen-boost-css-inspector = .tooltiptext = Ouvrir l'inspecteur -zen-bootst-color-contrast = Contraste -zen-bootst-color-brightness = Luminosité -zen-bootst-color-original-saturation = Saturation originale +zen-boost-color-contrast = Contraste +zen-boost-color-brightness = Luminosité +zen-boost-color-original-saturation = Saturation originale zen-add-zap-helper = Cliquez sur des éléments de la page pour les zapper zen-remove-zap-helper = ← Cliquez pour démasquer zen-select-this = Insérer un sélecteur pour ceci diff --git a/locales/ga-IE/browser/browser/zen-boosts.ftl b/locales/ga-IE/browser/browser/zen-boosts.ftl index 2491bae48..7477614f0 100644 --- a/locales/ga-IE/browser/browser/zen-boosts.ftl +++ b/locales/ga-IE/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Roghnóir Roghnaigh zen-boost-css-inspector = .tooltiptext = Oscail an Cigire -zen-bootst-color-contrast = Codarsnacht -zen-bootst-color-brightness = Gile -zen-bootst-color-original-saturation = Sáithiú Bunaidh +zen-boost-color-contrast = Codarsnacht +zen-boost-color-brightness = Gile +zen-boost-color-original-saturation = Sáithiú Bunaidh zen-add-zap-helper = Cliceáil ar eilimintí ar an leathanach chun iad a Zapáil zen-remove-zap-helper = ← Cliceáil chun Dízipáil zen-select-this = Cuir roghnóir isteach don seo diff --git a/locales/he/browser/browser/zen-boosts.ftl b/locales/he/browser/browser/zen-boosts.ftl index 3f9d31f62..66a825ec5 100644 --- a/locales/he/browser/browser/zen-boosts.ftl +++ b/locales/he/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = ניגודיות -zen-bootst-color-brightness = בהירות -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = ניגודיות +zen-boost-color-brightness = בהירות +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/hu/browser/browser/zen-boosts.ftl b/locales/hu/browser/browser/zen-boosts.ftl index 0437ff86a..30caaedb0 100644 --- a/locales/hu/browser/browser/zen-boosts.ftl +++ b/locales/hu/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Vizsgáló megnyitása -zen-bootst-color-contrast = Kontraszt -zen-bootst-color-brightness = Fényerő -zen-bootst-color-original-saturation = Eredeti szaturáció +zen-boost-color-contrast = Kontraszt +zen-boost-color-brightness = Fényerő +zen-boost-color-original-saturation = Eredeti szaturáció zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/id/browser/browser/zen-boosts.ftl b/locales/id/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/id/browser/browser/zen-boosts.ftl +++ b/locales/id/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/is/browser/browser/zen-boosts.ftl b/locales/is/browser/browser/zen-boosts.ftl index 52a18cd59..7cc8d4b91 100644 --- a/locales/is/browser/browser/zen-boosts.ftl +++ b/locales/is/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Veljari zen-boost-css-inspector = .tooltiptext = Opna rýni -zen-bootst-color-contrast = Birtuskil -zen-bootst-color-brightness = Birtustig -zen-bootst-color-original-saturation = Upprunaleg litmettun +zen-boost-color-contrast = Birtuskil +zen-boost-color-brightness = Birtustig +zen-boost-color-original-saturation = Upprunaleg litmettun zen-add-zap-helper = Smelltu á atriði á síðunni til að einangra þau (zap) zen-remove-zap-helper = ← Smelltu til að taka úr einangrun zen-select-this = Setja inn veljara fyrir þetta diff --git a/locales/it/browser/browser/zen-boosts.ftl b/locales/it/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/it/browser/browser/zen-boosts.ftl +++ b/locales/it/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/ja/browser/browser/zen-boosts.ftl b/locales/ja/browser/browser/zen-boosts.ftl index 5a5586994..6172b8b53 100644 --- a/locales/ja/browser/browser/zen-boosts.ftl +++ b/locales/ja/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = セレクターを選択 zen-boost-css-inspector = .tooltiptext = インスペクタを開く -zen-bootst-color-contrast = コントラスト -zen-bootst-color-brightness = 明るさ -zen-bootst-color-original-saturation = 元の彩度 +zen-boost-color-contrast = コントラスト +zen-boost-color-brightness = 明るさ +zen-boost-color-original-saturation = 元の彩度 zen-add-zap-helper = Zap にページ上の要素をクリックします zen-remove-zap-helper = ←クリックして解除 zen-select-this = このセレクターを挿入 diff --git a/locales/ko/browser/browser/zen-boosts.ftl b/locales/ko/browser/browser/zen-boosts.ftl index 1b49b35d9..057fbe3b0 100644 --- a/locales/ko/browser/browser/zen-boosts.ftl +++ b/locales/ko/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = 선택자 선택 zen-boost-css-inspector = .tooltiptext = 검사기 열기 -zen-bootst-color-contrast = 대비 -zen-bootst-color-brightness = 밝기 -zen-bootst-color-original-saturation = 원본 채도 +zen-boost-color-contrast = 대비 +zen-boost-color-brightness = 밝기 +zen-boost-color-original-saturation = 원본 채도 zen-add-zap-helper = 페이지 내의 요소를 클릭해서 날려버리세요 zen-remove-zap-helper = ← 클릭하여 되살리기 zen-select-this = 이 요소의 선택자 삽입 diff --git a/locales/lt/browser/browser/zen-boosts.ftl b/locales/lt/browser/browser/zen-boosts.ftl index 8a1c758ec..60b7619e8 100644 --- a/locales/lt/browser/browser/zen-boosts.ftl +++ b/locales/lt/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Kontrastas -zen-bootst-color-brightness = Šviesumas -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Kontrastas +zen-boost-color-brightness = Šviesumas +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/nb/browser/browser/zen-boosts.ftl b/locales/nb/browser/browser/zen-boosts.ftl index 658411d08..6831beb05 100644 --- a/locales/nb/browser/browser/zen-boosts.ftl +++ b/locales/nb/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Velgselektor zen-boost-css-inspector = .tooltiptext = Åpne inspektør -zen-bootst-color-contrast = Kontrast -zen-bootst-color-brightness = Lysstyrke -zen-bootst-color-original-saturation = Opprinnelig fargemetning +zen-boost-color-contrast = Kontrast +zen-boost-color-brightness = Lysstyrke +zen-boost-color-original-saturation = Opprinnelig fargemetning zen-add-zap-helper = Klikk elementer på siden for å Zappe dem zen-remove-zap-helper = ← Klikk for å avzappe zen-select-this = Sett inn slektor for dette diff --git a/locales/nl/browser/browser/zen-boosts.ftl b/locales/nl/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/nl/browser/browser/zen-boosts.ftl +++ b/locales/nl/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/nn-NO/browser/browser/zen-boosts.ftl b/locales/nn-NO/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/nn-NO/browser/browser/zen-boosts.ftl +++ b/locales/nn-NO/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/pl/browser/browser/zen-boosts.ftl b/locales/pl/browser/browser/zen-boosts.ftl index 7d1e3f33a..c2d9a4519 100644 --- a/locales/pl/browser/browser/zen-boosts.ftl +++ b/locales/pl/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Wybierz selektor zen-boost-css-inspector = .tooltiptext = Otwórz Inspektor -zen-bootst-color-contrast = Kontrast -zen-bootst-color-brightness = Jasność -zen-bootst-color-original-saturation = Oryginalne nasycenie +zen-boost-color-contrast = Kontrast +zen-boost-color-brightness = Jasność +zen-boost-color-original-saturation = Oryginalne nasycenie zen-add-zap-helper = Kliknij elementy na stronie, aby je ukryć zen-remove-zap-helper = ← Kliknij, aby cofnąć ukrycie zen-select-this = Wstaw selektor dla tego elementu diff --git a/locales/pt-BR/browser/browser/zen-boosts.ftl b/locales/pt-BR/browser/browser/zen-boosts.ftl index 6ae7e53e6..cf906da64 100644 --- a/locales/pt-BR/browser/browser/zen-boosts.ftl +++ b/locales/pt-BR/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Ferramenta de Seleção zen-boost-css-inspector = .tooltiptext = Abrir Inspetor -zen-bootst-color-contrast = Contraste -zen-bootst-color-brightness = Brilho -zen-bootst-color-original-saturation = Saturação Original +zen-boost-color-contrast = Contraste +zen-boost-color-brightness = Brilho +zen-boost-color-original-saturation = Saturação Original zen-add-zap-helper = Clique em elementos da página para dar um Zap neles zen-remove-zap-helper = Clique para Deszapar zen-select-this = Inserir seletor para isto diff --git a/locales/pt-PT/browser/browser/zen-boosts.ftl b/locales/pt-PT/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/pt-PT/browser/browser/zen-boosts.ftl +++ b/locales/pt-PT/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/ro/browser/browser/zen-boosts.ftl b/locales/ro/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/ro/browser/browser/zen-boosts.ftl +++ b/locales/ro/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/ru/browser/browser/zen-boosts.ftl b/locales/ru/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/ru/browser/browser/zen-boosts.ftl +++ b/locales/ru/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/sk/browser/browser/zen-boosts.ftl b/locales/sk/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/sk/browser/browser/zen-boosts.ftl +++ b/locales/sk/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/sv-SE/browser/browser/zen-boosts.ftl b/locales/sv-SE/browser/browser/zen-boosts.ftl index c010d0a98..ac1f44bc4 100644 --- a/locales/sv-SE/browser/browser/zen-boosts.ftl +++ b/locales/sv-SE/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Välj väljare zen-boost-css-inspector = .tooltiptext = Öppna inspektör -zen-bootst-color-contrast = Kontrast -zen-bootst-color-brightness = Ljusstyrka -zen-bootst-color-original-saturation = Ursprunglig mättnad +zen-boost-color-contrast = Kontrast +zen-boost-color-brightness = Ljusstyrka +zen-boost-color-original-saturation = Ursprunglig mättnad zen-add-zap-helper = Klicka på element på sidan för att zappa dem zen-remove-zap-helper = ← Klicka för att avzappa zen-select-this = Infoga väljare för detta diff --git a/locales/th/browser/browser/zen-boosts.ftl b/locales/th/browser/browser/zen-boosts.ftl index 7211fa124..eb42026a7 100644 --- a/locales/th/browser/browser/zen-boosts.ftl +++ b/locales/th/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Pick Selector zen-boost-css-inspector = .tooltiptext = Open Inspector -zen-bootst-color-contrast = Contrast -zen-bootst-color-brightness = Brightness -zen-bootst-color-original-saturation = Original Saturation +zen-boost-color-contrast = Contrast +zen-boost-color-brightness = Brightness +zen-boost-color-original-saturation = Original Saturation zen-add-zap-helper = Click elements on the page to Zap them zen-remove-zap-helper = ← Click to Unzap zen-select-this = Insert selector for this diff --git a/locales/tr/browser/browser/zen-boosts.ftl b/locales/tr/browser/browser/zen-boosts.ftl index 7c78e7c03..5e5e94f13 100644 --- a/locales/tr/browser/browser/zen-boosts.ftl +++ b/locales/tr/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Seçici seç zen-boost-css-inspector = .tooltiptext = Denetleyiciyi aç -zen-bootst-color-contrast = Kontrast -zen-bootst-color-brightness = Parlaklık -zen-bootst-color-original-saturation = Orijinal doygunluk +zen-boost-color-contrast = Kontrast +zen-boost-color-brightness = Parlaklık +zen-boost-color-original-saturation = Orijinal doygunluk zen-add-zap-helper = Sayfadaki ögelere tıklayarak onları Zap ile kaldırın zen-remove-zap-helper = ← Unzap için tıklayın zen-select-this = Bu öge için seçici ekle diff --git a/locales/uk/browser/browser/zen-boosts.ftl b/locales/uk/browser/browser/zen-boosts.ftl index 3e5c35c98..37e88193d 100644 --- a/locales/uk/browser/browser/zen-boosts.ftl +++ b/locales/uk/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Вибір селектора zen-boost-css-inspector = .tooltiptext = Відкрити інспектора -zen-bootst-color-contrast = Контраст -zen-bootst-color-brightness = Яскравість -zen-bootst-color-original-saturation = Оригінальна насиченість +zen-boost-color-contrast = Контраст +zen-boost-color-brightness = Яскравість +zen-boost-color-original-saturation = Оригінальна насиченість zen-add-zap-helper = Клацніть на елементи на сторінці, щоби сховати їх zen-remove-zap-helper = ← Клацніть, аби знову показати zen-select-this = Вставити селектор для цього diff --git a/locales/vi/browser/browser/zen-boosts.ftl b/locales/vi/browser/browser/zen-boosts.ftl index 734f49741..a29a84583 100644 --- a/locales/vi/browser/browser/zen-boosts.ftl +++ b/locales/vi/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = Chọn phần tử zen-boost-css-inspector = .tooltiptext = Mở trình kiểm tra -zen-bootst-color-contrast = Độ tương phản -zen-bootst-color-brightness = Độ sáng -zen-bootst-color-original-saturation = Độ bão hòa gốc +zen-boost-color-contrast = Độ tương phản +zen-boost-color-brightness = Độ sáng +zen-boost-color-original-saturation = Độ bão hòa gốc zen-add-zap-helper = Nhấp vào các phần tử trên trang để Khử chúng zen-remove-zap-helper = ← Nhấp để khôi phục zen-select-this = Nhập bộ chọn cho phần tử này diff --git a/locales/zh-CN/browser/browser/zen-boosts.ftl b/locales/zh-CN/browser/browser/zen-boosts.ftl index be617b8c7..650ea1dc6 100644 --- a/locales/zh-CN/browser/browser/zen-boosts.ftl +++ b/locales/zh-CN/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = 选取选择器 zen-boost-css-inspector = .tooltiptext = 打开查看器 -zen-bootst-color-contrast = 对比度 -zen-bootst-color-brightness = 亮度 -zen-bootst-color-original-saturation = 初始饱和度 +zen-boost-color-contrast = 对比度 +zen-boost-color-brightness = 亮度 +zen-boost-color-original-saturation = 初始饱和度 zen-add-zap-helper = 点击页面上的元素以将其屏蔽 zen-remove-zap-helper = ← 点击以取消屏蔽 zen-select-this = 插入此元素的选择器 diff --git a/locales/zh-TW/browser/browser/zen-boosts.ftl b/locales/zh-TW/browser/browser/zen-boosts.ftl index d9fefbb04..a195f6411 100644 --- a/locales/zh-TW/browser/browser/zen-boosts.ftl +++ b/locales/zh-TW/browser/browser/zen-boosts.ftl @@ -29,9 +29,9 @@ zen-boost-css-picker = .tooltiptext = 汲取選擇器 zen-boost-css-inspector = .tooltiptext = 開啟檢測器 -zen-bootst-color-contrast = 對比 -zen-bootst-color-brightness = 亮度 -zen-bootst-color-original-saturation = 飽和度 +zen-boost-color-contrast = 對比 +zen-boost-color-brightness = 亮度 +zen-boost-color-original-saturation = 飽和度 zen-add-zap-helper = 選擇要 Zap的元素 zen-remove-zap-helper = ← 按此取消zap zen-select-this = 加入此元素的選擇器 diff --git a/src/browser/themes/shared/zen-icons/icons.css b/src/browser/themes/shared/zen-icons/icons.css index bf563df95..918a36ec1 100644 --- a/src/browser/themes/shared/zen-icons/icons.css +++ b/src/browser/themes/shared/zen-icons/icons.css @@ -538,7 +538,9 @@ display: flex; width: 100%; height: 100%; - color: var(--color-accent-primary); + color: var(--zen-sidebar-themed-icon-fill); + -moz-context-properties: fill; + fill: currentColor; list-style-image: url("chrome://browser/content/zen-images/boost-indicator.svg"); transform: translateX(-20%); z-index: 0; diff --git a/src/zen/boosts/ZenBoostsEditor.mjs b/src/zen/boosts/ZenBoostsEditor.mjs index 6956c124a..c40a541d7 100644 --- a/src/zen/boosts/ZenBoostsEditor.mjs +++ b/src/zen/boosts/ZenBoostsEditor.mjs @@ -44,6 +44,7 @@ export class nsZenBoostEditor { this.lastDotSetPos = { x: 0, y: 0 }; this.currentBoostData = null; this.boostInfo = null; + this.isDarkMode = this.openerWindow.gZenThemePicker.isDarkMode; this.killOtherEditorInstances(); @@ -52,6 +53,7 @@ export class nsZenBoostEditor { }); this.init(); + this.initColorScheme(); this.initColorPicker(); this.initFonts(); this.loadBoost(domain); @@ -184,6 +186,17 @@ export class nsZenBoostEditor { } } + /** + * Initializes the color scheme of the editor window based on the current theme (dark or light mode) + */ + initColorScheme() { + if (this.isDarkMode) { + this.doc.documentElement.style.colorScheme = "dark"; + } else { + this.doc.documentElement.style.colorScheme = "light"; + } + } + /** * Initializes the code editor for the css editor */ @@ -205,7 +218,7 @@ export class nsZenBoostEditor { const editor = new Editor({ mode: Editor.modes.css, lineNumbers: true, - theme: "default", // default is light theme + theme: "mozilla", readOnly: false, gutters: ["CodeMirror-linenumbers"], }); @@ -1166,12 +1179,6 @@ ${cssSelector} { invertButton.classList.remove("zen-boost-button-active"); } - if (this.currentBoostData.smartInvert) { - invertButton.classList.add("zen-boost-button-active"); - } else { - invertButton.classList.remove("zen-boost-button-active"); - } - if (!this.currentBoostData.enableColorBoost) { disableButton.classList.add("zen-boost-button-active-transparent"); } else { diff --git a/src/zen/boosts/zen-advanced-color-options.css b/src/zen/boosts/zen-advanced-color-options.css index 4cc651de8..385d075ab 100644 --- a/src/zen/boosts/zen-advanced-color-options.css +++ b/src/zen/boosts/zen-advanced-color-options.css @@ -5,11 +5,10 @@ */ #zen-boost-advanced-color-options-panel { - color-scheme: light; --panel-padding: 15px; & p { - color: #3a3a3b; + color: var(--zen-boosts-primary-color); } & input { diff --git a/src/zen/boosts/zen-boost-editor.inc.xhtml b/src/zen/boosts/zen-boost-editor.inc.xhtml index 15c75f57c..9cd64e411 100644 --- a/src/zen/boosts/zen-boost-editor.inc.xhtml +++ b/src/zen/boosts/zen-boost-editor.inc.xhtml @@ -34,14 +34,6 @@ - - - @@ -120,15 +112,15 @@ -

+

-

+

-

+

@@ -164,5 +156,10 @@ + +
diff --git a/src/zen/boosts/zen-boosts.css b/src/zen/boosts/zen-boosts.css index f0b3c43ba..ba3e57b3c 100644 --- a/src/zen/boosts/zen-boosts.css +++ b/src/zen/boosts/zen-boosts.css @@ -5,10 +5,33 @@ */ #zenBoostWindow { - /* For the mica effect we want a white tint */ - @media not (-moz-platform: linux) { - color-scheme: light; - } + --zen-boosts-primary-background: light-dark(#fcfcfe, #171717); + --zen-boosts-secondary-background: light-dark(#f6f6f8, #1c1c1e); + --zen-boosts-mica-background: light-dark(#f6f6f8c0, #1c1c1ec0); + + --zen-boosts-primary-color: light-dark(#3a3a3b, #f3f3f3); + --zen-boosts-secondary-color: light-dark(#727272, #b1b1b1); + + --zen-boosts-button-background: light-dark(#ebebed, #262626); + + --mod-button-c1: light-dark(#ebebed, #262626); + --mod-button-c2: light-dark(#ebebed, #262626); + + --zen-boosts-active-button-background: light-dark(#3a3a3a, #cccccc); + --zen-boosts-active-button-hover-background: light-dark(#5b5b5c, #c9c9c9); + --zen-boosts-active-button-color: light-dark(#fcfcfe, #1c1c1e); + --zen-boosts-back-button-hover-background: light-dark(#e3e3e6, #3a3a3a); + + --zen-boosts-magic-theme-background: light-dark(white, #3a3a3a); + --zen-boosts-magic-theme-active-background: light-dark(#3a3a3a, white); + + --zen-boosts-font-wrapper-background: light-dark(white, #262626); + + --zen-boosts-primary-border-color: light-dark(#ededef, #3a3a3a); + + --zen-boosts-color-picker-background: light-dark(#fbfbfdea, #1c1c1eea); + --zen-boosts-color-picker-pattern-color: light-dark(#e3e9e4, #3a3a3a); + appearance: none; border: none; @@ -40,12 +63,12 @@ width: 100%; height: 40px; - background-color: #f6f6f8; + background-color: var(--zen-boosts-secondary-background); @media (-moz-windows-mica) { - background-color: #f6f6f8c0; + background-color: var(--zen-boosts-mica-background); } - border: solid 0 #ededef; + border: solid 0 var(--zen-boosts-primary-border-color); border-bottom-width: 1px; } @@ -58,6 +81,9 @@ margin-left: 4px; border-radius: 8px; + -moz-context-properties: fill; + fill: var(--zen-boosts-primary-color); + opacity: 0.75; background-color: transparent; @@ -79,12 +105,12 @@ width: 100%; height: 60px; - background-color: #f6f6f8; + background-color: var(--zen-boosts-secondary-background); @media (-moz-windows-mica) { - background-color: #f6f6f8c0; + background-color: var(--zen-boosts-mica-background); } - border: solid 0 #ededef; + border: solid 0 var(--zen-boosts-primary-border-color); border-top-width: 1px; display: flex; @@ -148,7 +174,7 @@ body { } .subviewbutton { - color: #3a3a3b; + color: var(--zen-boosts-primary-color); } #zen-boost-editor-view { @@ -166,11 +192,11 @@ body { min-height: 40px; max-height: 40px; align-items: center; - background-color: #f6f6f8; - border: solid 1px #e7e7e7ab; + background-color: var(--zen-boosts-secondary-background); + border: solid 1px var(--zen-boosts-primary-border-color); @media (-moz-windows-mica) { - background-color: #f6f6f8c0; + background-color: var(--zen-boosts-mica-background); border: none; } @@ -232,6 +258,7 @@ body { padding: 2px; & button { + color: var(--zen-boosts-primary-color) !important; padding: auto; margin: auto; background-color: transparent; @@ -243,7 +270,7 @@ body { gap: 14px; padding-top: 6px; - background-color: #fcfcfe; + background-color: var(--zen-boosts-primary-background); -moz-window-dragging: drag; & > * { @@ -328,7 +355,7 @@ body { border-radius: 12px; } - background-color: #ebebed; + background-color: var(--zen-boosts-button-background); transition: 0.4s background-color cubic-bezier(0.075, 0.82, 0.165, 1), @@ -348,7 +375,7 @@ body { } #zen-boost-case:not([case-mode="none"]) { - background-color: #ebebed; + background-color: var(--zen-boosts-button-background); font-weight: 600 !important; } @@ -373,7 +400,7 @@ body { font-size: 8pt; text-indent: 2px; vertical-align: middle; - color: #3a3a3b; + color: var(--zen-boosts-primary-color); justify-content: center; } @@ -397,12 +424,16 @@ body { } .zen-boost-button-active { - background-color: #3a3a3a; - color: #fcfcfe; + background-color: var(--zen-boosts-active-button-background); + color: var(--zen-boosts-active-button-color); + + &#zen-boost-magic-theme { + background-color: var(--zen-boosts-magic-theme-active-background); + } } .zen-boost-button-active:hover { - background-color: #5b5b5c; + background-color: var(--zen-boosts-active-button-hover-background); } .zen-boost-button-active-transparent { @@ -430,12 +461,13 @@ body { } &:not(.zen-boost-button-active) { - background: white; + background: var(--zen-boosts-magic-theme-background); + color: var(--zen-boosts-primary-color); } } .footer { - background-color: #F6F6F8; + background-color: var(--zen-boosts-secondary-background); padding: 20px; } @@ -471,7 +503,7 @@ body { transition: 0.2s opacity ease-in-out; - color: #727272; + color: var(--zen-boosts-secondary-color); background: none; font-size: 9pt; @@ -490,7 +522,7 @@ body { &[has-selection="true"] { opacity: 1; - background-color: #ebebed; + background-color: var(--zen-boosts-button-background); } } @@ -513,7 +545,7 @@ body { #zen-boost-font-wrapper { box-shadow: 0 2px 6px rgba(0,0,0,.15); - background-color: #ffffff; + background-color: var(--zen-boosts-font-wrapper-background); border-radius: 6px; @media (-moz-platform: macos) { @@ -564,35 +596,35 @@ body { @property --mod-button-c1 { syntax: ""; - inherits: false; - initial-value: #ebebed; + inherits: true; + initial-value: transparent; } @property --mod-button-c2 { syntax: ""; - inherits: false; - initial-value: #ebebed; + inherits: true; + initial-value: transparent; } .mod-button[mode="orange"] { color: #e3e9e4; - --mod-button-c1: #ffbb5d; - --mod-button-c2: #ffa01d; + --mod-button-c1: light-dark(#ffbb5d, #c37a3f); + --mod-button-c2: light-dark(#ffa01d, #9f5a2a); } .mod-button[mode="orange-red"] { color: #e3e9e4; - --mod-button-c1: #ff8758; - --mod-button-c2: #ff5b1b; + --mod-button-c1: light-dark(#ff8758, #bd6048); + --mod-button-c2: light-dark(#ff5b1b, #9b422b); } .mod-button[mode="red"] { color: #e3e9e4; - --mod-button-c1: #ff595f; - --mod-button-c2: #ff121b; + --mod-button-c1: light-dark(#ff595f, #b94a50); + --mod-button-c2: light-dark(#ff121b, #95272e); } .mod-button[mode="blue"] { color: #e3e9e4; - --mod-button-c1: #6650fc; - --mod-button-c2: #4125ff; + --mod-button-c1: light-dark(#6650fc, #5d56ca); + --mod-button-c2: light-dark(#4125ff, #453aa9); } .mod-button[mode] { background: linear-gradient(180deg, var(--mod-button-c1) 0%, var(--mod-button-c2) 100%) border-box; @@ -608,7 +640,7 @@ body { background-color: transparent; &:hover { - background-color: #e3e3e6; + background-color: var(--zen-boosts-back-button-hover-background); } } @@ -635,17 +667,17 @@ body { z-index: 2; - background: #fbfbfdea; + background: var(--zen-boosts-color-picker-background); background-position: -23px -23px; backdrop-filter: saturate(2) blur(15px); background-size: 6px 6px; - background-image: radial-gradient(#e3e9e4, 1px, transparent 0); + background-image: radial-gradient(var(--zen-boosts-color-picker-pattern-color), 1px, transparent 0); @media (-moz-platform: macos) { background-size: 4px 4px; - background-image: radial-gradient(#e3e9e4 0.5px, transparent 0); + background-image: radial-gradient(var(--zen-boosts-color-picker-pattern-color) 0.5px, transparent 0); } } diff --git a/src/zen/boosts/zen-selector.css b/src/zen/boosts/zen-selector.css index 7e7cde34e..13fff3f75 100644 --- a/src/zen/boosts/zen-selector.css +++ b/src/zen/boosts/zen-selector.css @@ -16,6 +16,12 @@ } #select-component { + --zen-boosts-selector-background: light-dark(#f5f7fb, #27272a); + --zen-boosts-selector-preview-background: light-dark(#e0e2e63d, #27272a3d); + --zen-boosts-selector-preview-color: light-dark(rgb(76, 78, 80), rgb(207, 200, 200)); + --zen-boosts-selector-button-color: light-dark(#dadada, #f1f1f1); + --zen-boosts-selector-outline-color: light-dark(#e0e2e6ae, #474749); + width: min-content; font-family: system-ui; @@ -29,7 +35,7 @@ position: fixed; z-index: 9999; - background-color: #f5f7fb; + background-color: var(--zen-boosts-selector-background); margin: 4px; border-radius: 12px; @@ -126,17 +132,29 @@ width: 250px !important; --related-elements-value: 100%; - background: linear-gradient(to top, rgb(247, 66, 0), rgb(245, 134, 86)); + background: linear-gradient( + to top, + light-dark(#ff5b1b, #9b422b), + light-dark(#ff8758, #bd6048) + ); border: none; - color: #dadada; + color: var(--zen-boosts-selector-button-color); box-shadow: 0 0 15px #00000052; &:hover { box-shadow: 0 0 14px #00000066; background: - linear-gradient(to right, transparent var(--related-elements-value), gray var(--related-elements-value)), - linear-gradient(to top, rgb(247, 66, 0), rgb(245, 134, 86)); + linear-gradient( + to right, + transparent var(--related-elements-value), + light-dark(gray, #363636) var(--related-elements-value) + ), + linear-gradient( + to top, + light-dark(#ff5b1b, #9b422b), + light-dark(#ff8758, #bd6048) + ); @media not (-moz-platform: windows) { box-shadow: 0 0 20px #00000077; @@ -147,30 +165,34 @@ #select-this { appearance: none; - background: linear-gradient(0deg, rgba(246, 27, 25, 1) 0%, rgba(254, 67, 59, 1) 100%); + background: linear-gradient( + 0deg, + light-dark(#ff121b, #95272e), + light-dark(#ff595f, #b94a50) + ); border: none; - color: #dadada; + color: var(--zen-boosts-selector-button-color); box-shadow: 0 0 15px #00000052; } #select-cancel { appearance: none; - - background: rgb(90, 94, 100); - @media (-moz-platform: macos) { - background: linear-gradient(0deg, rgba(81, 83, 85, 1) 0%, rgba(108, 110, 112, 1) 100%); - } + background: linear-gradient( + 0deg, + light-dark(#525355, #373739) 0%, + light-dark(#6c6e70, #4b4c4e) 100% + ); border: none; - color: #dadada; + color: var(--zen-boosts-selector-button-color); box-shadow: 0 0 15px #00000052; } #selector-preview { - background-color: #e0e2e63d; - outline: 1px solid #e0e2e6ae; + background-color: var(--zen-boosts-selector-preview-background); + outline: 1px solid var(--zen-boosts-selector-outline-color); width: 100%; height: 34px; text-indent: 18px; @@ -182,7 +204,7 @@ #selector-element-preview-text { font-family: Arial, Helvetica, sans-serif; font-size: 9pt; - color: rgb(76, 78, 80); + color: var(--zen-boosts-selector-preview-color); } #hover-div { From e04e910a6fa2fc2001c1daf3f3ed33962591d9af Mon Sep 17 00:00:00 2001 From: Rishab Shah <89949620+rishabshah0@users.noreply.github.com> Date: Wed, 3 Jun 2026 02:06:16 -0700 Subject: [PATCH 14/53] no-bug: bypass workspace container coercion for extension-opened tabs (gh-14001) Co-authored-by: pokeshah <89949620+pokeshah@users.noreply.github.com> --- configs/windows/mozconfig | 10 ++++++++-- .../components/tabbrowser/content/tabbrowser-js.patch | 2 +- src/zen/spaces/ZenSpaceManager.mjs | 9 ++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/configs/windows/mozconfig b/configs/windows/mozconfig index 065ed1291..83f4283a4 100644 --- a/configs/windows/mozconfig +++ b/configs/windows/mozconfig @@ -9,6 +9,12 @@ if test "$ZEN_CROSS_COMPILING"; then export WINE="$(echo ~)/win-cross/wine/bin/wine" export WINEDEBUG=-all + # Force Wine to load the genuine Microsoft d3dcompiler_47.dll that ships next + # to fxc.exe in the Windows SDK instead of its built-in vkd3d reimplementation, + # whose HLSL front-end mishandles #include directives (emits an #hlsl_full_path + # marker it then fails to re-parse, breaking shader compilation). + export WINEDLLOVERRIDES="d3dcompiler_47=n" + export MOZ_STUB_INSTALLER=1 export MOZ_PKG_FORMAT=TAR @@ -16,9 +22,9 @@ if test "$ZEN_CROSS_COMPILING"; then CROSS_COMPILE=1 if test "$SURFER_COMPAT" = "aarch64"; then - export WIN32_REDIST_DIR="$(echo ~)/win-cross/vs2026/VC/Redist/MSVC/14.50.35710/arm64/Microsoft.VC145.CRT" + export WIN32_REDIST_DIR="$WINSYSROOT/VC/Redist/MSVC/14.50.35710/arm64/Microsoft.VC145.CRT" else - export WIN32_REDIST_DIR="$(echo ~)/win-cross/vs2026/VC/Redist/MSVC/14.50.35710/x64/Microsoft.VC145.CRT" + export WIN32_REDIST_DIR="$WINSYSROOT/VC/Redist/MSVC/14.50.35710/x64/Microsoft.VC145.CRT" fi fi diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index e2458ba13..95bf58cee 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -281,7 +281,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 + let hasZenDefaultUserContextId = false; + let zenForcedWorkspaceId = undefined; + if (typeof gZenWorkspaces !== "undefined" && !_forZenEmptyTab) { -+ [userContextId, hasZenDefaultUserContextId, zenForcedWorkspaceId] = gZenWorkspaces.getContextIdIfNeeded(userContextId, fromExternal); ++ [userContextId, hasZenDefaultUserContextId, zenForcedWorkspaceId] = gZenWorkspaces.getContextIdIfNeeded(userContextId, fromExternal, triggeringPrincipal); + } + if (!UserInteraction.running("browser.tabs.opening", window)) { diff --git a/src/zen/spaces/ZenSpaceManager.mjs b/src/zen/spaces/ZenSpaceManager.mjs index bad548890..67d29725f 100644 --- a/src/zen/spaces/ZenSpaceManager.mjs +++ b/src/zen/spaces/ZenSpaceManager.mjs @@ -2979,11 +2979,18 @@ class nsZenWorkspaces { // Tab browser utilities - getContextIdIfNeeded(userContextId, fromExternal) { + getContextIdIfNeeded(userContextId, fromExternal, triggeringPrincipal) { if (!this.workspaceEnabled) { return [userContextId, false, undefined]; } + if ( + triggeringPrincipal && + triggeringPrincipal.isAddonOrExpandedAddonPrincipal + ) { + return [userContextId, false, undefined]; + } + if ( this.shouldForceContainerTabsToWorkspace && typeof userContextId !== "undefined" && From b812bff07e07ac690cd2175a623317736b258911 Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:04:25 +0200 Subject: [PATCH 15/53] no-bug: Change wine build download (gh-14002) --- .github/workflows/windows-release-build.yml | 2 +- configs/windows/mozconfig | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/windows-release-build.yml b/.github/workflows/windows-release-build.yml index d2570dbfd..8bbfbebef 100644 --- a/.github/workflows/windows-release-build.yml +++ b/.github/workflows/windows-release-build.yml @@ -154,7 +154,7 @@ jobs: zlib1g-dev \ aria2 echo Setup wine - aria2c "https://firefox-ci-tc.services.mozilla.com/api/index/v1/task/gecko.cache.level-1.toolchains.v3.linux64-wine.latest/artifacts/public%2Fbuild%2Fwine.tar.zst" -o wine.tar.zst + aria2c "https://firefox-ci-tc.services.mozilla.com/api/queue/v1/task/dQz_aHy8Rl-Lt0xf2WlrMw/artifacts/public/build/wine.tar.zst" -o wine.tar.zst tar --zstd -xf wine.tar.zst -C ~/win-cross rm wine.tar.zst echo Setup Visual Studio diff --git a/configs/windows/mozconfig b/configs/windows/mozconfig index 83f4283a4..5e599b0e6 100644 --- a/configs/windows/mozconfig +++ b/configs/windows/mozconfig @@ -9,12 +9,6 @@ if test "$ZEN_CROSS_COMPILING"; then export WINE="$(echo ~)/win-cross/wine/bin/wine" export WINEDEBUG=-all - # Force Wine to load the genuine Microsoft d3dcompiler_47.dll that ships next - # to fxc.exe in the Windows SDK instead of its built-in vkd3d reimplementation, - # whose HLSL front-end mishandles #include directives (emits an #hlsl_full_path - # marker it then fails to re-parse, breaking shader compilation). - export WINEDLLOVERRIDES="d3dcompiler_47=n" - export MOZ_STUB_INSTALLER=1 export MOZ_PKG_FORMAT=TAR From 5383737b238e2be8f278de1f1394a476b3bc4074 Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:38:39 +0200 Subject: [PATCH 16/53] no-bug: Fixed boosts window not resizing back properly (gh-14007) --- src/zen/boosts/ZenBoostsEditor.mjs | 16 +++++----------- src/zen/boosts/zen-boosts.css | 1 + src/zen/spaces/zen-workspaces.css | 1 - src/zen/tabs/zen-tabs/vertical-tabs.css | 1 - src/zen/tests/mochitests/sandbox/browser.toml | 9 ++++----- 5 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/zen/boosts/ZenBoostsEditor.mjs b/src/zen/boosts/ZenBoostsEditor.mjs index c40a541d7..c65b45e16 100644 --- a/src/zen/boosts/ZenBoostsEditor.mjs +++ b/src/zen/boosts/ZenBoostsEditor.mjs @@ -374,12 +374,6 @@ export class nsZenBoostEditor { } windowElem.setAttribute("editor", "code"); - // Store the old boost editor width. - // The window needs the outer width which includes - // window chrome. This results in the window - // being smaller than it should be - this._boostEditorWidth = this.editorWindow.outerWidth; - this.editorWindow.requestAnimationFrame(() => { this.editorWindow.resizeTo( this._codeEditorWidth, @@ -413,7 +407,11 @@ export class nsZenBoostEditor { } windowElem.setAttribute("editor", "boost"); - this.editorWindow.requestAnimationFrame(() => { + this.doc.getElementById("zen-boost-editor-root").style.display = "flex"; + this.doc.getElementById("zen-boost-code-editor-root").style.display = + "none"; + + this.editorWindow.promiseDocumentFlushed(() => { this.editorWindow.resizeTo( this._boostEditorWidth, this.editorWindow.outerHeight @@ -424,10 +422,6 @@ export class nsZenBoostEditor { this.editorWindow.screenY ); } - - this.doc.getElementById("zen-boost-editor-root").style.display = "flex"; - this.doc.getElementById("zen-boost-code-editor-root").style.display = - "none"; }); // Disable picker mode diff --git a/src/zen/boosts/zen-boosts.css b/src/zen/boosts/zen-boosts.css index ba3e57b3c..4537642f2 100644 --- a/src/zen/boosts/zen-boosts.css +++ b/src/zen/boosts/zen-boosts.css @@ -139,6 +139,7 @@ body { user-select: none; width: 100%; + min-height: 582px; height: 100%; padding: 0; margin: 0; diff --git a/src/zen/spaces/zen-workspaces.css b/src/zen/spaces/zen-workspaces.css index f2260e55e..0007b69da 100644 --- a/src/zen/spaces/zen-workspaces.css +++ b/src/zen/spaces/zen-workspaces.css @@ -322,7 +322,6 @@ zen-workspace { height: 100%; overflow: hidden; color: var(--toolbox-textcolor); - will-change: transform; @media not (prefers-reduced-motion: reduce) { transition: padding-top 0.1s; diff --git a/src/zen/tabs/zen-tabs/vertical-tabs.css b/src/zen/tabs/zen-tabs/vertical-tabs.css index 8dfbcbd58..c24e2cc71 100644 --- a/src/zen/tabs/zen-tabs/vertical-tabs.css +++ b/src/zen/tabs/zen-tabs/vertical-tabs.css @@ -1131,7 +1131,6 @@ transition: max-height 0.3s ease-out, grid-template-columns 0.3s ease-out; - will-change: transform; opacity: 1; --min-essentials-width-wrap: calc(var(--tab-min-height) + 4px); grid-template-columns: repeat(auto-fit, minmax(max(23.7%, var(--min-essentials-width-wrap)), 1fr)); diff --git a/src/zen/tests/mochitests/sandbox/browser.toml b/src/zen/tests/mochitests/sandbox/browser.toml index 8e5eb4723..f12d053b9 100644 --- a/src/zen/tests/mochitests/sandbox/browser.toml +++ b/src/zen/tests/mochitests/sandbox/browser.toml @@ -25,11 +25,11 @@ tags = "os_integration" ["browser_content_sandbox_fs.js"] skip-if = [ - "os == 'win' && os_version == '11.26100' && arch == 'x86' && debug", # bug 1379635 - "os == 'win' && os_version == '11.26200' && arch == 'x86' && debug", # bug 1379635 - "os == 'win' && os_version == '11.26100' && arch == 'x86_64' && debug", # bug 1379635 - "os == 'win' && os_version == '11.26200' && arch == 'x86_64' && debug", # bug 1379635 "os == 'mac' && os_version == '15.30' && arch == 'aarch64'", # Bug 2023967 + "os == 'win' && os_version == '11.26100' && arch == 'x86' && debug", # bug 1379635 + "os == 'win' && os_version == '11.26100' && arch == 'x86_64' && debug", # bug 1379635 + "os == 'win' && os_version == '11.26200' && arch == 'x86' && debug", # bug 1379635 + "os == 'win' && os_version == '11.26200' && arch == 'x86_64' && debug", # bug 1379635 ] ["browser_content_sandbox_syscalls.js"] @@ -38,7 +38,6 @@ skip-if = [ skip-if = [ "os == 'win' && os_version == '11.26200' && arch == 'x86' && debug", # bug 2028636 "os == 'win' && os_version == '11.26200' && arch == 'x86_64' && debug", # bug 2028636 - "os == 'linux' && debug && artifact", # bug 1945658 ] run-if = [ "debug", From b1be664f4d82e72bf34da8a0b4ed95e2b81f74ca Mon Sep 17 00:00:00 2001 From: Joe Goldin Date: Thu, 4 Jun 2026 04:12:02 -0700 Subject: [PATCH 17/53] gh-13027: fix window sync lockup that left synced tabs blank in new windows (gh-14012) --- src/zen/sessionstore/ZenWindowSync.sys.mjs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index 12993d74b..1c1ba7b22 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -521,10 +521,13 @@ class nsZenWindowSync { if (flags & SYNC_FLAG_ICON) { aTargetItem.zenStaticIcon = aOriginalItem.zenStaticIcon; if (gBrowser.isTab(aOriginalItem)) { - gBrowser.setIcon( - aTargetItem, - aOriginalItem.getAttribute("image") || gBrowser.getIcon(aOriginalItem) - ); + try { + gBrowser.setIcon( + aTargetItem, + aOriginalItem.getAttribute("image") || + gBrowser.getIcon(aOriginalItem) + ); + } catch {} } else if (aOriginalItem.isZenFolder) { // Icons are a zen-only feature for tab groups. gZenFolders.setFolderUserIcon(aTargetItem, aOriginalItem.iconURL); @@ -1542,6 +1545,7 @@ class nsZenWindowSync { console.error(`Error moving active tabs to other windows on close:`, e); } resolve(); + this.#docShellSwitchPromise = null; } on_WindowCloseAndBrowserFlushed(aBrowsers) { From 880d61df16db2bc039636025482e07a9c42767d0 Mon Sep 17 00:00:00 2001 From: fen4flo <75260616+FlorianButz@users.noreply.github.com> Date: Sat, 6 Jun 2026 14:11:42 +0200 Subject: [PATCH 18/53] gh-14044: Implement Space Routing (gh-13981) Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com> --- crowdin.yml | 2 + .../browser/browser/zen-space-routing.ftl | 25 + .../base/content/zen-assets.jar.inc.mn | 1 + .../base/content/zen-commands.inc.xhtml | 1 + .../base/content/zen-locales.inc.xhtml | 1 + .../base/content/zen-panels/popups.inc | 1 + .../tabbrowser/content/tabbrowser-js.patch | 163 ++++--- src/browser/themes/shared/zen-icons/icons.css | 14 +- .../themes/shared/zen-icons/jar.inc.mn | 3 + .../nucleo/arrow-corner-down-right.svg | 5 + src/zen/common/ZenPreloadedScripts.js | 5 + src/zen/common/styles/zen-panels/dialog.css | 4 +- src/zen/common/zen-sets.js | 4 + src/zen/glance/ZenGlanceManager.mjs | 1 + src/zen/moz.build | 1 + .../space-routing/ZenSpaceRoutingDialog.mjs | 454 ++++++++++++++++++ .../ZenSpaceRoutingManager.sys.mjs | 413 ++++++++++++++++ src/zen/space-routing/jar.inc.mn | 9 + src/zen/space-routing/moz.build | 8 + src/zen/space-routing/zen-space-routing.css | 315 ++++++++++++ .../space-routing/zen-space-routing.inc.xhtml | 108 +++++ src/zen/split-view/ZenViewSplitter.mjs | 5 +- src/zen/tests/moz.build | 1 + src/zen/tests/space_routing/browser.toml | 18 + .../browser_space_routing_crud.js | 115 +++++ .../browser_space_routing_dialog.js | 251 ++++++++++ .../browser_space_routing_on_add_tab.js | 363 ++++++++++++++ .../browser_space_routing_route_matching.js | 108 +++++ .../browser_space_routing_route_uri.js | 66 +++ src/zen/tests/space_routing/head.js | 80 +++ src/zen/urlbar/ZenUBGlobalActions.sys.mjs | 5 + src/zen/zen.globals.mjs | 2 + 32 files changed, 2475 insertions(+), 77 deletions(-) create mode 100644 locales/en-US/browser/browser/zen-space-routing.ftl create mode 100644 src/browser/themes/shared/zen-icons/nucleo/arrow-corner-down-right.svg create mode 100644 src/zen/space-routing/ZenSpaceRoutingDialog.mjs create mode 100644 src/zen/space-routing/ZenSpaceRoutingManager.sys.mjs create mode 100644 src/zen/space-routing/jar.inc.mn create mode 100644 src/zen/space-routing/moz.build create mode 100644 src/zen/space-routing/zen-space-routing.css create mode 100644 src/zen/space-routing/zen-space-routing.inc.xhtml create mode 100644 src/zen/tests/space_routing/browser.toml create mode 100644 src/zen/tests/space_routing/browser_space_routing_crud.js create mode 100644 src/zen/tests/space_routing/browser_space_routing_dialog.js create mode 100644 src/zen/tests/space_routing/browser_space_routing_on_add_tab.js create mode 100644 src/zen/tests/space_routing/browser_space_routing_route_matching.js create mode 100644 src/zen/tests/space_routing/browser_space_routing_route_uri.js create mode 100644 src/zen/tests/space_routing/head.js diff --git a/crowdin.yml b/crowdin.yml index 469c8a1cd..964f52d44 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -22,3 +22,5 @@ files: translation: browser/browser/zen-folders.ftl - source: en-US/browser/browser/zen-boosts.ftl translation: browser/browser/zen-boosts.ftl + - source: en-US/browser/browser/zen-space-routing.ftl + translation: browser/browser/zen-space-routing.ftl diff --git a/locales/en-US/browser/browser/zen-space-routing.ftl b/locales/en-US/browser/browser/zen-space-routing.ftl new file mode 100644 index 000000000..b8f063fb7 --- /dev/null +++ b/locales/en-US/browser/browser/zen-space-routing.ftl @@ -0,0 +1,25 @@ +# 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-space-routing-settings = + .label = Space Routing Settings +zen-space-routing-rulepanel-placeholder = Routes let you choose where specific sites open inside Zen. For example, you can route YouTube links to always open inside your Personal space. +zen-space-routing-dialog-title = Space Routing Settings +zen-space-routing-external-default = Default route for external links +zen-space-routing-new-route = New Route +zen-space-routing-open-in-space = Open in Space +zen-space-routing-most-recent-space = Most recent Space +zen-space-routing-close-button = + .aria-label = Close + .tooltiptext = Close + +zen-space-routing-contains = + .label = Contains +zen-space-routing-equal-to = + .label = Is Equal To +zen-space-routing-regex = + .label = RegEx + +zen-space-routing-open-in = Open In +zen-space-routing-url = URL \ No newline at end of file diff --git a/src/browser/base/content/zen-assets.jar.inc.mn b/src/browser/base/content/zen-assets.jar.inc.mn index dac39decc..07d61a897 100644 --- a/src/browser/base/content/zen-assets.jar.inc.mn +++ b/src/browser/base/content/zen-assets.jar.inc.mn @@ -20,3 +20,4 @@ #include ../../../zen/fonts/jar.inc.mn #include ../../../zen/boosts/jar.inc.mn #include ../../../zen/live-folders/jar.inc.mn +#include ../../../zen/space-routing/jar.inc.mn diff --git a/src/browser/base/content/zen-commands.inc.xhtml b/src/browser/base/content/zen-commands.inc.xhtml index 4f9e0fca9..2122c56f0 100644 --- a/src/browser/base/content/zen-commands.inc.xhtml +++ b/src/browser/base/content/zen-commands.inc.xhtml @@ -41,6 +41,7 @@ + diff --git a/src/browser/base/content/zen-locales.inc.xhtml b/src/browser/base/content/zen-locales.inc.xhtml index ed52e6fca..2b0ec6692 100644 --- a/src/browser/base/content/zen-locales.inc.xhtml +++ b/src/browser/base/content/zen-locales.inc.xhtml @@ -11,4 +11,5 @@ + diff --git a/src/browser/base/content/zen-panels/popups.inc b/src/browser/base/content/zen-panels/popups.inc index c888a26c2..bc9abc04e 100644 --- a/src/browser/base/content/zen-panels/popups.inc +++ b/src/browser/base/content/zen-panels/popups.inc @@ -30,6 +30,7 @@ + diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch index 95bf58cee..ef96e759e 100644 --- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch +++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js -index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88f4eec282 100644 +index 43fb79a3060e20f671ae6ffc26350c7abf497702..2da16e06541438ece4a3ae3a1663a1559780fe22 100644 --- a/browser/components/tabbrowser/content/tabbrowser.js +++ b/browser/components/tabbrowser/content/tabbrowser.js @@ -502,6 +502,7 @@ @@ -264,23 +264,32 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 focusUrlBar: true, }); resolve(this.selectedBrowser); -@@ -3285,6 +3369,9 @@ +@@ -3285,6 +3369,10 @@ schemelessInput, hasValidUserGestureActivation = false, textDirectiveUserActivation = false, + _forZenEmptyTab, + essential, + zenWorkspaceId, ++ skipRoute = false, } = {} ) { // all callers of addTab that pass a params object need to pass -@@ -3295,10 +3382,17 @@ +@@ -3295,10 +3383,25 @@ ); } ++ const beforeRouteResult = window.gZenSpaceRoutingManager.onBeforeAddTab(uriString, { skipRoute, pinned, tabGroup, fromExternal }, window); ++ if (beforeRouteResult.shouldEarlyExit) { ++ return null; ++ } ++ + let hasZenDefaultUserContextId = false; + let zenForcedWorkspaceId = undefined; -+ if (typeof gZenWorkspaces !== "undefined" && !_forZenEmptyTab) { ++ if (beforeRouteResult.userContextId) { ++ userContextId = beforeRouteResult.userContextId; ++ hasZenDefaultUserContextId = true; ++ } else if (typeof gZenWorkspaces !== "undefined" && !_forZenEmptyTab) { + [userContextId, hasZenDefaultUserContextId, zenForcedWorkspaceId] = gZenWorkspaces.getContextIdIfNeeded(userContextId, fromExternal, triggeringPrincipal); + } + @@ -292,7 +301,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 // If we're opening a foreground tab, set the owner by default. ownerTab ??= inBackground ? null : this.selectedTab; -@@ -3306,6 +3400,7 @@ +@@ -3306,6 +3409,7 @@ if (this.selectedTab.owner) { this.selectedTab.owner = null; } @@ -300,7 +309,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 // Find the tab that opened this one, if any. This is used for // determining positioning, and inherited attributes such as the -@@ -3358,6 +3453,22 @@ +@@ -3358,6 +3462,22 @@ noInitialLabel, skipBackgroundNotify, }); @@ -323,7 +332,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 if (insertTab) { // Insert the tab into the tab container in the correct position. this.#insertTabAtIndex(t, { -@@ -3366,6 +3477,7 @@ +@@ -3366,6 +3486,7 @@ ownerTab, openerTab, pinned, @@ -331,7 +340,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 bulkOrderedOpen, tabGroup: tabGroup ?? openerTab?.group, }); -@@ -3384,6 +3496,7 @@ +@@ -3384,6 +3505,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -339,7 +348,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 })); if (focusUrlBar) { -@@ -3508,6 +3621,12 @@ +@@ -3508,6 +3630,12 @@ } } @@ -352,7 +361,17 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 // Additionally send pinned tab events if (pinned) { this.#notifyPinnedStatus(t); -@@ -3750,6 +3869,7 @@ +@@ -3518,6 +3646,9 @@ + if (!inBackground) { + this.selectedTab = t; + } ++ ++ window.gZenSpaceRoutingManager.onAfterAddTab(uriString, t, { skipRoute: skipRoute || _forZenEmptyTab, fromExternal, pinned, tabGroup }, window, beforeRouteResult); ++ + return t; + } + +@@ -3750,6 +3881,7 @@ isAdoptingGroup = false, isUserTriggered = false, telemetryUserCreateSource = "unknown", @@ -360,7 +379,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 } = {} ) { if ( -@@ -3760,9 +3880,6 @@ +@@ -3760,9 +3892,6 @@ !this.isSplitViewWrapper(tabOrSplitView) ) ) { @@ -370,7 +389,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 } if (!color) { -@@ -3783,9 +3900,14 @@ +@@ -3783,9 +3912,14 @@ label, isAdoptingGroup ); @@ -387,7 +406,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 ); group.addTabs(tabsAndSplitViews); -@@ -3906,7 +4028,7 @@ +@@ -3906,7 +4040,7 @@ } this.#handleTabMove(tab, () => @@ -396,7 +415,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 ); } -@@ -3990,6 +4112,7 @@ +@@ -3990,6 +4124,7 @@ color: group.color, insertBefore: newTabs[0], isAdoptingGroup: true, @@ -404,7 +423,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 }); } -@@ -4200,6 +4323,7 @@ +@@ -4200,6 +4335,7 @@ openWindowInfo, skipLoad, triggeringRemoteType, @@ -412,7 +431,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 } ) { // If we don't have a preferred remote type (or it is `NOT_REMOTE`), and -@@ -4269,6 +4393,7 @@ +@@ -4269,6 +4405,7 @@ openWindowInfo, name, skipLoad, @@ -420,7 +439,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 }); } -@@ -4482,9 +4607,9 @@ +@@ -4482,9 +4619,9 @@ } // Add a new tab if needed. @@ -432,7 +451,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 let url = "about:blank"; if (tabData.entries?.length) { -@@ -4521,8 +4646,10 @@ +@@ -4521,8 +4658,10 @@ insertTab: false, skipLoad: true, preferredRemoteType, @@ -444,7 +463,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 if (select) { tabToSelect = tab; } -@@ -4544,7 +4671,8 @@ +@@ -4544,7 +4683,8 @@ this.pinTab(tab); // Then ensure all the tab open/pinning information is sent. this._fireTabOpen(tab, {}); @@ -454,7 +473,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 let { groupId } = tabData; const tabGroup = tabGroupWorkingData.get(groupId); // if a tab refers to a tab group we don't know, skip any group -@@ -4564,7 +4692,10 @@ +@@ -4564,7 +4704,10 @@ tabGroup.stateData.id, tabGroup.stateData.color, tabGroup.stateData.collapsed, @@ -466,7 +485,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 ); tabsFragment.appendChild(tabGroup.node); } -@@ -4619,9 +4750,21 @@ +@@ -4619,9 +4762,21 @@ // to remove the old selected tab. if (tabToSelect) { let leftoverTab = this.selectedTab; @@ -488,7 +507,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 if (tabs.length > 1 || !tabs[0].selected) { this._updateTabsAfterInsert(); -@@ -4812,11 +4955,14 @@ +@@ -4812,11 +4967,14 @@ if (ownerTab) { tab.owner = ownerTab; } @@ -504,7 +523,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 if ( !bulkOrderedOpen && ((openerTab && -@@ -4828,7 +4974,7 @@ +@@ -4828,7 +4986,7 @@ let lastRelatedTab = openerTab && this._lastRelatedTabMap.get(openerTab); let previousTab = lastRelatedTab || openerTab || this.selectedTab; @@ -513,7 +532,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 tabGroup = previousTab.group; } if ( -@@ -4844,7 +4990,7 @@ +@@ -4844,7 +5002,7 @@ previousTab.splitview ) + 1; } else if (previousTab.visible) { @@ -522,7 +541,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 } else if (previousTab == FirefoxViewHandler.tab) { elementIndex = 0; } -@@ -4872,14 +5018,14 @@ +@@ -4872,14 +5030,14 @@ } // Ensure index is within bounds. if (tab.pinned) { @@ -541,7 +560,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 if (pinned && !itemAfter?.pinned) { itemAfter = null; -@@ -4896,7 +5042,7 @@ +@@ -4896,7 +5054,7 @@ this.tabContainer._invalidateCachedTabs(); @@ -550,7 +569,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 if ( (this.isTab(itemAfter) && itemAfter.group == tabGroup) || this.isSplitViewWrapper(itemAfter) -@@ -4927,7 +5073,11 @@ +@@ -4927,7 +5085,11 @@ const tabContainer = pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; @@ -562,7 +581,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 } if (tab.group?.collapsed) { -@@ -4942,6 +5092,7 @@ +@@ -4942,6 +5104,7 @@ if (pinned) { this._updateTabBarForPinnedTabs(); } @@ -570,7 +589,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 TabBarVisibility.update(); } -@@ -5490,6 +5641,7 @@ +@@ -5490,6 +5653,7 @@ telemetrySource, } = {} ) { @@ -578,7 +597,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 // When 'closeWindowWithLastTab' pref is enabled, closing all tabs // can be considered equivalent to closing the window. if ( -@@ -5579,6 +5731,7 @@ +@@ -5579,6 +5743,7 @@ if (lastToClose) { this.removeTab(lastToClose, aParams); } @@ -586,7 +605,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 } catch (e) { console.error(e); } -@@ -5624,6 +5777,14 @@ +@@ -5624,6 +5789,14 @@ return; } @@ -601,7 +620,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 let isVisibleTab = aTab.visible; // We have to sample the tab width now, since _beginRemoveTab might // end up modifying the DOM in such a way that aTab gets a new -@@ -5631,6 +5792,9 @@ +@@ -5631,6 +5804,9 @@ // state). let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width; let isLastTab = this.#isLastTabInWindow(aTab); @@ -611,7 +630,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 if ( !this._beginRemoveTab(aTab, { closeWindowFastpath: true, -@@ -5642,13 +5806,14 @@ +@@ -5642,13 +5818,14 @@ telemetrySource, }) ) { @@ -627,7 +646,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 let lockTabSizing = !this.tabContainer.verticalMode && !aTab.pinned && -@@ -5679,7 +5844,13 @@ +@@ -5679,7 +5856,13 @@ // We're not animating, so we can cancel the animation stopwatch. Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId); aTab._closeTimeAnimTimerId = null; @@ -642,7 +661,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 return; } -@@ -5813,7 +5984,7 @@ +@@ -5813,7 +5996,7 @@ closeWindowWithLastTab != null ? closeWindowWithLastTab : !window.toolbar.visible || @@ -651,7 +670,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 if (closeWindow) { // We've already called beforeunload on all the relevant tabs if we get here, -@@ -5837,6 +6008,7 @@ +@@ -5837,6 +6020,7 @@ newTab = true; } @@ -659,7 +678,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 aTab._endRemoveArgs = [closeWindow, newTab]; // swapBrowsersAndCloseOther will take care of closing the window without animation. -@@ -5877,13 +6049,7 @@ +@@ -5877,13 +6061,7 @@ aTab._mouseleave(); if (newTab) { @@ -674,7 +693,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 } else { TabBarVisibility.update(); } -@@ -6016,6 +6182,7 @@ +@@ -6016,6 +6194,7 @@ this.tabs[i]._tPos = i; } @@ -682,7 +701,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 if (!this._windowIsClosing) { // update tab close buttons state this.tabContainer._updateCloseButtons(); -@@ -6201,6 +6368,7 @@ +@@ -6201,6 +6380,7 @@ memory_after: await getTotalMemoryUsage(), time_to_unload_in_ms: timeElapsed, }); @@ -690,7 +709,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 } /** -@@ -6246,6 +6414,7 @@ +@@ -6246,6 +6426,7 @@ } let excludeTabs = new Set(aExcludeTabs); @@ -698,7 +717,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 // If this tab has a successor, it should be selectable, since // hiding or closing a tab removes that tab as a successor. -@@ -6258,15 +6427,22 @@ +@@ -6258,15 +6439,22 @@ !excludeTabs.has(aTab.owner) && Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose") ) { @@ -723,7 +742,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 let tab = this.tabContainer.findNextTab(aTab, { direction: 1, filter: _tab => remainingTabs.includes(_tab), -@@ -6280,7 +6456,7 @@ +@@ -6280,7 +6468,7 @@ } if (tab) { @@ -732,7 +751,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 } // If no qualifying visible tab was found, see if there is a tab in -@@ -6301,7 +6477,7 @@ +@@ -6301,7 +6489,7 @@ }); } @@ -741,7 +760,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 } _blurTab(aTab) { -@@ -6312,7 +6488,7 @@ +@@ -6312,7 +6500,7 @@ * @returns {boolean} * False if swapping isn't permitted, true otherwise. */ @@ -750,7 +769,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 // Do not allow transfering a private tab to a non-private window // and vice versa. if ( -@@ -6366,6 +6542,7 @@ +@@ -6366,6 +6554,7 @@ // fire the beforeunload event in the process. Close the other // window if this was its last tab. if ( @@ -758,7 +777,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 !remoteBrowser._beginRemoveTab(aOtherTab, { adoptedByTab: aOurTab, closeWindowWithLastTab: true, -@@ -6377,7 +6554,7 @@ +@@ -6377,7 +6566,7 @@ // If this is the last tab of the window, hide the window // immediately without animation before the docshell swap, to avoid // about:blank being painted. @@ -767,7 +786,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 if (closeWindow) { let win = aOtherTab.ownerGlobal; win.windowUtils.suppressAnimation(true); -@@ -6511,11 +6688,13 @@ +@@ -6511,11 +6700,13 @@ } // Finish tearing down the tab that's going away. @@ -781,7 +800,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 this.setTabTitle(aOurTab); -@@ -6717,10 +6896,10 @@ +@@ -6717,10 +6908,10 @@ SessionStore.deleteCustomTabValue(aTab, "hiddenBy"); } @@ -794,7 +813,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 aTab.selected || aTab.closing || // Tabs that are sharing the screen, microphone or camera cannot be hidden. -@@ -6780,7 +6959,8 @@ +@@ -6780,7 +6971,8 @@ * @param {object} [aOptions={}] * Key-value pairs that will be serialized into the features string. */ @@ -804,7 +823,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 if (this.tabs.length == 1) { return null; } -@@ -6797,7 +6977,7 @@ +@@ -6797,7 +6989,7 @@ // tell a new window to take the "dropped" tab let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); args.appendElement(aTab.splitview ?? aTab); @@ -813,7 +832,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 private: PrivateBrowsingUtils.isWindowPrivate(window), features: Object.entries(aOptions) .map(([key, value]) => `${key}=${value}`) -@@ -6805,6 +6985,8 @@ +@@ -6805,6 +6997,8 @@ openerWindow: window, args, }); @@ -822,7 +841,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 } /** -@@ -6917,7 +7099,7 @@ +@@ -6917,7 +7111,7 @@ * `true` if element is a `` */ isTabGroup(element) { @@ -831,7 +850,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 } /** -@@ -7002,8 +7184,8 @@ +@@ -7002,8 +7196,8 @@ } // Don't allow mixing pinned and unpinned tabs. @@ -842,7 +861,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 } else { tabIndex = Math.max(tabIndex, this.pinnedTabCount); } -@@ -7049,8 +7231,8 @@ +@@ -7049,8 +7243,8 @@ this.#handleTabMove( element, () => { @@ -853,7 +872,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 neighbor = neighbor.group; } if (neighbor?.splitview) { -@@ -7061,6 +7243,12 @@ +@@ -7061,6 +7255,12 @@ return; } } @@ -866,7 +885,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 if (movingForwards && neighbor) { neighbor.after(element); -@@ -7119,23 +7307,31 @@ +@@ -7119,23 +7319,31 @@ #moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) { if (this.isTabGroupLabel(targetElement)) { targetElement = targetElement.group; @@ -904,7 +923,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 } else if (!element.pinned && targetElement && targetElement.pinned) { // If the caller asks to move an unpinned element next to a pinned // tab, move the unpinned element to be the first unpinned element -@@ -7148,12 +7344,35 @@ +@@ -7148,12 +7356,35 @@ // move the tab group right before the first unpinned tab. // 4. Moving a tab group and the first unpinned tab is grouped: // move the tab group right before the first unpinned tab's tab group. @@ -941,7 +960,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 // We want to include the splitview wrapper if it's the targetElement, but // not in the case where we want to reverse tabs within the same splitview. -@@ -7162,6 +7381,7 @@ +@@ -7162,6 +7393,7 @@ } let getContainer = () => @@ -949,7 +968,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 element.pinned ? this.tabContainer.pinnedTabsContainer : this.tabContainer; -@@ -7170,11 +7390,15 @@ +@@ -7170,11 +7402,15 @@ element, () => { if (moveBefore) { @@ -966,7 +985,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 } }, metricsContext -@@ -7248,11 +7472,15 @@ +@@ -7248,11 +7484,15 @@ * @param {TabMetricsContext} [metricsContext] */ moveTabToExistingGroup(aTab, aGroup, metricsContext) { @@ -985,7 +1004,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 } if (aTab.group && aTab.group.id === aGroup.id) { return; -@@ -7324,6 +7552,7 @@ +@@ -7324,6 +7564,7 @@ let state = { tabIndex: tab._tPos, @@ -993,7 +1012,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 }; if (tab.visible) { state.elementIndex = tab.elementIndex; -@@ -7355,7 +7584,7 @@ +@@ -7355,7 +7596,7 @@ let changedSplitView = previousTabState.splitViewId != currentTabState.splitViewId; @@ -1002,7 +1021,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 tab.dispatchEvent( new CustomEvent("TabMove", { bubbles: true, -@@ -7402,6 +7631,10 @@ +@@ -7402,6 +7643,10 @@ moveActionCallback(); @@ -1013,7 +1032,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 // Clear tabs cache after moving nodes because the order of tabs may have // changed. this.tabContainer._invalidateCachedTabs(); -@@ -7452,7 +7685,22 @@ +@@ -7452,7 +7697,22 @@ * @returns {object} * The new tab in the current window, null if the tab couldn't be adopted. */ @@ -1037,7 +1056,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 // Swap the dropped tab with a new one we create and then close // it in the other window (making it seem to have moved between // windows). We also ensure that the tab we create to swap into has -@@ -7495,6 +7743,8 @@ +@@ -7495,6 +7755,8 @@ } params.skipLoad = true; let newTab = this.addWebTab("about:blank", params); @@ -1046,7 +1065,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 aTab.container.tabDragAndDrop.finishAnimateTabMove(); -@@ -8205,7 +8455,7 @@ +@@ -8205,7 +8467,7 @@ // preventDefault(). It will still raise the window if appropriate. return; } @@ -1055,7 +1074,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 window.focus(); aEvent.preventDefault(); } -@@ -8222,7 +8472,6 @@ +@@ -8222,7 +8484,6 @@ on_TabGroupCollapse(aEvent) { aEvent.target.tabs.forEach(tab => { @@ -1063,7 +1082,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 }); } -@@ -8556,7 +8805,9 @@ +@@ -8556,7 +8817,9 @@ let filter = this._tabFilters.get(tab); if (filter) { @@ -1073,7 +1092,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 let listener = this._tabListeners.get(tab); if (listener) { -@@ -9359,6 +9610,7 @@ +@@ -9359,6 +9622,7 @@ aWebProgress.isTopLevel ) { this.mTab.setAttribute("busy", "true"); @@ -1081,7 +1100,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 gBrowser._tabAttrModified(this.mTab, ["busy"]); this.mTab._notselectedsinceload = !this.mTab.selected; } -@@ -9439,6 +9691,7 @@ +@@ -9439,6 +9703,7 @@ // known defaults. Note we use the original URL since about:newtab // redirects to a prerendered page. const shouldRemoveFavicon = @@ -1089,7 +1108,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 !this.mBrowser.mIconURL && !ignoreBlank && !(originalLocation.spec in FAVICON_DEFAULTS); -@@ -9613,13 +9866,6 @@ +@@ -9613,13 +9878,6 @@ this.mBrowser.originalURI = aRequest.originalURI; } @@ -1103,7 +1122,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..146b1559b8430773bd4ec173a8f4fe88 } let userContextId = this.mBrowser.getAttribute("usercontextid") || 0; -@@ -10507,7 +10753,8 @@ var TabContextMenu = { +@@ -10507,7 +10765,8 @@ var TabContextMenu = { ); contextUnpinSelectedTabs.hidden = !this.contextTab.pinned || !this.multiselected; diff --git a/src/browser/themes/shared/zen-icons/icons.css b/src/browser/themes/shared/zen-icons/icons.css index 918a36ec1..513683d2c 100644 --- a/src/browser/themes/shared/zen-icons/icons.css +++ b/src/browser/themes/shared/zen-icons/icons.css @@ -51,7 +51,8 @@ } #PanelUI-zen-gradient-generator-color-remove, -#zen-gradient-generator-color-remove { +#zen-gradient-generator-color-remove, +.sr-remove { list-style-image: url("unpin.svg") !important; } @@ -134,6 +135,10 @@ list-style-image: url("arrow-right.svg"); } +.sr-open-in-icon { + list-style-image: url("arrow-corner-down-right.svg"); +} + #PanelUI-menu-button, #appMenu-more-button2, .zen-workspaces-actions, @@ -1006,7 +1011,8 @@ } } -#zen-copy-url-button image { +#zen-copy-url-button image, +.sr-url-icon { list-style-image: url("link.svg"); fill-opacity: 0.65; } @@ -1090,3 +1096,7 @@ #zen-boost-load { list-style-image: url("open.svg"); } + +.sr-airplane { + list-style-image: url("selectable/airplane.svg"); +} \ No newline at end of file diff --git a/src/browser/themes/shared/zen-icons/jar.inc.mn b/src/browser/themes/shared/zen-icons/jar.inc.mn index b252b3f52..91c0cc49d 100644 --- a/src/browser/themes/shared/zen-icons/jar.inc.mn +++ b/src/browser/themes/shared/zen-icons/jar.inc.mn @@ -4,6 +4,7 @@ #ifdef XP_WIN * skin/classic/browser/zen-icons/algorithm.svg (../shared/zen-icons/nucleo/algorithm.svg) +* skin/classic/browser/zen-icons/arrow-corner-down-right.svg (../shared/zen-icons/nucleo/arrow-corner-down-right.svg) * skin/classic/browser/zen-icons/arrow-down.svg (../shared/zen-icons/nucleo/arrow-down.svg) * skin/classic/browser/zen-icons/arrow-left.svg (../shared/zen-icons/nucleo/arrow-left.svg) * skin/classic/browser/zen-icons/arrow-right.svg (../shared/zen-icons/nucleo/arrow-right.svg) @@ -153,6 +154,7 @@ #endif #ifdef XP_MACOSX * skin/classic/browser/zen-icons/algorithm.svg (../shared/zen-icons/nucleo/algorithm.svg) +* skin/classic/browser/zen-icons/arrow-corner-down-right.svg (../shared/zen-icons/nucleo/arrow-corner-down-right.svg) * skin/classic/browser/zen-icons/arrow-down.svg (../shared/zen-icons/nucleo/arrow-down.svg) * skin/classic/browser/zen-icons/arrow-left.svg (../shared/zen-icons/nucleo/arrow-left.svg) * skin/classic/browser/zen-icons/arrow-right.svg (../shared/zen-icons/nucleo/arrow-right.svg) @@ -302,6 +304,7 @@ #endif #ifdef XP_LINUX * skin/classic/browser/zen-icons/algorithm.svg (../shared/zen-icons/nucleo/algorithm.svg) +* skin/classic/browser/zen-icons/arrow-corner-down-right.svg (../shared/zen-icons/nucleo/arrow-corner-down-right.svg) * skin/classic/browser/zen-icons/arrow-down.svg (../shared/zen-icons/nucleo/arrow-down.svg) * skin/classic/browser/zen-icons/arrow-left.svg (../shared/zen-icons/nucleo/arrow-left.svg) * skin/classic/browser/zen-icons/arrow-right.svg (../shared/zen-icons/nucleo/arrow-right.svg) diff --git a/src/browser/themes/shared/zen-icons/nucleo/arrow-corner-down-right.svg b/src/browser/themes/shared/zen-icons/nucleo/arrow-corner-down-right.svg new file mode 100644 index 000000000..18b9d18df --- /dev/null +++ b/src/browser/themes/shared/zen-icons/nucleo/arrow-corner-down-right.svg @@ -0,0 +1,5 @@ +#filter dumbComments emptyLines substitution +# 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/. + \ No newline at end of file diff --git a/src/zen/common/ZenPreloadedScripts.js b/src/zen/common/ZenPreloadedScripts.js index 559d4c8e4..e8128603a 100644 --- a/src/zen/common/ZenPreloadedScripts.js +++ b/src/zen/common/ZenPreloadedScripts.js @@ -5,6 +5,11 @@ // prettier-ignore // eslint-disable-next-line no-lone-blocks { + ChromeUtils.defineESModuleGetters(this, { + gZenSpaceRoutingManager: + "resource:///modules/zen/spacerouting/ZenSpaceRoutingManager.sys.mjs", + }); + Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/ZenSpaceBookmarksStorage.js", this); let scripts = [ diff --git a/src/zen/common/styles/zen-panels/dialog.css b/src/zen/common/styles/zen-panels/dialog.css index b5979bb4c..e25f3e8d7 100644 --- a/src/zen/common/styles/zen-panels/dialog.css +++ b/src/zen/common/styles/zen-panels/dialog.css @@ -5,10 +5,10 @@ */ .dialogBox { border-radius: 12px !important; - border: 1px solid light-dark(rgba(168, 168, 169, 0.50), var(--zen-dialog-background)) !important; + border: 0.5px solid light-dark(rgba(0, 0, 0, 0.4), var(--zen-dialog-background)) !important; outline: 1px solid light-dark(transparent, rgba(168, 168, 169, 0.50)) !important; box-shadow: 0 10px 8px rgba(0, 0 , 0, 0.15) !important; - outline-offset: -1.5px; + outline-offset: -2px; @media not (prefers-reduced-motion: reduce) { animation: zen-dialog-fade-in 0.3s ease-out; diff --git a/src/zen/common/zen-sets.js b/src/zen/common/zen-sets.js index 1a1acb728..289dbe9eb 100644 --- a/src/zen/common/zen-sets.js +++ b/src/zen/common/zen-sets.js @@ -133,6 +133,10 @@ document.addEventListener( gZenWorkspaces.unloadAllOtherWorkspaces(); break; } + case "cmd_zenOpenSpaceRoutingSettings": { + gZenSpaceRoutingManager.openSpaceRoutingDialog(window); + break; + } case "cmd_zenNewNavigatorUnsynced": OpenBrowserWindow({ zenSyncedWindow: false }); break; diff --git a/src/zen/glance/ZenGlanceManager.mjs b/src/zen/glance/ZenGlanceManager.mjs index 2eba42111..affc555c2 100644 --- a/src/zen/glance/ZenGlanceManager.mjs +++ b/src/zen/glance/ZenGlanceManager.mjs @@ -214,6 +214,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature { skipAnimation: true, ownerTab: currentTab, triggeringPrincipal: data.triggeringPrincipal, + skipRoute: true, }; } diff --git a/src/zen/moz.build b/src/zen/moz.build index e1efea205..8b1426065 100644 --- a/src/zen/moz.build +++ b/src/zen/moz.build @@ -19,4 +19,5 @@ DIRS += [ "sessionstore", "share", "spaces", + "space-routing", ] diff --git a/src/zen/space-routing/ZenSpaceRoutingDialog.mjs b/src/zen/space-routing/ZenSpaceRoutingDialog.mjs new file mode 100644 index 000000000..3bdb6324f --- /dev/null +++ b/src/zen/space-routing/ZenSpaceRoutingDialog.mjs @@ -0,0 +1,454 @@ +/* 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/. */ + +// eslint-disable-next-line no-shadow +const { gZenSpaceRoutingManager } = ChromeUtils.importESModule( + "resource:///modules/zen/spacerouting/ZenSpaceRoutingManager.sys.mjs" +); + +export class nsZenSpaceRoutingDialog { + doc = null; + editorWindow = null; + openerWindow = null; + + static OBSERVERS = ["zen-space-routing-kill"]; + + /** + * Creates a new Space Routing dialog controller. + * + * @param {Document} doc - The document object for the dialog window. + * @param {Window} editorWindow - The Space Routing dialog window. + * @param {Window} openerWindow - The browser window that opened the dialog. + */ + constructor(doc, editorWindow, openerWindow) { + this.doc = doc; + this.editorWindow = editorWindow; + this.openerWindow = openerWindow; + + this.killOtherShareInstances(); + + nsZenSpaceRoutingDialog.OBSERVERS.forEach(observe => { + Services.obs.addObserver(this, observe); + }); + + this.init(); + this.editorWindow.addEventListener("unload", () => this.uninit(), { + once: true, + }); + } + + /** + * Initializes the boost share instance by setting up event listeners for all UI controls. + */ + init() { + this.editorWindow.addEventListener("unload", () => this.handleClose(), { + once: true, + }); + + this.doc + .getElementById("sr-close") + .addEventListener("click", this.onClosePressed.bind(this)); + this.doc + .getElementById("sr-new-route") + .addEventListener("click", this.onNewRoutePressed.bind(this)); + + const defaultRouteSelect = this.doc.getElementById( + "sr-default-external-open-in" + ); + this.createOpenInList( + defaultRouteSelect, + gZenSpaceRoutingManager.getDefaultExternalRoute() + ); + + defaultRouteSelect.addEventListener("command", e => + this.onRouteDefaultExternalChange(e.target.value) + ); + + this.doc.addEventListener("keydown", event => { + if ( + event.key === "Escape" || + (event.key === "w" && (event.ctrlKey || event.metaKey)) + ) { + this.onClosePressed(); + } + }); + + this.initRouteList(); + this.initialized = true; + } + + /** + * Initializes the routes list and loads all current routes from the disk + */ + initRouteList() { + const allRoutes = gZenSpaceRoutingManager.getAllRoutes(); + allRoutes.forEach(r => this.createRouteElement(r)); + } + + /** + * Will create a new route and update the route list + */ + onNewRoutePressed() { + const newRoute = gZenSpaceRoutingManager.createNewRoute(); + this.createRouteElement(newRoute); + } + + /** + * Will remove a route and update the list + * + * @param {string} routeId - The unique ID of the affected route + * @param {string} containerElement - The container element of the route in the list + */ + onRemoveRoutePressed(routeId, containerElement) { + gZenSpaceRoutingManager.removeRoute(routeId); + containerElement.remove(); + + this.updateShowNoRouteText(); + } + + /** + * Will create the rule element content and inject it into the ui + * + * @param {object} route - The target route + * @returns {Element} The created element for the route + */ + createRouteElement(route) { + const container = this.doc.getElementById("sr-content"); + + const root = this.doc.createXULElement("vbox"); + root.setAttribute("routeId", route.id); + root.className = "sr-rule-container"; + + // ---- Top row + + const topRow = this.doc.createXULElement("hbox"); + topRow.className = "sr-rule-row sr-rule-top"; + + const topLabelContainer = this.doc.createXULElement("hbox"); + topLabelContainer.className = "sr-label-container"; + + const urlIcon = this.doc.createXULElement("image"); + urlIcon.className = "sr-url-icon"; + + const urlLabel = this.doc.createXULElement("label"); + urlLabel.className = "sr-label"; + urlLabel.setAttribute("data-l10n-id", "zen-space-routing-url"); + + topLabelContainer.append(urlIcon, urlLabel); + + // Match type + + const matchTypeMenulist = this.doc.createXULElement("menulist"); + matchTypeMenulist.className = "select match-type-select"; + + const matchTypePopup = this.doc.createXULElement("menupopup"); + matchTypeMenulist.appendChild(matchTypePopup); + + ["contains", "equal-to", "regex"].forEach(id => { + const menuItem = this.doc.createXULElement("menuitem"); + menuItem.setAttribute("data-l10n-id", `zen-space-routing-${id}`); + menuItem.setAttribute("value", id); + matchTypePopup.appendChild(menuItem); + }); + + matchTypeMenulist.value = route.matchType; + + // Input domain + + const input = this.doc.createElement("input"); + input.className = "input"; + input.value = route.reference; + this.updateInputPlaceholder(route.matchType, input); + + const removeButton = this.doc.createXULElement("button"); + removeButton.className = "sr-remove"; + + topRow.append(topLabelContainer, matchTypeMenulist, input, removeButton); + + // ---- Bottom row + + const bottomRow = this.doc.createXULElement("hbox"); + bottomRow.className = "sr-rule-row sr-rule-bottom"; + + const bottomLabelContainer = this.doc.createXULElement("hbox"); + bottomLabelContainer.className = "sr-label-container"; + + const openInIcon = this.doc.createXULElement("image"); + openInIcon.className = "sr-open-in-icon"; + + const openInLabel = this.doc.createXULElement("label"); + openInLabel.className = "sr-label"; + openInLabel.setAttribute("data-l10n-id", "zen-space-routing-open-in"); + + bottomLabelContainer.append(openInIcon, openInLabel); + + // Open in + + const openInMenulist = this.doc.createXULElement("menulist"); + openInMenulist.className = "select open-in-select"; + + const openInMenupopup = this.doc.createXULElement("menupopup"); + openInMenulist.appendChild(openInMenupopup); + + this.createOpenInList(openInMenulist, route.openIn); + + bottomRow.append(bottomLabelContainer, openInMenulist); + + root.append(topRow, bottomRow); + container.appendChild(root); + + removeButton.addEventListener("click", () => { + this.onRemoveRoutePressed(route.id, root); + }); + + input.addEventListener("input", e => + this.onRouteReferenceChange(e.target.value, route.id, input) + ); + matchTypeMenulist.addEventListener("command", e => + this.onRouteMatchTypeChange(e.target.value, route.id, input) + ); + openInMenulist.addEventListener("command", e => + this.onRouteOpenInChange(e.target.value, route.id) + ); + + input.focus(); + + this.updateShowNoRouteText(); + + return root; + } + + /** + * Checks if the text for when no routes are + * created should be displayed + */ + updateShowNoRouteText() { + const container = this.doc.getElementById("sr-content"); + const noRoutesText = this.doc.getElementById("sr-empty-content"); + + // One because of the element itself + noRoutesText.style.display = + container.children.length == 1 ? "flex" : "none"; + } + + /** + * Callback for when the reference text changes + * + * @param {string} value - The new value + * @param {string} routeId - The ID of the affected route + * @param {Element} input - The input element + */ + onRouteReferenceChange(value, routeId, input) { + const route = gZenSpaceRoutingManager.getRoute(routeId); + route.reference = value; + + this.updateInputPlaceholder(route.matchType, input); + + // Don't update the route if the regex is invalid + if (route.matchType == "regex") { + if (!this.onCheckRegexValid(input)) { + return; + } + } + + gZenSpaceRoutingManager.updateRoute(route); + } + + /** + * Callback for when the open in attribute changes + * + * @param {string} value - The new value + * @param {string} routeId - The ID of the affected route + */ + onRouteOpenInChange(value, routeId) { + const route = gZenSpaceRoutingManager.getRoute(routeId); + route.openIn = value; + gZenSpaceRoutingManager.updateRoute(route); + } + + /** + * Callback for when the route match type changes + * + * @param {string} value - The new value + * @param {string} routeId - The ID of the affected route + * @param {Element} input - The text input + */ + onRouteMatchTypeChange(value, routeId, input) { + const route = gZenSpaceRoutingManager.getRoute(routeId); + route.matchType = value; + + this.updateInputPlaceholder(route.matchType, input); + + // Don't update the route if the regex is invalid + if (route.matchType == "regex") { + if (!this.onCheckRegexValid(input)) { + return; + } + } + + gZenSpaceRoutingManager.updateRoute(route); + } + + /** + * Updates the input placeholder based on the + * current route match type + * + * @param {string} matchType - The match type (e.g. "contains", "equal-to", "regex") + * @param {Element} input - The input element + */ + updateInputPlaceholder(matchType, input) { + switch (matchType) { + case "regex": + input.placeholder = "zen-browser\\.app"; + break; + default: + input.placeholder = "zen-browser.app"; + break; + } + } + + /** + * Will validate and return the validity of the + * regex. Applies a tint to the input if an error occurs. + * + * @param {Element} input - The input element for the regex + * @returns {bool} True if regex is valid + */ + onCheckRegexValid(input) { + const reference = input.value; + + // Ignore empty + if (reference.trim() == "") { + input.classList.remove("invalid"); + return true; + } + + try { + new RegExp(reference); + } catch (e) { + input.classList.add("invalid"); + return false; + } + input.classList.remove("invalid"); + return true; + } + + /** + * Callback for when the default external route changes + * + * @param {string} value - The new value + */ + onRouteDefaultExternalChange(value) { + gZenSpaceRoutingManager.setDefaultExternalRoute(value); + } + + /** + * Creates the options list selects + * + * @param {Element} selectElement - The menulist element + * @param {string} value - The initial value + */ + async createOpenInList(selectElement, value) { + const popupElement = + selectElement.querySelector("menupopup") || selectElement; + popupElement.replaceChildren(); // Clear existing + + const [openInSpace, mostRecentSpace] = await this.doc.l10n.formatMessages([ + "zen-space-routing-open-in-space", + "zen-space-routing-most-recent-space", + ]); + + const sectionHeader = this.doc.createXULElement("menuitem"); + sectionHeader.setAttribute("label", openInSpace.value); + sectionHeader.setAttribute("disabled", "true"); + sectionHeader.classList.add("menu-section-header"); + popupElement.appendChild(sectionHeader); + + let availOptions = []; + + let createXulItem = (text, id, iconPath = null) => { + if (text === "sep") { + popupElement.appendChild(this.doc.createXULElement("menuseparator")); + return; + } + + availOptions.push(id || text); + const menuItem = this.doc.createXULElement("menuitem"); + menuItem.setAttribute("label", text); + menuItem.setAttribute("value", id || text); + + if (iconPath) { + if (iconPath.startsWith("chrome://")) { + menuItem.setAttribute("class", "menuitem-iconic"); + menuItem.setAttribute("image", iconPath); + } else { + menuItem.setAttribute("label", `${iconPath} ${text}`); + } + } + + popupElement.appendChild(menuItem); + }; + + const workspaces = this.openerWindow.gZenWorkspaces.getWorkspaces(); + + createXulItem(mostRecentSpace.value, "most-recent-space"); + createXulItem("sep"); + + workspaces.forEach(workspace => { + createXulItem(workspace.name, workspace.uuid, workspace.icon); + }); + + // Check if the workspace still exists, if not use default + if (availOptions.includes(value)) { + selectElement.value = value; + } else { + selectElement.value = "most-recent-space"; + } + } + + /** + * Uninitializes the boost editor by cleaning up event listeners and observers. + */ + uninit() { + nsZenSpaceRoutingDialog.OBSERVERS.forEach(observe => { + Services.obs.removeObserver(this, observe); + }); + } + + /** + * Kills all other Space Routing dialog instances + */ + killOtherShareInstances() { + Services.obs.notifyObservers(null, "zen-space-routing-kill"); + } + + /** + * Observer callback that handles notifications from the observer service. + * Closes the control window when a 'zen-space-routing-kill' notification is received. + * + * @param {object} subject - The subject of the notification. + * @param {string} topic - The topic of the notification. + */ + observe(subject, topic) { + switch (topic) { + case "zen-space-routing-kill": + this.editorWindow.close(); + break; + } + } + + /** + * Callback for when the user presses the close button + */ + onClosePressed() { + this.editorWindow.close(); + } + + /** + * Handles the window close event + */ + handleClose() { + gZenSpaceRoutingManager.saveRoutes(); + } +} diff --git a/src/zen/space-routing/ZenSpaceRoutingManager.sys.mjs b/src/zen/space-routing/ZenSpaceRoutingManager.sys.mjs new file mode 100644 index 000000000..9109e8924 --- /dev/null +++ b/src/zen/space-routing/ZenSpaceRoutingManager.sys.mjs @@ -0,0 +1,413 @@ +/* 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 { JSONFile } from "resource://gre/modules/JSONFile.sys.mjs"; + +class nsZenSpaceRoutingManager { + #file = null; + #saveFilename = "zen-space-routing.jsonlz4"; + + static SKIP_TYPE = { + NONE: "none", + SKIPPED_TAB: "skipped_tab", + RESTORED_TAB: "restored_tab", + }; + + constructor() { + this.#readFromDisk(); + } + + /** + * Callback that will be executed from tabbrowser.js + * This method can be used to stop the tab from being created. + * + * @param {string} uriString - The URI as a string + * @param {object} options - The tab creation options + * @param {Window} win - The window which the tab will be added to + * @returns {object} Returns an object with { shouldEarlyExit, userContextId, isRouteFound, targetRoute } + */ + onBeforeAddTab(uriString, options, win) { + let userContextId = null; + let isRouteFound = false; + let targetRoute = null; + + if ( + this.#shouldSkipProcessing(options, win) != + nsZenSpaceRoutingManager.SKIP_TYPE.NONE + ) { + return { + shouldEarlyExit: false, + userContextId, + isRouteFound, + targetRoute, + }; + } + + targetRoute = this.routeUri(uriString, options); + switch (targetRoute) { + case "most-recent-space": + break; + default: { + const targetWorkspace = + win?.gZenWorkspaces?.getWorkspaceFromId(targetRoute); + + if (targetWorkspace) { + userContextId = targetWorkspace.containerTabId; + isRouteFound = true; + } + } + } + + return { shouldEarlyExit: false, userContextId, isRouteFound, targetRoute }; + } + + /** + * Callback that will be executed from tabbrowser.js + * + * @param {string} uriString - The URI as a string + * @param {Element} newTab - The tab element + * @param {object} options - The tab creation options + * @param {Window} win - The window which the tab was added to + * @param {object} [beforeResult] - The result returned by onBeforeAddTab for + * this tab. When present its precomputed targetRoute is reused instead of + * running routeUri() a second time. + */ + onAfterAddTab(uriString, newTab, options, win, beforeResult) { + const targetRoute = beforeResult?.targetRoute; + if (!targetRoute) { + return; + } + + this.#routeToWorkspace(targetRoute, newTab, win); + } + + /** + * Checks if the tab should be processed or not + * + * @param {object} options - The tab creation options + * @param {Window} win - The owning window + * @returns {SKIP_TYPE} The type of skip or null if not skipped + */ + #shouldSkipProcessing(options, win) { + if (options.skipRoute || options.pinned || options.tabGroup) { + return nsZenSpaceRoutingManager.SKIP_TYPE.SKIPPED_TAB; + } + + // addTab() is being called when the session restores. + // To avoid automatically routing these tabs, + // a check if the restore is already complete is needed + if (!win.gZenStartup.isReady) { + return nsZenSpaceRoutingManager.SKIP_TYPE.RESTORED_TAB; + } + + return nsZenSpaceRoutingManager.SKIP_TYPE.NONE; + } + + /** + * Will route the given tab to a space if a rule applies + * + * @param {string} targetRoute - The precomputed route for the tab + * @param {Element} newTab - The tab element + * @param {Window} win - The window which the tab was added to + * @private + */ + async #routeToWorkspace(targetRoute, newTab, win) { + try { + if (!newTab || !newTab.parentNode) { + return; + } + + switch (targetRoute) { + case "most-recent-space": + break; + + default: { + const workspaces = win?.gZenWorkspaces; + const targetWorkspace = workspaces?.getWorkspaceFromId?.(targetRoute); + + if (targetWorkspace) { + workspaces.moveTabToWorkspace(newTab, targetWorkspace.uuid); + const mostRecentWindow = + Services.wm.getMostRecentWindow("navigator:browser"); + const isOriginatingWindow = win === mostRecentWindow; + if (isOriginatingWindow) { + win.gZenWorkspaces.lastSelectedWorkspaceTabs[ + targetWorkspace.uuid + ] = newTab; + await win.gZenWorkspaces.changeWorkspace(targetWorkspace); + } + } + } + } + } catch (err) { + console.error("[ZenSpaceRouting]: Error moving tab to workspace:", err); + } + } + + /** + * This will give the id of the workspace this uri will + * route to, or "most-recent-space" + * + * @param {string} uriString - The uri which will be routed + * @param {object} options - The tab creation options + * @returns {string} Route instructions + */ + routeUri(uriString, options) { + const isExternal = options.fromExternal; + + // Go over all routes and return the open type for the first match + const allRoutes = this.getAllRoutes(); + for (const route of allRoutes) { + if (this.isRouteMatching(uriString, route)) { + return route.openIn; + } + } + + // If nothing matches and it's an external link, + // use the default external route + if (isExternal) { + return this.getDefaultExternalRoute(); + } + + // If nothing matches, open in most recent space + return "most-recent-space"; + } + + /** + * Checks if a given rule matches a uriString + * + * @param {string} uriString - The uri + * @param {object} route - The route + * @returns {boolean} True if the rule matches + */ + isRouteMatching(uriString, route) { + if (typeof uriString !== "string" || typeof route?.reference !== "string") { + return false; + } + + let reference = route.reference.toLowerCase(); + if (reference.trim() == "") { + return false; + } + + const uri = uriString.toLowerCase(); + switch (route.matchType) { + case "contains": + if (uri.includes(reference)) { + return true; + } + break; + case "equal-to": + if (this.#normalizeURL(uri) == this.#normalizeURL(reference)) { + return true; + } + break; + case "regex": { + let unmodifiedReference = route.reference; + try { + // Use unmodified parameters for the regex test + const regex = new RegExp(unmodifiedReference); + if (regex.test(uriString)) { + return true; + } + } catch (e) { + console.error( + "[ZenSpaceRouting] Failed to resolve regular expression:", + unmodifiedReference, + e + ); + } + break; + } + } + return false; + } + + /** + * Will remove any protocol sequences to normalize the url + * + * @param {string} uriString - The url + * @returns {string} The normalized url + */ + #normalizeURL(uriString) { + if (!uriString) { + return ""; + } + let clean = uriString.trim(); + + // Remove protocol sequences with regex + clean = clean.replace(/^https?:\/\//i, ""); + clean = clean.replace(/^www\./i, ""); + + // If there is a trailing slash, remove + if (clean.endsWith("/")) { + clean = clean.slice(0, -1); + } + + return clean; + } + + /** + * Opens the Space Routing editor in a new popup window. + * + * @param {Window} parentWindow - The parent browser window + * @returns {Window|null} The instanced editor window + */ + openSpaceRoutingDialog(parentWindow) { + const control = parentWindow.openDialog( + "chrome://browser/content/zen-components/windows/zen-space-routing.xhtml", + "", + "centerscreen,modal,dependent,resizable=no,titlebar=no", + { parentWindow } + ); + + control.focus(); + return control; + } + + /** + * @returns {object} Returns a new empty Space Routing route + */ + getEmptyRoute() { + return { + id: crypto.randomUUID(), + reference: "", + openIn: "most-recent-space", + matchType: "contains", + }; + } + + /** + * @returns {Array} A copy of the routes list + */ + getAllRoutes() { + if (!this.#file?.data?.routes) { + return []; + } + return structuredClone(this.#file.data.routes); + } + + /** + * Returns a specific route + * + * @param {string} id - The ID of the given route + * @returns {object|null} The route, or null if no route has the given id + */ + getRoute(id) { + const idx = this.#file.data.routes.findIndex(r => r.id === id); + if (idx === -1) { + return null; + } + return structuredClone(this.#file.data.routes[idx]); + } + + /** + * Will update an existing route + * + * @param {object} route - The updated route + */ + updateRoute(route) { + const idx = this.#file.data.routes.findIndex(r => r.id === route.id); + if (idx === -1) { + return; + } + this.#file.data.routes[idx] = structuredClone(route); + } + + /** + * Creates a new route and returns it + * + * @returns {object} Returns the empty route + */ + createNewRoute() { + const newRoute = this.getEmptyRoute(); + this.#file.data.routes.push(newRoute); + + return structuredClone(newRoute); + } + + /** + * Removes an existing route with the given id + * + * @param {string} id - The given id + */ + removeRoute(id) { + const objWithIdIndex = this.#file.data.routes.findIndex(r => r.id === id); + if (objWithIdIndex === -1) { + return; + } + this.#file.data.routes.splice(objWithIdIndex, 1); + } + + /** + * @returns {string} Returns the default route type for external links + */ + getDefaultExternalRoute() { + return this.#file?.data?.defaultRouteExternal ?? "most-recent-space"; + } + + /** + * @param {string} routeType - Sets the default route type for external links + */ + setDefaultExternalRoute(routeType) { + this.#file.data.defaultRouteExternal = routeType; + } + + /** + * Saves all routes + */ + saveRoutes() { + this.#writeToDisk(); + } + + /** + * Writes the Space Routing data back onto the disk. + * + * @private + */ + #writeToDisk() { + this.#file.saveSoon(); + } + + /** + * Reads Space Routing data from disk and decompresses it. + * + * @returns {Promise} A promise that resolves to an array of Space Routing rules. + * @private + */ + async #readFromDisk() { + this.#file = new JSONFile({ + path: this.#storePath, + compression: "lz4", + + dataPostProcessor(data) { + if (!data || typeof data !== "object") { + data = {}; + } + if (!Array.isArray(data.routes)) { + data.routes = []; + } + if (typeof data.defaultRouteExternal !== "string") { + data.defaultRouteExternal = "most-recent-space"; + } + return data; + }, + }); + + await this.#file.load(); + } + + /** + * Gets the file path where Space Routing data is stored in the user's profile directory. + * + * @returns {string} The full path to the Space Routing storage file. + * @private + */ + get #storePath() { + const profilePath = PathUtils.profileDir; + return PathUtils.join(profilePath, this.#saveFilename); + } +} + +export const gZenSpaceRoutingManager = new nsZenSpaceRoutingManager(); diff --git a/src/zen/space-routing/jar.inc.mn b/src/zen/space-routing/jar.inc.mn new file mode 100644 index 000000000..99e725681 --- /dev/null +++ b/src/zen/space-routing/jar.inc.mn @@ -0,0 +1,9 @@ +# 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/. + + # Styles + content/browser/zen-styles/zen-space-routing.css (../../zen/space-routing/zen-space-routing.css) + + # Windows +* content/browser/zen-components/windows/zen-space-routing.xhtml (../../zen/space-routing/zen-space-routing.inc.xhtml) diff --git a/src/zen/space-routing/moz.build b/src/zen/space-routing/moz.build new file mode 100644 index 000000000..9bb392f21 --- /dev/null +++ b/src/zen/space-routing/moz.build @@ -0,0 +1,8 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXTRA_JS_MODULES.zen.spacerouting += [ + "ZenSpaceRoutingDialog.mjs", + "ZenSpaceRoutingManager.sys.mjs", +] diff --git a/src/zen/space-routing/zen-space-routing.css b/src/zen/space-routing/zen-space-routing.css new file mode 100644 index 000000000..31aea297e --- /dev/null +++ b/src/zen/space-routing/zen-space-routing.css @@ -0,0 +1,315 @@ +/* + * 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/. + */ + +:root { + background: none; + appearance: none; + border: none; + outline: none; + color-scheme: light dark; + + --sr-background: light-dark(white, #212223); + --select-background-color: light-dark(#eceef0, #3e3f43); + --input-background-color: light-dark(#f1f2f4, #2c2d31); + --hr-color: light-dark(#cdced4, #313235); + + --sr-width: 510px; + --sr-height: 500px; + + --content-padding: 26px; + --content-padding-vertical: 16px; + + --text-color: light-dark(#4c4c4c, #dbdcdf); + --text-color-secondary: light-dark(#5c5e65, #8b8e98); + --text-color-error: light-dark(#9d2222, #d03535); + + --rules-gap: 14px; + + --sr-border-radius: 12px; +} + +.dialog-button-box { + /* Remove default dialog buttons */ + display: none !important; +} + +#zen-space-routing-dialog-container { + background-color: var(--sr-background); + padding: 0; + margin: 24px; + + height: var(--sr-height); + max-height: var(--sr-height); + min-height: var(--sr-height); + + width: var(--sr-width); + max-width: var(--sr-width); + min-width: var(--sr-width); +} + +#sr-container { + height: var(--sr-height); + width: var(--sr-width); + + border-radius: var(--sr-border-radius); + overflow: hidden; + font-family: system-ui !important; + + position: relative; + display: flex; +} + +#sr-empty-content { + display: flex; + flex-direction: column; + text-align: center; + gap: 24px; + + position: absolute; + left: 84px; + right: 84px; + + bottom: 100px; + top: 100px; + justify-content: center; + + & image { + aspect-ratio: 1; + height: 75px; + + min-height: 0; + max-height: none; + max-width: none; + min-width: 0; + + color: var(--text-color-secondary); + -moz-context-properties: fill, fill-opacity; + fill: currentColor; + fill-opacity: 0.65; + } + + & p { + font-weight: bold; + font-size: small; + + color: var(--text-color); + } +} + +.hr { + color: var(--hr-color); + padding: 0; + margin: 0; + margin-left: 32px; + margin-right: 32px; + border-style: solid; + border-width: 0.5px; +} + +h3 { + font-size: medium; + color: var(--text-color); + font-weight: normal; +} + +h4 { + font-size: small; + color: var(--text-color-secondary); +} + +p { + color: var(--text-color); +} + +.select { + width: 150px; + height: 26px; + padding: 4px; +} + +.select, +.select * { + background-color: var(--select-background-color); + color: var(--text-color); + margin: 0; + + &.match-type-select { + width: 100px; + } + &.open-in-select { + width: 165px; + } +} + +menulist[image] .menulist-icon, +menulist[image]::part(icon) { + width: 16px; + height: 16px; + min-width: 16px; + min-height: 16px; + -moz-context-properties: fill, stroke; + fill: currentColor; + margin-inline-end: 0; + padding-left: 2px; +} + +menulist { + -moz-box-align: center; +} + +.sr-rule-row { + display: flex; + align-items: center; + gap: 8px; +} + +.sr-rule-container { + display: flex; + flex-direction: column; + gap: 4px; + margin: 0 24px; + + & p { + color: var(--text-color-secondary); + } +} + +.sr-rule-bottom { + margin-left: 85px; +} + +.input, +.input * { + background-color: var(--input-background-color); + color: var(--text-color); + margin: 0; + + flex-grow: 1; +} + +.invalid, +.invalid * { + color: var(--text-color-error) !important; + + text-decoration-line: underline; + text-decoration-style: wavy; + text-decoration-thickness: 1px; + text-decoration-color: var(--text-color-error); +} + +button { + user-select: none !important; + + padding: 2px; + display: flex; + justify-content: center; + + margin: 0; + + & hbox { + display: none; + } +} + +.sr-remove, +.close-icon { + & hbox { + display: initial; + padding: 0; + margin: 0; + } + + opacity: 0.5; + + &:hover { + opacity: 0.8; + } + + transition: 0.2s opacity cubic-bezier(0.075, 0.82, 0.165, 1); + + appearance: none !important; + background: none; + border: none !important; + + aspect-ratio: 1; + height: 24px; + padding: 3px; + width: 24px; + + min-width: 0 !important; + color: var(--text-color-secondary); + + -moz-context-properties: fill, fill-opacity; + fill: currentColor; +} + +#sr-header { + width: 100%; + padding: var(--content-padding); + align-items: center; + + display: flex; + flex-direction: row; +} + +.sr-left { + margin-left: auto; + + display: flex; + flex-direction: row; + align-items: center; + gap: 6px; + + & button { + color: var(--text-color); + opacity: 1; + + &:hover .sr-remove { + opacity: 0.8; + } + } +} + +#sr-footer { + width: 100%; + padding: var(--content-padding); + align-items: center; + + display: flex; + flex-direction: row; +} + +#sr-content { + width: 100%; + overflow: scroll; + padding: var(--content-padding); + flex-grow: 1; + + display: flex; + flex-direction: column; + gap: var(--rules-gap); +} + +.sr-label-container { + display: flex; + align-items: center; + gap: 6px; + + color: var(--text-color-secondary); +} + +.sr-label { + margin: 0; +} + +.sr-url-icon, +.sr-open-in-icon { + width: 16px; + height: 16px; + -moz-context-properties: fill, fill-opacity; + fill: currentColor; + fill-opacity: 0.65; +} diff --git a/src/zen/space-routing/zen-space-routing.inc.xhtml b/src/zen/space-routing/zen-space-routing.inc.xhtml new file mode 100644 index 000000000..dfda34229 --- /dev/null +++ b/src/zen/space-routing/zen-space-routing.inc.xhtml @@ -0,0 +1,108 @@ +#filter substitution + + +# -*- 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/. + + + + + + + + + + + + + + + + + + + +

+ + + + +
+ + + + +

+
+# All rules will be injected here later +
+ + +

+ + + +# Select open in types will be injected here + + + +
+
+
+ + +
diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index 344938714..9deb7014d 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -1226,7 +1226,10 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature { const currentTab = gZenGlanceManager.getTabOrGlanceParent( window.gBrowser.selectedTab ); - const newTab = this.openAndSwitchToTab(url, { inBackground: false }); + const newTab = this.openAndSwitchToTab(url, { + skipRoute: true, + inBackground: false, + }); this.splitTabs([currentTab, newTab], undefined, 1); } diff --git a/src/zen/tests/moz.build b/src/zen/tests/moz.build index a79f5a7f4..fbbf132a8 100644 --- a/src/zen/tests/moz.build +++ b/src/zen/tests/moz.build @@ -13,6 +13,7 @@ BROWSER_CHROME_MANIFESTS += [ "pinned/browser.toml", "popover/browser.toml", "site_control/browser.toml", + "space_routing/browser.toml", "spaces/browser.toml", "split_view/browser.toml", "tabs/browser.toml", diff --git a/src/zen/tests/space_routing/browser.toml b/src/zen/tests/space_routing/browser.toml new file mode 100644 index 000000000..278473300 --- /dev/null +++ b/src/zen/tests/space_routing/browser.toml @@ -0,0 +1,18 @@ +# 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] +support-files = [ + "head.js", +] + +["browser_space_routing_crud.js"] + +["browser_space_routing_dialog.js"] + +["browser_space_routing_on_add_tab.js"] + +["browser_space_routing_route_matching.js"] + +["browser_space_routing_route_uri.js"] diff --git a/src/zen/tests/space_routing/browser_space_routing_crud.js b/src/zen/tests/space_routing/browser_space_routing_crud.js new file mode 100644 index 000000000..afff1e152 --- /dev/null +++ b/src/zen/tests/space_routing/browser_space_routing_crud.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + clearAllRoutes(); + const savedDefault = gZenSpaceRoutingManager.getDefaultExternalRoute(); + registerCleanupFunction(() => { + clearAllRoutes(); + gZenSpaceRoutingManager.setDefaultExternalRoute(savedDefault); + }); +}); + +add_task(async function test_empty_route_shape_and_unique_ids() { + const a = gZenSpaceRoutingManager.getEmptyRoute(); + const b = gZenSpaceRoutingManager.getEmptyRoute(); + + Assert.equal(a.reference, "", "Empty route starts with no reference"); + Assert.equal( + a.openIn, + "most-recent-space", + "Empty route defaults to most-recent-space" + ); + Assert.equal(a.matchType, "contains", "Empty route defaults to 'contains'"); + Assert.equal(typeof a.id, "string", "Empty route has a string id"); + ok(a.id.length, "Empty route id is non-empty"); + Assert.notEqual(a.id, b.id, "Each empty route gets a unique id"); +}); + +add_task(async function test_create_get_update_remove_lifecycle() { + clearAllRoutes(); + Assert.equal( + gZenSpaceRoutingManager.getAllRoutes().length, + 0, + "Precondition: no routes" + ); + + const created = gZenSpaceRoutingManager.createNewRoute(); + Assert.equal( + gZenSpaceRoutingManager.getAllRoutes().length, + 1, + "createNewRoute() appends one route" + ); + + created.reference = "zen-browser.app"; + created.openIn = "ws-42"; + created.matchType = "equal-to"; + gZenSpaceRoutingManager.updateRoute(created); + + const fetched = gZenSpaceRoutingManager.getRoute(created.id); + Assert.equal(fetched.reference, "zen-browser.app", "reference persisted"); + Assert.equal(fetched.openIn, "ws-42", "openIn persisted"); + Assert.equal(fetched.matchType, "equal-to", "matchType persisted"); + + gZenSpaceRoutingManager.removeRoute(created.id); + Assert.equal( + gZenSpaceRoutingManager.getAllRoutes().length, + 0, + "removeRoute() deletes the route" + ); +}); + +add_task(async function test_remove_only_targets_the_given_id() { + clearAllRoutes(); + const keep1 = addRoute({ reference: "a" }); + const drop = addRoute({ reference: "b" }); + const keep2 = addRoute({ reference: "c" }); + + gZenSpaceRoutingManager.removeRoute(drop.id); + + const ids = gZenSpaceRoutingManager.getAllRoutes().map(r => r.id); + Assert.deepEqual( + ids, + [keep1.id, keep2.id], + "Only the targeted route is removed; order of the rest is preserved" + ); +}); + +add_task(async function test_reads_return_copies_not_internal_refs() { + clearAllRoutes(); + const created = gZenSpaceRoutingManager.createNewRoute(); + + const fromGet = gZenSpaceRoutingManager.getRoute(created.id); + fromGet.reference = "mutated-via-getRoute"; + Assert.equal( + gZenSpaceRoutingManager.getRoute(created.id).reference, + "", + "getRoute() returns a copy; external mutation does not leak" + ); + + const all = gZenSpaceRoutingManager.getAllRoutes(); + all[0].reference = "mutated-via-getAllRoutes"; + Assert.equal( + gZenSpaceRoutingManager.getRoute(created.id).reference, + "", + "getAllRoutes() returns copies; external mutation does not leak" + ); +}); + +add_task(async function test_default_external_route_getter_setter() { + gZenSpaceRoutingManager.setDefaultExternalRoute("ws-default"); + Assert.equal( + gZenSpaceRoutingManager.getDefaultExternalRoute(), + "ws-default", + "setDefaultExternalRoute() round-trips through the getter" + ); + + gZenSpaceRoutingManager.setDefaultExternalRoute("most-recent-space"); + Assert.equal( + gZenSpaceRoutingManager.getDefaultExternalRoute(), + "most-recent-space", + "The default external route can be changed again" + ); +}); diff --git a/src/zen/tests/space_routing/browser_space_routing_dialog.js b/src/zen/tests/space_routing/browser_space_routing_dialog.js new file mode 100644 index 000000000..3733f7abd --- /dev/null +++ b/src/zen/tests/space_routing/browser_space_routing_dialog.js @@ -0,0 +1,251 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + clearAllRoutes(); + const savedDefault = gZenSpaceRoutingManager.getDefaultExternalRoute(); + registerCleanupFunction(() => { + clearAllRoutes(); + gZenSpaceRoutingManager.setDefaultExternalRoute(savedDefault); + }); +}); + +add_task(async function test_empty_placeholder_and_add_route() { + clearAllRoutes(); + const dlg = await openRoutingDialog(); + try { + const doc = dlg.document; + const emptyText = doc.getElementById("sr-empty-content"); + const content = doc.getElementById("sr-content"); + + Assert.notEqual( + emptyText.style.display, + "none", + "The empty-state placeholder is visible when there are no routes" + ); + + doc.getElementById("sr-new-route").click(); + + Assert.equal( + content.querySelectorAll(".sr-rule-container").length, + 1, + "Clicking 'New Route' injects one route element" + ); + Assert.equal( + emptyText.style.display, + "none", + "The empty-state placeholder is hidden once a route exists" + ); + Assert.equal( + gZenSpaceRoutingManager.getAllRoutes().length, + 1, + "The new route is persisted into the manager" + ); + } finally { + await closeRoutingDialog(dlg); + clearAllRoutes(); + } +}); + +add_task(async function test_remove_route_via_ui() { + clearAllRoutes(); + addRoute({ reference: "github.com" }); + const dlg = await openRoutingDialog(); + try { + const doc = dlg.document; + Assert.equal( + doc.querySelectorAll(".sr-rule-container").length, + 1, + "Existing route is rendered on open" + ); + + doc.querySelector(".sr-remove").click(); + + Assert.equal( + doc.querySelectorAll(".sr-rule-container").length, + 0, + "The route element is removed from the DOM" + ); + Assert.equal( + gZenSpaceRoutingManager.getAllRoutes().length, + 0, + "The route is removed from the manager" + ); + Assert.equal( + doc.getElementById("sr-empty-content").style.display, + "flex", + "The empty-state placeholder returns after the last route is removed" + ); + } finally { + await closeRoutingDialog(dlg); + clearAllRoutes(); + } +}); + +add_task(async function test_match_type_updates_placeholder_and_store() { + clearAllRoutes(); + const route = addRoute({ reference: "", matchType: "contains" }); + const dlg = await openRoutingDialog(); + try { + const doc = dlg.document; + const menulist = doc.querySelector(".sr-rule-container .match-type-select"); + const input = doc.querySelector(".sr-rule-container .input"); + + Assert.equal( + input.placeholder, + "zen-browser.app", + "The 'contains' placeholder is the plain domain" + ); + + menulist.value = "regex"; + menulist.dispatchEvent(new Event("command", { bubbles: true })); + + Assert.equal( + input.placeholder, + "zen-browser\\.app", + "Switching to 'regex' updates the placeholder to an escaped pattern" + ); + Assert.equal( + gZenSpaceRoutingManager.getRoute(route.id).matchType, + "regex", + "The match type change is persisted to the manager" + ); + } finally { + await closeRoutingDialog(dlg); + clearAllRoutes(); + } +}); + +add_task(async function test_invalid_regex_is_flagged_and_not_saved() { + clearAllRoutes(); + const route = addRoute({ reference: "", matchType: "regex" }); + const dlg = await openRoutingDialog(); + try { + const doc = dlg.document; + const input = doc.querySelector(".sr-rule-container .input"); + + input.value = "(["; + input.dispatchEvent(new Event("input", { bubbles: true })); + + ok( + input.classList.contains("invalid"), + "An invalid regex marks the input as invalid" + ); + Assert.equal( + gZenSpaceRoutingManager.getRoute(route.id).reference, + "", + "An invalid regex is NOT written to the route" + ); + + input.value = "zen.*app"; + input.dispatchEvent(new Event("input", { bubbles: true })); + + ok( + !input.classList.contains("invalid"), + "A subsequently valid regex clears the invalid state" + ); + Assert.equal( + gZenSpaceRoutingManager.getRoute(route.id).reference, + "zen.*app", + "A valid regex is written to the route" + ); + } finally { + await closeRoutingDialog(dlg); + clearAllRoutes(); + } +}); + +add_task(async function test_default_external_select_updates_store() { + clearAllRoutes(); + await gZenWorkspaces.promiseInitialized; + gZenSpaceRoutingManager.setDefaultExternalRoute("most-recent-space"); + + const dlg = await openRoutingDialog(); + try { + const doc = dlg.document; + const select = doc.getElementById("sr-default-external-open-in"); + + await TestUtils.waitForCondition( + () => select.querySelectorAll("menuitem").length > 1, + "External-default options were populated" + ); + + const workspaceUuid = gZenWorkspaces.getWorkspaces()[0].uuid; + select.value = workspaceUuid; + select.dispatchEvent(new Event("command", { bubbles: true })); + + Assert.equal( + gZenSpaceRoutingManager.getDefaultExternalRoute(), + workspaceUuid, + "Changing the external-default select updates the manager" + ); + } finally { + await closeRoutingDialog(dlg); + gZenSpaceRoutingManager.setDefaultExternalRoute("most-recent-space"); + } +}); + +add_task(async function test_routes_are_saved_on_close() { + clearAllRoutes(); + const dlg = await openRoutingDialog(); + + let saveCalls = 0; + const realSave = gZenSpaceRoutingManager.saveRoutes; + gZenSpaceRoutingManager.saveRoutes = function () { + saveCalls++; + return realSave.call(this); + }; + + try { + const closed = BrowserTestUtils.domWindowClosed(dlg); + dlg.close(); + await TestUtils.waitForCondition( + () => saveCalls > 0, + "Closing the dialog flushes routes to disk via saveRoutes()" + ); + await closed; + } finally { + delete gZenSpaceRoutingManager.saveRoutes; + } +}); + +add_task(async function test_open_broadcasts_kill_to_other_instances() { + clearAllRoutes(); + + let killNotified = false; + const observer = { + observe(_subject, topic) { + if (topic === "zen-space-routing-kill") { + killNotified = true; + } + }, + }; + Services.obs.addObserver(observer, "zen-space-routing-kill"); + + let dlg; + try { + dlg = await openRoutingDialog(); + ok( + killNotified, + "Opening a dialog broadcasts 'zen-space-routing-kill' so others can close" + ); + } finally { + Services.obs.removeObserver(observer, "zen-space-routing-kill"); + if (dlg) { + await closeRoutingDialog(dlg); + } + } +}); + +add_task(async function test_kill_notification_closes_dialog() { + clearAllRoutes(); + const dlg = await openRoutingDialog(); + + const closed = BrowserTestUtils.domWindowClosed(dlg); + Services.obs.notifyObservers(null, "zen-space-routing-kill"); + await closed; + + ok(dlg.closed, "A 'zen-space-routing-kill' notification closes the dialog"); +}); diff --git a/src/zen/tests/space_routing/browser_space_routing_on_add_tab.js b/src/zen/tests/space_routing/browser_space_routing_on_add_tab.js new file mode 100644 index 000000000..8fa66ceaa --- /dev/null +++ b/src/zen/tests/space_routing/browser_space_routing_on_add_tab.js @@ -0,0 +1,363 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TARGET_WS = { uuid: "ws-target", containerTabId: 7 }; + +add_setup(async function () { + clearAllRoutes(); + registerCleanupFunction(() => clearAllRoutes()); +}); + +add_task(async function test_onBeforeAddTab_resolves_container_for_match() { + clearAllRoutes(); + addRoute({ + reference: "github.com", + matchType: "contains", + openIn: TARGET_WS.uuid, + }); + const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] }); + + const result = gZenSpaceRoutingManager.onBeforeAddTab( + "https://github.com/zen", + {}, + win + ); + + Assert.deepEqual( + result, + { + shouldEarlyExit: false, + userContextId: TARGET_WS.containerTabId, + isRouteFound: true, + targetRoute: TARGET_WS.uuid, + }, + "A matching route resolves to the workspace's containerTabId" + ); +}); + +add_task(async function test_onBeforeAddTab_no_match_returns_no_route() { + clearAllRoutes(); + const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] }); + + const result = gZenSpaceRoutingManager.onBeforeAddTab( + "https://example.com", + {}, + win + ); + + Assert.deepEqual( + result, + { + shouldEarlyExit: false, + userContextId: null, + isRouteFound: false, + targetRoute: "most-recent-space", + }, + "An unmatched URL (most-recent-space) reports no container and no route" + ); +}); + +add_task(async function test_onBeforeAddTab_route_to_missing_workspace() { + clearAllRoutes(); + addRoute({ + reference: "github.com", + matchType: "contains", + openIn: "ws-does-not-exist", + }); + const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] }); + + const result = gZenSpaceRoutingManager.onBeforeAddTab( + "https://github.com", + {}, + win + ); + + Assert.deepEqual( + result, + { + shouldEarlyExit: false, + userContextId: null, + isRouteFound: false, + targetRoute: "ws-does-not-exist", + }, + "A route to a non-existent workspace yields no container and no route" + ); +}); + +add_task(async function test_onBeforeAddTab_skips_special_tab_options() { + clearAllRoutes(); + addRoute({ + reference: "github.com", + matchType: "contains", + openIn: TARGET_WS.uuid, + }); + const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] }); + + for (const skipOption of ["skipRoute", "pinned", "tabGroup"]) { + const result = gZenSpaceRoutingManager.onBeforeAddTab( + "https://github.com/zen", + { [skipOption]: true }, + win + ); + Assert.deepEqual( + result, + { + shouldEarlyExit: false, + userContextId: null, + isRouteFound: false, + targetRoute: null, + }, + `Option '${skipOption}' skips routing even though a rule matches` + ); + } +}); + +add_task(async function test_onBeforeAddTab_skips_until_startup_ready() { + clearAllRoutes(); + addRoute({ + reference: "github.com", + matchType: "contains", + openIn: TARGET_WS.uuid, + }); + const win = makeFakeWindow({ ready: false, workspaces: [TARGET_WS] }); + + const result = gZenSpaceRoutingManager.onBeforeAddTab( + "https://github.com/zen", + {}, + win + ); + + Assert.deepEqual( + result, + { + shouldEarlyExit: false, + userContextId: null, + isRouteFound: false, + targetRoute: null, + }, + "While gZenStartup.isReady is false (session restore), routing is skipped" + ); +}); + +add_task(async function test_onAfterAddTab_moves_tab_on_non_origin_window() { + clearAllRoutes(); + addRoute({ + reference: "github.com", + matchType: "contains", + openIn: TARGET_WS.uuid, + }); + const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] }); + const fakeTab = { parentNode: {} }; + + gZenSpaceRoutingManager.onAfterAddTab( + "https://github.com/zen", + fakeTab, + {}, + win, + { targetRoute: TARGET_WS.uuid } + ); + + await TestUtils.waitForCondition( + () => win.gZenWorkspaces.moveCalls.length === 1, + "moveTabToWorkspace was called once" + ); + Assert.equal( + win.gZenWorkspaces.moveCalls[0].uuid, + TARGET_WS.uuid, + "The tab is moved to the matched workspace" + ); + Assert.equal( + win.gZenWorkspaces.moveCalls[0].tab, + fakeTab, + "The correct tab element is moved" + ); + Assert.equal( + win.gZenWorkspaces.changeCalls.length, + 0, + "A non-originating window does not switch the active workspace" + ); +}); + +add_task(async function test_onAfterAddTab_reuses_before_result() { + clearAllRoutes(); + const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] }); + const fakeTab = { parentNode: {} }; + + // No routes exist, so a fresh routeUri() would yield "most-recent-space" and + // move nothing. The tab is still moved to TARGET_WS, proving onAfterAddTab + // routes purely from the precomputed result rather than recomputing. + gZenSpaceRoutingManager.onAfterAddTab( + "https://example.com", + fakeTab, + {}, + win, + { targetRoute: TARGET_WS.uuid } + ); + + await TestUtils.waitForCondition( + () => win.gZenWorkspaces.moveCalls.length === 1, + "moveTabToWorkspace used the precomputed route" + ); + Assert.equal( + win.gZenWorkspaces.moveCalls[0].uuid, + TARGET_WS.uuid, + "onAfterAddTab routes using the precomputed targetRoute, not a fresh routeUri()" + ); +}); + +add_task(async function test_onAfterAddTab_ignores_detached_tab() { + clearAllRoutes(); + const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] }); + + gZenSpaceRoutingManager.onAfterAddTab( + "https://github.com/zen", + { parentNode: null }, + {}, + win, + { targetRoute: TARGET_WS.uuid } + ); + await flushEventLoop(); + + Assert.equal( + win.gZenWorkspaces.moveCalls.length, + 0, + "A detached tab (no parentNode) is never moved" + ); +}); + +add_task( + async function test_onAfterAddTab_does_nothing_for_most_recent_space() { + clearAllRoutes(); + const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] }); + + gZenSpaceRoutingManager.onAfterAddTab( + "https://example.com", + { parentNode: {} }, + {}, + win, + { targetRoute: "most-recent-space" } + ); + await flushEventLoop(); + + Assert.equal( + win.gZenWorkspaces.moveCalls.length, + 0, + "A 'most-recent-space' route does not move the tab" + ); + } +); + +add_task(async function test_onAfterAddTab_does_nothing_when_skipped() { + clearAllRoutes(); + const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] }); + + // onBeforeAddTab reports targetRoute null for skipped/unready tabs; without a + // route there is nothing for onAfterAddTab to do. + gZenSpaceRoutingManager.onAfterAddTab( + "https://github.com/zen", + { parentNode: {} }, + {}, + win, + { targetRoute: null } + ); + await flushEventLoop(); + + Assert.equal( + win.gZenWorkspaces.moveCalls.length, + 0, + "A null targetRoute (skipped tab) is not routed" + ); +}); + +add_task(async function test_onAfterAddTab_ignores_missing_before_result() { + clearAllRoutes(); + const win = makeFakeWindow({ ready: true, workspaces: [TARGET_WS] }); + + gZenSpaceRoutingManager.onAfterAddTab( + "https://github.com/zen", + { parentNode: {} }, + {}, + win + ); + await flushEventLoop(); + + Assert.equal( + win.gZenWorkspaces.moveCalls.length, + 0, + "Without a beforeResult there is no precomputed route, so nothing is moved" + ); +}); + +add_task(async function test_onAfterAddTab_activates_workspace_on_origin() { + clearAllRoutes(); + await gZenWorkspaces.promiseInitialized; + + await gZenWorkspaces.createAndSaveWorkspace("SR Origin Test"); + const workspaces = gZenWorkspaces.getWorkspaces(); + const target = workspaces[workspaces.length - 1]; + + const isOriginating = + window === Services.wm.getMostRecentWindow("navigator:browser"); + ok(isOriginating, "Precondition: the test window is the most-recent window"); + + const ws = window.gZenWorkspaces; + const origMove = ws.moveTabToWorkspace; + const origChange = ws.changeWorkspace; + const origLastSelected = ws.lastSelectedWorkspaceTabs; + + let moved = null; + let changedTo = null; + ws.lastSelectedWorkspaceTabs = {}; + ws.moveTabToWorkspace = (tab, uuid) => { + moved = { tab, uuid }; + }; + ws.changeWorkspace = workspace => { + changedTo = workspace; + return Promise.resolve(); + }; + + const tab = BrowserTestUtils.addTab(gBrowser, "about:blank", { + skipAnimation: true, + skipRoute: true, + }); + + try { + gZenSpaceRoutingManager.onAfterAddTab( + "https://github.com/zen", + tab, + {}, + window, + { targetRoute: target.uuid } + ); + + await TestUtils.waitForCondition( + () => moved, + "moveTabToWorkspace was called" + ); + Assert.equal(moved.uuid, target.uuid, "Moved to the matched workspace"); + Assert.equal(moved.tab, tab, "Moved the tab we passed in"); + + await TestUtils.waitForCondition( + () => changedTo, + "changeWorkspace was called on the originating window" + ); + Assert.equal( + changedTo.uuid, + target.uuid, + "Activated the matched workspace" + ); + Assert.equal( + ws.lastSelectedWorkspaceTabs[target.uuid], + tab, + "The moved tab is remembered as the workspace's last-selected tab" + ); + } finally { + ws.moveTabToWorkspace = origMove; + ws.changeWorkspace = origChange; + ws.lastSelectedWorkspaceTabs = origLastSelected; + BrowserTestUtils.removeTab(tab); + await gZenWorkspaces.removeWorkspace(target.uuid); + } +}); diff --git a/src/zen/tests/space_routing/browser_space_routing_route_matching.js b/src/zen/tests/space_routing/browser_space_routing_route_matching.js new file mode 100644 index 000000000..5db7a422c --- /dev/null +++ b/src/zen/tests/space_routing/browser_space_routing_route_matching.js @@ -0,0 +1,108 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_contains_is_case_insensitive_substring() { + const route = { reference: "GitHub", matchType: "contains" }; + + ok( + gZenSpaceRoutingManager.isRouteMatching("https://github.com/zen", route), + "'contains' matches a substring regardless of case" + ); + ok( + gZenSpaceRoutingManager.isRouteMatching("https://api.GITHUB.com/v3", route), + "'contains' matches when the URL casing differs from the reference" + ); + ok( + !gZenSpaceRoutingManager.isRouteMatching("https://gitlab.com/zen", route), + "'contains' rejects a URL that does not include the reference" + ); +}); + +add_task(async function test_equal_to_normalizes_protocol_and_www() { + const route = { reference: "github.com", matchType: "equal-to" }; + + ok( + gZenSpaceRoutingManager.isRouteMatching("https://www.github.com/", route), + "'equal-to' ignores https://, www. and a trailing slash" + ); + ok( + gZenSpaceRoutingManager.isRouteMatching("HTTP://GitHub.com", route), + "'equal-to' is case-insensitive and strips http://" + ); + ok( + !gZenSpaceRoutingManager.isRouteMatching("https://github.com/zen", route), + "'equal-to' does not match when a path is present (not an exact host)" + ); + ok( + !gZenSpaceRoutingManager.isRouteMatching("https://notgithub.com", route), + "'equal-to' requires the whole normalized URL to be equal" + ); +}); + +add_task(async function test_regex_match_is_case_sensitive_on_raw_uri() { + ok( + gZenSpaceRoutingManager.isRouteMatching("https://zen-browser.app", { + reference: "^https://.*\\.app$", + matchType: "regex", + }), + "'regex' matches against the raw URI" + ); + + ok( + !gZenSpaceRoutingManager.isRouteMatching("https://github.com", { + reference: "GitHub", + matchType: "regex", + }), + "'regex' is case-sensitive (no implicit lower-casing like 'contains')" + ); +}); + +add_task(async function test_invalid_regex_is_swallowed() { + let threw = false; + let result; + try { + result = gZenSpaceRoutingManager.isRouteMatching( + "https://zen-browser.app", + { + reference: "([", + matchType: "regex", + } + ); + } catch (e) { + threw = true; + } + + ok(!threw, "An invalid regex does not throw out of isRouteMatching"); + Assert.strictEqual(result, false, "An invalid regex never matches"); +}); + +add_task(async function test_empty_reference_never_matches() { + for (const matchType of ["contains", "equal-to", "regex"]) { + ok( + !gZenSpaceRoutingManager.isRouteMatching("https://github.com", { + reference: "", + matchType, + }), + `An empty reference never matches (${matchType})` + ); + ok( + !gZenSpaceRoutingManager.isRouteMatching("https://github.com", { + reference: " ", + matchType, + }), + `A whitespace-only reference never matches (${matchType})` + ); + } +}); + +add_task(async function test_unknown_match_type_does_not_match() { + ok( + !gZenSpaceRoutingManager.isRouteMatching("https://github.com", { + reference: "github.com", + matchType: "starts-with", + }), + "An unsupported match type falls through to no match" + ); +}); diff --git a/src/zen/tests/space_routing/browser_space_routing_route_uri.js b/src/zen/tests/space_routing/browser_space_routing_route_uri.js new file mode 100644 index 000000000..2763116ef --- /dev/null +++ b/src/zen/tests/space_routing/browser_space_routing_route_uri.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(async function () { + clearAllRoutes(); + const savedDefault = gZenSpaceRoutingManager.getDefaultExternalRoute(); + registerCleanupFunction(() => { + clearAllRoutes(); + gZenSpaceRoutingManager.setDefaultExternalRoute(savedDefault); + }); +}); + +add_task(async function test_no_match_returns_most_recent_space() { + clearAllRoutes(); + addRoute({ reference: "github.com", matchType: "contains", openIn: "ws-1" }); + + Assert.equal( + gZenSpaceRoutingManager.routeUri("https://example.com", {}), + "most-recent-space", + "A non-matching, non-external URL routes to most-recent-space" + ); +}); + +add_task(async function test_first_matching_route_wins() { + clearAllRoutes(); + addRoute({ reference: "github", matchType: "contains", openIn: "ws-first" }); + addRoute({ reference: "github", matchType: "contains", openIn: "ws-second" }); + + Assert.equal( + gZenSpaceRoutingManager.routeUri("https://github.com/zen", {}), + "ws-first", + "The openIn of the first matching route is returned, later matches ignored" + ); +}); + +add_task(async function test_external_default_only_applies_without_match() { + clearAllRoutes(); + gZenSpaceRoutingManager.setDefaultExternalRoute("ws-external"); + addRoute({ reference: "github", matchType: "contains", openIn: "ws-rule" }); + + Assert.equal( + gZenSpaceRoutingManager.routeUri("https://github.com", { + fromExternal: true, + }), + "ws-rule", + "A matching rule wins even for external links" + ); + + Assert.equal( + gZenSpaceRoutingManager.routeUri("https://example.com", { + fromExternal: true, + }), + "ws-external", + "An unmatched external link uses the default external route" + ); + + Assert.equal( + gZenSpaceRoutingManager.routeUri("https://example.com", { + fromExternal: false, + }), + "most-recent-space", + "An unmatched internal link ignores the external default" + ); +}); diff --git a/src/zen/tests/space_routing/head.js b/src/zen/tests/space_routing/head.js new file mode 100644 index 000000000..ab6717d65 --- /dev/null +++ b/src/zen/tests/space_routing/head.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { gZenSpaceRoutingManager } = ChromeUtils.importESModule( + "resource:///modules/zen/spacerouting/ZenSpaceRoutingManager.sys.mjs" +); + +const SR_DIALOG_URI = + "chrome://browser/content/zen-components/windows/zen-space-routing.xhtml"; + +function clearAllRoutes() { + for (const route of gZenSpaceRoutingManager.getAllRoutes()) { + gZenSpaceRoutingManager.removeRoute(route.id); + } +} + +function addRoute({ + reference = "", + openIn = "most-recent-space", + matchType = "contains", +} = {}) { + const route = gZenSpaceRoutingManager.createNewRoute(); + route.reference = reference; + route.openIn = openIn; + route.matchType = matchType; + gZenSpaceRoutingManager.updateRoute(route); + return route; +} + +function makeFakeWindow({ ready = true, workspaces = [] } = {}) { + return { + gZenStartup: { isReady: ready }, + gZenWorkspaces: { + moveCalls: [], + changeCalls: [], + lastSelectedWorkspaceTabs: {}, + getWorkspaceFromId(id) { + return workspaces.find(w => w.uuid === id) || null; + }, + moveTabToWorkspace(tab, uuid) { + this.moveCalls.push({ tab, uuid }); + }, + changeWorkspace(workspace) { + this.changeCalls.push(workspace); + return Promise.resolve(); + }, + }, + }; +} + +async function flushEventLoop() { + for (let i = 0; i < 5; i++) { + await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); + } +} + +async function openRoutingDialog() { + const dialogPromise = BrowserTestUtils.domWindowOpenedAndLoaded(null, win => + win.document?.documentURI?.includes("zen-space-routing.xhtml") + ); + executeSoon(() => gZenSpaceRoutingManager.openSpaceRoutingDialog(window)); + const dialogWin = await dialogPromise; + await SimpleTest.promiseFocus(dialogWin); + await TestUtils.waitForCondition( + () => dialogWin.spaceroutingDialog?.initialized, + "Space Routing dialog finished initializing" + ); + return dialogWin; +} + +async function closeRoutingDialog(dialogWin) { + if (dialogWin.closed) { + return; + } + const closed = BrowserTestUtils.domWindowClosed(dialogWin); + dialogWin.close(); + await closed; +} diff --git a/src/zen/urlbar/ZenUBGlobalActions.sys.mjs b/src/zen/urlbar/ZenUBGlobalActions.sys.mjs index bfc789bb5..613c0073f 100644 --- a/src/zen/urlbar/ZenUBGlobalActions.sys.mjs +++ b/src/zen/urlbar/ZenUBGlobalActions.sys.mjs @@ -81,6 +81,11 @@ const globalActionsTemplate = [ return !tab.hasAttribute("zen-empty-tab") && tab.pinned; }, }, + { + label: "Open Space Routing", + command: "cmd_zenOpenSpaceRoutingSettings", + icon: "chrome://browser/skin/zen-icons/selectable/airplane.svg", + }, { label: "New Boost", icon: "chrome://browser/skin/zen-icons/boost.svg", diff --git a/src/zen/zen.globals.mjs b/src/zen/zen.globals.mjs index 058a4d413..204cf37b7 100644 --- a/src/zen/zen.globals.mjs +++ b/src/zen/zen.globals.mjs @@ -40,6 +40,8 @@ export default [ "gZenViewSplitter", + "gZenSpaceRoutingManager", + "Ci", "Cu", "Cc", From e7e4c5a452289a62df45462ec1bc88f8a86760e6 Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Sat, 6 Jun 2026 17:34:43 +0200 Subject: [PATCH 19/53] gh-14045: Fixed sidebar notifications overflowing text (gh-14047) --- src/zen/common/styles/zen-sidebar-notification.css | 8 +++++++- src/zen/spaces/ZenSpaceManager.mjs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/zen/common/styles/zen-sidebar-notification.css b/src/zen/common/styles/zen-sidebar-notification.css index fdddbe549..b80f94e9b 100644 --- a/src/zen/common/styles/zen-sidebar-notification.css +++ b/src/zen/common/styles/zen-sidebar-notification.css @@ -27,7 +27,7 @@ display: flex; position: relative; flex: 1; - padding: 8px; + padding: 16px; border-bottom: 1px solid color-mix(in srgb, currentColor 10%, transparent); } @@ -38,6 +38,12 @@ white-space: nowrap; font-weight: 500; flex: 1; + position: absolute; + top: 50%; + left: 0; + transform: translateY(-50%); + width: calc(100% - 40px); + padding: 0 16px; } .zen-sidebar-notification-close-button { diff --git a/src/zen/spaces/ZenSpaceManager.mjs b/src/zen/spaces/ZenSpaceManager.mjs index 67d29725f..b75733b81 100644 --- a/src/zen/spaces/ZenSpaceManager.mjs +++ b/src/zen/spaces/ZenSpaceManager.mjs @@ -955,7 +955,7 @@ class nsZenWorkspaces { } gZenEmojiPicker.open(anchor, { closeOnSelect: false, - allowNone: hasNoIcon, + allowNone: !hasNoIcon, onSelect: async icon => { const workspace = this.getWorkspaceFromId(workspaceId); if (!workspace) { From 965bb9391da5ca543b9ab79125c7bbab8f645e7a Mon Sep 17 00:00:00 2001 From: fen4flo <75260616+FlorianButz@users.noreply.github.com> Date: Sun, 7 Jun 2026 11:33:54 +0200 Subject: [PATCH 20/53] gh-14044: Change to use gDialogBox instead of openDialog (gh-14054) Co-authored-by: mr. m --- .../base/content/zen-panels/popups.inc | 3 +- src/zen/boosts/ZenBoostsEditor.mjs | 2 +- .../space-routing/ZenSpaceRoutingDialog.mjs | 12 ++- .../ZenSpaceRoutingManager.sys.mjs | 16 ++-- src/zen/space-routing/zen-space-routing.css | 73 +++++++---------- .../space-routing/zen-space-routing.inc.xhtml | 80 ++++++++----------- 6 files changed, 86 insertions(+), 100 deletions(-) diff --git a/src/browser/base/content/zen-panels/popups.inc b/src/browser/base/content/zen-panels/popups.inc index bc9abc04e..2ff92383d 100644 --- a/src/browser/base/content/zen-panels/popups.inc +++ b/src/browser/base/content/zen-panels/popups.inc @@ -30,10 +30,11 @@ - + + diff --git a/src/zen/boosts/ZenBoostsEditor.mjs b/src/zen/boosts/ZenBoostsEditor.mjs index c65b45e16..7254ac3fd 100644 --- a/src/zen/boosts/ZenBoostsEditor.mjs +++ b/src/zen/boosts/ZenBoostsEditor.mjs @@ -218,7 +218,7 @@ export class nsZenBoostEditor { const editor = new Editor({ mode: Editor.modes.css, lineNumbers: true, - theme: "mozilla", + theme: this.isDarkMode ? "mozilla" : "default", readOnly: false, gutters: ["CodeMirror-linenumbers"], }); diff --git a/src/zen/space-routing/ZenSpaceRoutingDialog.mjs b/src/zen/space-routing/ZenSpaceRoutingDialog.mjs index 3bdb6324f..786a4fc01 100644 --- a/src/zen/space-routing/ZenSpaceRoutingDialog.mjs +++ b/src/zen/space-routing/ZenSpaceRoutingDialog.mjs @@ -196,8 +196,18 @@ export class nsZenSpaceRoutingDialog { bottomRow.append(bottomLabelContainer, openInMenulist); root.append(topRow, bottomRow); + + root.style.display = "none"; container.appendChild(root); + // Wait for l10n to catch up and then show the element to avoid flickering. + this.editorWindow.promiseDocumentFlushed(() => + this.editorWindow.requestAnimationFrame(() => { + root.style.display = ""; + input.focus(); + }) + ); + removeButton.addEventListener("click", () => { this.onRemoveRoutePressed(route.id, root); }); @@ -212,8 +222,6 @@ export class nsZenSpaceRoutingDialog { this.onRouteOpenInChange(e.target.value, route.id) ); - input.focus(); - this.updateShowNoRouteText(); return root; diff --git a/src/zen/space-routing/ZenSpaceRoutingManager.sys.mjs b/src/zen/space-routing/ZenSpaceRoutingManager.sys.mjs index 9109e8924..b5d07c7a3 100644 --- a/src/zen/space-routing/ZenSpaceRoutingManager.sys.mjs +++ b/src/zen/space-routing/ZenSpaceRoutingManager.sys.mjs @@ -254,16 +254,16 @@ class nsZenSpaceRoutingManager { * @param {Window} parentWindow - The parent browser window * @returns {Window|null} The instanced editor window */ - openSpaceRoutingDialog(parentWindow) { - const control = parentWindow.openDialog( + async openSpaceRoutingDialog(parentWindow) { + await parentWindow.gDialogBox.open( "chrome://browser/content/zen-components/windows/zen-space-routing.xhtml", - "", - "centerscreen,modal,dependent,resizable=no,titlebar=no", - { parentWindow } + { + features: "resizable=no", + sizeTo: "available", + allowDuplicateDialogs: false, + parentWindow, + } ); - - control.focus(); - return control; } /** diff --git a/src/zen/space-routing/zen-space-routing.css b/src/zen/space-routing/zen-space-routing.css index 31aea297e..172263809 100644 --- a/src/zen/space-routing/zen-space-routing.css +++ b/src/zen/space-routing/zen-space-routing.css @@ -1,17 +1,13 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public +/* 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/. - */ + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@namespace html "http://www.w3.org/1999/xhtml"; +@namespace xul "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; :root { - background: none; - appearance: none; - border: none; - outline: none; - color-scheme: light dark; + --background-color-canvas: light-dark(white, #212223) !important; - --sr-background: light-dark(white, #212223); --select-background-color: light-dark(#eceef0, #3e3f43); --input-background-color: light-dark(#f1f2f4, #2c2d31); --hr-color: light-dark(#cdced4, #313235); @@ -36,11 +32,7 @@ display: none !important; } -#zen-space-routing-dialog-container { - background-color: var(--sr-background); - padding: 0; - margin: 24px; - +window { height: var(--sr-height); max-height: var(--sr-height); min-height: var(--sr-height); @@ -92,20 +84,22 @@ } & p { - font-weight: bold; font-size: small; - + opacity: 0.8; color: var(--text-color); } } -.hr { - color: var(--hr-color); +#sr-new-route { + padding: 4px 6px !important; +} + +html|hr { + border-color: var(--hr-color) !important; padding: 0; margin: 0; - margin-left: 32px; - margin-right: 32px; - border-style: solid; + margin-left: 22px; + margin-right: 22px; border-width: 0.5px; } @@ -126,13 +120,14 @@ p { .select { width: 150px; + min-height: unset; height: 26px; padding: 4px; } .select, .select * { - background-color: var(--select-background-color); + background-color: var(--select-background-color) !important; color: var(--text-color); margin: 0; @@ -178,7 +173,7 @@ menulist { } .sr-rule-bottom { - margin-left: 85px; + margin-left: 87px; } .input, @@ -201,9 +196,10 @@ menulist { } button { - user-select: none !important; + background-color: var(--select-background-color) !important; + min-height: unset !important; - padding: 2px; + padding: 2px !important; display: flex; justify-content: center; @@ -217,38 +213,31 @@ button { .sr-remove, .close-icon { & hbox { - display: initial; - padding: 0; - margin: 0; + display: flex; } opacity: 0.5; - - &:hover { - opacity: 0.8; - } - - transition: 0.2s opacity cubic-bezier(0.075, 0.82, 0.165, 1); - appearance: none !important; - background: none; border: none !important; - aspect-ratio: 1; - height: 24px; + height: 32px; + width: 32px; padding: 3px; - width: 24px; min-width: 0 !important; color: var(--text-color-secondary); -moz-context-properties: fill, fill-opacity; fill: currentColor; + + &:not(:hover) { + background: none !important; + } } #sr-header { width: 100%; - padding: var(--content-padding); + padding: var(--content-padding) 20px; align-items: center; display: flex; @@ -285,7 +274,7 @@ button { #sr-content { width: 100%; overflow: scroll; - padding: var(--content-padding); + padding: 20px var(--content-padding); flex-grow: 1; display: flex; diff --git a/src/zen/space-routing/zen-space-routing.inc.xhtml b/src/zen/space-routing/zen-space-routing.inc.xhtml index dfda34229..7b4cdc858 100644 --- a/src/zen/space-routing/zen-space-routing.inc.xhtml +++ b/src/zen/space-routing/zen-space-routing.inc.xhtml @@ -18,10 +18,10 @@ dialogroot="true" > - - - - - - -

- - - - -
- - - - -

-
-# All rules will be injected here later + + +

+ + + + +
+ + + + +

- - -

- - - -# Select open in types will be injected here - - - -
+# All rules will be injected here later
+ + +

+ + + +# Select open in types will be injected here + + + +