diff --git a/package.json b/package.json
index 0ebf49c20..6e7fd8fd0 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +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",
"ffprefs": "cd tools/ffprefs && cargo run --bin ffprefs -- ../../",
"lc": "surfer license-check",
"lc:fix": "surfer license-check --fix",
diff --git a/prefs/zen/boosts.yaml b/prefs/zen/boosts.yaml
index b0de3d3fa..b9ab5b473 100644
--- a/prefs/zen/boosts.yaml
+++ b/prefs/zen/boosts.yaml
@@ -3,7 +3,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
- name: zen.boosts.enabled
- value: "@IS_TWILIGHT@"
+ value: true
- name: zen.boosts.dissolve-on-zap
value: true
diff --git a/src/browser/themes/shared/zen-icons/icons.css b/src/browser/themes/shared/zen-icons/icons.css
index 1f7779e35..cb1b6f892 100644
--- a/src/browser/themes/shared/zen-icons/icons.css
+++ b/src/browser/themes/shared/zen-icons/icons.css
@@ -924,7 +924,7 @@
position: relative;
- list-style-image: url("paintbrush.svg");
+ list-style-image: url("boost.svg");
& .toolbarbutton-text {
display: none;
@@ -937,6 +937,8 @@
& image {
-moz-context-properties: fill, fill-opacity;
fill: currentColor;
+ width: 14px;
+ fill-opacity: 0.7;
}
}
@@ -1066,7 +1068,7 @@
}
#zen-boost-shuffle {
- list-style-image: url("arrow-rotate-anticlockwise.svg");
+ list-style-image: url("dice.svg");
}
#zen-boost-css-picker {
diff --git a/src/browser/themes/shared/zen-icons/jar.inc.mn b/src/browser/themes/shared/zen-icons/jar.inc.mn
index be1479dcf..b252b3f52 100644
--- a/src/browser/themes/shared/zen-icons/jar.inc.mn
+++ b/src/browser/themes/shared/zen-icons/jar.inc.mn
@@ -13,13 +13,13 @@
* skin/classic/browser/zen-icons/autoplay-media-fill.svg (../shared/zen-icons/nucleo/autoplay-media-fill.svg)
* skin/classic/browser/zen-icons/autoplay-media.svg (../shared/zen-icons/nucleo/autoplay-media.svg)
* skin/classic/browser/zen-icons/back.svg (../shared/zen-icons/nucleo/back.svg)
+* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
* skin/classic/browser/zen-icons/block.svg (../shared/zen-icons/nucleo/block.svg)
* skin/classic/browser/zen-icons/bolt.svg (../shared/zen-icons/nucleo/bolt.svg)
* skin/classic/browser/zen-icons/bookmark-hollow.svg (../shared/zen-icons/nucleo/bookmark-hollow.svg)
* skin/classic/browser/zen-icons/bookmark-star-on-tray.svg (../shared/zen-icons/nucleo/bookmark-star-on-tray.svg)
* skin/classic/browser/zen-icons/bookmark.svg (../shared/zen-icons/nucleo/bookmark.svg)
* skin/classic/browser/zen-icons/boost.svg (../shared/zen-icons/nucleo/boost.svg)
-* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
* skin/classic/browser/zen-icons/brackets-curly.svg (../shared/zen-icons/nucleo/brackets-curly.svg)
* skin/classic/browser/zen-icons/camera-blocked.svg (../shared/zen-icons/nucleo/camera-blocked.svg)
* skin/classic/browser/zen-icons/camera-fill.svg (../shared/zen-icons/nucleo/camera-fill.svg)
@@ -37,6 +37,7 @@
* skin/classic/browser/zen-icons/desktop-notification-fill.svg (../shared/zen-icons/nucleo/desktop-notification-fill.svg)
* skin/classic/browser/zen-icons/desktop-notification.svg (../shared/zen-icons/nucleo/desktop-notification.svg)
* skin/classic/browser/zen-icons/developer.svg (../shared/zen-icons/nucleo/developer.svg)
+* skin/classic/browser/zen-icons/dice.svg (../shared/zen-icons/nucleo/dice.svg)
* skin/classic/browser/zen-icons/downloads.svg (../shared/zen-icons/nucleo/downloads.svg)
* skin/classic/browser/zen-icons/drag-indicator.svg (../shared/zen-icons/nucleo/drag-indicator.svg)
* skin/classic/browser/zen-icons/duplicate-tab.svg (../shared/zen-icons/nucleo/duplicate-tab.svg)
@@ -102,26 +103,26 @@
* skin/classic/browser/zen-icons/popup-fill.svg (../shared/zen-icons/nucleo/popup-fill.svg)
* skin/classic/browser/zen-icons/popup.svg (../shared/zen-icons/nucleo/popup.svg)
* skin/classic/browser/zen-icons/print.svg (../shared/zen-icons/nucleo/print.svg)
-* skin/classic/browser/zen-icons/private-window.svg (../shared/zen-icons/nucleo/private-window.svg)
* skin/classic/browser/zen-icons/privateBrowsing.svg (../shared/zen-icons/nucleo/privateBrowsing.svg)
+* skin/classic/browser/zen-icons/private-window.svg (../shared/zen-icons/nucleo/private-window.svg)
* skin/classic/browser/zen-icons/reader-mode.svg (../shared/zen-icons/nucleo/reader-mode.svg)
* skin/classic/browser/zen-icons/reload.svg (../shared/zen-icons/nucleo/reload.svg)
* skin/classic/browser/zen-icons/save.svg (../shared/zen-icons/nucleo/save.svg)
* skin/classic/browser/zen-icons/screen-blocked.svg (../shared/zen-icons/nucleo/screen-blocked.svg)
-* skin/classic/browser/zen-icons/screen.svg (../shared/zen-icons/nucleo/screen.svg)
* skin/classic/browser/zen-icons/screenshot.svg (../shared/zen-icons/nucleo/screenshot.svg)
+* skin/classic/browser/zen-icons/screen.svg (../shared/zen-icons/nucleo/screen.svg)
* skin/classic/browser/zen-icons/search-glass.svg (../shared/zen-icons/nucleo/search-glass.svg)
* skin/classic/browser/zen-icons/search-page.svg (../shared/zen-icons/nucleo/search-page.svg)
* skin/classic/browser/zen-icons/security-broken.svg (../shared/zen-icons/nucleo/security-broken.svg)
-* skin/classic/browser/zen-icons/security-warning.svg (../shared/zen-icons/nucleo/security-warning.svg)
* skin/classic/browser/zen-icons/security.svg (../shared/zen-icons/nucleo/security.svg)
+* skin/classic/browser/zen-icons/security-warning.svg (../shared/zen-icons/nucleo/security-warning.svg)
* skin/classic/browser/zen-icons/send-to-device.svg (../shared/zen-icons/nucleo/send-to-device.svg)
* skin/classic/browser/zen-icons/settings-fill.svg (../shared/zen-icons/nucleo/settings-fill.svg)
* skin/classic/browser/zen-icons/settings.svg (../shared/zen-icons/nucleo/settings.svg)
* skin/classic/browser/zen-icons/share.svg (../shared/zen-icons/nucleo/share.svg)
* skin/classic/browser/zen-icons/sidebar-right.svg (../shared/zen-icons/nucleo/sidebar-right.svg)
-* skin/classic/browser/zen-icons/sidebar.svg (../shared/zen-icons/nucleo/sidebar.svg)
* skin/classic/browser/zen-icons/sidebars-right.svg (../shared/zen-icons/nucleo/sidebars-right.svg)
+* skin/classic/browser/zen-icons/sidebar.svg (../shared/zen-icons/nucleo/sidebar.svg)
* skin/classic/browser/zen-icons/sliders.svg (../shared/zen-icons/nucleo/sliders.svg)
* skin/classic/browser/zen-icons/sparkles.svg (../shared/zen-icons/nucleo/sparkles.svg)
* skin/classic/browser/zen-icons/spell-check.svg (../shared/zen-icons/nucleo/spell-check.svg)
@@ -161,13 +162,13 @@
* skin/classic/browser/zen-icons/autoplay-media-fill.svg (../shared/zen-icons/nucleo/autoplay-media-fill.svg)
* skin/classic/browser/zen-icons/autoplay-media.svg (../shared/zen-icons/nucleo/autoplay-media.svg)
* skin/classic/browser/zen-icons/back.svg (../shared/zen-icons/nucleo/back.svg)
+* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
* skin/classic/browser/zen-icons/block.svg (../shared/zen-icons/nucleo/block.svg)
* skin/classic/browser/zen-icons/bolt.svg (../shared/zen-icons/nucleo/bolt.svg)
* skin/classic/browser/zen-icons/bookmark-hollow.svg (../shared/zen-icons/nucleo/bookmark-hollow.svg)
* skin/classic/browser/zen-icons/bookmark-star-on-tray.svg (../shared/zen-icons/nucleo/bookmark-star-on-tray.svg)
* skin/classic/browser/zen-icons/bookmark.svg (../shared/zen-icons/nucleo/bookmark.svg)
* skin/classic/browser/zen-icons/boost.svg (../shared/zen-icons/nucleo/boost.svg)
-* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
* skin/classic/browser/zen-icons/brackets-curly.svg (../shared/zen-icons/nucleo/brackets-curly.svg)
* skin/classic/browser/zen-icons/camera-blocked.svg (../shared/zen-icons/nucleo/camera-blocked.svg)
* skin/classic/browser/zen-icons/camera-fill.svg (../shared/zen-icons/nucleo/camera-fill.svg)
@@ -185,6 +186,7 @@
* skin/classic/browser/zen-icons/desktop-notification-fill.svg (../shared/zen-icons/nucleo/desktop-notification-fill.svg)
* skin/classic/browser/zen-icons/desktop-notification.svg (../shared/zen-icons/nucleo/desktop-notification.svg)
* skin/classic/browser/zen-icons/developer.svg (../shared/zen-icons/nucleo/developer.svg)
+* skin/classic/browser/zen-icons/dice.svg (../shared/zen-icons/nucleo/dice.svg)
* skin/classic/browser/zen-icons/downloads.svg (../shared/zen-icons/nucleo/downloads.svg)
* skin/classic/browser/zen-icons/drag-indicator.svg (../shared/zen-icons/nucleo/drag-indicator.svg)
* skin/classic/browser/zen-icons/duplicate-tab.svg (../shared/zen-icons/nucleo/duplicate-tab.svg)
@@ -250,26 +252,26 @@
* skin/classic/browser/zen-icons/popup-fill.svg (../shared/zen-icons/nucleo/popup-fill.svg)
* skin/classic/browser/zen-icons/popup.svg (../shared/zen-icons/nucleo/popup.svg)
* skin/classic/browser/zen-icons/print.svg (../shared/zen-icons/nucleo/print.svg)
-* skin/classic/browser/zen-icons/private-window.svg (../shared/zen-icons/nucleo/private-window.svg)
* skin/classic/browser/zen-icons/privateBrowsing.svg (../shared/zen-icons/nucleo/privateBrowsing.svg)
+* skin/classic/browser/zen-icons/private-window.svg (../shared/zen-icons/nucleo/private-window.svg)
* skin/classic/browser/zen-icons/reader-mode.svg (../shared/zen-icons/nucleo/reader-mode.svg)
* skin/classic/browser/zen-icons/reload.svg (../shared/zen-icons/nucleo/reload.svg)
* skin/classic/browser/zen-icons/save.svg (../shared/zen-icons/nucleo/save.svg)
* skin/classic/browser/zen-icons/screen-blocked.svg (../shared/zen-icons/nucleo/screen-blocked.svg)
-* skin/classic/browser/zen-icons/screen.svg (../shared/zen-icons/nucleo/screen.svg)
* skin/classic/browser/zen-icons/screenshot.svg (../shared/zen-icons/nucleo/screenshot.svg)
+* skin/classic/browser/zen-icons/screen.svg (../shared/zen-icons/nucleo/screen.svg)
* skin/classic/browser/zen-icons/search-glass.svg (../shared/zen-icons/nucleo/search-glass.svg)
* skin/classic/browser/zen-icons/search-page.svg (../shared/zen-icons/nucleo/search-page.svg)
* skin/classic/browser/zen-icons/security-broken.svg (../shared/zen-icons/nucleo/security-broken.svg)
-* skin/classic/browser/zen-icons/security-warning.svg (../shared/zen-icons/nucleo/security-warning.svg)
* skin/classic/browser/zen-icons/security.svg (../shared/zen-icons/nucleo/security.svg)
+* skin/classic/browser/zen-icons/security-warning.svg (../shared/zen-icons/nucleo/security-warning.svg)
* skin/classic/browser/zen-icons/send-to-device.svg (../shared/zen-icons/nucleo/send-to-device.svg)
* skin/classic/browser/zen-icons/settings-fill.svg (../shared/zen-icons/nucleo/settings-fill.svg)
* skin/classic/browser/zen-icons/settings.svg (../shared/zen-icons/nucleo/settings.svg)
* skin/classic/browser/zen-icons/share.svg (../shared/zen-icons/nucleo/share.svg)
* skin/classic/browser/zen-icons/sidebar-right.svg (../shared/zen-icons/nucleo/sidebar-right.svg)
-* skin/classic/browser/zen-icons/sidebar.svg (../shared/zen-icons/nucleo/sidebar.svg)
* skin/classic/browser/zen-icons/sidebars-right.svg (../shared/zen-icons/nucleo/sidebars-right.svg)
+* skin/classic/browser/zen-icons/sidebar.svg (../shared/zen-icons/nucleo/sidebar.svg)
* skin/classic/browser/zen-icons/sliders.svg (../shared/zen-icons/nucleo/sliders.svg)
* skin/classic/browser/zen-icons/sparkles.svg (../shared/zen-icons/nucleo/sparkles.svg)
* skin/classic/browser/zen-icons/spell-check.svg (../shared/zen-icons/nucleo/spell-check.svg)
@@ -309,13 +311,13 @@
* skin/classic/browser/zen-icons/autoplay-media-fill.svg (../shared/zen-icons/nucleo/autoplay-media-fill.svg)
* skin/classic/browser/zen-icons/autoplay-media.svg (../shared/zen-icons/nucleo/autoplay-media.svg)
* skin/classic/browser/zen-icons/back.svg (../shared/zen-icons/nucleo/back.svg)
+* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
* skin/classic/browser/zen-icons/block.svg (../shared/zen-icons/nucleo/block.svg)
* skin/classic/browser/zen-icons/bolt.svg (../shared/zen-icons/nucleo/bolt.svg)
* skin/classic/browser/zen-icons/bookmark-hollow.svg (../shared/zen-icons/nucleo/bookmark-hollow.svg)
* skin/classic/browser/zen-icons/bookmark-star-on-tray.svg (../shared/zen-icons/nucleo/bookmark-star-on-tray.svg)
* skin/classic/browser/zen-icons/bookmark.svg (../shared/zen-icons/nucleo/bookmark.svg)
* skin/classic/browser/zen-icons/boost.svg (../shared/zen-icons/nucleo/boost.svg)
-* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
* skin/classic/browser/zen-icons/brackets-curly.svg (../shared/zen-icons/nucleo/brackets-curly.svg)
* skin/classic/browser/zen-icons/camera-blocked.svg (../shared/zen-icons/nucleo/camera-blocked.svg)
* skin/classic/browser/zen-icons/camera-fill.svg (../shared/zen-icons/nucleo/camera-fill.svg)
@@ -333,6 +335,7 @@
* skin/classic/browser/zen-icons/desktop-notification-fill.svg (../shared/zen-icons/nucleo/desktop-notification-fill.svg)
* skin/classic/browser/zen-icons/desktop-notification.svg (../shared/zen-icons/nucleo/desktop-notification.svg)
* skin/classic/browser/zen-icons/developer.svg (../shared/zen-icons/nucleo/developer.svg)
+* skin/classic/browser/zen-icons/dice.svg (../shared/zen-icons/nucleo/dice.svg)
* skin/classic/browser/zen-icons/downloads.svg (../shared/zen-icons/nucleo/downloads.svg)
* skin/classic/browser/zen-icons/drag-indicator.svg (../shared/zen-icons/nucleo/drag-indicator.svg)
* skin/classic/browser/zen-icons/duplicate-tab.svg (../shared/zen-icons/nucleo/duplicate-tab.svg)
@@ -398,26 +401,26 @@
* skin/classic/browser/zen-icons/popup-fill.svg (../shared/zen-icons/nucleo/popup-fill.svg)
* skin/classic/browser/zen-icons/popup.svg (../shared/zen-icons/nucleo/popup.svg)
* skin/classic/browser/zen-icons/print.svg (../shared/zen-icons/nucleo/print.svg)
-* skin/classic/browser/zen-icons/private-window.svg (../shared/zen-icons/nucleo/private-window.svg)
* skin/classic/browser/zen-icons/privateBrowsing.svg (../shared/zen-icons/nucleo/privateBrowsing.svg)
+* skin/classic/browser/zen-icons/private-window.svg (../shared/zen-icons/nucleo/private-window.svg)
* skin/classic/browser/zen-icons/reader-mode.svg (../shared/zen-icons/nucleo/reader-mode.svg)
* skin/classic/browser/zen-icons/reload.svg (../shared/zen-icons/nucleo/reload.svg)
* skin/classic/browser/zen-icons/save.svg (../shared/zen-icons/nucleo/save.svg)
* skin/classic/browser/zen-icons/screen-blocked.svg (../shared/zen-icons/nucleo/screen-blocked.svg)
-* skin/classic/browser/zen-icons/screen.svg (../shared/zen-icons/nucleo/screen.svg)
* skin/classic/browser/zen-icons/screenshot.svg (../shared/zen-icons/nucleo/screenshot.svg)
+* skin/classic/browser/zen-icons/screen.svg (../shared/zen-icons/nucleo/screen.svg)
* skin/classic/browser/zen-icons/search-glass.svg (../shared/zen-icons/nucleo/search-glass.svg)
* skin/classic/browser/zen-icons/search-page.svg (../shared/zen-icons/nucleo/search-page.svg)
* skin/classic/browser/zen-icons/security-broken.svg (../shared/zen-icons/nucleo/security-broken.svg)
-* skin/classic/browser/zen-icons/security-warning.svg (../shared/zen-icons/nucleo/security-warning.svg)
* skin/classic/browser/zen-icons/security.svg (../shared/zen-icons/nucleo/security.svg)
+* skin/classic/browser/zen-icons/security-warning.svg (../shared/zen-icons/nucleo/security-warning.svg)
* skin/classic/browser/zen-icons/send-to-device.svg (../shared/zen-icons/nucleo/send-to-device.svg)
* skin/classic/browser/zen-icons/settings-fill.svg (../shared/zen-icons/nucleo/settings-fill.svg)
* skin/classic/browser/zen-icons/settings.svg (../shared/zen-icons/nucleo/settings.svg)
* skin/classic/browser/zen-icons/share.svg (../shared/zen-icons/nucleo/share.svg)
* skin/classic/browser/zen-icons/sidebar-right.svg (../shared/zen-icons/nucleo/sidebar-right.svg)
-* skin/classic/browser/zen-icons/sidebar.svg (../shared/zen-icons/nucleo/sidebar.svg)
* skin/classic/browser/zen-icons/sidebars-right.svg (../shared/zen-icons/nucleo/sidebars-right.svg)
+* skin/classic/browser/zen-icons/sidebar.svg (../shared/zen-icons/nucleo/sidebar.svg)
* skin/classic/browser/zen-icons/sliders.svg (../shared/zen-icons/nucleo/sliders.svg)
* skin/classic/browser/zen-icons/sparkles.svg (../shared/zen-icons/nucleo/sparkles.svg)
* skin/classic/browser/zen-icons/spell-check.svg (../shared/zen-icons/nucleo/spell-check.svg)
@@ -453,8 +456,8 @@
* skin/classic/browser/zen-icons/selectable/basket.svg (../shared/zen-icons/common/selectable/basket.svg)
* skin/classic/browser/zen-icons/selectable/bed.svg (../shared/zen-icons/common/selectable/bed.svg)
* skin/classic/browser/zen-icons/selectable/bell.svg (../shared/zen-icons/common/selectable/bell.svg)
-* skin/classic/browser/zen-icons/selectable/book.svg (../shared/zen-icons/common/selectable/book.svg)
* skin/classic/browser/zen-icons/selectable/bookmark.svg (../shared/zen-icons/common/selectable/bookmark.svg)
+* skin/classic/browser/zen-icons/selectable/book.svg (../shared/zen-icons/common/selectable/book.svg)
* skin/classic/browser/zen-icons/selectable/briefcase.svg (../shared/zen-icons/common/selectable/briefcase.svg)
* skin/classic/browser/zen-icons/selectable/brush.svg (../shared/zen-icons/common/selectable/brush.svg)
* skin/classic/browser/zen-icons/selectable/bug.svg (../shared/zen-icons/common/selectable/bug.svg)
@@ -516,8 +519,8 @@
* skin/classic/browser/zen-icons/selectable/shapes.svg (../shared/zen-icons/common/selectable/shapes.svg)
* skin/classic/browser/zen-icons/selectable/shirt.svg (../shared/zen-icons/common/selectable/shirt.svg)
* skin/classic/browser/zen-icons/selectable/skull.svg (../shared/zen-icons/common/selectable/skull.svg)
-* skin/classic/browser/zen-icons/selectable/square.svg (../shared/zen-icons/common/selectable/square.svg)
* skin/classic/browser/zen-icons/selectable/squares.svg (../shared/zen-icons/common/selectable/squares.svg)
+* skin/classic/browser/zen-icons/selectable/square.svg (../shared/zen-icons/common/selectable/square.svg)
* skin/classic/browser/zen-icons/selectable/star-1.svg (../shared/zen-icons/common/selectable/star-1.svg)
* skin/classic/browser/zen-icons/selectable/star.svg (../shared/zen-icons/common/selectable/star.svg)
* skin/classic/browser/zen-icons/selectable/stats-chart.svg (../shared/zen-icons/common/selectable/stats-chart.svg)
diff --git a/src/browser/themes/shared/zen-icons/nucleo/dice.svg b/src/browser/themes/shared/zen-icons/nucleo/dice.svg
new file mode 100644
index 000000000..7a6197e79
--- /dev/null
+++ b/src/browser/themes/shared/zen-icons/nucleo/dice.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/.
+
diff --git a/src/gfx/layers/AnimationInfo-cpp.patch b/src/gfx/layers/AnimationInfo-cpp.patch
index e9779a101..aa58cdcbc 100644
--- a/src/gfx/layers/AnimationInfo-cpp.patch
+++ b/src/gfx/layers/AnimationInfo-cpp.patch
@@ -1,5 +1,5 @@
diff --git a/gfx/layers/AnimationInfo.cpp b/gfx/layers/AnimationInfo.cpp
-index 1d330056bd7a4e89aac5e5296a3c164fb42b5c42..ef112715580b6bb7238e8f37bbe3133e187685dc 100644
+index 1d330056bd7a4e89aac5e5296a3c164fb42b5c42..38bfbcfcaf0c791ee817aafbd24b1cad67974e62 100644
--- a/gfx/layers/AnimationInfo.cpp
+++ b/gfx/layers/AnimationInfo.cpp
@@ -14,6 +14,7 @@
@@ -10,12 +10,22 @@ index 1d330056bd7a4e89aac5e5296a3c164fb42b5c42..ef112715580b6bb7238e8f37bbe3133e
#include "nsIContent.h"
#include "nsLayoutUtils.h"
#include "nsRefreshDriver.h"
-@@ -343,7 +344,7 @@ static void SetAnimatable(NonCustomCSSPropertyId aProperty,
+@@ -343,7 +344,17 @@ static void SetAnimatable(NonCustomCSSPropertyId aProperty,
// resolve currentColor at this moment.
nscolor foreground =
aFrame->Style()->GetVisitedDependentColor(&nsStyleText::mColor);
- aAnimatable = aAnimationValue.GetColor(foreground);
-+ aAnimatable = zen::nsZenBoostsBackend::FilterColorFromPresContext(aAnimationValue.GetColor(foreground), aFrame->PresContext());
++ nscolor resolved = aAnimationValue.GetColor(foreground);
++ // |foreground| is already boost-resolved through
++ // StyleAbsoluteColor::ToColor, so a currentColor keyframe is already
++ // filtered; only absolute keyframe colors still need the boost applied
++ // here, exactly once, so the composited transition endpoint matches the
++ // resting/static paint and doesn't snap when the transition ends.
++ aAnimatable =
++ aAnimationValue.IsCurrentColor()
++ ? resolved
++ : zen::nsZenBoostsBackend::FilterColorFromPresContext(
++ resolved, aFrame->PresContext());
break;
}
case eCSSProperty_opacity:
diff --git a/src/layout/style/StyleColor-cpp.patch b/src/layout/style/StyleColor-cpp.patch
index 6f84f6327..ef81c99dc 100644
--- a/src/layout/style/StyleColor-cpp.patch
+++ b/src/layout/style/StyleColor-cpp.patch
@@ -1,5 +1,5 @@
diff --git a/layout/style/StyleColor.cpp b/layout/style/StyleColor.cpp
-index 95c7ae6abea5032bef0466e8d59d212374d7a4d0..8dbfbb846b786d51af288989163aacfae12e787c 100644
+index 95c7ae6abea5032bef0466e8d59d212374d7a4d0..3b2118e224141f5151a31ac663dfbe17864ef182 100644
--- a/layout/style/StyleColor.cpp
+++ b/layout/style/StyleColor.cpp
@@ -8,6 +8,7 @@
@@ -10,25 +10,7 @@ index 95c7ae6abea5032bef0466e8d59d212374d7a4d0..8dbfbb846b786d51af288989163aacfa
namespace mozilla {
-@@ -21,6 +22,8 @@ bool StyleColor::MaybeTransparent() const {
- template <>
- StyleAbsoluteColor StyleColor::ResolveColor(
- const StyleAbsoluteColor& aForegroundColor) const {
-+ auto ResolveColorInner = [this,
-+ &aForegroundColor]() -> StyleAbsoluteColor {
- if (IsAbsolute()) {
- return AsAbsolute();
- }
-@@ -30,6 +33,8 @@ StyleAbsoluteColor StyleColor::ResolveColor(
- }
-
- return Servo_ResolveColor(this, &aForegroundColor);
-+ };
-+ return zen::nsZenBoostsBackend::ResolveStyleColor(ResolveColorInner());
- }
-
- template <>
-@@ -68,10 +73,11 @@ nscolor StyleAbsoluteColor::ToColor() const {
+@@ -68,10 +69,11 @@ nscolor StyleAbsoluteColor::ToColor() const {
auto green = std::clamp(srgb.components._1, 0.0f, 1.0f);
auto blue = std::clamp(srgb.components._2, 0.0f, 1.0f);
diff --git a/src/zen/boosts/ZenBoostsManager.sys.mjs b/src/zen/boosts/ZenBoostsManager.sys.mjs
index 67abb7bc4..0525b5378 100644
--- a/src/zen/boosts/ZenBoostsManager.sys.mjs
+++ b/src/zen/boosts/ZenBoostsManager.sys.mjs
@@ -539,6 +539,12 @@ class nsZenBoostsManager {
const directoryPath = this.#cssPath;
const savePath = PathUtils.join(directoryPath, fileName);
+ if (!css || css.trim() === "") {
+ if (await IOUtils.exists(savePath)) {
+ await IOUtils.remove(savePath);
+ }
+ return;
+ }
await IOUtils.makeDirectory(directoryPath, { createAncestors: true });
await IOUtils.writeUTF8(savePath, css);
}
diff --git a/src/zen/boosts/ZenZapOverlayChild.sys.mjs b/src/zen/boosts/ZenZapOverlayChild.sys.mjs
index df7c10cc0..7b8b4ed35 100644
--- a/src/zen/boosts/ZenZapOverlayChild.sys.mjs
+++ b/src/zen/boosts/ZenZapOverlayChild.sys.mjs
@@ -26,6 +26,7 @@ export class ZapOverlay {
#dissolvePoolSize = 5;
#dissolveEffectPool = [];
#currentDissolveIndex = 0;
+ #onZapDoneClick = null;
/**
* @param {*} document Webpage document
@@ -77,10 +78,8 @@ export class ZapOverlay {
*/
#initializeElements() {
this.zapDoneButton = this.getElementById("zap-done");
- this.zapDoneButton.addEventListener(
- "click",
- this.#disableZapMode.bind(this)
- );
+ this.#onZapDoneClick = this.#disableZapMode.bind(this);
+ this.zapDoneButton.addEventListener("click", this.#onZapDoneClick);
this.#updateZappedList();
}
@@ -355,6 +354,12 @@ export class ZapOverlay {
dissolve.tearDown();
});
+ if (this.zapDoneButton && this.#onZapDoneClick) {
+ this.zapDoneButton.removeEventListener("click", this.#onZapDoneClick);
+ }
+ this.#onZapDoneClick = null;
+ this.zapDoneButton = null;
+
if (this.#content) {
try {
this.document.removeAnonymousContent(this.#content);
diff --git a/src/zen/boosts/actors/ZenBoostsChild.sys.mjs b/src/zen/boosts/actors/ZenBoostsChild.sys.mjs
index 1ea6b1e60..78260f30a 100644
--- a/src/zen/boosts/actors/ZenBoostsChild.sys.mjs
+++ b/src/zen/boosts/actors/ZenBoostsChild.sys.mjs
@@ -322,13 +322,18 @@ export class ZenBoostsChild extends JSWindowActorChild {
return p;
}
+ get #hostWithoutPort() {
+ const host = this.browsingContext.topWindow?.location.host;
+ return host?.split(":")[0];
+ }
+
/**
* Aquires the boost data for this website
*
* @returns {object} Boost data for the current website
*/
getWebsiteBoost() {
- const domain = this.browsingContext.topWindow?.location?.host;
+ const domain = this.#hostWithoutPort;
if (!domain) {
return null;
}
@@ -362,6 +367,7 @@ export class ZenBoostsChild extends JSWindowActorChild {
this.#loadStyleSheet(boost.styleSheet);
}
+ browsingContext.fullZoom = boostData.sizeOverride;
browsingContext.isZenBoostsInverted = boostData.smartInvert;
if (boostData.enableColorBoost) {
let primaryColor;
@@ -484,7 +490,7 @@ export class ZenBoostsChild extends JSWindowActorChild {
}
addZapSelector(selector) {
- const domain = this.browsingContext.topWindow?.location?.host;
+ const domain = this.#hostWithoutPort;
this.sendQuery("ZenBoost:ZapSelector", {
action: "add",
selector,
@@ -493,7 +499,7 @@ export class ZenBoostsChild extends JSWindowActorChild {
}
removeZapSelector(selector) {
- const domain = this.browsingContext.topWindow?.location?.host;
+ const domain = this.#hostWithoutPort;
this.sendQuery("ZenBoost:ZapSelector", {
action: "remove",
selector,
diff --git a/src/zen/boosts/gtest/TestZenBoostsColorFilter.cpp b/src/zen/boosts/gtest/TestZenBoostsColorFilter.cpp
new file mode 100644
index 000000000..e378451d0
--- /dev/null
+++ b/src/zen/boosts/gtest/TestZenBoostsColorFilter.cpp
@@ -0,0 +1,121 @@
+/* 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::FilterColorChannel;
+using zen::detail::InvertColorChannel;
+using zen::detail::PrecomputeAccent;
+using zen::detail::RotateAccent;
+
+namespace {
+
+// A spread of representative input colors (opaque unless noted).
+const nscolor kColors[] = {
+ NS_RGBA(255, 0, 0, 255), // pure red
+ NS_RGBA(0, 255, 0, 255), // pure green
+ NS_RGBA(0, 0, 255, 255), // pure blue
+ NS_RGBA(0, 0, 0, 255), // black
+ NS_RGBA(255, 255, 255, 255), // white
+ NS_RGBA(128, 128, 128, 255), // mid gray
+ NS_RGBA(18, 52, 86, 200), // arbitrary, semi-transparent
+ NS_RGBA(240, 17, 99, 1), // near-min alpha
+};
+
+// The accent stores the contrast/strength in its alpha byte
+// (NS_GET_CONTRAST == NS_GET_A). 0 means "no tint".
+zen::nsZenAccentOklab MakeAccent(uint8_t r, uint8_t g, uint8_t b,
+ uint8_t contrast) {
+ return PrecomputeAccent(NS_RGBA(r, g, b, contrast));
+}
+
+} // namespace
+
+// The headline invariant: filtering must never change opacity. The whole
+// pipeline overloads the alpha byte for contrast on the *accent*, but a
+// filtered *content* color must keep its original alpha.
+TEST(ZenBoostsColorFilter, PreservesAlpha)
+{
+ const zen::nsZenAccentOklab accent = MakeAccent(80, 120, 200, 180);
+ const zen::nsZenAccentOklab complementary = RotateAccent(accent, 180.0f);
+
+ for (nscolor c : kColors) {
+ const nscolor out = FilterColorChannel(c, accent, complementary);
+ EXPECT_EQ(NS_GET_A(out), NS_GET_A(c)) << "alpha changed for input " << c;
+ }
+}
+
+// Fully transparent colors are invisible; the filter must pass them through
+// untouched (and must not interpret their zero alpha as contrast).
+TEST(ZenBoostsColorFilter, TransparentPassthrough)
+{
+ const zen::nsZenAccentOklab accent = MakeAccent(80, 120, 200, 180);
+ const zen::nsZenAccentOklab complementary = RotateAccent(accent, 90.0f);
+
+ const nscolor transparent = NS_RGBA(255, 0, 0, 0);
+ EXPECT_EQ(FilterColorChannel(transparent, accent, complementary),
+ transparent);
+ EXPECT_EQ(InvertColorChannel(transparent), transparent);
+}
+
+// Same inputs must always yield the same output (no hidden global state in
+// the math itself; the production cache lives outside these primitives).
+TEST(ZenBoostsColorFilter, Deterministic)
+{
+ const zen::nsZenAccentOklab accent = MakeAccent(33, 200, 90, 200);
+ const zen::nsZenAccentOklab complementary = RotateAccent(accent, 200.0f);
+
+ for (nscolor c : kColors) {
+ const nscolor a = FilterColorChannel(c, accent, complementary);
+ const nscolor b = FilterColorChannel(c, accent, complementary);
+ EXPECT_EQ(a, b);
+ EXPECT_EQ(InvertColorChannel(c), InvertColorChannel(c));
+ }
+}
+
+// A zero-contrast accent means "no boost strength": the color must come back
+// essentially unchanged (allow +/-1 per channel for sRGB<->Oklab rounding).
+TEST(ZenBoostsColorFilter, ZeroContrastIsNearIdentity)
+{
+ const zen::nsZenAccentOklab accent = MakeAccent(200, 50, 50, 0);
+ const zen::nsZenAccentOklab complementary = RotateAccent(accent, 180.0f);
+
+ for (nscolor c : kColors) {
+ if (NS_GET_A(c) == 0) {
+ continue;
+ }
+ const nscolor out = FilterColorChannel(c, accent, complementary);
+ EXPECT_NEAR(NS_GET_R(out), NS_GET_R(c), 1);
+ EXPECT_NEAR(NS_GET_G(out), NS_GET_G(c), 1);
+ EXPECT_NEAR(NS_GET_B(out), NS_GET_B(c), 1);
+ EXPECT_EQ(NS_GET_A(out), NS_GET_A(c));
+ }
+}
+
+// Guards against a regression that turns the filter into a no-op: a strong
+// accent applied to a neutral gray must actually move the color.
+TEST(ZenBoostsColorFilter, StrongAccentActuallyTints)
+{
+ const zen::nsZenAccentOklab accent = MakeAccent(20, 130, 240, 255);
+ const zen::nsZenAccentOklab complementary = RotateAccent(accent, 30.0f);
+
+ const nscolor gray = NS_RGBA(128, 128, 128, 255);
+ const nscolor out = FilterColorChannel(gray, accent, complementary);
+
+ const bool moved = NS_GET_R(out) != NS_GET_R(gray) ||
+ NS_GET_G(out) != NS_GET_G(gray) ||
+ NS_GET_B(out) != NS_GET_B(gray);
+ EXPECT_TRUE(moved) << "a full-strength accent should tint mid gray";
+ EXPECT_EQ(NS_GET_A(out), NS_GET_A(gray));
+}
+
+// Inversion must also preserve opacity.
+TEST(ZenBoostsColorFilter, InvertPreservesAlpha)
+{
+ for (nscolor c : kColors) {
+ EXPECT_EQ(NS_GET_A(InvertColorChannel(c)), NS_GET_A(c));
+ }
+}
diff --git a/src/zen/boosts/gtest/moz.build b/src/zen/boosts/gtest/moz.build
new file mode 100644
index 000000000..cc14100b7
--- /dev/null
+++ b/src/zen/boosts/gtest/moz.build
@@ -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/.
+
+UNIFIED_SOURCES += [
+ "TestZenBoostsColorFilter.cpp",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/src/zen/boosts/moz.build b/src/zen/boosts/moz.build
index 211d68fc2..a436cf3de 100644
--- a/src/zen/boosts/moz.build
+++ b/src/zen/boosts/moz.build
@@ -26,3 +26,7 @@ SOURCES += [
]
FINAL_LIBRARY = "xul"
+
+TEST_DIRS += [
+ "gtest",
+]
diff --git a/src/zen/boosts/nsZenBCOverrides.cpp b/src/zen/boosts/nsZenBCOverrides.cpp
index 0be59563e..306d35bf7 100644
--- a/src/zen/boosts/nsZenBCOverrides.cpp
+++ b/src/zen/boosts/nsZenBCOverrides.cpp
@@ -43,7 +43,7 @@ static void RefreshBoostCacheIfMatchesCurrent(BrowsingContext* aChanged) {
if (!backend) {
return;
}
- auto current = backend->GetCurrentBrowsingContext();
+ RefPtr current = backend->GetCurrentBrowsingContext();
if (!current || current->Top() != aChanged) {
return;
}
diff --git a/src/zen/boosts/nsZenBoostsBackend.cpp b/src/zen/boosts/nsZenBoostsBackend.cpp
index 1968c6ad1..1a54b0264 100644
--- a/src/zen/boosts/nsZenBoostsBackend.cpp
+++ b/src/zen/boosts/nsZenBoostsBackend.cpp
@@ -4,7 +4,9 @@
#include
#include
+#include
#include
+#include
#include "nsZenBoostsBackend.h"
@@ -48,10 +50,6 @@ namespace zen {
NS_IMPL_ISUPPORTS0(nsZenBoostsBackend)
-nsZenAccentOklab nsZenBoostsBackend::mCachedAccent{0};
-nsZenAccentOklab nsZenBoostsBackend::mCachedComplementary{0};
-float nsZenBoostsBackend::mCachedComplementaryRotationDeg = 0.0f;
-
namespace {
/**
@@ -83,17 +81,43 @@ static inline float linearToSrgb(float c) {
static inline float fastCbrt(float x) {
if (x == 0.0f) return 0.0f;
float a = std::abs(x);
- union {
- float f;
- uint32_t i;
- } u = {a};
- u.i = u.i / 3 + 0x2a504a2e;
- float y = u.f;
+ // Bit-level initial guess. Use memcpy rather than a union to avoid the
+ // undefined behaviour of type-punning through a union member in C++.
+ uint32_t i;
+ std::memcpy(&i, &a, sizeof(i));
+ i = i / 3 + 0x2a504a2e;
+ float y;
+ std::memcpy(&y, &i, sizeof(y));
y = (2.0f * y + a / (y * y)) * (1.0f / 3.0f);
y = (2.0f * y + a / (y * y)) * (1.0f / 3.0f);
return x < 0.0f ? -y : y;
}
+/**
+ * @brief sRGB(0..255) -> linear lookup table. The filter only ever feeds
+ * integer 8-bit channels through srgbToLinear, so the 256 possible results
+ * are precomputed once instead of calling std::pow three times per color on
+ * the per-color hot path. Built lazily on first use; the function-local
+ * static makes initialization thread-safe.
+ */
+static inline const std::array& SrgbLinearTable() {
+ static const std::array kTable = [] {
+ std::array table{};
+ for (int i = 0; i < 256; ++i) {
+ table[i] = srgbToLinear(i * (1.0f / 255.0f));
+ }
+ return table;
+ }();
+ return kTable;
+}
+
+/**
+ * @brief Linearizes an 8-bit sRGB channel via the precomputed table.
+ */
+static inline float srgbToLinear8(uint8_t aChannel) {
+ return SrgbLinearTable()[aChannel];
+}
+
/**
* @brief Precomputes the Oklab values for a given accent color. This allows us
* to efficiently apply the accent color as a filter to other colors without
@@ -107,13 +131,9 @@ ZEN_HOT_FUNCTION
inline static auto zenPrecomputeAccent(nscolor aAccentColor) {
constexpr float inv255 = 1.0f / 255.0f;
- const float r = NS_GET_R(aAccentColor) * inv255;
- const float g = NS_GET_G(aAccentColor) * inv255;
- const float b = NS_GET_B(aAccentColor) * inv255;
-
- const float lr = srgbToLinear(r);
- const float lg = srgbToLinear(g);
- const float lb = srgbToLinear(b);
+ const float lr = srgbToLinear8(NS_GET_R(aAccentColor));
+ const float lg = srgbToLinear8(NS_GET_G(aAccentColor));
+ const float lb = srgbToLinear8(NS_GET_B(aAccentColor));
const float l_ =
fastCbrt(0.4122214708f * lr + 0.5363325363f * lg + 0.0514459929f * lb);
@@ -156,6 +176,45 @@ inline static nsZenAccentOklab zenRotateAccent(const nsZenAccentOklab& aBase,
};
}
+/**
+ * @brief Small round-robin cache of precomputed accents. Painting several
+ * boosted tabs with different accents interleaved would otherwise recompute
+ * the Oklab base accent (with cbrt) and the rotated complementary accent on
+ * every single color. Keyed by the accent nscolor and the complementary hue
+ * rotation. Main-thread only (same threading assumption as the per-color
+ * paint path it serves).
+ */
+struct AccentCacheEntry {
+ nscolor accentNS = 0;
+ float rotationDeg = 0.0f;
+ bool valid = false;
+ nsZenAccentOklab accent{};
+ nsZenAccentOklab complementary{};
+};
+
+static constexpr size_t kAccentCacheSize = 4;
+static AccentCacheEntry sAccentCache[kAccentCacheSize];
+static size_t sAccentCacheNext = 0;
+
+ZEN_HOT_FUNCTION
+static const AccentCacheEntry& GetCachedAccent(nscolor aAccentNS,
+ float aRotationDeg) {
+ for (const auto& entry : sAccentCache) {
+ if (entry.valid && entry.accentNS == aAccentNS &&
+ entry.rotationDeg == aRotationDeg) {
+ return entry;
+ }
+ }
+ AccentCacheEntry& slot = sAccentCache[sAccentCacheNext];
+ sAccentCacheNext = (sAccentCacheNext + 1) % kAccentCacheSize;
+ slot.accentNS = aAccentNS;
+ slot.rotationDeg = aRotationDeg;
+ slot.accent = zenPrecomputeAccent(aAccentNS);
+ slot.complementary = zenRotateAccent(slot.accent, aRotationDeg);
+ slot.valid = true;
+ return slot;
+}
+
/**
* @brief Applies a duotone color filter to transform an original color toward
* one of two accent colors. The original color's perceived lightness decides
@@ -183,10 +242,10 @@ inline static nsZenAccentOklab zenRotateAccent(const nsZenAccentOklab& aBase,
constexpr float inv255 = 1.0f / 255.0f;
const float blendFactor = contrast * inv255;
- // sRGB -> linear
- const float lr = srgbToLinear(NS_GET_R(aOriginalColor) * inv255);
- const float lg = srgbToLinear(NS_GET_G(aOriginalColor) * inv255);
- const float lb = srgbToLinear(NS_GET_B(aOriginalColor) * inv255);
+ // sRGB -> linear (8-bit channels via the precomputed table)
+ const float lr = srgbToLinear8(NS_GET_R(aOriginalColor));
+ const float lg = srgbToLinear8(NS_GET_G(aOriginalColor));
+ const float lb = srgbToLinear8(NS_GET_B(aOriginalColor));
// Linear RGB -> LMS -> cube root -> Oklab (fused)
const float l_ =
@@ -347,6 +406,32 @@ inline static void GetZenBoostsDataFromBrowsingContext(
} // namespace
+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);
+}
+
+nsZenAccentOklab RotateAccent(const nsZenAccentOklab& aBase,
+ float aRotationDeg) {
+ return zenRotateAccent(aBase, aRotationDeg);
+}
+
+nscolor FilterColorChannel(nscolor aOriginalColor,
+ const nsZenAccentOklab& aAccent,
+ const nsZenAccentOklab& aComplementary) {
+ return zenFilterColorChannel(aOriginalColor, aAccent, aComplementary);
+}
+
+nscolor InvertColorChannel(nscolor aColor) {
+ return zenInvertColorChannel(aColor);
+}
+
+} // namespace detail
+
static mozilla::StaticRefPtr sZenBoostsBackend;
auto nsZenBoostsBackend::GetInstance() -> nsZenBoostsBackend* {
@@ -376,18 +461,25 @@ auto nsZenBoostsBackend::onPresShellEntered(mozilla::dom::Document* aDocument)
if (!browsingContext) {
return;
}
- mCurrentBrowsingContext = browsingContext;
+ mCurrentBrowsingContextId = browsingContext->Id();
RefreshCachedBoostState();
}
+already_AddRefed
+nsZenBoostsBackend::GetCurrentBrowsingContext() const {
+ return mozilla::dom::BrowsingContext::Get(mCurrentBrowsingContextId);
+}
+
auto nsZenBoostsBackend::RefreshCachedBoostState() -> void {
- if (!mCurrentBrowsingContext) {
+ RefPtr current =
+ mozilla::dom::BrowsingContext::Get(mCurrentBrowsingContextId);
+ if (!current) {
mCachedCurrentAccent = 0;
mCachedCurrentComplementaryRotation = 0.0f;
mCachedCurrentInverted = false;
return;
}
- auto top = mCurrentBrowsingContext->Top();
+ auto top = current->Top();
mCachedCurrentAccent = top->ZenBoostsData();
mCachedCurrentComplementaryRotation = top->ZenBoostsComplementaryRotation();
mCachedCurrentInverted = top->IsZenBoostsInverted();
@@ -397,33 +489,27 @@ auto nsZenBoostsBackend::RefreshCachedBoostState() -> void {
nsZenBoostsBackend::FilterColorFromPresContext(nscolor aColor,
nsPresContext* aPresContext)
-> nscolor {
+ if (NS_GET_A(aColor) == 0) {
+ // Skip processing fully transparent colors since they won't be visible and
+ // we want to avoid unnecessary computations. This also prevents issues with
+ // using the alpha channel for contrast information in the accent color.
+ return aColor;
+ }
ZenBoostData accentNS = 0;
float complementaryRotation = 0.0f;
bool invertColors = false;
GetZenBoostsDataFromBrowsingContext(&accentNS, &complementaryRotation,
&invertColors, aPresContext);
if (accentNS) {
- if (mCachedAccent.accentNS != accentNS) {
- mCachedAccent = zenPrecomputeAccent(accentNS);
- // Trigger a recompute of the complementary accent since
- // it depends on the base accent.
- mCachedComplementary.accentNS = 0;
- }
- // Derive the complementary accent by rotating the base accent's hue by the
- // boost's complementary rotation. Cached so the per-color hot path only
- // recomputes it when the base accent or rotation changes.
- if (mCachedComplementary.accentNS != accentNS ||
- mCachedComplementaryRotationDeg != complementaryRotation) {
- mCachedComplementary =
- zenRotateAccent(mCachedAccent, complementaryRotation);
- mCachedComplementaryRotationDeg = complementaryRotation;
- }
- // Apply a filter-like tint:
+ // Resolve (and cache) the base + complementary accent for this accent and
+ // complementary rotation. Apply a filter-like tint:
// - Preserve the original color's perceived luminance
// - Map hue/chroma toward the base or complementary accent depending on
// the original color's lightness
// - Keep the original alpha
- aColor = zenFilterColorChannel(aColor, mCachedAccent, mCachedComplementary);
+ const AccentCacheEntry& cached =
+ GetCachedAccent(accentNS, complementaryRotation);
+ aColor = zenFilterColorChannel(aColor, cached.accent, cached.complementary);
}
if (invertColors) {
aColor = zenInvertColorChannel(aColor);
@@ -431,20 +517,8 @@ nsZenBoostsBackend::FilterColorFromPresContext(nscolor aColor,
return aColor;
}
-[[nodiscard]] ZEN_HOT_FUNCTION auto nsZenBoostsBackend::ResolveStyleColor(
- mozilla::StyleAbsoluteColor aColor) -> mozilla::StyleAbsoluteColor {
- const auto resultColor = FilterColorFromPresContext(aColor.ToColor());
- return mozilla::StyleAbsoluteColor::FromColor(resultColor);
-}
-
[[nodiscard]] ZEN_HOT_FUNCTION auto nsZenBoostsBackend::ResolveStyleColor(
nscolor aColor) -> nscolor {
- if (NS_GET_A(aColor) == 0) {
- // Skip processing fully transparent colors since they won't be visible and
- // we want to avoid unnecessary computations. This also prevents issues with
- // using the alpha channel for contrast information in the accent color.
- return aColor;
- }
return FilterColorFromPresContext(aColor);
}
diff --git a/src/zen/boosts/nsZenBoostsBackend.h b/src/zen/boosts/nsZenBoostsBackend.h
index 67f3af6e8..81ca1d95b 100644
--- a/src/zen/boosts/nsZenBoostsBackend.h
+++ b/src/zen/boosts/nsZenBoostsBackend.h
@@ -10,6 +10,13 @@
#include "nsPresContext.h"
#include "mozilla/RefPtr.h"
+#include "mozilla/AlreadyAddRefed.h"
+
+#include
+
+namespace mozilla::dom {
+class BrowsingContext;
+}
#define ZEN_BOOSTS_BACKEND_CONTRACTID "@mozilla.org/zen/boosts-backend;1"
@@ -23,6 +30,19 @@ struct nsZenAccentOklab {
float contrastFactor;
};
+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);
+nscolor FilterColorChannel(nscolor aOriginalColor,
+ const nsZenAccentOklab& aAccent,
+ const nsZenAccentOklab& aComplementary);
+nscolor InvertColorChannel(nscolor aColor);
+} // namespace detail
+
class nsZenBoostsBackend final : public nsISupports {
public:
NS_DECL_ISUPPORTS
@@ -36,17 +56,15 @@ class nsZenBoostsBackend final : public nsISupports {
bool mCurrentFrameIsAnonymousContent = false;
/**
- * @brief Resolve a StyleAbsoluteColor to take into account Zen boosts.
+ * @brief Resolve a color to take into account Zen boosts. This is the single
+ * place style colors are filtered; it is reached for every style color via
+ * StyleAbsoluteColor::ToColor. Do not add a second StyleColor::ResolveColor
+ * filter on top of this or colors get filtered multiple times (which also
+ * makes resting colors disagree with composited transition endpoints).
* @param aColor The color to resolve.
* @return The resolved color with Zen boost filters applied, or the original
* color if no boost is active.
- * @see StyleColor::ResolveColor for reference.
- */
- static auto ResolveStyleColor(mozilla::StyleAbsoluteColor aColor)
- -> mozilla::StyleAbsoluteColor;
-
- /**
- * @see ResolveStyleColor for reference.
+ * @see StyleAbsoluteColor::ToColor for reference.
*/
static auto ResolveStyleColor(nscolor aColor) -> nscolor;
@@ -73,10 +91,13 @@ class nsZenBoostsBackend final : public nsISupports {
*/
auto RefreshCachedBoostState() -> void;
- [[nodiscard]]
- inline auto GetCurrentBrowsingContext() const {
- return mCurrentBrowsingContext;
- }
+ /**
+ * Resolves the current top BrowsingContext from its stored id. May return
+ * null if it has since been discarded. Not on the per-color hot path; the
+ * hot path uses the mCachedCurrent* fields instead.
+ */
+ [[nodiscard]] already_AddRefed
+ GetCurrentBrowsingContext() const;
/**
* Cached boost data for the current top BrowsingContext, refreshed on
@@ -95,15 +116,12 @@ class nsZenBoostsBackend final : public nsISupports {
~nsZenBoostsBackend() = default;
/**
- * The presshell of the current document being rendered.
+ * Id of the top BrowsingContext of the current document being rendered.
+ * Stored as an id rather than a strong RefPtr so the process-wide singleton
+ * does not keep a navigated-away BrowsingContext (and its subtree) alive
+ * until the next presshell entry.
*/
- RefPtr mCurrentBrowsingContext;
-
- static nsZenAccentOklab mCachedAccent;
- // Base accent with its Oklab hue rotated by mCachedComplementaryRotationDeg,
- // recomputed only when the base accent or rotation changes.
- static nsZenAccentOklab mCachedComplementary;
- static float mCachedComplementaryRotationDeg;
+ uint64_t mCurrentBrowsingContextId = 0;
public:
/**
diff --git a/src/zen/tests/boosts/browser.toml b/src/zen/tests/boosts/browser.toml
index c24f62f6a..9cacfe47f 100644
--- a/src/zen/tests/boosts/browser.toml
+++ b/src/zen/tests/boosts/browser.toml
@@ -8,5 +8,6 @@ support-files = [
]
["browser_boost_selector_basic.js"]
+["browser_boost_selector_escaping.js"]
["browser_boost_selector_invalid.js"]
["browser_boost_selector_nthchild.js"]
diff --git a/src/zen/tests/boosts/browser_boost_selector_escaping.js b/src/zen/tests/boosts/browser_boost_selector_escaping.js
new file mode 100644
index 000000000..5a3e299b6
--- /dev/null
+++ b/src/zen/tests/boosts/browser_boost_selector_escaping.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Covers code paths the basic/invalid/nthchild tests don't:
+// - getIdentification() running ids/classes through CSS.escape()
+// - the ancestor-disambiguation while-loop in traverse(), which only runs
+// when the exact path still matches more than one element.
+
+add_task(async function test_getSelectionPath_escapesSpecialChars() {
+ const doc = document.implementation.createHTMLDocument("TestEscape");
+
+ const container = doc.createElement("div");
+ // Characters that are invalid in a CSS selector unless escaped.
+ container.id = "with.dot:and#hash";
+ const target = doc.createElement("span");
+ target.className = "foo:bar baz.qux";
+ target.textContent = "target";
+ container.appendChild(target);
+ doc.body.appendChild(container);
+
+ const component = new SelectorComponent(doc, null, [], () => {});
+
+ const path = component.getSelectionPath(doc, 0, target);
+ ok(path, "A path should be generated for an element with special chars");
+
+ // The unescaped raw strings must not leak into the selector verbatim.
+ ok(
+ !path.includes("with.dot:and#hash"),
+ "Raw unescaped id must not appear in the selector"
+ );
+
+ // The generated selector must be valid and resolve back to the target.
+ let matched;
+ try {
+ matched = doc.querySelectorAll(path);
+ } catch (e) {
+ ok(false, `Generated selector should be parseable, got: ${e}`);
+ return;
+ }
+ ok(
+ Array.from(matched).includes(target),
+ "Escaped selector must still match the original element"
+ );
+ Assert.equal(
+ matched.length,
+ 1,
+ "Selector should uniquely identify the element"
+ );
+});
+
+add_task(async function test_getSelectionPath_disambiguatesAncestors() {
+ const doc = document.implementation.createHTMLDocument("TestAncestors");
+
+ // Two structurally identical subtrees. The leaf elements carry no id/class,
+ // so disambiguation must climb ancestors until the path is unique. The two
+ // wrappers differ only by id, forcing the ancestor-walk loop in traverse().
+ const makeBranch = wrapperId => {
+ const wrapper = doc.createElement("section");
+ wrapper.id = wrapperId;
+ const mid = doc.createElement("div");
+ const leaf = doc.createElement("span");
+ leaf.textContent = "leaf";
+ mid.appendChild(leaf);
+ wrapper.appendChild(mid);
+ doc.body.appendChild(wrapper);
+ return leaf;
+ };
+
+ const leafA = makeBranch("branch-a");
+ const leafB = makeBranch("branch-b");
+
+ const component = new SelectorComponent(doc, null, [], () => {});
+
+ for (const [leaf, label] of [
+ [leafA, "branch-a"],
+ [leafB, "branch-b"],
+ ]) {
+ const path = component.getSelectionPath(doc, 0, leaf);
+ ok(path, `Path generated for the leaf under ${label}`);
+
+ const matched = doc.querySelectorAll(path);
+ Assert.equal(
+ matched.length,
+ 1,
+ `Selector for the ${label} leaf must be unique despite an identical sibling subtree`
+ );
+ ok(
+ matched[0] === leaf,
+ `Selector must resolve to the correct ${label} leaf, not the other branch`
+ );
+ }
+});