/* -*- Mode: C++; tab-width: 40; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* 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 "ThemeColors.h" #include "mozilla/RelativeLuminanceUtils.h" #include "mozilla/StaticPrefs_layout.h" #include "mozilla/StaticPrefs_widget.h" #include "ThemeDrawing.h" #include "nsNativeTheme.h" using namespace mozilla::gfx; namespace mozilla::widget { struct ColorPalette { ColorPalette(nscolor aAccent, nscolor aForeground); constexpr ColorPalette(sRGBColor aAccent, sRGBColor aForeground, sRGBColor aLight, sRGBColor aDark, sRGBColor aDarker) : mAccent(aAccent), mForeground(aForeground), mAccentLight(aLight), mAccentDark(aDark), mAccentDarker(aDarker) {} constexpr static ColorPalette Default() { return ColorPalette( sDefaultAccent, sDefaultAccentText, sRGBColor::UnusualFromARGB(0x4d008deb), // Luminance: 25.04791% sRGBColor::UnusualFromARGB(0xff0250bb), // Luminance: 9.33808% sRGBColor::UnusualFromARGB(0xff054096) // Luminance: 5.90106% ); } // Ensure accent color is opaque by blending with white. This serves two // purposes: On one hand, it avoids surprises if we overdraw. On the other, it // makes our math below make more sense, as we want to match the browser // style, which has an opaque accent color. static nscolor EnsureOpaque(nscolor aAccent) { if (NS_GET_A(aAccent) != 0xff) { return NS_ComposeColors(NS_RGB(0xff, 0xff, 0xff), aAccent); } return aAccent; } static nscolor GetLight(nscolor aAccent) { // The luminance from the light color divided by the one of the accent color // in the default palette. constexpr float kLightLuminanceScale = 25.048f / 13.693f; const float lightLuminanceAdjust = ThemeColors::ScaleLuminanceBy( RelativeLuminanceUtils::Compute(aAccent), kLightLuminanceScale); nscolor lightColor = RelativeLuminanceUtils::Adjust(aAccent, lightLuminanceAdjust); return NS_RGBA(NS_GET_R(lightColor), NS_GET_G(lightColor), NS_GET_B(lightColor), 0x4d); } static nscolor GetDark(nscolor aAccent) { // Same deal as above (but without the alpha). constexpr float kDarkLuminanceScale = 9.338f / 13.693f; const float darkLuminanceAdjust = ThemeColors::ScaleLuminanceBy( RelativeLuminanceUtils::Compute(aAccent), kDarkLuminanceScale); return RelativeLuminanceUtils::Adjust(aAccent, darkLuminanceAdjust); } static nscolor GetDarker(nscolor aAccent) { // Same deal as above. constexpr float kDarkerLuminanceScale = 5.901f / 13.693f; const float darkerLuminanceAdjust = ThemeColors::ScaleLuminanceBy( RelativeLuminanceUtils::Compute(aAccent), kDarkerLuminanceScale); return RelativeLuminanceUtils::Adjust(aAccent, darkerLuminanceAdjust); } sRGBColor mAccent; sRGBColor mForeground; // Note that depending on the exact accent color, lighter/darker might really // be inverted. sRGBColor mAccentLight; sRGBColor mAccentDark; sRGBColor mAccentDarker; }; static nscolor GetAccentColor(bool aBackground, ColorScheme aScheme) { auto useStandins = LookAndFeel::UseStandins( !StaticPrefs::widget_non_native_theme_use_theme_accent()); return ColorPalette::EnsureOpaque( LookAndFeel::Color(aBackground ? LookAndFeel::ColorID::Accentcolor : LookAndFeel::ColorID::Accentcolortext, aScheme, useStandins)); } static ColorPalette sDefaultLightPalette = ColorPalette::Default(); static ColorPalette sDefaultDarkPalette = ColorPalette::Default(); ColorPalette::ColorPalette(nscolor aAccent, nscolor aForeground) { mAccent = sRGBColor::FromABGR(aAccent); mForeground = sRGBColor::FromABGR(aForeground); mAccentLight = sRGBColor::FromABGR(GetLight(aAccent)); mAccentDark = sRGBColor::FromABGR(GetDark(aAccent)); mAccentDarker = sRGBColor::FromABGR(GetDarker(aAccent)); } ThemeAccentColor::ThemeAccentColor(const ComputedStyle& aStyle, ColorScheme aScheme) : mDefaultPalette(aScheme == ColorScheme::Light ? &sDefaultLightPalette : &sDefaultDarkPalette) { const auto& color = aStyle.StyleUI()->mAccentColor; if (color.IsAuto()) { return; } MOZ_ASSERT(color.IsColor()); nscolor accentColor = ColorPalette::EnsureOpaque(color.AsColor().CalcColor(aStyle)); if (sRGBColor::FromABGR(accentColor) == mDefaultPalette->mAccent) { return; } mAccentColor.emplace(accentColor); } sRGBColor ThemeAccentColor::Get() const { if (!mAccentColor) { return mDefaultPalette->mAccent; } return sRGBColor::FromABGR(*mAccentColor); } sRGBColor ThemeAccentColor::GetForeground() const { if (!mAccentColor) { return mDefaultPalette->mForeground; } return sRGBColor::FromABGR( ThemeColors::ComputeCustomAccentForeground(*mAccentColor)); } sRGBColor ThemeAccentColor::GetLight() const { if (!mAccentColor) { return mDefaultPalette->mAccentLight; } return sRGBColor::FromABGR(ColorPalette::GetLight(*mAccentColor)); } sRGBColor ThemeAccentColor::GetDark() const { if (!mAccentColor) { return mDefaultPalette->mAccentDark; } return sRGBColor::FromABGR(ColorPalette::GetDark(*mAccentColor)); } sRGBColor ThemeAccentColor::GetDarker() const { if (!mAccentColor) { return mDefaultPalette->mAccentDarker; } return sRGBColor::FromABGR(ColorPalette::GetDarker(*mAccentColor)); } auto ThemeColors::ShouldBeHighContrast(const nsPresContext& aPc) -> HighContrastInfo { // We make sure that we're drawing backgrounds, since otherwise layout will // darken our used text colors etc anyways, and that can cause contrast issues // with dark high-contrast themes. if (!aPc.GetBackgroundColorDraw()) { return {}; } const auto& prefs = PreferenceSheet::PrefsFor(*aPc.Document()); return {prefs.NonNativeThemeShouldBeHighContrast(), prefs.mMustUseLightSystemColors}; } ColorScheme ThemeColors::ColorSchemeForWidget(const nsIFrame* aFrame, StyleAppearance aAppearance, const HighContrastInfo& aInfo) { if (aInfo.mMustUseLightSystemColors) { return ColorScheme::Light; } if (!nsNativeTheme::IsWidgetScrollbarPart(aAppearance)) { return LookAndFeel::ColorSchemeForFrame(aFrame); } // Scrollbars are a bit tricky. Their used color-scheme depends on whether the // background they are on is light or dark. // // TODO(emilio): This heuristic effectively predates the color-scheme CSS // property. Perhaps we should check whether the style or the document set // `color-scheme` to something that isn't `normal`, and if so go through the // code-path above. if (StaticPrefs::widget_disable_dark_scrollbar()) { return ColorScheme::Light; } return nsNativeTheme::IsDarkBackgroundForScrollbar( const_cast(aFrame)) ? ColorScheme::Dark : ColorScheme::Light; } /*static*/ void ThemeColors::RecomputeAccentColors() { MOZ_RELEASE_ASSERT(NS_IsMainThread()); sDefaultLightPalette = ColorPalette(GetAccentColor(true, ColorScheme::Light), GetAccentColor(false, ColorScheme::Light)); sDefaultDarkPalette = ColorPalette(GetAccentColor(true, ColorScheme::Dark), GetAccentColor(false, ColorScheme::Dark)); } /*static*/ nscolor ThemeColors::ComputeCustomAccentForeground(nscolor aColor) { // Contrast ratio is defined in // https://www.w3.org/TR/WCAG20/#contrast-ratiodef as: // // (L1 + 0.05) / (L2 + 0.05) // // Where L1 is the lighter color, and L2 is the darker one. So we determine // whether we're dark or light and resolve the equation for the target ratio. // // So when lightening: // // L1 = k * (L2 + 0.05) - 0.05 // // And when darkening: // // L2 = (L1 + 0.05) / k - 0.05 // const float luminance = RelativeLuminanceUtils::Compute(aColor); // We generally prefer white unless we can't because the color is really light // and we can't provide reasonable contrast. const float ratioWithWhite = 1.05f / (luminance + 0.05f); const bool canBeWhite = ratioWithWhite >= StaticPrefs::layout_css_accent_color_min_contrast_ratio(); if (canBeWhite) { return NS_RGB(0xff, 0xff, 0xff); } const float targetRatio = StaticPrefs::layout_css_accent_color_darkening_target_contrast_ratio(); const float targetLuminance = (luminance + 0.05f) / targetRatio - 0.05f; return RelativeLuminanceUtils::Adjust(aColor, targetLuminance); } nscolor ThemeColors::AdjustUnthemedScrollbarThumbColor( nscolor aFaceColor, dom::ElementState aStates) { // In Windows 10, scrollbar thumb has the following colors: // // State | Color | Luminance // -------+----------+---------- // Normal | Gray 205 | 61.0% // Hover | Gray 166 | 38.1% // Active | Gray 96 | 11.7% // // This function is written based on the ratios between the values. bool isActive = aStates.HasState(dom::ElementState::ACTIVE); bool isHover = aStates.HasState(dom::ElementState::HOVER); if (!isActive && !isHover) { return aFaceColor; } float luminance = RelativeLuminanceUtils::Compute(aFaceColor); if (isActive) { // 11.7 / 61.0 luminance = ScaleLuminanceBy(luminance, 0.192f); } else { // 38.1 / 61.0 luminance = ScaleLuminanceBy(luminance, 0.625f); } return RelativeLuminanceUtils::Adjust(aFaceColor, luminance); } } // namespace mozilla::widget