// 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.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "ui/native_theme/native_theme_win.h"
#include <windows.h>
#include <stddef.h>
#include <uxtheme.h>
#include <vsstyle.h>
#include <vssym32.h>
#include <optional>
#include <tuple>
#include "base/check.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "base/win/dark_mode_support.h"
#include "base/win/scoped_gdi_object.h"
#include "base/win/scoped_hdc.h"
#include "base/win/scoped_select_object.h"
#include "base/win/win_util.h"
#include "cc/paint/paint_canvas.h"
#include "cc/paint/paint_flags.h"
#include "skia/ext/platform_canvas.h"
#include "skia/ext/skia_utils_win.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkColorPriv.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/core/SkRefCnt.h"
#include "third_party/skia/include/core/SkShader.h"
#include "third_party/skia/include/core/SkSurface.h"
#include "ui/base/ui_base_features.h"
#include "ui/base/ui_base_switches.h"
#include "ui/color/color_provider.h"
#include "ui/display/win/screen_win.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/gdi_util.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/native_theme/common_theme.h"
#include "ui/native_theme/native_theme.h"
// This was removed from Winvers.h but is still used.
#if !defined(COLOR_MENUHIGHLIGHT)
#define COLOR_MENUHIGHLIGHT 29
#endif
namespace {
// Windows system color IDs cached and updated by the native theme.
const int kSysColors[] = {
COLOR_BTNFACE, COLOR_BTNTEXT, COLOR_GRAYTEXT, COLOR_HIGHLIGHT,
COLOR_HIGHLIGHTTEXT, COLOR_HOTLIGHT, COLOR_MENUHIGHLIGHT, COLOR_SCROLLBAR,
COLOR_WINDOW, COLOR_WINDOWTEXT,
};
void SetCheckerboardShader(SkPaint* paint, const RECT& align_rect) {
// Create a 2x2 checkerboard pattern using the 3D face and highlight colors.
const SkColor face = color_utils::GetSysSkColor(COLOR_3DFACE);
const SkColor highlight = color_utils::GetSysSkColor(COLOR_3DHILIGHT);
SkColor buffer[] = { face, highlight, highlight, face };
// Confusing bit: we first create a temporary bitmap with our desired pattern,
// then copy it to another bitmap. The temporary bitmap doesn't take
// ownership of the pixel data, and so will point to garbage when this
// function returns. The copy will copy the pixel data into a place owned by
// the bitmap, which is in turn owned by the shader, etc., so it will live
// until we're done using it.
SkImageInfo info = SkImageInfo::MakeN32Premul(2, 2);
SkBitmap temp_bitmap;
temp_bitmap.installPixels(info, buffer, info.minRowBytes());
SkBitmap bitmap;
if (bitmap.tryAllocPixels(info))
temp_bitmap.readPixels(info, bitmap.getPixels(), bitmap.rowBytes(), 0, 0);
// Align the pattern with the upper corner of |align_rect|.
SkMatrix local_matrix;
local_matrix.setTranslate(SkIntToScalar(align_rect.left),
SkIntToScalar(align_rect.top));
paint->setShader(bitmap.makeShader(SkTileMode::kRepeat, SkTileMode::kRepeat,
SkSamplingOptions(), &local_matrix));
}
// <-a->
// [ ***** ]
// ____ | |
// <-a-> <------b----->
// a: object_width
// b: frame_width
// *: animating object
//
// - the animation goes from "[" to "]" repeatedly.
// - the animation offset is at first "|"
//
int ComputeAnimationProgress(int frame_width,
int object_width,
int pixels_per_second,
double animated_seconds) {
int animation_width = frame_width + object_width;
double interval = static_cast<double>(animation_width) / pixels_per_second;
double ratio = fmod(animated_seconds, interval) / interval;
return static_cast<int>(animation_width * ratio) - object_width;
}
// Custom scoped object for storing DC and a bitmap that was selected into it,
// and making sure that they are deleted in the right order.
class ScopedCreateDCWithBitmap {
public:
explicit ScopedCreateDCWithBitmap(base::win::ScopedCreateDC::Handle hdc)
: dc_(hdc) {}
ScopedCreateDCWithBitmap(const ScopedCreateDCWithBitmap&) = delete;
ScopedCreateDCWithBitmap& operator=(const ScopedCreateDCWithBitmap&) = delete;
~ScopedCreateDCWithBitmap() {
// Delete DC before the bitmap, since objects should not be deleted while
// selected into a DC.
dc_.Close();
}
bool IsValid() const { return dc_.IsValid(); }
base::win::ScopedCreateDC::Handle Get() const { return dc_.Get(); }
// Selects |handle| to bitmap into DC. Returns false if handle is not valid.
bool SelectBitmap(base::win::ScopedBitmap::element_type handle) {
bitmap_.reset(handle);
if (!bitmap_.is_valid())
return false;
SelectObject(dc_.Get(), bitmap_.get());
return true;
}
private:
base::win::ScopedCreateDC dc_;
base::win::ScopedBitmap bitmap_;
};
base::win::RegKey OpenThemeRegKey(REGSAM access) {
base::win::RegKey hkcu_themes_regkey;
// Validity is checked at time-of-use.
std::ignore = hkcu_themes_regkey.Open(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\"
L"CurrentVersion\\Themes\\Personalize",
access);
return hkcu_themes_regkey;
}
base::win::RegKey OpenColorFilteringRegKey(REGSAM access) {
base::win::RegKey hkcu_color_filtering_regkey;
// Validity is checked at time-of-use.
std::ignore = hkcu_color_filtering_regkey.Open(
HKEY_CURRENT_USER, L"Software\\Microsoft\\ColorFiltering", access);
return hkcu_color_filtering_regkey;
}
} // namespace
namespace ui {
NativeTheme::SystemThemeColor SysColorToSystemThemeColor(int system_color) {
switch (system_color) {
case COLOR_BTNFACE:
return NativeTheme::SystemThemeColor::kButtonFace;
case COLOR_BTNTEXT:
return NativeTheme::SystemThemeColor::kButtonText;
case COLOR_GRAYTEXT:
return NativeTheme::SystemThemeColor::kGrayText;
case COLOR_HIGHLIGHT:
return NativeTheme::SystemThemeColor::kHighlight;
case COLOR_HIGHLIGHTTEXT:
return NativeTheme::SystemThemeColor::kHighlightText;
case COLOR_HOTLIGHT:
return NativeTheme::SystemThemeColor::kHotlight;
case COLOR_MENUHIGHLIGHT:
return NativeTheme::SystemThemeColor::kMenuHighlight;
case COLOR_SCROLLBAR:
return NativeTheme::SystemThemeColor::kScrollbar;
case COLOR_WINDOW:
return NativeTheme::SystemThemeColor::kWindow;
case COLOR_WINDOWTEXT:
return NativeTheme::SystemThemeColor::kWindowText;
default:
return NativeTheme::SystemThemeColor::kNotSupported;
}
}
NativeTheme* NativeTheme::GetInstanceForNativeUi() {
static base::NoDestructor<NativeThemeWin> s_native_theme(true, false);
return s_native_theme.get();
}
NativeTheme* NativeTheme::GetInstanceForDarkUI() {
static base::NoDestructor<NativeThemeWin> s_dark_native_theme(false, true);
return s_dark_native_theme.get();
}
// static
bool NativeTheme::SystemDarkModeSupported() {
static bool system_supports_dark_mode =
([]() { return OpenThemeRegKey(KEY_READ).Valid(); })();
return system_supports_dark_mode;
}
// static
void NativeThemeWin::CloseHandles() {
static_cast<NativeThemeWin*>(NativeTheme::GetInstanceForNativeUi())
->CloseHandlesInternal();
}
gfx::Size NativeThemeWin::GetPartSize(Part part,
State state,
const ExtraParams& extra) const {
// The GetThemePartSize call below returns the default size without
// accounting for user customization (crbug/218291).
switch (part) {
case kScrollbarDownArrow:
case kScrollbarLeftArrow:
case kScrollbarRightArrow:
case kScrollbarUpArrow:
case kScrollbarHorizontalThumb:
case kScrollbarVerticalThumb:
case kScrollbarHorizontalTrack:
case kScrollbarVerticalTrack: {
int size = display::win::ScreenWin::GetSystemMetricsInDIP(SM_CXVSCROLL);
if (size == 0)
size = 17;
return gfx::Size(size, size);
}
default:
break;
}
int part_id = GetWindowsPart(part, state, extra);
int state_id = GetWindowsState(part, state, extra);
base::win::ScopedGetDC screen_dc(nullptr);
SIZE size;
HANDLE handle = GetThemeHandle(GetThemeName(part));
if (handle && SUCCEEDED(GetThemePartSize(handle, screen_dc, part_id, state_id,
nullptr, TS_TRUE, &size)))
return gfx::Size(size.cx, size.cy);
// TODO(rogerta): For now, we need to support radio buttons and checkboxes
// when theming is not enabled. Support for other parts can be added
// if/when needed.
return (part == kCheckbox || part == kRadio) ?
gfx::Size(13, 13) : gfx::Size();
}
void NativeThemeWin::Paint(cc::PaintCanvas* canvas,
const ui::ColorProvider* color_provider,
Part part,
State state,
const gfx::Rect& rect,
const ExtraParams& extra,
ColorScheme color_scheme,
bool in_forced_colors,
const std::optional<SkColor>& accent_color) const {
if (rect.IsEmpty())
return;
switch (part) {
case kMenuPopupGutter:
PaintMenuGutter(canvas, color_provider, rect);
return;
case kMenuPopupSeparator:
PaintMenuSeparator(canvas, color_provider,
absl::get<MenuSeparatorExtraParams>(extra));
return;
case kMenuPopupBackground:
PaintMenuBackground(canvas, color_provider, rect);
return;
case kMenuItemBackground:
CommonThemePaintMenuItemBackground(this, color_provider, canvas, state,
rect,
absl::get<MenuItemExtraParams>(extra));
return;
default:
PaintIndirect(canvas, part, state, rect, extra);
return;
}
}
NativeThemeWin::NativeThemeWin(bool configure_web_instance,
bool should_only_use_dark_colors)
: NativeTheme(should_only_use_dark_colors),
supports_windows_dark_mode_(base::win::IsDarkModeAvailable()),
color_change_listener_(this) {
// By default UI should not use the system accent color.
set_should_use_system_accent_color(false);
// 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();
hkcu_themes_regkey_ = OpenThemeRegKey(KEY_READ | KEY_NOTIFY);
if (hkcu_themes_regkey_.Valid()) {
if (!should_only_use_dark_colors && !IsForcedDarkMode() &&
!IsForcedHighContrast()) {
UpdateDarkModeStatus();
}
UpdatePrefersReducedTransparency();
if (observers_can_operate) {
RegisterThemeRegkeyObserver();
}
}
hkcu_color_filtering_regkey_ =
OpenColorFilteringRegKey(KEY_READ | KEY_NOTIFY);
if (hkcu_color_filtering_regkey_.Valid()) {
UpdateInvertedColors();
if (observers_can_operate) {
RegisterColorFilteringRegkeyObserver();
}
}
if (!IsForcedHighContrast()) {
set_forced_colors(IsUsingHighContrastThemeInternal());
}
// Initialize the cached system colors.
UpdateSystemColors();
set_preferred_color_scheme(CalculatePreferredColorScheme());
SetPreferredContrast(CalculatePreferredContrast());
memset(theme_handles_, 0, sizeof(theme_handles_));
if (configure_web_instance)
ConfigureWebInstance();
}
void NativeThemeWin::ConfigureWebInstance() {
// Add the web native theme as an observer to stay in sync with color scheme
// changes.
color_scheme_observer_ =
std::make_unique<NativeTheme::ColorSchemeNativeThemeObserver>(
NativeTheme::GetInstanceForWeb());
AddObserver(color_scheme_observer_.get());
// Initialize the native theme web instance with the system color info.
NativeTheme* web_instance = NativeTheme::GetInstanceForWeb();
web_instance->set_use_dark_colors(ShouldUseDarkColors());
web_instance->set_forced_colors(InForcedColorsMode());
web_instance->set_preferred_color_scheme(GetPreferredColorScheme());
web_instance->SetPreferredContrast(GetPreferredContrast());
web_instance->set_prefers_reduced_transparency(
GetPrefersReducedTransparency());
web_instance->set_system_colors(GetSystemColors());
web_instance->set_should_use_system_accent_color(
should_use_system_accent_color());
}
std::optional<base::TimeDelta> NativeThemeWin::GetPlatformCaretBlinkInterval()
const {
static const size_t system_value = ::GetCaretBlinkTime();
if (system_value != 0) {
return (system_value == INFINITE) ? base::TimeDelta()
: base::Milliseconds(system_value);
}
return std::nullopt;
}
NativeThemeWin::~NativeThemeWin() {
// TODO(crbug.com/40551168): Calling CloseHandles() here breaks
// certain tests and the reliability bots.
// CloseHandles();
}
bool NativeThemeWin::IsUsingHighContrastThemeInternal() const {
HIGHCONTRAST result;
result.cbSize = sizeof(HIGHCONTRAST);
return SystemParametersInfo(SPI_GETHIGHCONTRAST, result.cbSize, &result, 0) &&
(result.dwFlags & HCF_HIGHCONTRASTON) == HCF_HIGHCONTRASTON;
}
void NativeThemeWin::CloseHandlesInternal() {
for (int i = 0; i < LAST; ++i) {
if (theme_handles_[i]) {
CloseThemeData(theme_handles_[i]);
theme_handles_[i] = nullptr;
}
}
}
void NativeThemeWin::OnSysColorChange() {
UpdateSystemColors();
if (!IsForcedHighContrast())
set_forced_colors(IsUsingHighContrastThemeInternal());
set_preferred_color_scheme(CalculatePreferredColorScheme());
SetPreferredContrast(CalculatePreferredContrast());
NotifyOnNativeThemeUpdated();
}
void NativeThemeWin::UpdateSystemColors() {
for (int sys_color : kSysColors)
system_colors_[SysColorToSystemThemeColor(sys_color)] =
color_utils::GetSysSkColor(sys_color);
}
void NativeThemeWin::PaintMenuSeparator(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
const MenuSeparatorExtraParams& params) const {
DCHECK(color_provider);
const gfx::RectF rect(*params.paint_rect);
gfx::PointF start = rect.CenterPoint();
gfx::PointF end = start;
if (params.type == ui::VERTICAL_SEPARATOR) {
start.set_y(rect.y());
end.set_y(rect.bottom());
} else {
start.set_x(rect.x());
end.set_x(rect.right());
}
cc::PaintFlags flags;
flags.setColor(color_provider->GetColor(kColorMenuSeparator));
canvas->drawLine(start.x(), start.y(), end.x(), end.y(), flags);
}
void NativeThemeWin::PaintMenuGutter(cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
const gfx::Rect& rect) const {
DCHECK(color_provider);
cc::PaintFlags flags;
flags.setColor(color_provider->GetColor(kColorMenuSeparator));
int position_x = rect.x() + rect.width() / 2;
canvas->drawLine(position_x, rect.y(), position_x, rect.bottom(), flags);
}
void NativeThemeWin::PaintMenuBackground(cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
const gfx::Rect& rect) const {
DCHECK(color_provider);
cc::PaintFlags flags;
flags.setColor(color_provider->GetColor(kColorMenuBackground));
canvas->drawRect(gfx::RectToSkRect(rect), flags);
}
void NativeThemeWin::PaintDirect(SkCanvas* destination_canvas,
HDC hdc,
Part part,
State state,
const gfx::Rect& rect,
const ExtraParams& extra) const {
if (part == kScrollbarCorner) {
// Special-cased here since there is no theme name for kScrollbarCorner.
destination_canvas->drawColor(SK_ColorWHITE, SkBlendMode::kSrc);
return;
}
RECT rect_win = rect.ToRECT();
if (part == kTrackbarTrack) {
// Make the channel be 4 px thick in the center of the supplied rect. (4 px
// matches what XP does in various menus; GetThemePartSize() doesn't seem to
// return good values here.)
constexpr int kChannelThickness = 4;
if (absl::get<TrackbarExtraParams>(extra).vertical) {
rect_win.top += (rect_win.bottom - rect_win.top - kChannelThickness) / 2;
rect_win.bottom = rect_win.top + kChannelThickness;
} else {
rect_win.left += (rect_win.right - rect_win.left - kChannelThickness) / 2;
rect_win.right = rect_win.left + kChannelThickness;
}
}
// Most parts can be drawn simply when there is a theme handle.
const HANDLE handle = GetThemeHandle(GetThemeName(part));
const int part_id = GetWindowsPart(part, state, extra);
const int state_id = GetWindowsState(part, state, extra);
if (handle) {
switch (part) {
case kMenuPopupArrow:
// The right-pointing arrow can use the common code, but the
// left-pointing one needs custom code.
if (!absl::get<MenuArrowExtraParams>(extra).pointing_right) {
PaintLeftMenuArrowThemed(hdc, handle, part_id, state_id, rect);
return;
}
[[fallthrough]];
case kCheckbox:
case kInnerSpinButton:
case kMenuCheck:
case kMenuCheckBackground:
case kMenuList:
case kProgressBar:
case kPushButton:
case kRadio:
case kScrollbarHorizontalTrack:
case kScrollbarVerticalTrack:
case kTabPanelBackground:
case kTrackbarThumb:
case kTrackbarTrack:
case kWindowResizeGripper:
DrawThemeBackground(handle, hdc, part_id, state_id, &rect_win, nullptr);
if (part == kProgressBar)
break; // Further painting to do below.
return;
case kScrollbarDownArrow:
case kScrollbarHorizontalGripper:
case kScrollbarHorizontalThumb:
case kScrollbarLeftArrow:
case kScrollbarRightArrow:
case kScrollbarUpArrow:
case kScrollbarVerticalGripper:
case kScrollbarVerticalThumb:
PaintScaledTheme(handle, hdc, part_id, state_id, rect);
return;
case kTextField:
break; // Handled entirely below.
case kMenuItemBackground:
case kMenuPopupBackground:
case kMenuPopupGutter:
case kMenuPopupSeparator:
case kScrollbarCorner:
case kSliderTrack:
case kSliderThumb:
case kMaxPart:
NOTREACHED_IN_MIGRATION();
}
}
// Do any further painting the common code couldn't handle.
switch (part) {
case kCheckbox:
case kPushButton:
case kRadio:
PaintButtonClassic(hdc, part, state, &rect_win,
absl::get<ButtonExtraParams>(extra));
return;
case kInnerSpinButton:
DrawFrameControl(
hdc, &rect_win, DFC_SCROLL,
absl::get<InnerSpinButtonExtraParams>(extra).classic_state);
return;
case kMenuCheck: {
const auto& menu_check = absl::get<MenuCheckExtraParams>(extra);
PaintFrameControl(hdc, rect, DFC_MENU,
menu_check.is_radio ? DFCS_MENUBULLET : DFCS_MENUCHECK,
menu_check.is_selected, state);
return;
}
case kMenuList:
DrawFrameControl(hdc, &rect_win, DFC_SCROLL,
DFCS_SCROLLCOMBOBOX |
absl::get<MenuListExtraParams>(extra).classic_state);
return;
case kMenuPopupArrow: {
const auto& menu_arrow = absl::get<MenuArrowExtraParams>(extra);
// For some reason, Windows uses the name DFCS_MENUARROWRIGHT to indicate
// a left pointing arrow.
PaintFrameControl(
hdc, rect, DFC_MENU,
menu_arrow.pointing_right ? DFCS_MENUARROW : DFCS_MENUARROWRIGHT,
menu_arrow.is_selected, state);
return;
}
case kProgressBar: {
const auto& progress_bar = absl::get<ProgressBarExtraParams>(extra);
RECT value_rect =
gfx::Rect(progress_bar.value_rect_x, progress_bar.value_rect_y,
progress_bar.value_rect_width,
progress_bar.value_rect_height)
.ToRECT();
if (handle) {
PaintProgressBarOverlayThemed(hdc, handle, &rect_win, &value_rect,
progress_bar);
} else {
FillRect(hdc, &rect_win, GetSysColorBrush(COLOR_BTNFACE));
FillRect(hdc, &value_rect, GetSysColorBrush(COLOR_BTNSHADOW));
DrawEdge(hdc, &rect_win, EDGE_SUNKEN, BF_RECT | BF_ADJUST);
}
return;
}
case kScrollbarDownArrow:
case kScrollbarLeftArrow:
case kScrollbarRightArrow:
case kScrollbarUpArrow:
PaintScrollbarArrowClassic(hdc, part, state, &rect_win);
return;
case kScrollbarHorizontalThumb:
case kScrollbarVerticalThumb:
DrawEdge(hdc, &rect_win, EDGE_RAISED, BF_RECT | BF_MIDDLE);
return;
case kScrollbarHorizontalTrack:
case kScrollbarVerticalTrack:
PaintScrollbarTrackClassic(destination_canvas, hdc, &rect_win,
absl::get<ScrollbarTrackExtraParams>(extra));
return;
case kTabPanelBackground:
// Classic just renders a flat color background.
FillRect(hdc, &rect_win, reinterpret_cast<HBRUSH>(COLOR_3DFACE + 1));
return;
case kTextField: {
// TODO(mpcomplete): can we detect if the color is specified by the user,
// and if not, just use the system color?
// CreateSolidBrush() accepts a RGB value but alpha must be 0.
const auto& text_field = absl::get<TextFieldExtraParams>(extra);
base::win::ScopedGDIObject<HBRUSH> bg_brush(CreateSolidBrush(
skia::SkColorToCOLORREF(text_field.background_color)));
if (handle) {
PaintTextFieldThemed(hdc, handle, bg_brush.get(), part_id, state_id,
&rect_win, text_field);
} else {
PaintTextFieldClassic(hdc, bg_brush.get(), &rect_win, text_field);
}
return;
}
case kTrackbarThumb: {
const auto& trackbar = absl::get<TrackbarExtraParams>(extra);
if (trackbar.vertical) {
DrawEdge(hdc, &rect_win, EDGE_RAISED, BF_RECT | BF_SOFT | BF_MIDDLE);
} else {
PaintHorizontalTrackbarThumbClassic(destination_canvas, hdc, rect_win,
trackbar);
}
return;
}
case kTrackbarTrack:
DrawEdge(hdc, &rect_win, EDGE_SUNKEN, BF_RECT);
return;
case kWindowResizeGripper:
// Draw a windows classic scrollbar gripper.
DrawFrameControl(hdc, &rect_win, DFC_SCROLL, DFCS_SCROLLSIZEGRIP);
return;
case kMenuCheckBackground:
case kScrollbarHorizontalGripper:
case kScrollbarVerticalGripper:
return; // No further painting necessary.
case kMenuItemBackground:
case kMenuPopupBackground:
case kMenuPopupGutter:
case kMenuPopupSeparator:
case kScrollbarCorner:
case kSliderTrack:
case kSliderThumb:
case kMaxPart:
NOTREACHED_IN_MIGRATION();
}
}
bool NativeThemeWin::SupportsNinePatch(Part part) const {
// The only nine-patch resources currently supported (overlay scrollbar) are
// painted by NativeThemeAura on Windows.
return false;
}
gfx::Size NativeThemeWin::GetNinePatchCanvasSize(Part part) const {
NOTREACHED_IN_MIGRATION()
<< "NativeThemeWin doesn't support nine-patch resources.";
return gfx::Size();
}
gfx::Rect NativeThemeWin::GetNinePatchAperture(Part part) const {
NOTREACHED_IN_MIGRATION()
<< "NativeThemeWin doesn't support nine-patch resources.";
return gfx::Rect();
}
bool NativeThemeWin::ShouldUseDarkColors() const {
// Windows high contrast modes are entirely different themes,
// so let them take priority over dark mode.
// ...unless --force-dark-mode was specified in which case caveat emptor.
if (InForcedColorsMode() && !IsForcedDarkMode())
return false;
return NativeTheme::ShouldUseDarkColors();
}
NativeTheme::PreferredColorScheme
NativeThemeWin::CalculatePreferredColorScheme() const {
if (!InForcedColorsMode())
return NativeTheme::CalculatePreferredColorScheme();
// According to the spec, the preferred color scheme for web content is 'dark'
// if 'Canvas' has L<33% and 'light' if L>67%. On Windows, the 'Canvas'
// keyword is mapped to the 'Window' system color. As such, we use the
// luminance of 'Window' to calculate the corresponding luminance of 'Canvas'.
// https://www.w3.org/TR/css-color-adjust-1/#forced
SkColor bg_color = system_colors_[SystemThemeColor::kWindow];
float luminance = color_utils::GetRelativeLuminance(bg_color);
if (luminance < 0.33)
return NativeTheme::PreferredColorScheme::kDark;
return NativeTheme::PreferredColorScheme::kLight;
}
NativeTheme::PreferredContrast NativeThemeWin::CalculatePreferredContrast()
const {
if (!InForcedColorsMode())
return NativeTheme::CalculatePreferredContrast();
// TODO([email protected]): 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
SkColor bg_color = system_colors_[SystemThemeColor::kWindow];
SkColor fg_color = system_colors_[SystemThemeColor::kWindowText];
float contrast_ratio = color_utils::GetContrastRatio(bg_color, fg_color);
if (contrast_ratio >= 7)
return NativeTheme::PreferredContrast::kMore;
return contrast_ratio <= 2.5 ? NativeTheme::PreferredContrast::kLess
: NativeTheme::PreferredContrast::kCustom;
}
NativeTheme::ColorScheme NativeThemeWin::GetDefaultSystemColorScheme() const {
return InForcedColorsMode() ? ColorScheme::kPlatformHighContrast
: NativeTheme::GetDefaultSystemColorScheme();
}
void NativeThemeWin::PaintIndirect(cc::PaintCanvas* destination_canvas,
Part part,
State state,
const gfx::Rect& rect,
const ExtraParams& extra) const {
// TODO(asvitkine): This path is pretty inefficient - for each paint operation
// it creates a new offscreen bitmap Skia canvas. This can be sped up by doing
// it only once per part/state and keeping a cache of the resulting bitmaps.
//
// TODO(enne): This could also potentially be sped up for software raster
// by moving these draw ops into PaintRecord itself and then moving the
// PaintDirect code to be part of the raster for PaintRecord.
// If this process doesn't have access to GDI, we'd need to use shared memory
// segment instead but that is not supported right now.
if (!base::win::IsUser32AndGdi32Available())
return;
ScopedCreateDCWithBitmap offscreen_hdc(CreateCompatibleDC(nullptr));
if (!offscreen_hdc.IsValid())
return;
skia::InitializeDC(offscreen_hdc.Get());
HRGN clip = CreateRectRgn(0, 0, rect.width(), rect.height());
if ((SelectClipRgn(offscreen_hdc.Get(), clip) == ERROR) ||
!DeleteObject(clip)) {
return;
}
base::win::ScopedBitmap hbitmap = skia::CreateHBitmapXRGB8888(
rect.width(), rect.height(), nullptr, nullptr);
if (!offscreen_hdc.SelectBitmap(hbitmap.release()))
return;
// Will be NULL if lower-level Windows calls fail, or if the backing
// allocated is 0 pixels in size (which should never happen according to
// Windows documentation).
sk_sp<SkSurface> offscreen_surface =
skia::MapPlatformSurface(offscreen_hdc.Get());
if (!offscreen_surface)
return;
SkCanvas* offscreen_canvas = offscreen_surface->getCanvas();
DCHECK(offscreen_canvas);
// Some of the Windows theme drawing operations do not write correct alpha
// values for fully-opaque pixels; instead the pixels get alpha 0. This is
// especially a problem on Windows XP or when using the Classic theme.
//
// To work-around this, mark all pixels with a placeholder value, to detect
// which pixels get touched by the paint operation. After paint, set any
// pixels that have alpha 0 to opaque and placeholders to fully-transparent.
constexpr SkColor placeholder = SkColorSetARGB(1, 0, 0, 0);
offscreen_canvas->clear(placeholder);
// Offset destination rects to have origin (0,0).
gfx::Rect adjusted_rect(rect.size());
ExtraParams adjusted_extra = extra;
switch (part) {
case kProgressBar: {
auto progress_bar = absl::get<ProgressBarExtraParams>(adjusted_extra);
progress_bar.value_rect_x = 0;
progress_bar.value_rect_y = 0;
break;
}
case kScrollbarHorizontalTrack:
case kScrollbarVerticalTrack: {
auto scrollbar_track =
absl::get<ScrollbarTrackExtraParams>(adjusted_extra);
scrollbar_track.track_x = 0;
scrollbar_track.track_y = 0;
break;
}
default:
break;
}
// Draw the theme controls using existing HDC-drawing code.
PaintDirect(offscreen_canvas, offscreen_hdc.Get(), part, state,
adjusted_rect, adjusted_extra);
SkBitmap offscreen_bitmap = skia::MapPlatformBitmap(offscreen_hdc.Get());
// Post-process the pixels to fix up the alpha values (see big comment above).
const SkPMColor placeholder_value = SkPreMultiplyColor(placeholder);
const int pixel_count = rect.width() * rect.height();
SkPMColor* pixels = offscreen_bitmap.getAddr32(0, 0);
for (int i = 0; i < pixel_count; i++) {
if (pixels[i] == placeholder_value) {
// Pixel wasn't touched - make it fully transparent.
pixels[i] = SkPackARGB32(0, 0, 0, 0);
} else if (SkGetPackedA32(pixels[i]) == 0) {
// Pixel was touched but has incorrect alpha of 0, make it fully opaque.
pixels[i] = SkPackARGB32(0xFF,
SkGetPackedR32(pixels[i]),
SkGetPackedG32(pixels[i]),
SkGetPackedB32(pixels[i]));
}
}
destination_canvas->drawImage(
cc::PaintImage::CreateFromBitmap(std::move(offscreen_bitmap)), rect.x(),
rect.y());
}
void NativeThemeWin::PaintButtonClassic(HDC hdc,
Part part,
State state,
RECT* rect,
const ButtonExtraParams& extra) const {
int classic_state = extra.classic_state;
switch (part) {
case kCheckbox:
classic_state |= DFCS_BUTTONCHECK;
break;
case kPushButton:
classic_state |= DFCS_BUTTONRADIO;
break;
case kRadio:
classic_state |= DFCS_BUTTONPUSH;
break;
default:
NOTREACHED_IN_MIGRATION();
break;
}
if (state == kDisabled)
classic_state |= DFCS_INACTIVE;
else if (state == kPressed)
classic_state |= DFCS_PUSHED;
if (extra.checked)
classic_state |= DFCS_CHECKED;
if ((part == kPushButton) && ((state == kPressed) || extra.is_default)) {
// Pressed or defaulted buttons have a shadow replacing the outer 1 px.
HBRUSH brush = GetSysColorBrush(COLOR_3DDKSHADOW);
if (brush) {
FrameRect(hdc, rect, brush);
InflateRect(rect, -1, -1);
}
}
DrawFrameControl(hdc, rect, DFC_BUTTON, classic_state);
// Draw a focus rectangle (the dotted line box) on defaulted buttons.
if ((part == kPushButton) && extra.is_default) {
InflateRect(rect, -GetSystemMetrics(SM_CXEDGE),
-GetSystemMetrics(SM_CYEDGE));
DrawFocusRect(hdc, rect);
}
// Classic theme doesn't support indeterminate checkboxes. We draw a
// recangle inside a checkbox like IE10 does.
if ((part == kCheckbox) && extra.indeterminate) {
RECT inner_rect = *rect;
// "4 / 13" is same as IE10 in classic theme.
int padding = (inner_rect.right - inner_rect.left) * 4 / 13;
InflateRect(&inner_rect, -padding, -padding);
int color_index = (state == kDisabled) ? COLOR_GRAYTEXT : COLOR_WINDOWTEXT;
FillRect(hdc, &inner_rect, GetSysColorBrush(color_index));
}
}
void NativeThemeWin::PaintLeftMenuArrowThemed(HDC hdc,
HANDLE handle,
int part_id,
int state_id,
const gfx::Rect& rect) const {
// There is no way to tell the uxtheme API to draw a left pointing arrow; it
// doesn't have a flag equivalent to DFCS_MENUARROWRIGHT. But they are needed
// for RTL locales on Vista. So use a memory DC and mirror the region with
// GDI's StretchBlt.
base::win::ScopedCreateDC mem_dc(CreateCompatibleDC(hdc));
base::win::ScopedBitmap mem_bitmap(
CreateCompatibleBitmap(hdc, rect.width(), rect.height()));
base::win::ScopedSelectObject select_bitmap(mem_dc.Get(), mem_bitmap.get());
// Copy and horizontally mirror the background from hdc into mem_dc. Use a
// negative-width source rect, starting at the rightmost pixel.
StretchBlt(mem_dc.Get(), 0, 0, rect.width(), rect.height(), hdc,
rect.right() - 1, rect.y(), -rect.width(), rect.height(), SRCCOPY);
// Draw the arrow.
RECT theme_rect = {0, 0, rect.width(), rect.height()};
DrawThemeBackground(handle, mem_dc.Get(), part_id, state_id, &theme_rect,
nullptr);
// Copy and mirror the result back into mem_dc.
StretchBlt(hdc, rect.x(), rect.y(), rect.width(), rect.height(), mem_dc.Get(),
rect.width() - 1, 0, -rect.width(), rect.height(), SRCCOPY);
}
void NativeThemeWin::PaintScrollbarArrowClassic(HDC hdc,
Part part,
State state,
RECT* rect) const {
int classic_state = DFCS_SCROLLDOWN;
switch (part) {
case kScrollbarDownArrow:
break;
case kScrollbarLeftArrow:
classic_state = DFCS_SCROLLLEFT;
break;
case kScrollbarRightArrow:
classic_state = DFCS_SCROLLRIGHT;
break;
case kScrollbarUpArrow:
classic_state = DFCS_SCROLLUP;
break;
default:
NOTREACHED_IN_MIGRATION();
break;
}
switch (state) {
case kDisabled:
classic_state |= DFCS_INACTIVE;
break;
case kHovered:
classic_state |= DFCS_HOT;
break;
case kNormal:
break;
case kPressed:
classic_state |= DFCS_PUSHED;
break;
case kNumStates:
NOTREACHED_IN_MIGRATION();
break;
}
DrawFrameControl(hdc, rect, DFC_SCROLL, classic_state);
}
void NativeThemeWin::PaintScrollbarTrackClassic(
SkCanvas* canvas,
HDC hdc,
RECT* rect,
const ScrollbarTrackExtraParams& extra) const {
if ((system_colors_[SystemThemeColor::kScrollbar] !=
system_colors_[SystemThemeColor::kButtonFace]) &&
(system_colors_[SystemThemeColor::kScrollbar] !=
system_colors_[SystemThemeColor::kWindow])) {
FillRect(hdc, rect, reinterpret_cast<HBRUSH>(COLOR_SCROLLBAR + 1));
} else {
SkPaint paint;
RECT align_rect = gfx::Rect(extra.track_x, extra.track_y, extra.track_width,
extra.track_height)
.ToRECT();
SetCheckerboardShader(&paint, align_rect);
canvas->drawIRect(skia::RECTToSkIRect(*rect), paint);
}
if (extra.classic_state & DFCS_PUSHED)
InvertRect(hdc, rect);
}
void NativeThemeWin::PaintHorizontalTrackbarThumbClassic(
SkCanvas* canvas,
HDC hdc,
const RECT& rect,
const TrackbarExtraParams& extra) const {
// Split rect into top and bottom pieces.
RECT top_section = rect;
RECT bottom_section = rect;
top_section.bottom -= ((bottom_section.right - bottom_section.left) / 2);
bottom_section.top = top_section.bottom;
DrawEdge(hdc, &top_section, EDGE_RAISED,
BF_LEFT | BF_TOP | BF_RIGHT | BF_SOFT | BF_MIDDLE | BF_ADJUST);
// Split triangular piece into two diagonals.
RECT& left_half = bottom_section;
RECT right_half = bottom_section;
right_half.left += ((bottom_section.right - bottom_section.left) / 2);
left_half.right = right_half.left;
DrawEdge(hdc, &left_half, EDGE_RAISED,
BF_DIAGONAL_ENDTOPLEFT | BF_SOFT | BF_MIDDLE | BF_ADJUST);
DrawEdge(hdc, &right_half, EDGE_RAISED,
BF_DIAGONAL_ENDBOTTOMLEFT | BF_SOFT | BF_MIDDLE | BF_ADJUST);
// If the button is pressed, draw hatching.
if (extra.classic_state & DFCS_PUSHED) {
SkPaint paint;
SetCheckerboardShader(&paint, rect);
// Fill all three pieces with the pattern.
canvas->drawIRect(skia::RECTToSkIRect(top_section), paint);
SkScalar left_triangle_top = SkIntToScalar(left_half.top);
SkScalar left_triangle_right = SkIntToScalar(left_half.right);
SkPath left_triangle;
left_triangle.moveTo(SkIntToScalar(left_half.left), left_triangle_top);
left_triangle.lineTo(left_triangle_right, left_triangle_top);
left_triangle.lineTo(left_triangle_right, SkIntToScalar(left_half.bottom));
left_triangle.close();
canvas->drawPath(left_triangle, paint);
SkScalar right_triangle_left = SkIntToScalar(right_half.left);
SkScalar right_triangle_top = SkIntToScalar(right_half.top);
SkPath right_triangle;
right_triangle.moveTo(right_triangle_left, right_triangle_top);
right_triangle.lineTo(SkIntToScalar(right_half.right), right_triangle_top);
right_triangle.lineTo(right_triangle_left,
SkIntToScalar(right_half.bottom));
right_triangle.close();
canvas->drawPath(right_triangle, paint);
}
}
void NativeThemeWin::PaintProgressBarOverlayThemed(
HDC hdc,
HANDLE handle,
RECT* bar_rect,
RECT* value_rect,
const ProgressBarExtraParams& extra) const {
// There is no documentation about the animation speed, frame-rate, nor
// size of moving overlay of the indeterminate progress bar.
// So we just observed real-world programs and guessed following parameters.
constexpr int kDeterminateOverlayWidth = 120;
constexpr int kDeterminateOverlayPixelsPerSecond = 300;
constexpr int kIndeterminateOverlayWidth = 120;
constexpr int kIndeterminateOverlayPixelsPerSecond = 175;
int bar_width = bar_rect->right - bar_rect->left;
if (!extra.determinate) {
// The glossy overlay for the indeterminate progress bar has a small pause
// after each animation. We emulate this by adding an invisible margin the
// animation has to traverse.
int width_with_margin = bar_width + kIndeterminateOverlayPixelsPerSecond;
int overlay_width = kIndeterminateOverlayWidth;
RECT overlay_rect = *bar_rect;
overlay_rect.left += ComputeAnimationProgress(
width_with_margin, overlay_width, kIndeterminateOverlayPixelsPerSecond,
extra.animated_seconds);
overlay_rect.right = overlay_rect.left + overlay_width;
DrawThemeBackground(handle, hdc, PP_MOVEOVERLAY, 0, &overlay_rect,
bar_rect);
return;
}
// We care about the direction here because PP_CHUNK painting is asymmetric.
// TODO(morrita): This RTL guess can be wrong. We should pass in the
// direction from WebKit.
const bool mirror = bar_rect->right == value_rect->right &&
bar_rect->left != value_rect->left;
const DTBGOPTS value_draw_options = {sizeof(DTBGOPTS),
mirror ? DTBG_MIRRORDC : 0u, *bar_rect};
// On Vista or later, the progress bar part has a single-block value part
// and a glossy effect. The value part has exactly same height as the bar
// part, so we don't need to shrink the rect.
DrawThemeBackgroundEx(handle, hdc, PP_FILL, 0, value_rect,
&value_draw_options);
RECT overlay_rect = *value_rect;
overlay_rect.left += ComputeAnimationProgress(
bar_width, kDeterminateOverlayWidth, kDeterminateOverlayPixelsPerSecond,
extra.animated_seconds);
overlay_rect.right = overlay_rect.left + kDeterminateOverlayWidth;
DrawThemeBackground(handle, hdc, PP_MOVEOVERLAY, 0, &overlay_rect,
value_rect);
}
void NativeThemeWin::PaintTextFieldThemed(
HDC hdc,
HANDLE handle,
HBRUSH bg_brush,
int part_id,
int state_id,
RECT* rect,
const TextFieldExtraParams& extra) const {
static constexpr DTBGOPTS kOmitBorderOptions = {
sizeof(DTBGOPTS), DTBG_OMITBORDER, {0, 0, 0, 0}};
DrawThemeBackgroundEx(handle, hdc, part_id, state_id, rect,
extra.draw_edges ? nullptr : &kOmitBorderOptions);
if (extra.fill_content_area) {
RECT content_rect;
GetThemeBackgroundContentRect(handle, hdc, part_id, state_id, rect,
&content_rect);
FillRect(hdc, &content_rect, bg_brush);
}
}
void NativeThemeWin::PaintTextFieldClassic(
HDC hdc,
HBRUSH bg_brush,
RECT* rect,
const TextFieldExtraParams& extra) const {
if (extra.draw_edges)
DrawEdge(hdc, rect, EDGE_SUNKEN, BF_RECT | BF_ADJUST);
if (extra.fill_content_area) {
if (extra.classic_state & DFCS_INACTIVE)
bg_brush = reinterpret_cast<HBRUSH>(COLOR_BTNFACE + 1);
FillRect(hdc, rect, bg_brush);
}
}
void NativeThemeWin::PaintScaledTheme(HANDLE theme,
HDC hdc,
int part_id,
int state_id,
const gfx::Rect& rect) const {
// Correct the scaling and positioning of sub-components such as scrollbar
// arrows and thumb grippers in the event that the world transform applies
// scaling (e.g. in high-DPI mode).
XFORM save_transform;
if (GetWorldTransform(hdc, &save_transform)) {
float scale = save_transform.eM11;
if (scale != 1 && save_transform.eM12 == 0) {
ModifyWorldTransform(hdc, NULL, MWT_IDENTITY);
gfx::Rect scaled_rect = gfx::ScaleToEnclosedRect(rect, scale);
scaled_rect.Offset(save_transform.eDx, save_transform.eDy);
RECT bounds = scaled_rect.ToRECT();
DrawThemeBackground(theme, hdc, part_id, state_id, &bounds, nullptr);
SetWorldTransform(hdc, &save_transform);
return;
}
}
RECT bounds = rect.ToRECT();
DrawThemeBackground(theme, hdc, part_id, state_id, &bounds, nullptr);
}
// static
NativeThemeWin::ThemeName NativeThemeWin::GetThemeName(Part part) {
switch (part) {
case kCheckbox:
case kPushButton:
case kRadio:
return BUTTON;
case kMenuList:
case kMenuCheck:
case kMenuCheckBackground:
case kMenuPopupArrow:
case kMenuPopupGutter:
case kMenuPopupSeparator:
return MENU;
case kProgressBar:
return PROGRESS;
case kScrollbarDownArrow:
case kScrollbarLeftArrow:
case kScrollbarRightArrow:
case kScrollbarUpArrow:
case kScrollbarHorizontalGripper:
case kScrollbarVerticalGripper:
case kScrollbarHorizontalThumb:
case kScrollbarVerticalThumb:
case kScrollbarHorizontalTrack:
case kScrollbarVerticalTrack:
return SCROLLBAR;
case kInnerSpinButton:
return SPIN;
case kWindowResizeGripper:
return STATUS;
case kTabPanelBackground:
return TAB;
case kTextField:
return TEXTFIELD;
case kTrackbarThumb:
case kTrackbarTrack:
return TRACKBAR;
case kMenuPopupBackground:
case kMenuItemBackground:
case kScrollbarCorner:
case kSliderTrack:
case kSliderThumb:
case kMaxPart:
NOTREACHED_IN_MIGRATION();
}
return LAST;
}
// static
int NativeThemeWin::GetWindowsPart(Part part,
State state,
const ExtraParams& extra) {
switch (part) {
case kCheckbox:
return BP_CHECKBOX;
case kPushButton:
return BP_PUSHBUTTON;
case kRadio:
return BP_RADIOBUTTON;
case kMenuList:
return CP_DROPDOWNBUTTON;
case kTextField:
return EP_EDITTEXT;
case kMenuCheck:
return MENU_POPUPCHECK;
case kMenuCheckBackground:
return MENU_POPUPCHECKBACKGROUND;
case kMenuPopupGutter:
return MENU_POPUPGUTTER;
case kMenuPopupSeparator:
return MENU_POPUPSEPARATOR;
case kMenuPopupArrow:
return MENU_POPUPSUBMENU;
case kProgressBar:
return PP_BAR;
case kScrollbarDownArrow:
case kScrollbarLeftArrow:
case kScrollbarRightArrow:
case kScrollbarUpArrow:
return SBP_ARROWBTN;
case kScrollbarHorizontalGripper:
return SBP_GRIPPERHORZ;
case kScrollbarVerticalGripper:
return SBP_GRIPPERVERT;
case kScrollbarHorizontalThumb:
return SBP_THUMBBTNHORZ;
case kScrollbarVerticalThumb:
return SBP_THUMBBTNVERT;
case kScrollbarHorizontalTrack:
return absl::get<ScrollbarTrackExtraParams>(extra).is_upper
? SBP_UPPERTRACKHORZ
: SBP_LOWERTRACKHORZ;
case kScrollbarVerticalTrack:
return absl::get<ScrollbarTrackExtraParams>(extra).is_upper
? SBP_UPPERTRACKVERT
: SBP_LOWERTRACKVERT;
case kWindowResizeGripper:
// Use the status bar gripper. There doesn't seem to be a standard
// gripper in Windows for the space between scrollbars. This is pretty
// close, but it's supposed to be painted over a status bar.
return SP_GRIPPER;
case kInnerSpinButton:
return absl::get<InnerSpinButtonExtraParams>(extra).spin_up ? SPNP_UP
: SPNP_DOWN;
case kTabPanelBackground:
return TABP_BODY;
case kTrackbarThumb:
return absl::get<TrackbarExtraParams>(extra).vertical ? TKP_THUMBVERT
: TKP_THUMBBOTTOM;
case kTrackbarTrack:
return absl::get<TrackbarExtraParams>(extra).vertical ? TKP_TRACKVERT
: TKP_TRACK;
case kMenuPopupBackground:
case kMenuItemBackground:
case kScrollbarCorner:
case kSliderTrack:
case kSliderThumb:
case kMaxPart:
NOTREACHED_IN_MIGRATION();
}
return 0;
}
int NativeThemeWin::GetWindowsState(Part part,
State state,
const ExtraParams& extra) {
switch (part) {
case kScrollbarDownArrow:
switch (state) {
case kDisabled:
return ABS_DOWNDISABLED;
case kHovered:
return absl::get<ScrollbarArrowExtraParams>(extra).is_hovering
? ABS_DOWNHOVER
: ABS_DOWNHOT;
case kNormal:
return ABS_DOWNNORMAL;
case kPressed:
return ABS_DOWNPRESSED;
case kNumStates:
NOTREACHED_IN_MIGRATION();
return 0;
}
case kScrollbarLeftArrow:
switch (state) {
case kDisabled:
return ABS_LEFTDISABLED;
case kHovered:
return absl::get<ScrollbarArrowExtraParams>(extra).is_hovering
? ABS_LEFTHOVER
: ABS_LEFTHOT;
case kNormal:
return ABS_LEFTNORMAL;
case kPressed:
return ABS_LEFTPRESSED;
case kNumStates:
NOTREACHED_IN_MIGRATION();
return 0;
}
case kScrollbarRightArrow:
switch (state) {
case kDisabled:
return ABS_RIGHTDISABLED;
case kHovered:
return absl::get<ScrollbarArrowExtraParams>(extra).is_hovering
? ABS_RIGHTHOVER
: ABS_RIGHTHOT;
case kNormal:
return ABS_RIGHTNORMAL;
case kPressed:
return ABS_RIGHTPRESSED;
case kNumStates:
NOTREACHED_IN_MIGRATION();
return 0;
}
case kScrollbarUpArrow:
switch (state) {
case kDisabled:
return ABS_UPDISABLED;
case kHovered:
return absl::get<ScrollbarArrowExtraParams>(extra).is_hovering
? ABS_UPHOVER
: ABS_UPHOT;
case kNormal:
return ABS_UPNORMAL;
case kPressed:
return ABS_UPPRESSED;
case kNumStates:
NOTREACHED_IN_MIGRATION();
return 0;
}
case kCheckbox: {
const auto& button = absl::get<ButtonExtraParams>(extra);
switch (state) {
case kDisabled:
return button.checked
? CBS_CHECKEDDISABLED
: (button.indeterminate ? CBS_MIXEDDISABLED
: CBS_UNCHECKEDDISABLED);
case kHovered:
return button.checked
? CBS_CHECKEDHOT
: (button.indeterminate ? CBS_MIXEDHOT : CBS_UNCHECKEDHOT);
case kNormal:
return button.checked ? CBS_CHECKEDNORMAL
: (button.indeterminate ? CBS_MIXEDNORMAL
: CBS_UNCHECKEDNORMAL);
case kPressed:
return button.checked ? CBS_CHECKEDPRESSED
: (button.indeterminate ? CBS_MIXEDPRESSED
: CBS_UNCHECKEDPRESSED);
case kNumStates:
NOTREACHED_IN_MIGRATION();
return 0;
}
}
case kMenuList:
switch (state) {
case kDisabled:
return CBXS_DISABLED;
case kHovered:
return CBXS_HOT;
case kNormal:
return CBXS_NORMAL;
case kPressed:
return CBXS_PRESSED;
case kNumStates:
NOTREACHED_IN_MIGRATION();
return 0;
}
case kTextField:
switch (state) {
case kDisabled:
return ETS_DISABLED;
case kHovered:
return ETS_HOT;
case kNormal: {
const auto& text_filed = absl::get<TextFieldExtraParams>(extra);
if (text_filed.is_read_only) {
return ETS_READONLY;
}
return text_filed.is_focused ? ETS_FOCUSED : ETS_NORMAL;
}
case kPressed:
return ETS_SELECTED;
case kNumStates:
NOTREACHED_IN_MIGRATION();
return 0;
}
case kMenuPopupArrow:
return (state == kDisabled) ? MSM_DISABLED : MSM_NORMAL;
case kMenuCheck: {
const auto& menu_check = absl::get<MenuCheckExtraParams>(extra);
if (state == kDisabled) {
return menu_check.is_radio ? MC_BULLETDISABLED : MC_CHECKMARKDISABLED;
}
return menu_check.is_radio ? MC_BULLETNORMAL : MC_CHECKMARKNORMAL;
}
case kMenuCheckBackground:
return (state == kDisabled) ? MCB_DISABLED : MCB_NORMAL;
case kPushButton:
switch (state) {
case kDisabled:
return PBS_DISABLED;
case kHovered:
return PBS_HOT;
case kNormal:
return absl::get<ButtonExtraParams>(extra).is_default ? PBS_DEFAULTED
: PBS_NORMAL;
case kPressed:
return PBS_PRESSED;
case kNumStates:
NOTREACHED_IN_MIGRATION();
return 0;
}
case kRadio: {
const auto& button = absl::get<ButtonExtraParams>(extra);
switch (state) {
case kDisabled:
return button.checked ? RBS_CHECKEDDISABLED : RBS_UNCHECKEDDISABLED;
case kHovered:
return button.checked ? RBS_CHECKEDHOT : RBS_UNCHECKEDHOT;
case kNormal:
return button.checked ? RBS_CHECKEDNORMAL : RBS_UNCHECKEDNORMAL;
case kPressed:
return button.checked ? RBS_CHECKEDPRESSED : RBS_UNCHECKEDPRESSED;
case kNumStates:
NOTREACHED_IN_MIGRATION();
return 0;
}
}
case kScrollbarHorizontalGripper:
case kScrollbarVerticalGripper:
case kScrollbarHorizontalThumb:
case kScrollbarVerticalThumb:
if ((state == kHovered) &&
!absl::get<ScrollbarThumbExtraParams>(extra).is_hovering) {
return SCRBS_HOT;
}
[[fallthrough]];
case kScrollbarHorizontalTrack:
case kScrollbarVerticalTrack:
switch (state) {
case kDisabled:
return SCRBS_DISABLED;
case kHovered:
return SCRBS_HOVER;
case kNormal:
return SCRBS_NORMAL;
case kPressed:
return SCRBS_PRESSED;
case kNumStates:
NOTREACHED_IN_MIGRATION();
return 0;
}
case kTrackbarThumb:
case kTrackbarTrack:
switch (state) {
case kDisabled:
return TUS_DISABLED;
case kHovered:
return TUS_HOT;
case kNormal:
return TUS_NORMAL;
case kPressed:
return TUS_PRESSED;
case kNumStates:
NOTREACHED_IN_MIGRATION();
return 0;
}
case kInnerSpinButton: {
const auto& inner_spin = absl::get<InnerSpinButtonExtraParams>(extra);
switch (state) {
case kDisabled:
return inner_spin.spin_up ? static_cast<int>(UPS_DISABLED)
: static_cast<int>(DNS_DISABLED);
case kHovered:
return inner_spin.spin_up ? static_cast<int>(UPS_HOT)
: static_cast<int>(DNS_HOT);
case kNormal:
return inner_spin.spin_up ? static_cast<int>(UPS_NORMAL)
: static_cast<int>(DNS_NORMAL);
case kPressed:
return inner_spin.spin_up ? static_cast<int>(UPS_PRESSED)
: static_cast<int>(DNS_PRESSED);
case kNumStates:
NOTREACHED_IN_MIGRATION();
return 0;
}
}
case kMenuPopupGutter:
case kMenuPopupSeparator:
case kProgressBar:
case kTabPanelBackground:
case kWindowResizeGripper:
switch (state) {
case kDisabled:
case kHovered:
case kNormal:
case kPressed:
return 0;
case kNumStates:
NOTREACHED_IN_MIGRATION();
return 0;
}
case kMenuPopupBackground:
case kMenuItemBackground:
case kScrollbarCorner:
case kSliderTrack:
case kSliderThumb:
case kMaxPart:
NOTREACHED_IN_MIGRATION();
}
return 0;
}
HRESULT NativeThemeWin::PaintFrameControl(HDC hdc,
const gfx::Rect& rect,
UINT type,
UINT state,
bool is_selected,
State control_state) const {
const int width = rect.width();
const int height = rect.height();
// DrawFrameControl for menu arrow/check wants a monochrome bitmap.
base::win::ScopedBitmap mask_bitmap(CreateBitmap(width, height, 1, 1, NULL));
if (mask_bitmap == NULL)
return E_OUTOFMEMORY;
base::win::ScopedCreateDC bitmap_dc(CreateCompatibleDC(NULL));
base::win::ScopedSelectObject select_bitmap(bitmap_dc.Get(),
mask_bitmap.get());
RECT local_rect = { 0, 0, width, height };
DrawFrameControl(bitmap_dc.Get(), &local_rect, type, state);
// We're going to use BitBlt with a b&w mask. This results in using the dest
// dc's text color for the black bits in the mask, and the dest dc's
// background color for the white bits in the mask. DrawFrameControl draws the
// check in black, and the background in white.
int bg_color_key = COLOR_MENU;
int text_color_key = COLOR_MENUTEXT;
switch (control_state) {
case kDisabled:
bg_color_key = is_selected ? COLOR_HIGHLIGHT : COLOR_MENU;
text_color_key = COLOR_GRAYTEXT;
break;
case kHovered:
bg_color_key = COLOR_HIGHLIGHT;
text_color_key = COLOR_HIGHLIGHTTEXT;
break;
case kNormal:
break;
case kPressed:
case kNumStates:
NOTREACHED_IN_MIGRATION();
break;
}
COLORREF old_bg_color = SetBkColor(hdc, GetSysColor(bg_color_key));
COLORREF old_text_color = SetTextColor(hdc, GetSysColor(text_color_key));
BitBlt(hdc, rect.x(), rect.y(), width, height, bitmap_dc.Get(), 0, 0,
SRCCOPY);
SetBkColor(hdc, old_bg_color);
SetTextColor(hdc, old_text_color);
return S_OK;
}
HANDLE NativeThemeWin::GetThemeHandle(ThemeName theme_name) const {
if (theme_name < 0 || theme_name >= LAST)
return nullptr;
if (theme_handles_[theme_name])
return theme_handles_[theme_name];
// Not found, try to load it.
HANDLE handle = nullptr;
switch (theme_name) {
case BUTTON:
handle = OpenThemeData(nullptr, L"Button");
break;
case LIST:
handle = OpenThemeData(nullptr, L"Listview");
break;
case MENU:
handle = OpenThemeData(nullptr, L"Menu");
break;
case MENULIST:
handle = OpenThemeData(nullptr, L"Combobox");
break;
case SCROLLBAR:
handle = OpenThemeData(nullptr, supports_windows_dark_mode_
? L"Explorer::Scrollbar"
: L"Scrollbar");
break;
case STATUS:
handle = OpenThemeData(nullptr, L"Status");
break;
case TAB:
handle = OpenThemeData(nullptr, L"Tab");
break;
case TEXTFIELD:
handle = OpenThemeData(nullptr, L"Edit");
break;
case TRACKBAR:
handle = OpenThemeData(nullptr, L"Trackbar");
break;
case WINDOW:
handle = OpenThemeData(nullptr, L"Window");
break;
case PROGRESS:
handle = OpenThemeData(nullptr, L"Progress");
break;
case SPIN:
handle = OpenThemeData(nullptr, L"Spin");
break;
case LAST:
NOTREACHED_IN_MIGRATION();
break;
}
theme_handles_[theme_name] = handle;
return handle;
}
void NativeThemeWin::RegisterThemeRegkeyObserver() {
DCHECK(hkcu_themes_regkey_.Valid());
DCHECK(base::SequencedTaskRunner::HasCurrentDefault());
hkcu_themes_regkey_.StartWatching(base::BindOnce(
[](NativeThemeWin* native_theme) {
native_theme->UpdateDarkModeStatus();
native_theme->UpdatePrefersReducedTransparency();
// RegKey::StartWatching only provides one notification. Reregistration
// is required to get future notifications.
native_theme->RegisterThemeRegkeyObserver();
},
base::Unretained(this)));
}
void NativeThemeWin::RegisterColorFilteringRegkeyObserver() {
DCHECK(hkcu_color_filtering_regkey_.Valid());
DCHECK(base::SequencedTaskRunner::HasCurrentDefault());
hkcu_color_filtering_regkey_.StartWatching(base::BindOnce(
[](NativeThemeWin* native_theme) {
native_theme->UpdateInvertedColors();
// RegKey::StartWatching only provides one notification. Reregistration
// is required to get future notifications.
native_theme->RegisterColorFilteringRegkeyObserver();
},
base::Unretained(this)));
}
void NativeThemeWin::UpdateDarkModeStatus() {
bool dark_mode_enabled = false;
if (hkcu_themes_regkey_.Valid()) {
DWORD apps_use_light_theme = 1;
hkcu_themes_regkey_.ReadValueDW(L"AppsUseLightTheme",
&apps_use_light_theme);
dark_mode_enabled = (apps_use_light_theme == 0);
}
set_use_dark_colors(dark_mode_enabled);
set_preferred_color_scheme(CalculatePreferredColorScheme());
CloseHandlesInternal();
NotifyOnNativeThemeUpdated();
}
void NativeThemeWin::UpdatePrefersReducedTransparency() {
bool prefers_reduced_transparency = false;
if (hkcu_themes_regkey_.Valid()) {
DWORD enable_transparency = 1;
hkcu_themes_regkey_.ReadValueDW(L"EnableTransparency",
&enable_transparency);
prefers_reduced_transparency = (enable_transparency == 0);
}
set_prefers_reduced_transparency(prefers_reduced_transparency);
CloseHandlesInternal();
NotifyOnNativeThemeUpdated();
}
void NativeThemeWin::UpdateInvertedColors() {
bool inverted_colors = false;
if (hkcu_color_filtering_regkey_.Valid()) {
DWORD active = 0;
hkcu_color_filtering_regkey_.ReadValueDW(L"Active", &active);
if (active == 1) {
// 0 = Greyscale
// 1 = Invert
// 2 = Greyscale Inverted
// 3 = Deuteranopia
// 4 = Protanopia
// 5 = Tritanopia
DWORD filter_type = 0;
hkcu_color_filtering_regkey_.ReadValueDW(L"FilterType", &filter_type);
inverted_colors = (filter_type == 1);
}
}
set_inverted_colors(inverted_colors);
CloseHandlesInternal();
NotifyOnNativeThemeUpdated();
}
} // namespace ui