blob: e09cf382304fa258f3d8bca4fae116301076a011 [file] [log] [blame]
// Copyright 2012 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/native_theme.h"
#include <stddef.h>
#include <algorithm>
#include <cmath>
#include <optional>
#include <utility>
#include "base/callback_list.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/observer_list.h"
#include "base/sequence_checker.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "base/types/pass_key.h"
#include "build/build_config.h"
#include "cc/paint/paint_canvas.h"
#include "cc/paint/paint_flags.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/ui_base_switches.h"
#include "ui/color/color_id.h"
#include "ui/color/color_metrics.h"
#include "ui/color/color_provider.h"
#include "ui/color/color_provider_key.h"
#include "ui/color/color_provider_manager.h"
#include "ui/color/system_theme.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/native_theme/native_theme_observer.h"
#include "ui/native_theme/os_settings_provider.h"
#if BUILDFLAG(IS_MAC)
#include "ui/native_theme/native_theme_aura.h"
#include "ui/native_theme/native_theme_mac.h"
#elif defined(USE_AURA)
#include "ui/native_theme/features/native_theme_features.h"
#include "ui/native_theme/native_theme_aura.h"
#include "ui/native_theme/native_theme_fluent.h"
#if BUILDFLAG(IS_WIN)
#include "ui/native_theme/native_theme_win.h"
#endif
#else
#include "ui/native_theme/native_theme_mobile.h"
#endif
namespace ui {
namespace {
#if BUILDFLAG(IS_MAC)
using NativeUiTheme = NativeThemeMac;
using WebUiTheme = NativeThemeAura;
#elif defined(USE_AURA)
#if BUILDFLAG(IS_WIN)
using NativeUiTheme = NativeThemeWin;
#else
using NativeUiTheme = NativeThemeAura;
#endif
NativeTheme* GetInstanceForWebImpl() {
static const bool use_fluent = IsFluentScrollbarEnabled();
if (use_fluent) {
static base::NoDestructor<NativeThemeFluent> s_web_theme;
return s_web_theme.get();
}
static base::NoDestructor<NativeThemeAura> s_web_theme(
#if BUILDFLAG(IS_CHROMEOS)
true
#else
IsOverlayScrollbarEnabledByFeatureFlag()
#endif
);
return s_web_theme.get();
}
#else
using NativeUiTheme = NativeThemeMobile;
using WebUiTheme = NativeThemeMobile;
#endif
} // namespace
NativeTheme::MenuListExtraParams::MenuListExtraParams() = default;
NativeTheme::MenuListExtraParams::MenuListExtraParams(
const NativeTheme::MenuListExtraParams&) = default;
NativeTheme::MenuListExtraParams& NativeTheme::MenuListExtraParams::operator=(
const NativeTheme::MenuListExtraParams&) = default;
NativeTheme::TextFieldExtraParams::TextFieldExtraParams() = default;
NativeTheme::TextFieldExtraParams::TextFieldExtraParams(
const NativeTheme::TextFieldExtraParams&) = default;
NativeTheme::TextFieldExtraParams& NativeTheme::TextFieldExtraParams::operator=(
const NativeTheme::TextFieldExtraParams&) = default;
NativeTheme::UpdateNotificationDelayScoper::UpdateNotificationDelayScoper() {
++num_instances_;
}
NativeTheme::UpdateNotificationDelayScoper::UpdateNotificationDelayScoper(
const UpdateNotificationDelayScoper&) {
++num_instances_;
}
NativeTheme::UpdateNotificationDelayScoper::UpdateNotificationDelayScoper(
UpdateNotificationDelayScoper&&) {
++num_instances_;
}
NativeTheme::UpdateNotificationDelayScoper::~UpdateNotificationDelayScoper() {
if (--num_instances_ == 0) {
GetDelayedNotifications().Notify();
}
}
// static
base::CallbackListSubscription
NativeTheme::UpdateNotificationDelayScoper::RegisterCallback(
base::PassKey<NativeTheme>,
base::OnceClosure cb) {
return GetDelayedNotifications().Add(std::move(cb));
}
// static
base::OnceClosureList&
NativeTheme::UpdateNotificationDelayScoper::GetDelayedNotifications() {
static base::NoDestructor<base::OnceClosureList> s_delayed_notifications;
return *s_delayed_notifications;
}
// static
size_t NativeTheme::UpdateNotificationDelayScoper::num_instances_ = 0;
// static
NativeTheme* NativeTheme::GetInstanceForNativeUi() {
static base::NoDestructor<NativeUiTheme> s_native_theme;
static bool initialized = false;
if (!initialized) {
s_native_theme->BeginObservingOsSettingChanges();
initialized = true;
}
return s_native_theme.get();
}
// static
NativeTheme* NativeTheme::GetInstanceForWeb() {
#if defined(USE_AURA)
NativeTheme* const native_theme = GetInstanceForWebImpl();
#else
static base::NoDestructor<WebUiTheme> s_web_theme;
NativeTheme* const native_theme = s_web_theme.get();
#endif
static bool initialized = false;
if (!initialized) {
GetInstanceForNativeUi()->SetAssociatedWebInstance(native_theme);
initialized = true;
}
return native_theme;
}
// static
float NativeTheme::AdjustBorderWidthByZoom(float border_width,
float zoom_level) {
return std::max(std::floor(border_width * zoom_level), 1.0f);
}
// static
float NativeTheme::AdjustBorderRadiusByZoom(Part part,
float border_radius,
float zoom) {
return (part == kCheckbox || part == kTextField || part == kPushButton)
? AdjustBorderWidthByZoom(border_radius, zoom)
: border_radius;
}
gfx::Size NativeTheme::GetPartSize(Part part,
State state,
const ExtraParams& extra_params) const {
return {};
}
int NativeTheme::GetPaintedScrollbarTrackInset() const {
return 0;
}
gfx::Insets NativeTheme::GetScrollbarSolidColorThumbInsets(Part part) const {
return {};
}
float NativeTheme::GetBorderRadiusForPart(Part part,
float width,
float height) const {
return 0;
}
bool NativeTheme::SupportsNinePatch(Part part) const {
return false;
}
gfx::Size NativeTheme::GetNinePatchCanvasSize(Part part) const {
NOTREACHED();
}
gfx::Rect NativeTheme::GetNinePatchAperture(Part part) const {
NOTREACHED();
}
SkColor NativeTheme::GetScrollbarThumbColor(
const ColorProvider* color_provider,
State state,
const ScrollbarThumbExtraParams& extra_params) const {
NOTREACHED();
}
SkColor NativeTheme::GetSystemButtonPressedColor(SkColor base_color) const {
return base_color;
}
void NativeTheme::BeginObservingOsSettingChanges() {
os_settings_changed_subscription_ =
OsSettingsProvider::RegisterOsSettingsChangedCallback(base::BindRepeating(
&NativeTheme::OnToolkitSettingsChanged, base::Unretained(this)));
UpdateVariablesForToolkitSettings();
}
void NativeTheme::AddObserver(NativeThemeObserver* observer) {
native_theme_observers_.AddObserver(observer);
}
void NativeTheme::RemoveObserver(NativeThemeObserver* observer) {
native_theme_observers_.RemoveObserver(observer);
}
void NativeTheme::NotifyOnNativeThemeUpdated() {
// This specific method is prone to being mistakenly called on the wrong
// sequence, because it is often invoked from a platform-specific event
// listener, and those events may be delivered on unexpected sequences.
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (base::PassKey<NativeTheme> pass_key;
UpdateNotificationDelayScoper::exists(pass_key)) {
// At least one scoper exists, so delay notifications until it's gone.
if (!update_delay_subscription_) {
update_delay_subscription_ =
UpdateNotificationDelayScoper::RegisterCallback(
pass_key, base::BindOnce(&NativeTheme::NotifyOnNativeThemeUpdated,
base::Unretained(this)));
}
return;
}
if (update_delay_subscription_) {
// No scopers exist, but a subscription does: this is the callback from the
// last scoper being destroyed. Reset the subscription so it can be
// recreated in the future when necessary.
update_delay_subscription_ = {};
}
base::ElapsedTimer timer;
auto& color_provider_manager = ColorProviderManager::Get();
const size_t initial_providers_initialized =
color_provider_manager.num_providers_initialized();
// Reset the ColorProviderManager's cache so that ColorProviders requested
// from this point onwards incorporate the changes to the system theme.
color_provider_manager.ResetColorProviderCache();
NotifyOnNativeThemeUpdatedImpl();
RecordNumColorProvidersInitializedDuringOnNativeThemeUpdated(
color_provider_manager.num_providers_initialized() -
initial_providers_initialized);
RecordTimeSpentProcessingOnNativeThemeUpdatedEvent(timer.Elapsed());
}
void NativeTheme::NotifyOnCaptionStyleUpdated() {
// This specific method is prone to being mistakenly called on the wrong
// sequence, because it is often invoked from a platform-specific event
// listener, and those events may be delivered on unexpected sequences.
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
native_theme_observers_.Notify(&NativeThemeObserver::OnCaptionStyleUpdated);
}
void NativeTheme::Paint(cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
Part part,
State state,
const gfx::Rect& rect,
const ExtraParams& extra_params,
bool forced_colors,
PreferredColorScheme color_scheme,
PreferredContrast contrast,
std::optional<SkColor> accent_color) const {
if (rect.IsEmpty()) {
return;
}
// For `color_scheme`, `kNoPreference` means "use current".
const bool dark_mode =
color_scheme == PreferredColorScheme::kDark ||
(color_scheme == PreferredColorScheme::kNoPreference &&
preferred_color_scheme() == PreferredColorScheme::kDark);
// Form control accents shouldn't be drawn with any transparency.
// TODO(C++23): Replace the below with:
// ```
// const std::optional<SkColor> accent_color_opaque = accent_color.transform(
// [](SkColor c) { return SkColorSetA(c, SK_AlphaOPAQUE); });
// ```
const std::optional<SkColor> accent_color_opaque =
accent_color.has_value() ? std::make_optional(SkColorSetA(
accent_color.value(), SK_AlphaOPAQUE))
: std::nullopt;
gfx::Canvas gfx_canvas(canvas, 1.0f);
gfx::ScopedCanvas scoped_canvas(&gfx_canvas);
gfx_canvas.ClipRect(rect);
PaintImpl(canvas, color_provider, part, state, rect, extra_params,
forced_colors, dark_mode, contrast, accent_color_opaque);
}
ColorProviderKey NativeTheme::GetColorProviderKey(
scoped_refptr<ColorProviderKey::ThemeInitializerSupplier> custom_theme,
bool use_custom_frame) const {
ColorProviderKey key;
key.color_mode = preferred_color_scheme() == PreferredColorScheme::kDark
? ColorProviderKey::ColorMode::kDark
: ColorProviderKey::ColorMode::kLight;
key.contrast_mode = preferred_contrast() == PreferredContrast::kMore
? ColorProviderKey::ContrastMode::kHigh
: ColorProviderKey::ContrastMode::kNormal;
key.forced_colors = forced_colors();
key.system_theme = system_theme();
key.frame_type = use_custom_frame ? ColorProviderKey::FrameType::kChromium
: ColorProviderKey::FrameType::kNative;
key.user_color_source = preferred_color_source_;
key.user_color = user_color();
key.scheme_variant = scheme_variant();
key.custom_theme = std::move(custom_theme);
return key;
}
NativeTheme::NativeTheme(SystemTheme system_theme)
: system_theme_(system_theme) {}
NativeTheme::~NativeTheme() = default;
bool NativeTheme::IsForcedDarkMode() {
static bool kIsForcedDarkMode =
base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kForceDarkMode);
return kIsForcedDarkMode;
}
bool NativeTheme::IsForcedHighContrast() {
static bool kIsForcedHighContrast =
base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kForceHighContrast);
return kIsForcedHighContrast;
}
void NativeTheme::PaintMenuItemBackground(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const MenuItemExtraParams& extra_params) const {
const SkScalar radius = SkIntToScalar(extra_params.corner_radius);
cc::PaintFlags flags;
const ColorId id = (state == kHovered)
#if BUILDFLAG(IS_CHROMEOS)
? kColorAshSystemUIMenuItemBackgroundSelected
: kColorAshSystemUIMenuBackground;
#else
? kColorMenuItemBackgroundSelected
: kColorMenuBackground;
#endif
flags.setColor(color_provider->GetColor(id));
canvas->drawRoundRect(gfx::RectToSkRect(rect), radius, radius, flags);
}
void NativeTheme::OnToolkitSettingsChanged(bool force_notify) {
if (UpdateVariablesForToolkitSettings() || force_notify) {
NotifyOnNativeThemeUpdated();
}
}
void NativeTheme::SetAssociatedWebInstance(
NativeTheme* associated_web_instance) {
if (associated_web_instance_ != associated_web_instance) {
associated_web_instance_ = associated_web_instance;
if (UpdateWebInstance()) {
associated_web_instance_->NotifyOnNativeThemeUpdatedImpl();
}
}
}
bool NativeTheme::UpdateWebInstance() const {
if (!associated_web_instance_) {
return false;
}
// NOTE: Intentionally does not copy the native "overlay scrollbar" setting to
// the web instance, as the web instance often wants to differ there.
// TODO(crbug.com/444399080): If we had a notion somewhere about "web wants
// overlay scrollbars even when native doesn't", we could probably copy the
// setting fearlessly here (and have that override it on the web instance
// side), making callers who want to toggle overlay scrollbars on/off globally
// simpler and safer.
// TODO(pkasting): The code duplication between this function and
// `UpdateVariablesForToolkitSettings()` is error-prone; e.g. it's easy to
// forget to update the web instance properly when adding a new member.
// Refactor to a settings struct or similar.
bool updated_web_instance = false;
if (associated_web_instance_->forced_colors() != forced_colors()) {
associated_web_instance_->forced_colors_ = forced_colors();
updated_web_instance = true;
}
if (associated_web_instance_->preferred_color_scheme() !=
preferred_color_scheme()) {
associated_web_instance_->preferred_color_scheme_ =
preferred_color_scheme();
updated_web_instance = true;
}
if (associated_web_instance_->preferred_contrast() != preferred_contrast()) {
associated_web_instance_->preferred_contrast_ = preferred_contrast();
updated_web_instance = true;
}
if (associated_web_instance_->prefers_reduced_transparency() !=
prefers_reduced_transparency()) {
associated_web_instance_->prefers_reduced_transparency_ =
prefers_reduced_transparency();
updated_web_instance = true;
}
if (associated_web_instance_->inverted_colors() != inverted_colors()) {
associated_web_instance_->inverted_colors_ = inverted_colors();
updated_web_instance = true;
}
if (associated_web_instance_->user_color() != user_color()) {
associated_web_instance_->user_color_ = user_color();
updated_web_instance = true;
}
if (associated_web_instance_->scheme_variant() != scheme_variant()) {
associated_web_instance_->scheme_variant_ = scheme_variant();
updated_web_instance = true;
}
if (associated_web_instance_->preferred_color_source_ !=
preferred_color_source_) {
associated_web_instance_->preferred_color_source_ = preferred_color_source_;
updated_web_instance = true;
}
if (associated_web_instance_->caret_blink_interval() !=
caret_blink_interval()) {
associated_web_instance_->caret_blink_interval_ = caret_blink_interval();
updated_web_instance = true;
}
return updated_web_instance;
}
void NativeTheme::NotifyOnNativeThemeUpdatedImpl() {
// Update any associated web instance's settings before notifying observers,
// since those observers may attempt to override the web instance's settings
// (e.g. to implement web-content-specific forced colors).
const bool updated_web_instance = UpdateWebInstance();
native_theme_observers_.Notify(&NativeThemeObserver::OnNativeThemeUpdated,
this);
// If the web instance was modified above, also notify its observers. This is
// done last so any of our observers that modify the web instance will have
// already run.
//
// NOTE: If any above observers already called `NotifyOnNativeThemeUpdated()`
// on the web theme, this is unnecessary jank; however, it's not worth the
// hassle to try to detect this.
//
// TODO(pkasting): Adding a scoping object to batch updates would address
// this; see comments in header above accessors.
if (updated_web_instance) {
// Calling `NotifyOnNativeThemeUpdated()` here would unnecessarily churn the
// color provider cache.
associated_web_instance_->NotifyOnNativeThemeUpdatedImpl();
}
}
bool NativeTheme::UpdateVariablesForToolkitSettings() {
// This should not be called except in an instance that is monitoring OS
// setting changes. Otherwise, either:
// * This is the associated web instance of another instance, and that
// instance will update our members via `UpdateWebInstance()`, so updating
// them here is both wasteful and potentially incorrect
// * Or, whoever created this instance didn't call
// `BeginObservingOsSettingChanges()` and should have
// Getting this right is important, because calling
// `OsSettingsProvider::Get()` in the renderer may not return the expected
// instance (see comments on that function), so we shouldn't be introducing
// new calls to it carelessly.
CHECK(os_settings_changed_subscription_);
// Calculate updated values.
const auto& os_settings_provider = OsSettingsProvider::Get();
const auto new_forced_colors = CalculateForcedColors();
const auto new_preferred_color_scheme = CalculatePreferredColorScheme();
const auto new_preferred_contrast = CalculatePreferredContrast();
const auto new_prefers_reduced_transparency =
os_settings_provider.PrefersReducedTransparency();
const auto new_inverted_colors = os_settings_provider.PrefersInvertedColors();
const auto new_user_color = os_settings_provider.AccentColor();
const auto new_scheme_variant = os_settings_provider.SchemeVariant();
const auto new_preferred_color_source =
os_settings_provider.PreferredColorSource();
const auto new_caret_blink_interval =
os_settings_provider.CaretBlinkInterval();
// Set updated values and see if anything changed.
bool updated = false;
if (forced_colors() != new_forced_colors) {
forced_colors_ = new_forced_colors;
updated = true;
}
if (preferred_color_scheme() != new_preferred_color_scheme) {
preferred_color_scheme_ = new_preferred_color_scheme;
updated = true;
}
if (preferred_contrast() != new_preferred_contrast) {
preferred_contrast_ = new_preferred_contrast;
updated = true;
}
if (prefers_reduced_transparency() != new_prefers_reduced_transparency) {
prefers_reduced_transparency_ = new_prefers_reduced_transparency;
updated = true;
}
if (inverted_colors() != new_inverted_colors) {
inverted_colors_ = new_inverted_colors;
updated = true;
}
if (user_color() != new_user_color) {
user_color_ = new_user_color;
updated = true;
}
if (scheme_variant() != new_scheme_variant) {
scheme_variant_ = new_scheme_variant;
updated = true;
}
if (preferred_color_source_ != new_preferred_color_source) {
preferred_color_source_ = new_preferred_color_source;
updated = true;
}
if (caret_blink_interval() != new_caret_blink_interval) {
caret_blink_interval_ = new_caret_blink_interval;
updated = true;
}
return updated;
}
ColorProviderKey::ForcedColors NativeTheme::CalculateForcedColors() const {
return (IsForcedHighContrast() ||
OsSettingsProvider::Get().ForcedColorsActive())
? ColorProviderKey::ForcedColors::kSystem
: ColorProviderKey::ForcedColors::kNone;
}
NativeTheme::PreferredColorScheme NativeTheme::CalculatePreferredColorScheme()
const {
return IsForcedDarkMode() ? PreferredColorScheme::kDark
: OsSettingsProvider::Get().PreferredColorScheme();
}
NativeTheme::PreferredContrast NativeTheme::CalculatePreferredContrast() const {
return IsForcedHighContrast() ? PreferredContrast::kMore
: OsSettingsProvider::Get().PreferredContrast();
}
} // namespace ui