/* 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 "nsZenBoostsBackend.h" #include "nsIXULRuntime.h" #include "nsPresContext.h" #include "mozilla/StaticPtr.h" #include "mozilla/ServoStyleConsts.h" #include "mozilla/ServoStyleConstsInlines.h" #include "mozilla/MediaFeatureChange.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/DocumentInlines.h" #include "mozilla/dom/BrowsingContext.h" #define COLOR_CHANNEL_MIDPOINT 128 // It's a bit of a hacky solution, but instead of using alpha as what it is // (opacity), we use it to store contrast information for now. // We do this primarily to avoid having to deal with WebIDL structs and // serialization/deserialization between parent and content processes. #define NS_GET_CONTRAST(_c) NS_GET_A(_c) #define MARK_MEDIA_FEATURE_CHANGED(_pc) \ (_pc)->MediaFeatureValuesChanged( \ {mozilla::RestyleHint::RecascadeSubtree(), NS_STYLE_HINT_VISUAL, \ mozilla::MediaFeatureChangeReason::PreferenceChange}, \ mozilla::MediaFeatureChangePropagation::All); #define TRIGGER_PRES_CONTEXT_RESTYLE() \ WalkPresContexts( \ [&](nsPresContext* aPc) { MARK_MEDIA_FEATURE_CHANGED(aPc); }); using BrowsingContext = mozilla::dom::BrowsingContext; template void BrowsingContext::WalkPresContexts(Callback&& aCallback) { PreOrderWalk([&](BrowsingContext* aContext) { if (nsIDocShell* shell = aContext->GetDocShell()) { if (RefPtr pc = shell->GetPresContext()) { aCallback(pc.get()); } } }); } /** * @brief Called when the ZenBoostsData field is set on a browsing context. * Triggers a restyle if the boost data has changed. * @param aOldValue The previous value of the boost data. */ void BrowsingContext::DidSet(FieldIndex, ZenBoostData aOldValue) { MOZ_ASSERT(IsTop()); if (ZenBoostsData() == aOldValue) { return; } PresContextAffectingFieldChanged(); TRIGGER_PRES_CONTEXT_RESTYLE(); } /** * @brief Called when the IsZenBoostsInverted field is set on a browsing * context. Triggers a restyle if the value has changed. * @param aOldValue The previous value of the IsZenBoostsInverted flag. */ void BrowsingContext::DidSet(FieldIndex, bool aOldValue) { MOZ_ASSERT(IsTop()); if (IsZenBoostsInverted() == aOldValue) { return; } PresContextAffectingFieldChanged(); TRIGGER_PRES_CONTEXT_RESTYLE(); } namespace zen { nsZenAccentOklab nsZenBoostsBackend::mCachedAccent{0}; namespace { struct Lab { float L, a, b; }; struct RGB { float r, g, b; }; /** * @brief Clamps a value to the range [0, 255] using branchless operations. * @param v The value to clamp. * @return The clamped value in the range [0, 255]. */ static __inline int32_t clamp255(int32_t v) { // llvm x86 is poor at ternary operator, so use branchless min/max. v = v & ~(v >> 31); return (v | ((255 - v) >> 31)) & 255; } /** * @brief A fast approximation of the cube root function using bit manipulation * and two Newton-Raphson iterations. This is used to optimize the Oklab color * conversion in the color filtering process. */ inline static float fast_cbrt(float x) { // Bit-level initial approximation (works for positive floats only — fine // here) uint32_t bits; memcpy(&bits, &x, 4); bits = (bits / 3) + 0x2A512400u; // magic constant for cube root float y; memcpy(&y, &bits, 4); // Two Newton-Raphson iterations: y = y - (y³ - x) / (3y²) y = (2.0f / 3.0f) * y + (1.0f / 3.0f) * x / (y * y); y = (2.0f / 3.0f) * y + (1.0f / 3.0f) * x / (y * y); return y; } /** * @brief Converts an Oklab color back to the RGB color space. * @param c The Oklab color to convert. * @return The corresponding RGB color. */ [[nodiscard]] static inline auto oklab2rgb(Lab c) -> RGB { float l_ = c.L + 0.3963377774f * c.a + 0.2158037573f * c.b; float m_ = c.L - 0.1055613458f * c.a - 0.0638541728f * c.b; float s_ = c.L - 0.0894841775f * c.a - 1.2914855480f * c.b; // Cubing is just 2 multiplies — no cbrtf needed on the way back return { 4.0767416621f * (l_ * l_ * l_) - 3.3077115913f * (m_ * m_ * m_) + 0.2309699292f * (s_ * s_ * s_), -1.2684380046f * (l_ * l_ * l_) + 2.6097574011f * (m_ * m_ * m_) - 0.3413193965f * (s_ * s_ * s_), -0.0041960863f * (l_ * l_ * l_) - 0.7034186147f * (m_ * m_ * m_) + 1.7076147010f * (s_ * s_ * s_), }; } /** * @brief Applies a color filter to transform an original color toward an accent * color. Preserves the original color's perceived luminance while shifting * hue/chroma toward the accent. Uses the alpha channel of the accent color to * store contrast information. * @param aOriginalColor The original color to filter. * @param aAccentColor The accent color to filter toward (alpha channel contains * contrast value). * @return The filtered color with transformations applied. */ [[nodiscard]] static inline Lab rgb2oklab_fast(RGB c) { float l = 0.4122214708f * c.r + 0.5363325363f * c.g + 0.0514459929f * c.b; float m = 0.2119034982f * c.r + 0.6806995451f * c.g + 0.1073969566f * c.b; float s = 0.0883024619f * c.r + 0.2817188376f * c.g + 0.6299787005f * c.b; float l_ = fast_cbrt(l), m_ = fast_cbrt(m), s_ = fast_cbrt(s); return { 0.2104542553f * l_ + 0.7936177850f * m_ - 0.0040720468f * s_, 1.9779984951f * l_ - 2.4285922050f * m_ + 0.4505937099f * s_, 0.0259040371f * l_ + 0.7827717662f * m_ - 0.8086757660f * s_, }; } inline static auto zenPrecomputeAccent(nscolor aAccentColor) -> nsZenAccentOklab { constexpr float kInv255 = 1.0f / 255.0f; RGB rgb = {NS_GET_R(aAccentColor) * kInv255, NS_GET_G(aAccentColor) * kInv255, NS_GET_B(aAccentColor) * kInv255}; auto lab = rgb2oklab_fast(rgb); float contrast = NS_GET_CONTRAST(aAccentColor); float vibranceBase = 1.0f - ((contrast - 128.0f) * (1.0f / 128.0f)); return {lab.L, lab.a, lab.b, vibranceBase, 0.25f + lab.L}; } /** * @brief Applies a color filter to transform an original color toward an accent * color. Preserves the original color's perceived luminance while shifting * hue/chroma toward the accent. Uses the alpha channel of the accent color to * store contrast information. * @param aOriginalColor The original color to filter. * @param aAccentColor The accent color to filter toward (alpha channel contains * contrast value). * @return The filtered color with transformations applied. */ [[nodiscard]] static nscolor zenFilterColorChannel(nscolor aOriginalColor, const nsZenAccentOklab& aAccent) { const uint8_t a1 = NS_GET_A(aOriginalColor); if (a1 == 0) return aOriginalColor; constexpr float kInv255 = 1.0f / 255.0f; constexpr float kTint = 0.6f; constexpr float kInvTint = 1.0f - kTint; RGB orig = { NS_GET_R(aOriginalColor) * kInv255, NS_GET_G(aOriginalColor) * kInv255, NS_GET_B(aOriginalColor) * kInv255, }; const auto lab = rgb2oklab_fast(orig); const float aBlend = kInvTint * lab.a + kTint * aAccent.a; const float bBlend = kInvTint * lab.b + kTint * aAccent.b; // Avoid sqrt: compare squared chroma against squared threshold (0.4^2 = 0.16) // vibranceFactor = chromaMixed/chromaBlend which simplifies to just // vibranceFactor since the sqrt cancels in the normalize+scale round-trip // (see below) const float chromaSq = aBlend * aBlend + bBlend * bBlend; const float saturation = (chromaSq < 0.16f) ? chromaSq * (1.0f / 0.16f) : 1.0f; const float vibranceFactor = 1.0f + aAccent.vibranceBase * (1.0f - saturation); // sqrt cancellation: chromaMixed/chromaBlend = // (chromaBlend*vibranceFactor)/chromaBlend = vibranceFactor, so we multiply // directly with no sqrt needed const float aMixed = aBlend * vibranceFactor; const float bMixed = bBlend * vibranceFactor; float LMixed = 0.5f + (lab.L - 0.5f) * (1.0f + aAccent.vibranceBase * 0.5f); LMixed *= aAccent.accentLOffset; if (LMixed < 0.0f) LMixed = 0.0f; if (LMixed > 1.0f) LMixed = 1.0f; const auto rgb = oklab2rgb({LMixed, aMixed, bMixed}); return NS_RGBA(clamp255((int32_t)(rgb.r * 255.0f + 0.5f)), clamp255((int32_t)(rgb.g * 255.0f + 0.5f)), clamp255((int32_t)(rgb.b * 255.0f + 0.5f)), a1); } /** * @brief Inverts a color by inverting each RGB channel while preserving * perceived luminance. This is done by inverting the color and then shifting it * based on the sum of the inverted channels. * @param aColor The color to invert. * @return The inverted color with luminance preservation. */ inline static nscolor zenInvertColorChannel(nscolor aColor) { const auto r = NS_GET_R(aColor); const auto g = NS_GET_G(aColor); const auto b = NS_GET_B(aColor); const auto a = NS_GET_A(aColor); if (a == 0) { // Skip processing fully transparent colors since they won't be visible and // we want to avoid unnecessary computations. return aColor; } const auto rInv = 255 - r; const auto gInv = 255 - g; const auto bInv = 255 - b; const auto max = std::max({rInv, gInv, bInv}); const auto min = std::min({rInv, gInv, bInv}); const auto sum = max + min; const auto rShifted = sum - rInv; const auto gShifted = sum - gInv; const auto bShifted = sum - bInv; return NS_RGBA(rShifted, gShifted, bShifted, a); } /** * @brief Retrieves the current boost data from the browsing context. */ inline static void GetZenBoostsDataFromBrowsingContext( ZenBoostData* aData, bool* aIsInverted, nsPresContext* aPresContext = nullptr) { auto zenBoosts = nsZenBoostsBackend::GetInstance(); if (!zenBoosts || (zenBoosts->mCurrentFrameIsAnonymousContent)) { return; } if (aPresContext) { if (auto document = aPresContext->Document()) { if (auto browsingContext = document->GetBrowsingContext()) { *aData = browsingContext->ZenBoostsData(); *aIsInverted = browsingContext->IsZenBoostsInverted(); } } } else if (auto currentBrowsingContext = zenBoosts->GetCurrentBrowsingContext()) { *aData = currentBrowsingContext->ZenBoostsData(); *aIsInverted = currentBrowsingContext->IsZenBoostsInverted(); } } } // namespace auto nsZenBoostsBackend::GetInstance() -> nsZenBoostsBackend* { static nsZenBoostsBackend* zenBoosts; if (!XRE_IsContentProcess()) { // Zen boosts are only supported in content, so if we're in the parent // process, just return null. return nullptr; } if (!zenBoosts) { zenBoosts = new nsZenBoostsBackend(); } return zenBoosts; } auto nsZenBoostsBackend::onPresShellEntered(mozilla::dom::Document* aDocument) -> void { // Note that aDocument can be null when entering anonymous content frames. // We explicitly do this to prevent applying boosts to anonymous content, such // as devtools or screenshots. mozilla::dom::BrowsingContext* browsingContext = aDocument ? aDocument->GetBrowsingContext() : nullptr; if (!browsingContext) { return; } mCurrentBrowsingContext = browsingContext; } auto nsZenBoostsBackend::FilterColorFromPresContext(nscolor aColor, nsPresContext* aPresContext) -> nscolor { if (!XRE_IsContentProcess()) { // Zen boosts are only supported in content, so if we somehow end up here // without a prescontext or in the parent process, just return the original // color. return aColor; } ZenBoostData accentNS = 0; bool invertColors = false; GetZenBoostsDataFromBrowsingContext(&accentNS, &invertColors, aPresContext); if (accentNS) { if (mCachedAccent.accentNS != accentNS) { mCachedAccent = zenPrecomputeAccent(accentNS); } // Apply a filter-like tint: // - Preserve the original color's perceived luminance // - Map hue/chroma toward the accent by scaling the accent's RGB // to match the original luminance // - Keep the original alpha // Convert both colors to nscolor to access channels aColor = zenFilterColorChannel(aColor, mCachedAccent); } if (invertColors) { aColor = zenInvertColorChannel(aColor); } return aColor; } auto nsZenBoostsBackend::ResolveStyleColor(mozilla::StyleAbsoluteColor aColor) -> mozilla::StyleAbsoluteColor { if (aColor.alpha == 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; } const auto resultColor = FilterColorFromPresContext(aColor.ToColor()); aColor = mozilla::StyleAbsoluteColor::FromColor(resultColor); return aColor; } } // namespace zen