| // 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_win.h" |
| |
| #include <windows.h> |
| |
| #include <array> |
| #include <optional> |
| #include <utility> |
| |
| #include "base/check.h" |
| #include "base/containers/flat_map.h" |
| #include "base/functional/bind.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/time/time.h" |
| #include "base/win/dark_mode_support.h" |
| #include "base/win/registry.h" |
| #include "third_party/skia/include/core/SkColor.h" |
| #include "ui/color/win/accent_color_observer.h" |
| #include "ui/color/win/native_color_mixers_win.h" |
| #include "ui/native_theme/native_theme.h" |
| |
| namespace ui { |
| |
| namespace { |
| |
| bool IsSystemForcedColorsActive() { |
| if (HIGHCONTRAST result = {.cbSize = sizeof(HIGHCONTRAST)}; |
| SystemParametersInfo(SPI_GETHIGHCONTRAST, result.cbSize, &result, 0)) { |
| return !!(result.dwFlags & HCF_HIGHCONTRASTON); |
| } |
| return false; |
| } |
| |
| } // namespace |
| |
| OsSettingsProviderWin::OsSettingsProviderWin() |
| : OsSettingsProvider(PriorityLevel::kProduction) { |
| // If there's no sequenced task runner handle, we can't be called back for |
| // registry changes. This generally happens in tests. |
| const bool observers_can_operate = |
| base::SequencedTaskRunner::HasCurrentDefault(); |
| |
| // Set initial state, and register for future changes if applicable. |
| if (hkcu_themes_regkey_.Open( |
| HKEY_CURRENT_USER, |
| L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", |
| KEY_READ | KEY_NOTIFY) == ERROR_SUCCESS) { |
| UpdateForThemesRegkey(); |
| if (observers_can_operate) { |
| RegisterThemesRegkeyObserver(); |
| } |
| } |
| if (hkcu_color_filtering_regkey_.Open( |
| HKEY_CURRENT_USER, L"Software\\Microsoft\\ColorFiltering", |
| KEY_READ | KEY_NOTIFY) == ERROR_SUCCESS) { |
| UpdateForColorFilteringRegkey(); |
| if (observers_can_operate) { |
| RegisterColorFilteringRegkeyObserver(); |
| } |
| } |
| UpdateColors(); |
| |
| // Initialize forced colors (high contrast) state. |
| forced_colors_active_ = IsSystemForcedColorsActive(); |
| |
| // Histogram high contrast state. |
| // NOTE: Reported in metrics; do not reorder, add additional values at end. |
| enum class HighContrastColorScheme { |
| kNone = 0, |
| kDark = 1, |
| kLight = 2, |
| kMaxValue = kLight, |
| }; |
| auto color_scheme = HighContrastColorScheme::kNone; |
| if (PreferredContrast() == NativeTheme::PreferredContrast::kMore) { |
| color_scheme = |
| (PreferredColorScheme() == NativeTheme::PreferredColorScheme::kDark) |
| ? HighContrastColorScheme::kDark |
| : HighContrastColorScheme::kLight; |
| } |
| base::UmaHistogramEnumeration("Accessibility.WinHighContrastTheme", |
| color_scheme); |
| } |
| |
| OsSettingsProviderWin::~OsSettingsProviderWin() = default; |
| |
| bool OsSettingsProviderWin::DarkColorSchemeAvailable() const { |
| return base::win::IsDarkModeAvailable(); |
| } |
| |
| NativeTheme::PreferredColorScheme OsSettingsProviderWin::PreferredColorScheme() |
| const { |
| if (const NativeTheme::PreferredColorScheme preferred_color_scheme = |
| OsSettingsProvider::PreferredColorScheme(); |
| preferred_color_scheme != |
| NativeTheme::PreferredColorScheme::kNoPreference) { |
| return preferred_color_scheme; |
| } |
| |
| return in_dark_mode_ ? NativeTheme::PreferredColorScheme::kDark |
| : NativeTheme::PreferredColorScheme::kLight; |
| } |
| |
| ColorProviderKey::UserColorSource OsSettingsProviderWin::PreferredColorSource() |
| const { |
| return ColorProviderKey::UserColorSource::kBaseline; |
| } |
| |
| bool OsSettingsProviderWin::PrefersReducedTransparency() const { |
| return prefers_reduced_transparency_; |
| } |
| |
| bool OsSettingsProviderWin::PrefersInvertedColors() const { |
| return prefers_inverted_colors_; |
| } |
| |
| bool OsSettingsProviderWin::ForcedColorsActive() const { |
| return forced_colors_active_; |
| } |
| |
| std::optional<SkColor> OsSettingsProviderWin::AccentColor() const { |
| return accent_color_; |
| } |
| |
| std::optional<SkColor> OsSettingsProviderWin::Color(ColorId color_id) const { |
| const auto entry = colors_.find(color_id); |
| return (entry == colors_.end()) ? std::nullopt |
| : std::make_optional(entry->second); |
| } |
| |
| base::TimeDelta OsSettingsProviderWin::CaretBlinkInterval() const { |
| // Unfortunately Windows does not seem to have any way to monitor changes to |
| // this value; MSDN suggests apps "occasionally check the cursor settings — |
| // for instance, when the dialog is loaded" |
| // (https://learn.microsoft.com/en-us/previous-versions/windows/desktop/dnacc/flashing-user-interface-and-the-getcaretblinktime-function#using-getcaretblinktime). |
| // Given how rarely users change this, it doesn't seem worth trying to plumb |
| // something to e.g. check for caret blink time changes when Chrome regains |
| // focus. |
| const UINT caret_blink_time = ::GetCaretBlinkTime(); |
| if (!caret_blink_time) { |
| return OsSettingsProvider::CaretBlinkInterval(); |
| } |
| return (caret_blink_time == INFINITE) ? base::TimeDelta() |
| : base::Milliseconds(caret_blink_time); |
| } |
| |
| void OsSettingsProviderWin::RegisterThemesRegkeyObserver() { |
| CHECK(hkcu_themes_regkey_.Valid()); |
| CHECK(base::SequencedTaskRunner::HasCurrentDefault()); |
| hkcu_themes_regkey_.StartWatching(base::BindOnce( |
| [](OsSettingsProviderWin* provider) { |
| const NativeTheme::PreferredColorScheme old_preferred_color_scheme = |
| provider->PreferredColorScheme(); |
| const bool old_prefers_reduced_transparency = |
| provider->PrefersReducedTransparency(); |
| provider->UpdateForThemesRegkey(); |
| if (provider->PreferredColorScheme() != old_preferred_color_scheme || |
| provider->PrefersReducedTransparency() != |
| old_prefers_reduced_transparency) { |
| provider->NotifyOnSettingsChanged(); |
| } |
| |
| // `StartWatching()`'s callback is one-shot and must be re-registered |
| // for future notifications. |
| provider->RegisterThemesRegkeyObserver(); |
| }, |
| base::Unretained(this))); |
| } |
| |
| void OsSettingsProviderWin::RegisterColorFilteringRegkeyObserver() { |
| CHECK(hkcu_color_filtering_regkey_.Valid()); |
| CHECK(base::SequencedTaskRunner::HasCurrentDefault()); |
| hkcu_color_filtering_regkey_.StartWatching(base::BindOnce( |
| [](OsSettingsProviderWin* provider) { |
| const bool old_prefers_inverted_colors = |
| provider->PrefersInvertedColors(); |
| provider->UpdateForColorFilteringRegkey(); |
| if (provider->PrefersInvertedColors() != old_prefers_inverted_colors) { |
| provider->NotifyOnSettingsChanged(); |
| } |
| |
| // `StartWatching()`'s callback is one-shot and must be re-registered |
| // for future notifications. |
| provider->RegisterColorFilteringRegkeyObserver(); |
| }, |
| base::Unretained(this))); |
| } |
| |
| void OsSettingsProviderWin::UpdateForThemesRegkey() { |
| CHECK(hkcu_themes_regkey_.Valid()); |
| |
| DWORD apps_use_light_theme = 1; |
| hkcu_themes_regkey_.ReadValueDW(L"AppsUseLightTheme", &apps_use_light_theme); |
| in_dark_mode_ = !apps_use_light_theme; |
| |
| DWORD enable_transparency = 1; |
| hkcu_themes_regkey_.ReadValueDW(L"EnableTransparency", &enable_transparency); |
| prefers_reduced_transparency_ = !enable_transparency; |
| } |
| |
| void OsSettingsProviderWin::UpdateForColorFilteringRegkey() { |
| CHECK(hkcu_color_filtering_regkey_.Valid()); |
| |
| DWORD active = 0, filter_type = 0; |
| hkcu_color_filtering_regkey_.ReadValueDW(L"Active", &active); |
| if (active == 1) { |
| hkcu_color_filtering_regkey_.ReadValueDW(L"FilterType", &filter_type); |
| } |
| // 0 = Greyscale |
| // 1 = Invert |
| // 2 = Greyscale Inverted |
| // 3 = Deuteranopia |
| // 4 = Protanopia |
| // 5 = Tritanopia |
| prefers_inverted_colors_ = filter_type == 1; |
| } |
| |
| void OsSettingsProviderWin::OnAccentColorMaybeChanged() { |
| const auto accent_color = AccentColorObserver::Get()->accent_color(); |
| if (std::exchange(accent_color_, accent_color) != accent_color) { |
| NotifyOnSettingsChanged(); |
| } |
| } |
| |
| void OsSettingsProviderWin::UpdateColors() { |
| static constexpr auto kColors = |
| std::to_array<std::pair<ColorId, ui::ColorId>>( |
| {{ColorId::kButtonFace, kColorNativeBtnFace}, |
| {ColorId::kButtonHighlight, kColorNativeBtnHighlight}, |
| {ColorId::kScrollbar, kColorNativeScrollbar}, |
| {ColorId::kWindow, kColorNativeWindow}, |
| {ColorId::kWindowText, kColorNativeWindowText}}); |
| const auto sys_colors = GetCurrentSysColors(); |
| for (const auto& entry : kColors) { |
| colors_[entry.first] = sys_colors.at(entry.second); |
| } |
| } |
| |
| void OsSettingsProviderWin::OnWndProc(HWND hwnd, |
| UINT message, |
| WPARAM wparam, |
| LPARAM lparam) { |
| if (message == WM_SYSCOLORCHANGE) { |
| UpdateColors(); |
| if (ForcedColorsActive()) { |
| NotifyOnSettingsChanged(true); |
| } |
| } else if (message == WM_SETTINGCHANGE && wparam == SPI_SETHIGHCONTRAST) { |
| const bool old_forced_colors_active = ForcedColorsActive(); |
| forced_colors_active_ = IsSystemForcedColorsActive(); |
| if (ForcedColorsActive() != old_forced_colors_active) { |
| NotifyOnSettingsChanged(); |
| } |
| } |
| } |
| |
| } // namespace ui |