mirror of
https://github.com/zen-browser/desktop.git
synced 2026-05-20 03:51:20 +00:00
449 lines
17 KiB
C++
449 lines
17 KiB
C++
/* 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 <cmath>
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
|
|
#include "nsZenBoostsBackend.h"
|
|
|
|
#include "nsIXULRuntime.h"
|
|
#include "nsPresContext.h"
|
|
|
|
#include "mozilla/ClearOnShutdown.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"
|
|
|
|
#include "mozilla/StaticPrefs_zen.h"
|
|
|
|
// Lower bound applied to inverted channels so that pure white doesn't invert
|
|
// all the way to pure black, which makes inverted pages feel too dark.
|
|
#define INVERT_CHANNEL_FLOOR() \
|
|
(mozilla::StaticPrefs::zen_boosts_invert_channel_floor_AtStartup())
|
|
|
|
#define SHOULD_APPLY_BOOSTS_TO_ANONYMOUS_CONTENT() \
|
|
(!mozilla::StaticPrefs::zen_boosts_disable_on_anonymous_content_AtStartup())
|
|
|
|
#if defined(__clang__) || defined(__GNUC__)
|
|
# define ZEN_HOT_FUNCTION __attribute__((hot))
|
|
#else
|
|
# define ZEN_HOT_FUNCTION
|
|
#endif
|
|
|
|
// 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)
|
|
|
|
namespace zen {
|
|
|
|
nsZenAccentOklab nsZenBoostsBackend::mCachedAccent{0};
|
|
nsZenAccentOklab nsZenBoostsBackend::mCachedComplementary{0};
|
|
float nsZenBoostsBackend::mCachedComplementaryRotationDeg = 0.0f;
|
|
|
|
namespace {
|
|
|
|
/**
|
|
* @brief Converts an sRGB color component to linear space.
|
|
* @param c The sRGB color component value (0.0 to 1.0).
|
|
* @return The linear color component value.
|
|
*/
|
|
static inline float srgbToLinear(float c) {
|
|
return c <= 0.04045f ? c * (1.0f / 12.92f)
|
|
: std::pow((c + 0.055f) * (1.0f / 1.055f), 2.4f);
|
|
}
|
|
|
|
/**
|
|
* @brief Converts a linear color component to sRGB space.
|
|
* @param c The linear color component value.
|
|
* @return The sRGB color component value (0.0 to 1.0).
|
|
*/
|
|
static inline float linearToSrgb(float c) {
|
|
c = std::max(0.0f, c);
|
|
return c <= 0.0031308f ? 12.92f * c
|
|
: 1.055f * std::pow(c, 1.0f / 2.4f) - 0.055f;
|
|
}
|
|
|
|
/*
|
|
* @brief Fast approximation of the cube root of a number.
|
|
* @param x The input value.
|
|
* @return The approximate cube root of the input value.
|
|
*/
|
|
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;
|
|
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 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
|
|
* having to convert the accent color from sRGB to Oklab space on every filter
|
|
* operation.
|
|
* @param aAccentColor The accent color in nscolor format.
|
|
* @return A struct containing the precomputed Oklab values and contrast factor
|
|
* for the accent color.
|
|
*/
|
|
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 l_ =
|
|
fastCbrt(0.4122214708f * lr + 0.5363325363f * lg + 0.0514459929f * lb);
|
|
const float m_ =
|
|
fastCbrt(0.2119034982f * lr + 0.6806995451f * lg + 0.1073969566f * lb);
|
|
const float s_ =
|
|
fastCbrt(0.0883024619f * lr + 0.2817188376f * lg + 0.6299787005f * lb);
|
|
|
|
return nsZenAccentOklab{
|
|
.accentNS = aAccentColor,
|
|
.accL = 0.2104542553f * l_ + 0.7936177850f * m_ - 0.0040720468f * s_,
|
|
.accA = 1.9779984951f * l_ - 2.4285922050f * m_ + 0.4505937099f * s_,
|
|
.accB = 0.0259040371f * l_ + 0.7827717662f * m_ - 0.8086757660f * s_,
|
|
.contrastFactor = NS_GET_CONTRAST(aAccentColor) * inv255,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @brief Derives the complementary accent from the base accent by rotating its
|
|
* hue in the Oklab a/b plane by the given angle. Lightness, contrast and the
|
|
* source nscolor are kept; only the hue changes. A zero rotation returns the
|
|
* base accent unchanged so the duotone collapses to a single-accent tint.
|
|
* @param aBase The precomputed base accent.
|
|
* @param aRotationDeg The hue rotation to apply, in degrees.
|
|
* @return The complementary accent.
|
|
*/
|
|
ZEN_HOT_FUNCTION
|
|
inline static nsZenAccentOklab zenRotateAccent(const nsZenAccentOklab& aBase,
|
|
float aRotationDeg) {
|
|
constexpr float kDegToRad = 3.14159265358979323846f / 180.0f;
|
|
const float angle = aRotationDeg * kDegToRad;
|
|
const float cosR = std::cos(angle);
|
|
const float sinR = std::sin(angle);
|
|
return nsZenAccentOklab{
|
|
.accentNS = aBase.accentNS,
|
|
.accL = aBase.accL,
|
|
.accA = aBase.accA * cosR - aBase.accB * sinR,
|
|
.accB = aBase.accA * sinR + aBase.accB * cosR,
|
|
.contrastFactor = aBase.contrastFactor,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @brief Applies a duotone color filter to transform an original color toward
|
|
* one of two accent colors. The original color's perceived lightness decides
|
|
* which accent it is tinted toward: dark colors are pulled to the base accent,
|
|
* light colors to the complementary accent, with a smooth crossfade between
|
|
* them. The contrast value (stored in the accent's alpha channel) controls both
|
|
* the overall tint strength and how hard that dark/light split is. The
|
|
* original color's perceived luminance is otherwise preserved.
|
|
* @param aOriginalColor The original color to filter.
|
|
* @param aAccent The base accent, tinted toward by dark colors (alpha channel
|
|
* contains the contrast value).
|
|
* @param aComplementary The complementary accent, tinted toward by light
|
|
* colors.
|
|
* @return The filtered color with transformations applied.
|
|
*/
|
|
[[nodiscard]] ZEN_HOT_FUNCTION static inline nscolor zenFilterColorChannel(
|
|
nscolor aOriginalColor, const nsZenAccentOklab& aAccent,
|
|
const nsZenAccentOklab& aComplementary) {
|
|
const uint8_t oL = NS_GET_A(aOriginalColor);
|
|
const uint8_t contrast = NS_GET_CONTRAST(aAccent.accentNS);
|
|
if (oL == 0) {
|
|
return aOriginalColor;
|
|
}
|
|
|
|
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);
|
|
|
|
// Linear RGB -> LMS -> cube root -> Oklab (fused)
|
|
const float l_ =
|
|
fastCbrt(0.4122214708f * lr + 0.5363325363f * lg + 0.0514459929f * lb);
|
|
const float m_ =
|
|
fastCbrt(0.2119034982f * lr + 0.6806995451f * lg + 0.1073969566f * lb);
|
|
const float s_ =
|
|
fastCbrt(0.0883024619f * lr + 0.2817188376f * lg + 0.6299787005f * lb);
|
|
|
|
const float origL =
|
|
0.2104542553f * l_ + 0.7936177850f * m_ - 0.0040720468f * s_;
|
|
const float origA =
|
|
1.9779984951f * l_ - 2.4285922050f * m_ + 0.4505937099f * s_;
|
|
const float origB =
|
|
0.0259040371f * l_ + 0.7827717662f * m_ - 0.8086757660f * s_;
|
|
|
|
// Duotone selection. origL is the original color's Oklab lightness (~0..1).
|
|
// A smoothstep around a fixed mid-lightness pivot crossfades from the base
|
|
// accent (dark colors, t=0) to the complementary accent (light colors, t=1).
|
|
// A stronger tint (higher blendFactor) narrows the crossfade band toward a
|
|
// hard two-tone split; a weaker one keeps it a gentle gradient.
|
|
constexpr float kPivot = 0.5f;
|
|
const float halfWidth = std::clamp(0.5f - blendFactor * 0.45f, 0.05f, 0.5f);
|
|
float t = std::clamp((origL - (kPivot - halfWidth)) / (2.0f * halfWidth),
|
|
0.0f, 1.0f);
|
|
t = t * t * (3.0f - 2.0f * t);
|
|
|
|
const float selA = aAccent.accA + (aComplementary.accA - aAccent.accA) * t;
|
|
const float selB = aAccent.accB + (aComplementary.accB - aAccent.accB) * t;
|
|
const float selL = aAccent.accL + (aComplementary.accL - aAccent.accL) * t;
|
|
const float selContrastFactor =
|
|
aAccent.contrastFactor +
|
|
(aComplementary.contrastFactor - aAccent.contrastFactor) * t;
|
|
|
|
// Blend chroma toward the selected accent
|
|
const float bA = origA + (selA - origA) * blendFactor;
|
|
const float bB = origB + (selB - origB) * blendFactor;
|
|
|
|
// Luminance: at low contrast stay near the original, the higher the contrast,
|
|
// the more we shift toward the accent luminance, but we never go fully to
|
|
// the accent luminance to preserve some of the original color's character.
|
|
const float lumDelta = selL - origL;
|
|
const float fL = origL + lumDelta * (blendFactor * selContrastFactor * 0.5f);
|
|
|
|
// Rotate hue in the Oklab a/b plane. Direction follows the luminance shift:
|
|
// pushing darker rotates clockwise ("right"), pushing lighter rotates the
|
|
// other way. Magnitude scales with blend strength so subtle accents stay
|
|
// subtle.
|
|
const float rotAngle = (lumDelta > 0.0f ? -1.0f : 1.0f) * blendFactor *
|
|
selContrastFactor * 0.25f;
|
|
const float cosR = std::cos(rotAngle);
|
|
const float sinR = std::sin(rotAngle);
|
|
const float fA = bA * cosR - bB * sinR;
|
|
const float fB = bA * sinR + bB * cosR;
|
|
|
|
// Oklab -> LMS
|
|
const float fl_ = fL + 0.3963377774f * fA + 0.2158037573f * fB;
|
|
const float fm_ = fL - 0.1055613458f * fA - 0.0638541728f * fB;
|
|
const float fs_ = fL - 0.0894841775f * fA - 1.2914855480f * fB;
|
|
|
|
// Cube
|
|
const float fl = fl_ * fl_ * fl_;
|
|
const float fm = fm_ * fm_ * fm_;
|
|
const float fs = fs_ * fs_ * fs_;
|
|
|
|
// LMS -> linear RGB
|
|
const float rF = 4.0767416621f * fl - 3.3077115913f * fm + 0.2309699292f * fs;
|
|
const float gF =
|
|
-1.2684380046f * fl + 2.6097574011f * fm - 0.3413193965f * fs;
|
|
const float bF =
|
|
-0.0041960863f * fl - 0.7034186147f * fm + 1.7076147010f * fs;
|
|
|
|
// Linear -> sRGB -> uint8
|
|
return NS_RGBA(static_cast<uint8_t>(std::clamp(
|
|
linearToSrgb(rF) * 255.0f + 0.5f, 0.0f, 255.0f)),
|
|
static_cast<uint8_t>(std::clamp(
|
|
linearToSrgb(gF) * 255.0f + 0.5f, 0.0f, 255.0f)),
|
|
static_cast<uint8_t>(std::clamp(
|
|
linearToSrgb(bF) * 255.0f + 0.5f, 0.0f, 255.0f)),
|
|
oL);
|
|
}
|
|
|
|
/**
|
|
* @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.
|
|
*/
|
|
ZEN_HOT_FUNCTION
|
|
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;
|
|
|
|
// Compress the channel range into [FLOOR, 255] so dark inversions are
|
|
// lifted while light inversions are left untouched. This preserves hue
|
|
// since all three channels are scaled by the same factor.
|
|
const auto channelFloor = INVERT_CHANNEL_FLOOR();
|
|
const uint32_t range = 255 - channelFloor;
|
|
const auto lift = [channelFloor, range](uint8_t c) -> uint8_t {
|
|
return static_cast<uint8_t>(channelFloor + (c * range) / 255);
|
|
};
|
|
|
|
return NS_RGBA(lift(rShifted), lift(gShifted), lift(bShifted), a);
|
|
}
|
|
|
|
/**
|
|
* @brief Retrieves the current boost data from the browsing context. When
|
|
* called without aPresContext, reads the precomputed cache populated on
|
|
* presshell entry; otherwise resolves from the supplied PresContext.
|
|
*/
|
|
ZEN_HOT_FUNCTION
|
|
inline static void GetZenBoostsDataFromBrowsingContext(
|
|
ZenBoostData* aData, float* aComplementaryRotation, bool* aIsInverted,
|
|
nsPresContext* aPresContext = nullptr) {
|
|
auto zenBoosts = nsZenBoostsBackend::GetInstance();
|
|
if (!zenBoosts || (zenBoosts->mCurrentFrameIsAnonymousContent &&
|
|
!SHOULD_APPLY_BOOSTS_TO_ANONYMOUS_CONTENT())) {
|
|
return;
|
|
}
|
|
if (!aPresContext) {
|
|
*aData = zenBoosts->mCachedCurrentAccent;
|
|
*aComplementaryRotation = zenBoosts->mCachedCurrentComplementaryRotation;
|
|
*aIsInverted = zenBoosts->mCachedCurrentInverted;
|
|
return;
|
|
}
|
|
mozilla::dom::BrowsingContext* browsingContext = nullptr;
|
|
if (auto document = aPresContext->Document()) {
|
|
browsingContext = document->GetBrowsingContext();
|
|
}
|
|
if (!browsingContext) {
|
|
return;
|
|
}
|
|
browsingContext = browsingContext->Top();
|
|
*aData = browsingContext->ZenBoostsData();
|
|
*aComplementaryRotation = browsingContext->ZenBoostsComplementaryRotation();
|
|
*aIsInverted = browsingContext->IsZenBoostsInverted();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
static mozilla::StaticAutoPtr<nsZenBoostsBackend> sZenBoostsBackend;
|
|
|
|
auto nsZenBoostsBackend::GetInstance() -> nsZenBoostsBackend* {
|
|
if (!XRE_IsContentProcess()) {
|
|
// Zen boosts are only supported in content, so if we're in the parent
|
|
// process, just return null.
|
|
return nullptr;
|
|
}
|
|
if (!sZenBoostsBackend) {
|
|
sZenBoostsBackend = new nsZenBoostsBackend();
|
|
mozilla::ClearOnShutdown(&sZenBoostsBackend);
|
|
}
|
|
return sZenBoostsBackend.get();
|
|
}
|
|
|
|
auto nsZenBoostsBackend::onPresShellEntered(mozilla::dom::Document* aDocument)
|
|
-> void {
|
|
if (auto displayDoc = aDocument->GetDisplayDocument()) {
|
|
onPresShellEntered(displayDoc);
|
|
return;
|
|
}
|
|
// 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;
|
|
RefreshCachedBoostState();
|
|
}
|
|
|
|
auto nsZenBoostsBackend::RefreshCachedBoostState() -> void {
|
|
if (!mCurrentBrowsingContext) {
|
|
mCachedCurrentAccent = 0;
|
|
mCachedCurrentComplementaryRotation = 0.0f;
|
|
mCachedCurrentInverted = false;
|
|
return;
|
|
}
|
|
auto top = mCurrentBrowsingContext->Top();
|
|
mCachedCurrentAccent = top->ZenBoostsData();
|
|
mCachedCurrentComplementaryRotation = top->ZenBoostsComplementaryRotation();
|
|
mCachedCurrentInverted = top->IsZenBoostsInverted();
|
|
}
|
|
|
|
[[nodiscard]] ZEN_HOT_FUNCTION auto
|
|
nsZenBoostsBackend::FilterColorFromPresContext(nscolor aColor,
|
|
nsPresContext* aPresContext)
|
|
-> nscolor {
|
|
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:
|
|
// - 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);
|
|
}
|
|
if (invertColors) {
|
|
aColor = zenInvertColorChannel(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);
|
|
}
|
|
|
|
} // namespace zen
|