Raw File
ThemeColors.cpp
/* -*- 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<nsIFrame*>(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
back to top