blob: bd1e6f78daf23a6daa083e2ad854898389f8e429 [file] [edit]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/native_theme/os_settings_provider.h"
#include <array>
#include <forward_list>
#include <optional>
#include <tuple>
#include <utility>
#include "base/callback_list.h"
#include "base/dcheck_is_on.h"
#include "base/functional/callback.h"
#include "base/no_destructor.h"
#include "base/sequence_checker.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/color/color_provider_key.h"
#include "ui/gfx/color_conversions.h"
#include "ui/gfx/color_utils.h"
#include "ui/native_theme/native_theme.h"
// `OsSettingsProviderImpl` is an alias to a forward-declared type; to construct
// it in `Get()` below, we must have the full type definition.
#if BUILDFLAG(IS_ANDROID)
#include "ui/native_theme/os_settings_provider_android.h"
#elif BUILDFLAG(IS_CHROMEOS)
#include "ui/native_theme/os_settings_provider_ash.h"
#elif BUILDFLAG(IS_MAC)
#include "ui/native_theme/os_settings_provider_mac.h"
#elif BUILDFLAG(IS_WIN)
#include "base/win/win_util.h"
#include "ui/native_theme/os_settings_provider_win.h"
#endif
namespace ui {
namespace {
// Returns the global list of constructed `OsSettingsProvider`s. Each entry
// overrides subsequent ones.
std::forward_list<OsSettingsProvider*>& GetOsSettingsProviders(
OsSettingsProvider::PriorityLevel priority_level) {
// All `OsSettingsProvider`s must access on the same thread to avoid data
// races on the lists below.
#if DCHECK_IS_ON() // Guard to avoid assertion failure from `NoDestructor`.
static base::NoDestructor<base::SequenceChecker> s_sequence_checker;
DCHECK_CALLED_ON_VALID_SEQUENCE(*s_sequence_checker);
#endif
static base::NoDestructor<std::array<
std::forward_list<OsSettingsProvider*>,
std::to_underlying(OsSettingsProvider::PriorityLevel::kLast) + 1>>
s_providers;
return (*s_providers)[std::to_underlying(priority_level)];
}
// Returns the global list of callbacks to notify on setting changes. This is
// not a non-static member of `OsSettingsProvider` since callers of
// `RegisterCallback` should be agnostic to changes in the active provider (e.g.
// when tests override it).
using CallbackList =
base::RepeatingCallbackList<OsSettingsProvider::SettingsChangedCallbackT>;
CallbackList* GetOsSettingsChangedCallbacks() {
// All `OsSettingsProvider`s must access on the same thread to avoid data
// races on the list below.
#if DCHECK_IS_ON() // Guard to avoid assertion failure from `NoDestructor`.
static base::NoDestructor<base::SequenceChecker> s_sequence_checker;
DCHECK_CALLED_ON_VALID_SEQUENCE(*s_sequence_checker);
#endif
static base::NoDestructor<CallbackList> s_callbacks;
return s_callbacks.get();
}
} // namespace
OsSettingsProvider::OsSettingsProvider(PriorityLevel priority_level)
: priority_level_(priority_level) {
GetOsSettingsProviders(priority_level_).push_front(this);
NotifyOnSettingsChanged();
}
OsSettingsProvider::~OsSettingsProvider() {
auto& providers = GetOsSettingsProviders(priority_level_);
const bool was_active = providers.front() == this;
providers.remove(this);
// Switching from one provider to another is effectively a settings change. By
// contrast, when the last provider is destroyed, we're in test code and
// likely shutting down, so notifying is pointless at best and could trigger
// strange behavior at worst.
if (was_active && !providers.empty()) {
NotifyOnSettingsChanged();
}
}
// static
OsSettingsProvider& OsSettingsProvider::Get() {
// Return any higher-than-production-priority providers first.
for (auto i = PriorityLevel::kLast; i > PriorityLevel::kProduction;
i = static_cast<PriorityLevel>(std::to_underlying(i) - 1)) {
if (const auto& providers = GetOsSettingsProviders(i); !providers.empty()) {
return *providers.front();
}
}
// If there is no production provider, create one.
const auto& providers = GetOsSettingsProviders(PriorityLevel::kProduction);
if (providers.empty()) {
#if BUILDFLAG(IS_WIN)
// `OsSettingsProviderWin` attempts calls to user32.dll, so avoid
// instantiating it if those calls are not possible.
if (!base::win::IsUser32AndGdi32Available()) {
static base::NoDestructor<OsSettingsProvider>
s_fallback_settings_provider(PriorityLevel::kProduction);
return *s_fallback_settings_provider;
}
#endif
// Construct an `OsSettingsProviderImpl` by default. This is conditional so
// that if e.g. a test constructs a provider before the first call to
// `Get()`, that provider won't be overridden.
static base::NoDestructor<OsSettingsProviderImpl> s_settings_provider;
// Since the above provider is never destroyed, `providers` should never be
// empty again, even if other providers are subsequently created/destroyed.
CHECK(!providers.empty());
}
// The first item on the list is the most recently constructed.
return *providers.front();
}
// static
base::CallbackListSubscription
OsSettingsProvider::RegisterOsSettingsChangedCallback(
base::RepeatingCallback<SettingsChangedCallbackT> cb) {
return GetOsSettingsChangedCallbacks()->Add(std::move(cb));
}
bool OsSettingsProvider::DarkColorSchemeAvailable() const {
return true;
}
NativeTheme::PreferredColorScheme OsSettingsProvider::PreferredColorScheme()
const {
if (ForcedColorsActive()) {
// According to the spec, the preferred color scheme for web content is
// "dark" if the Canvas color has L<33% and "light" if L>67%, where "L" is
// LAB lightness. The Canvas color is mapped to the Window system color.
// https://www.w3.org/TR/css-color-adjust-1/#forced
if (const auto bg_color = Color(ColorId::kWindow)) {
const SkColor srgb_legacy = bg_color.value();
const auto [r, g, b] = gfx::SRGBLegacyToSRGB(SkColorGetR(srgb_legacy),
SkColorGetG(srgb_legacy),
SkColorGetB(srgb_legacy));
const auto [x, y, z] = gfx::SRGBToXYZD50(r, g, b);
const float lab_lightness = std::get<0>(gfx::XYZD50ToLab(x, y, z));
if (lab_lightness < 33.0f) {
return NativeTheme::PreferredColorScheme::kDark;
}
if (lab_lightness > 67.0f) {
return NativeTheme::PreferredColorScheme::kLight;
}
}
}
return NativeTheme::PreferredColorScheme::kNoPreference;
}
ColorProviderKey::UserColorSource OsSettingsProvider::PreferredColorSource()
const {
return ColorProviderKey::UserColorSource::kAccent;
}
NativeTheme::PreferredContrast OsSettingsProvider::PreferredContrast() const {
if (ForcedColorsActive()) {
// TODO(sartang@microsoft.com): Update the spec page at
// https://www.w3.org/TR/css-color-adjust-1/#forced, it currently does not
// mention the relation between forced-colors-active and prefers-contrast.
//
// According to spec [1], "in addition to forced-colors: active, the user
// agent must also match one of prefers-contrast: more or prefers-contrast:
// less if it can determine that the forced color palette chosen by the user
// has a particularly high or low contrast, and must make prefers-contrast:
// custom match otherwise".
//
// Using WCAG definitions [2], we have decided to match 'more' in Forced
// Colors Mode if the contrast ratio between the foreground and background
// color is 7:1 or greater.
//
// "A contrast ratio of 3:1 is the minimum level recommended by
// [[ISO-9241-3]] and [[ANSI-HFES-100-1988]] for standard text and
// vision"[2]. Given this, we will start by matching to 'less' in Forced
// Colors Mode if the contrast ratio between the foreground and background
// color is 2.5:1 or less.
//
// These ratios will act as an experimental baseline that we can adjust
// based on user feedback.
//
// [1]
// https://drafts.csswg.org/mediaqueries-5/#valdef-media-forced-colors-active
// [2] https://www.w3.org/WAI/WCAG21/Understanding/contrast-enhanced
if (const auto bg_color = Color(ColorId::kWindow),
fg_color = Color(ColorId::kWindowText);
bg_color.has_value() && fg_color.has_value()) {
const float contrast_ratio =
color_utils::GetContrastRatio(bg_color.value(), fg_color.value());
if (contrast_ratio >= 7) {
return NativeTheme::PreferredContrast::kMore;
}
return contrast_ratio <= 2.5 ? NativeTheme::PreferredContrast::kLess
: NativeTheme::PreferredContrast::kCustom;
}
}
return NativeTheme::PreferredContrast::kNoPreference;
}
bool OsSettingsProvider::PrefersReducedTransparency() const {
return false;
}
bool OsSettingsProvider::PrefersInvertedColors() const {
return false;
}
bool OsSettingsProvider::ForcedColorsActive() const {
return false;
}
std::optional<SkColor> OsSettingsProvider::AccentColor() const {
return std::nullopt;
}
std::optional<SkColor> OsSettingsProvider::Color(ColorId color_id) const {
return std::nullopt;
}
std::optional<ColorProviderKey::SchemeVariant>
OsSettingsProvider::SchemeVariant() const {
return std::nullopt;
}
base::TimeDelta OsSettingsProvider::CaretBlinkInterval() const {
return kDefaultCaretBlinkInterval;
}
void OsSettingsProvider::NotifyOnSettingsChanged(bool force_notify) {
// Don't notify if this provider isn't the active one.
if (&Get() == this) {
GetOsSettingsChangedCallbacks()->Notify(force_notify);
}
}
} // namespace ui