blob: b7b7b25b7ab465fa4d14ca0ac964eb269ae42c16 [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_base.h"
#include <algorithm>
#include <array>
#include <optional>
#include <utility>
#include <variant>
#include "base/check.h"
#include "base/containers/fixed_flat_map.h"
#include "base/containers/span.h"
#include "base/notimplemented.h"
#include "base/notreached.h"
#include "base/numerics/safe_conversions.h"
#include "build/build_config.h"
#include "cc/paint/paint_canvas.h"
#include "cc/paint/paint_flags.h"
#include "third_party/skia/include/core/SkBlendMode.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkMatrix.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/core/SkRRect.h"
#include "third_party/skia/include/core/SkRect.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/native_theme/features/native_theme_features.h"
#include "ui/native_theme/native_theme.h"
namespace ui {
namespace {
static constexpr gfx::Size kCheckboxSize(13, 13);
static constexpr int kSliderTrackThickness = 8;
static constexpr int kSliderThumbThickness = 16;
static constexpr float kBorderWidth = 1.0f;
SkRect AlignSliderTrack(const gfx::Rect& slider_rect,
const NativeTheme::SliderExtraParams& extra_params,
bool is_value,
float thickness) {
const gfx::RectF r(slider_rect);
const gfx::PointF center = r.CenterPoint();
const float half_track_thickness = thickness / 2;
if (extra_params.vertical) {
float top = r.y();
float bottom = r.bottom();
if (is_value) {
// Extend to ensure the thumb completely covers the end of the value rect.
// Because the thumb radius is greater than `half_track_thickness`,
// extending that much is guaranteed to be sufficient.
(extra_params.right_to_left ? top : bottom) =
top + extra_params.thumb_y + half_track_thickness;
}
return SkRect::MakeLTRB(
std::max(r.x(), center.x() - half_track_thickness), top,
std::min(r.right(), center.x() + half_track_thickness), bottom);
}
float left = r.x();
float right = r.right();
if (is_value) {
(extra_params.right_to_left ? left : right) =
left + extra_params.thumb_x + half_track_thickness;
}
return SkRect::MakeLTRB(
left, std::max(r.y(), center.y() - half_track_thickness), right,
std::min(r.bottom(), center.y() + half_track_thickness));
}
} // namespace
gfx::Size NativeThemeBase::GetPartSize(Part part,
State state,
const ExtraParams& extra_params) const {
if (part == kCheckbox || part == kRadio) {
return kCheckboxSize;
}
if (part == kSliderThumb) {
return gfx::Size(kSliderThumbThickness, kSliderThumbThickness);
}
if (part != kInnerSpinButton &&
(part < kScrollbarDownArrow || part > kScrollbarVerticalTrack)) {
return gfx::Size(); // No default size.
}
gfx::Size size =
(part == kScrollbarHorizontalThumb || part == kScrollbarVerticalThumb)
? GetVerticalScrollbarThumbSize()
: GetVerticalScrollbarButtonSize();
if (part == kInnerSpinButton || part == kScrollbarHorizontalTrack ||
part == kScrollbarVerticalTrack) {
size.set_height(0);
}
if (part == kScrollbarLeftArrow || part == kScrollbarRightArrow ||
part == kScrollbarHorizontalThumb || part == kScrollbarHorizontalTrack) {
size.Transpose();
}
return size;
}
float NativeThemeBase::GetBorderRadiusForPart(Part part,
float width,
float height) const {
if (part == kCheckbox || part == kPushButton || part == kTextField) {
return 2.0f;
}
if (part == kProgressBar || part == kSliderTrack) {
// In the common case, the thickness is small enough that this has the same
// effect as the radio/slider thumb code below.
return 40.0f;
}
return (part == kRadio || part == kSliderThumb)
? (std::max(width, height) * 0.5f)
: 0;
}
SkColor NativeThemeBase::GetScrollbarThumbColor(
const ColorProvider* color_provider,
State state,
const ScrollbarThumbExtraParams& extra_params) const {
const SkColor bg_color =
GetContrastingColorForScrollbarPart(extra_params.track_color,
std::nullopt, state)
.value_or(GetControlColor(kScrollbarTrack, {}, {}, color_provider));
if (const std::optional<SkColor> color = GetContrastingColorForScrollbarPart(
extra_params.thumb_color, bg_color, state)) {
return color.value();
}
if (const std::optional<ColorId> id =
GetScrollbarThumbColorId(state, extra_params)) {
return color_provider->GetColor(id.value());
}
static constexpr auto kScrollbarThumbColors =
std::to_array({kScrollbarThumb, kScrollbarThumbHovered, kScrollbarThumb,
kScrollbarThumbPressed});
return GetControlColorForState(kScrollbarThumbColors, state, {}, {},
color_provider);
}
NativeThemeBase::~NativeThemeBase() = default;
void NativeThemeBase::PaintImpl(cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
Part part,
State state,
const gfx::Rect& rect,
const ExtraParams& extra_params,
bool forced_colors,
bool dark_mode,
PreferredContrast contrast,
std::optional<SkColor> accent_color) const {
switch (part) {
case kCheckbox:
PaintCheckbox(canvas, color_provider, state, rect,
std::get<ButtonExtraParams>(extra_params), dark_mode,
contrast, accent_color);
break;
#if BUILDFLAG(IS_LINUX)
case kFrameTopArea:
PaintFrameTopArea(canvas, state, rect,
std::get<FrameTopAreaExtraParams>(extra_params));
break;
#endif
case kInnerSpinButton:
PaintInnerSpinButton(canvas, color_provider, state, rect,
std::get<InnerSpinButtonExtraParams>(extra_params),
forced_colors, dark_mode, contrast);
break;
case kMenuList:
PaintMenuList(canvas, color_provider, state, rect,
std::get<MenuListExtraParams>(extra_params), dark_mode,
contrast);
break;
case kMenuPopupBackground:
PaintMenuPopupBackground(
canvas, color_provider, rect.size(),
std::get<MenuBackgroundExtraParams>(extra_params));
break;
case kMenuPopupSeparator:
PaintMenuSeparator(canvas, color_provider, state, rect,
std::get<MenuSeparatorExtraParams>(extra_params));
break;
case kMenuItemBackground:
PaintMenuItemBackground(canvas, color_provider, state, rect,
std::get<MenuItemExtraParams>(extra_params));
break;
case kProgressBar:
PaintProgressBar(canvas, color_provider, state, rect,
std::get<ProgressBarExtraParams>(extra_params),
dark_mode, contrast, accent_color);
break;
case kPushButton:
PaintButton(canvas, color_provider, state, rect,
std::get<ButtonExtraParams>(extra_params), dark_mode,
contrast);
break;
case kRadio:
PaintRadio(canvas, color_provider, state, rect,
std::get<ButtonExtraParams>(extra_params), dark_mode, contrast,
accent_color);
break;
case kScrollbarDownArrow:
case kScrollbarUpArrow:
case kScrollbarLeftArrow:
case kScrollbarRightArrow:
if (!GetVerticalScrollbarButtonSize().IsEmpty()) {
PaintArrowButton(canvas, color_provider, rect, part, state,
forced_colors, dark_mode, contrast,
std::get<ScrollbarArrowExtraParams>(extra_params));
}
break;
case kScrollbarHorizontalThumb:
case kScrollbarVerticalThumb:
PaintScrollbarThumb(canvas, color_provider, part, state, rect,
std::get<ScrollbarThumbExtraParams>(extra_params));
break;
case kScrollbarHorizontalTrack:
case kScrollbarVerticalTrack:
PaintScrollbarTrack(canvas, color_provider, part, state,
std::get<ScrollbarTrackExtraParams>(extra_params),
rect, forced_colors, contrast);
break;
case kScrollbarHorizontalGripper:
case kScrollbarVerticalGripper:
// Paints nothing in this or any subclass.
break;
case kScrollbarCorner:
PaintScrollbarCorner(canvas, color_provider, state, rect,
std::get<ScrollbarTrackExtraParams>(extra_params));
break;
case kSliderTrack:
PaintSliderTrack(canvas, color_provider, state, rect,
std::get<SliderExtraParams>(extra_params), dark_mode,
contrast, accent_color);
break;
case kSliderThumb:
PaintSliderThumb(canvas, color_provider, state, rect,
std::get<SliderExtraParams>(extra_params), dark_mode,
contrast, accent_color);
break;
case kTextField:
PaintTextField(canvas, color_provider, state, rect,
std::get<TextFieldExtraParams>(extra_params), dark_mode,
contrast);
break;
case kTabPanelBackground:
case kTrackbarThumb:
case kTrackbarTrack:
case kWindowResizeGripper:
NOTIMPLEMENTED();
break;
default:
NOTREACHED();
}
}
gfx::Size NativeThemeBase::GetVerticalScrollbarButtonSize() const {
return gfx::Size(15, 14);
}
gfx::Size NativeThemeBase::GetVerticalScrollbarThumbSize() const {
const int thickness = GetVerticalScrollbarButtonSize().width();
return gfx::Size(thickness, thickness * 2);
}
gfx::RectF NativeThemeBase::GetArrowRect(const gfx::Rect& rect,
Part part,
State state) const {
// Note: Using initializer_list form forces returning by copy, not ref.
const auto [min_side, max_side] = std::minmax({rect.width(), rect.height()});
const int side_length_inset = (max_side + 3) / 4;
const int side_length = std::min(min_side, max_side - side_length_inset * 2);
// When there are an odd number of pixels, put the extra on the top/left.
return gfx::RectF(gfx::Rect(rect.x() + (rect.width() - side_length + 1) / 2,
rect.y() + (rect.height() - side_length + 1) / 2,
side_length, side_length));
}
SkColor NativeThemeBase::GetControlColor(
ControlColorId color_id,
bool dark_mode,
PreferredContrast contrast,
const ColorProvider* color_provider) const {
static constexpr auto kColorMap = base::MakeFixedFlatMap<ControlColorId,
ColorId>(
{{kBorder, kColorWebNativeControlBorder},
{kDisabledBorder, kColorWebNativeControlBorderDisabled},
{kHoveredBorder, kColorWebNativeControlBorderHovered},
{kPressedBorder, kColorWebNativeControlBorderPressed},
{kAccent, kColorWebNativeControlAccent},
{kDisabledAccent, kColorWebNativeControlAccentDisabled},
{kHoveredAccent, kColorWebNativeControlAccentHovered},
{kPressedAccent, kColorWebNativeControlAccentPressed},
{kCheckboxBackground, kColorWebNativeControlCheckboxBackground},
{kDisabledCheckboxBackground,
kColorWebNativeControlCheckboxBackgroundDisabled},
{kFill, kColorWebNativeControlFill},
{kDisabledFill, kColorWebNativeControlFillDisabled},
{kHoveredFill, kColorWebNativeControlFillHovered},
{kPressedFill, kColorWebNativeControlFillPressed},
{kLightenLayer, kColorWebNativeControlLightenLayer},
{kProgressValue, kColorWebNativeControlProgressValue},
{kSlider, kColorWebNativeControlSlider},
{kDisabledSlider, kColorWebNativeControlSliderDisabled},
{kHoveredSlider, kColorWebNativeControlSliderHovered},
{kPressedSlider, kColorWebNativeControlSliderPressed},
{kSliderBorder, kColorWebNativeControlSliderBorder},
{kHoveredSliderBorder, kColorWebNativeControlSliderBorderHovered},
{kPressedSliderBorder, kColorWebNativeControlSliderBorderPressed},
{kAutoCompleteBackground, kColorWebNativeControlAutoCompleteBackground},
{kScrollbarArrowBackground, kColorWebNativeControlScrollbarTrack},
{kScrollbarArrowBackgroundDisabled,
kColorWebNativeControlScrollbarArrowBackgroundDisabled},
{kScrollbarArrowBackgroundHovered,
kColorWebNativeControlScrollbarArrowBackgroundHovered},
{kScrollbarArrowBackgroundPressed,
kColorWebNativeControlScrollbarArrowBackgroundPressed},
{kScrollbarArrow, kColorWebNativeControlScrollbarArrowForeground},
{kScrollbarArrowDisabled,
kColorWebNativeControlScrollbarArrowForegroundDisabled},
{kScrollbarArrowHovered, kColorWebNativeControlScrollbarArrowForeground},
{kScrollbarArrowPressed,
kColorWebNativeControlScrollbarArrowForegroundPressed},
{kScrollbarCornerControlColorId, kColorWebNativeControlScrollbarCorner},
{kScrollbarTrack, kColorWebNativeControlScrollbarTrack},
{kScrollbarThumb, kColorWebNativeControlScrollbarThumb},
{kScrollbarThumbHovered, kColorWebNativeControlScrollbarThumbHovered},
{kScrollbarThumbPressed, kColorWebNativeControlScrollbarThumbPressed},
{kButtonBorder, kColorWebNativeControlButtonBorder},
{kButtonDisabledBorder, kColorWebNativeControlButtonBorderDisabled},
{kButtonHoveredBorder, kColorWebNativeControlButtonBorderHovered},
{kButtonPressedBorder, kColorWebNativeControlButtonBorderPressed},
{kButtonFill, kColorWebNativeControlButtonFill},
{kButtonDisabledFill, kColorWebNativeControlButtonFillDisabled},
{kButtonHoveredFill, kColorWebNativeControlButtonFillHovered},
{kButtonPressedFill, kColorWebNativeControlButtonFillPressed}});
CHECK(color_provider);
return color_provider->GetColor(kColorMap.at(color_id));
}
std::optional<ColorId> NativeThemeBase::GetScrollbarThumbColorId(
State state,
const ScrollbarThumbExtraParams& extra_params) const {
return std::nullopt;
}
float NativeThemeBase::GetScrollbarPartContrastRatioForState(
State state) const {
return 2.15f;
}
void NativeThemeBase::PaintFrameTopArea(
cc::PaintCanvas* canvas,
State state,
const gfx::Rect& rect,
const FrameTopAreaExtraParams& extra_params) const {
cc::PaintFlags flags;
flags.setColor(extra_params.default_background_color);
canvas->drawRect(gfx::RectToSkRect(rect), flags);
}
void NativeThemeBase::PaintMenuPopupBackground(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
const gfx::Size& size,
const MenuBackgroundExtraParams& extra_params) const {
CHECK(color_provider);
// TODO(crbug.com/40219248): Remove FromColor and make all SkColor4f.
canvas->drawColor(
SkColor4f::FromColor(color_provider->GetColor(kColorMenuBackground)),
SkBlendMode::kSrc);
}
void NativeThemeBase::PaintMenuSeparator(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const MenuSeparatorExtraParams& extra_params) const {
CHECK(color_provider);
cc::PaintFlags flags;
flags.setColor(color_provider->GetColor(extra_params.color_id));
canvas->drawRect(gfx::RectToSkRect(*extra_params.paint_rect), flags);
}
void NativeThemeBase::PaintArrowButton(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
const gfx::Rect& rect,
Part part,
State state,
bool forced_colors,
bool dark_mode,
PreferredContrast contrast,
const ScrollbarArrowExtraParams& extra_params) const {
NOTIMPLEMENTED();
}
void NativeThemeBase::PaintScrollbarThumb(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
Part part,
State state,
const gfx::Rect& rect,
const ScrollbarThumbExtraParams& extra_params) const {
NOTIMPLEMENTED();
}
void NativeThemeBase::PaintScrollbarTrack(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
Part part,
State state,
const ScrollbarTrackExtraParams& extra_params,
const gfx::Rect& rect,
bool forced_colors,
PreferredContrast contrast) const {
NOTIMPLEMENTED();
}
void NativeThemeBase::PaintScrollbarCorner(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const ScrollbarTrackExtraParams& extra_params) const {
NOTIMPLEMENTED();
}
SkColor NativeThemeBase::GetControlColorForState(
base::span<const ControlColorId, 4> colors,
State state,
bool dark_mode,
PreferredContrast contrast,
const ColorProvider* color_provider) const {
return GetControlColor(colors[state], dark_mode, contrast, color_provider);
}
SkColor NativeThemeBase::GetScrollbarArrowBackgroundColor(
const ScrollbarArrowExtraParams& extra_params,
State state,
bool dark_mode,
PreferredContrast contrast,
const ColorProvider* color_provider) const {
static constexpr auto kScrollbarArrowBackgroundColors = std::to_array(
{kScrollbarArrowBackgroundDisabled, kScrollbarArrowBackgroundHovered,
kScrollbarArrowBackground, kScrollbarArrowBackgroundPressed});
return GetContrastingColorForScrollbarPart(extra_params.track_color,
std::nullopt, state)
.value_or(GetControlColorForState(kScrollbarArrowBackgroundColors, state,
dark_mode, contrast, color_provider));
}
SkColor NativeThemeBase::GetScrollbarArrowForegroundColor(
SkColor bg_color,
const ScrollbarArrowExtraParams& extra_params,
State state,
bool dark_mode,
PreferredContrast contrast,
const ColorProvider* color_provider) const {
static constexpr auto kScrollbarArrowColors =
std::to_array({kScrollbarArrowDisabled, kScrollbarArrowHovered,
kScrollbarArrow, kScrollbarArrowPressed});
return GetContrastingColorForScrollbarPart(extra_params.thumb_color, bg_color,
state)
.value_or(GetControlColorForState(kScrollbarArrowColors, state, dark_mode,
contrast, color_provider));
}
void NativeThemeBase::PaintLightenLayer(cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
const SkRect& skrect,
State state,
float border_radius,
bool dark_mode,
PreferredContrast contrast) const {
if (state != kDisabled) {
return;
}
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(
GetControlColor(kLightenLayer, dark_mode, contrast, color_provider));
canvas->drawRoundRect(skrect, border_radius, border_radius, flags);
}
void NativeThemeBase::PaintArrow(cc::PaintCanvas* canvas,
const gfx::Rect& rect,
Part part,
State state,
SkColor color) const {
cc::PaintFlags flags;
flags.setColor(color);
canvas->drawPath(PathForArrow(GetArrowRect(rect, part, state), part), flags);
}
// static
SkPath NativeThemeBase::PathForArrow(const gfx::RectF& rect, Part part) {
SkPath path;
if (part == kScrollbarUpArrow || part == kScrollbarDownArrow) {
// Draw up-pointing arrow.
const int arrow_height = base::ClampRound(rect.height()) / 2 + 1;
path.moveTo(rect.x(), rect.bottom() - arrow_height / 2 + 1);
path.rLineTo(rect.width(), 0);
path.rLineTo(-rect.width() / 2.0f, -arrow_height);
} else {
// Draw right-pointing arrow.
const int arrow_width = base::ClampRound(rect.width()) / 2 + 1;
path.moveTo(rect.x() + arrow_width / 2, rect.y());
path.rLineTo(0, rect.height());
path.rLineTo(arrow_width, -rect.height() / 2.0f);
}
path.close();
// Mirror the above path for down/left.
if (part == kScrollbarDownArrow || part == kScrollbarLeftArrow) {
SkMatrix transform;
const gfx::PointF center = rect.CenterPoint();
const bool vert = part == kScrollbarDownArrow;
transform.setScale(vert ? 1 : -1, vert ? -1 : 1, center.x(), center.y());
path.transform(transform);
}
return path;
}
SkColor NativeThemeBase::GetAccentOrControlColorForState(
std::optional<SkColor> accent_color,
base::span<const ControlColorId, 4> colors,
State state,
bool dark_mode,
PreferredContrast contrast,
const ColorProvider* color_provider) const {
if (state == kDisabled || !accent_color.has_value()) {
return GetControlColorForState(colors, state, dark_mode, contrast,
color_provider);
}
if (state == kNormal) {
return accent_color.value();
}
// Approximates the lightness difference between `kAccent` and
// `kHoveredAccent`.
static constexpr double kLightnessAdjust = 0.11;
double l_adjust = (state == kPressed) ? kLightnessAdjust : -kLightnessAdjust;
if (dark_mode) {
l_adjust = -l_adjust;
}
color_utils::HSL hsl;
color_utils::SkColorToHSL(accent_color.value(), &hsl);
hsl.l = std::clamp(hsl.l + l_adjust, 0.0, 1.0);
return color_utils::HSLToSkColor(hsl, SkColorGetA(accent_color.value()));
}
std::optional<SkColor> NativeThemeBase::GetContrastingColorForScrollbarPart(
std::optional<SkColor> fg_color,
std::optional<SkColor> bg_color,
State state) const {
if (!fg_color.has_value() || (state != kHovered && state != kPressed) ||
SkColorGetA(fg_color.value()) == SK_AlphaTRANSPARENT) {
return fg_color;
}
const SkColor resulting_color =
color_utils::BlendForMinContrast(
fg_color.value(), SkColorSetA(fg_color.value(), SK_AlphaOPAQUE),
std::nullopt, GetScrollbarPartContrastRatioForState(state))
.color;
if (!bg_color.has_value()) {
return resulting_color;
}
// Guaranteeing contrast with the background is prioritized over having
// contrast with the original part color. Making a second pass with the
// transforming function might make the final color not contrast as much with
// the original color, but the result is better than using
// `PickGoogleColorTwoBackgrounds()`, which tries to guarantee contrast with
// both (original and background) colors simultaneously, and ends up creating
// contrast changes that are too harsh.
return color_utils::BlendForMinContrast(
resulting_color, SkColorSetA(bg_color.value(), SK_AlphaOPAQUE),
std::nullopt, color_utils::kMinimumVisibleContrastRatio)
.color;
}
void NativeThemeBase::PaintCheckbox(cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const ButtonExtraParams& extra_params,
bool dark_mode,
PreferredContrast contrast,
std::optional<SkColor> accent_color) const {
// Paint the background and border.
const float radius =
GetBorderRadiusForPart(kCheckbox, rect.width(), rect.height());
const SkRect skrect = PaintCheckboxRadioCommon(
canvas, color_provider, state, rect, extra_params, true, radius,
dark_mode, contrast, accent_color);
if (skrect.isEmpty()) {
return;
}
cc::PaintFlags flags;
flags.setAntiAlias(true);
if (extra_params.indeterminate || extra_params.checked) {
// Paint an accent-colored background.
flags.setColor(GetAccentOrControlColorForState(accent_color, kAccentColors,
state, dark_mode, contrast,
color_provider));
canvas->drawRoundRect(skrect, radius, radius, flags);
}
flags.setColor(GetControlColorForState(kCheckboxBackgroundColors, state,
dark_mode, contrast, color_provider));
if (extra_params.indeterminate) {
// Paint the dash.
static constexpr gfx::Size kDashSize(8, 2);
static constexpr float kXInset =
(kCheckboxSize.width() - kDashSize.width()) / 2.0f;
static constexpr float kYInset =
(kCheckboxSize.height() - kDashSize.height()) / 2.0f;
canvas->drawRoundRect(
skrect.makeInset(kXInset * skrect.width() / kCheckboxSize.width(),
kYInset * skrect.height() / kCheckboxSize.height()),
radius, radius, flags);
} else if (extra_params.checked) {
// Paint the checkmark.
SkPath check;
check.moveTo(skrect.x() + skrect.width() * 0.2f, skrect.centerY());
check.rLineTo(skrect.width() * 0.2f, skrect.height() * 0.2f);
check.lineTo(skrect.right() - skrect.width() * 0.2f,
skrect.y() + skrect.height() * 0.2f);
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(skrect.height() * 0.16f);
canvas->drawPath(check, flags);
}
}
void NativeThemeBase::PaintInnerSpinButton(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
gfx::Rect rect,
const InnerSpinButtonExtraParams& extra_params,
bool forced_colors,
bool dark_mode,
PreferredContrast contrast) const {
if (extra_params.read_only) {
state = kDisabled;
}
State increase_state = state;
State decrease_state = state;
(extra_params.spin_up ? decrease_state : increase_state) =
(state == kDisabled) ? kDisabled : kNormal;
const ScrollbarArrowExtraParams arrow = {.zoom = 1.0f};
if (extra_params.spin_arrows_direction == SpinArrowsDirection::kUpDown) {
rect.set_height(rect.height() / 2);
PaintArrowButton(canvas, color_provider, rect, kScrollbarUpArrow,
increase_state, forced_colors, dark_mode, contrast, arrow);
rect.set_y(rect.bottom());
PaintArrowButton(canvas, color_provider, rect, kScrollbarDownArrow,
decrease_state, forced_colors, dark_mode, contrast, arrow);
} else {
rect.set_width(rect.width() / 2);
PaintArrowButton(canvas, color_provider, rect, kScrollbarLeftArrow,
decrease_state, forced_colors, dark_mode, contrast, arrow);
rect.set_x(rect.right());
PaintArrowButton(canvas, color_provider, rect, kScrollbarRightArrow,
increase_state, forced_colors, dark_mode, contrast, arrow);
}
}
void NativeThemeBase::PaintMenuList(cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const MenuListExtraParams& extra_params,
bool dark_mode,
PreferredContrast contrast) const {
// If a border radius is specified, Blink will paint the background and the
// border.
// TODO(pkasting): The comment above seems untrue;
// `ThemePainterDefault::PaintMenuList()` always returns false, so I believe
// Blink never paints the CSS border/background?
if (!extra_params.has_border_radius) {
TextFieldExtraParams params;
params.background_color = extra_params.background_color;
params.has_border = extra_params.has_border;
params.zoom = extra_params.zoom;
PaintTextField(canvas, color_provider, state, rect, params, dark_mode,
contrast);
}
// The arrow base is twice the arrow height, giving 45 degree sides.
static constexpr float kAspectRatio = 2.0f;
SkPath path;
if (extra_params.arrow_direction == ArrowDirection::kDown) {
int intended_width = extra_params.arrow_size;
int intended_height = base::ClampFloor(intended_width / kAspectRatio);
// `extra_params.arrow_x` is the left edge, but `extra_params.arrow_y` is
// the vertical center.
// TODO(pkasting): This leads to complication and bugs; change
// `ThemePainterDefault::SetupMenuListArrow()` to pass the arrow center
// point and fix `NativeTheme` implementations accordingly.
gfx::Rect arrow(extra_params.arrow_x,
extra_params.arrow_y - (intended_height / 2),
intended_width, intended_height);
// Fit the arrow within the paint rect.
arrow.Intersect(rect);
if (arrow.width() != intended_width || arrow.height() != intended_height) {
// Shrink the arrow so it's not clipped. Pick the dimension that was
// clipped "more" (keeping in mind that each px of height is worth
// `kAspectRatio` px of width) and compute the other dimension based on
// that.
const int height_clip = (intended_height - arrow.height()) * kAspectRatio;
const int width_clip = intended_width - arrow.width();
if (height_clip > width_clip) {
arrow.set_width(arrow.height() * kAspectRatio);
} else {
arrow.set_height(arrow.width() / kAspectRatio);
}
arrow.set_origin(
{extra_params.arrow_x + (intended_width - arrow.width()) / 2,
extra_params.arrow_y - arrow.height() / 2});
}
path.moveTo(arrow.x(), arrow.y());
path.lineTo(arrow.x() + arrow.width() / 2, arrow.bottom());
path.lineTo(arrow.right(), arrow.y());
} else {
// Arrow direction is either left or right.
int intended_height = extra_params.arrow_size;
int intended_width = base::ClampFloor(intended_height / kAspectRatio);
// `extra_params.arrow_x` is the horizontal center, but
// `extra_params.arrow_y` is the top edge.
// TODO(pkasting): This leads to complication and bugs; change
// `ThemePainterDefault::SetupMenuListArrow()` to pass the arrow center
// point and fix `NativeTheme` implementations accordingly.
gfx::Rect arrow(extra_params.arrow_x - (intended_width / 2),
extra_params.arrow_y, intended_width, intended_height);
// Fit the arrow within the paint rect.
arrow.Intersect(rect);
if (arrow.width() != intended_width || arrow.height() != intended_height) {
// Shrink the arrow so it's not clipped. Pick the dimension that was
// clipped "more" (keeping in mind that each px of width is worth
// `kAspectRatio` px of height) and compute the other dimension based on
// that.
const int height_clip = intended_height - arrow.height();
const int width_clip = (intended_width - arrow.width()) * kAspectRatio;
if (height_clip > width_clip) {
arrow.set_width(arrow.height() / kAspectRatio);
} else {
arrow.set_height(arrow.width() * kAspectRatio);
}
arrow.set_origin(
{extra_params.arrow_x - arrow.width() / 2,
extra_params.arrow_y + (intended_height - arrow.height()) / 2});
}
if (extra_params.arrow_direction == ArrowDirection::kLeft) {
path.moveTo(arrow.right(), arrow.y());
path.lineTo(arrow.x(), arrow.y() + arrow.height() / 2);
path.lineTo(arrow.right(), arrow.bottom());
} else {
path.moveTo(arrow.x(), arrow.y());
path.lineTo(arrow.right(), arrow.y() + arrow.height() / 2);
path.lineTo(arrow.x(), arrow.bottom());
}
}
// NOTE: Do not close the path; we want a "v" shape, not a triangle.
cc::PaintFlags flags;
flags.setColor(extra_params.arrow_color);
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(2.0f);
canvas->drawPath(path, flags);
}
void NativeThemeBase::PaintProgressBar(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const ProgressBarExtraParams& extra_params,
bool dark_mode,
PreferredContrast contrast,
std::optional<SkColor> accent_color) const {
CHECK(!rect.IsEmpty());
const SliderExtraParams slider = {.vertical = !extra_params.is_horizontal};
// Sliders in a larger area keep the same thickness while progress bars scale,
// but using the slider sizes in a ratio here means that when both are drawn
// in a default-sized space (i.e. thickness = `kSliderThumbThickness`), both
// will be the same thickness.
const float thickness = (slider.vertical ? rect.width() : rect.height()) *
static_cast<float>(kSliderTrackThickness) /
kSliderThumbThickness;
const SkRect track_rect = AlignSliderTrack(rect, slider, false, thickness);
const float radius = GetBorderRadiusForPart(kProgressBar, track_rect.width(),
track_rect.height());
// Paint the track.
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(GetControlColorForState(kFillColors, state, dark_mode,
contrast, color_provider));
canvas->drawRoundRect(track_rect, radius, radius, flags);
// Paint the progress value bar.
int value_height = extra_params.value_rect_height;
int value_width = extra_params.value_rect_width;
int& value_length = slider.vertical ? value_height : value_width;
if (value_length > 0) {
value_length = std::max(value_length, 2);
}
const SkRect value_rect = AlignSliderTrack(
gfx::Rect(extra_params.value_rect_x, extra_params.value_rect_y,
value_width, value_height),
slider, false, thickness);
flags.setColor(GetAccentOrControlColorForState(
accent_color, kAccentColors, state, dark_mode, contrast, color_provider));
if (extra_params.determinate) {
canvas->clipRRect(SkRRect::MakeRectXY(track_rect, radius, radius), true);
canvas->drawRect(value_rect, flags);
} else {
canvas->drawRoundRect(value_rect, radius, radius, flags);
}
// Paint the border.
const float border_width =
AdjustBorderWidthByZoom(kBorderWidth, extra_params.zoom);
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(border_width);
flags.setColor(GetControlColorForState(kSliderBorderColors, state, dark_mode,
contrast, color_provider));
canvas->drawRoundRect(
track_rect.makeInset(border_width / 2, border_width / 2), radius, radius,
flags);
}
void NativeThemeBase::PaintButton(cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const ButtonExtraParams& extra_params,
bool dark_mode,
PreferredContrast contrast) const {
SkRect skrect = gfx::RectToSkRect(rect);
cc::PaintFlags flags;
flags.setAntiAlias(true);
static constexpr auto kButtonFillColors =
std::to_array({kButtonDisabledFill, kButtonHoveredFill, kButtonFill,
kButtonPressedFill});
flags.setColor(GetControlColorForState(kButtonFillColors, state, dark_mode,
contrast, color_provider));
// If the button is too small, fallback to drawing a solid color rect.
if (rect.width() < 5 || rect.height() < 5) {
canvas->drawRect(skrect, flags);
return;
}
const float border_width =
AdjustBorderWidthByZoom(kBorderWidth, extra_params.zoom);
skrect.inset(border_width / 2, border_width / 2);
const float radius = AdjustBorderRadiusByZoom(
kPushButton,
GetBorderRadiusForPart(kPushButton, skrect.width(), skrect.height()),
extra_params.zoom);
// Paint the background.
PaintLightenLayer(canvas, color_provider, skrect, state, radius, dark_mode,
contrast);
canvas->drawRoundRect(skrect, radius, radius, flags);
// Paint the border.
if (extra_params.has_border) {
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(border_width);
flags.setColor(GetControlColorForState(
kButtonBorderColors, state, dark_mode, contrast, color_provider));
canvas->drawRoundRect(skrect, radius, radius, flags);
}
}
void NativeThemeBase::PaintRadio(cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const ButtonExtraParams& extra_params,
bool dark_mode,
PreferredContrast contrast,
std::optional<SkColor> accent_color) const {
// Paint the background and border.
const float radius =
GetBorderRadiusForPart(kRadio, rect.width(), rect.height());
const SkRect skrect = PaintCheckboxRadioCommon(
canvas, color_provider, state, rect, extra_params, false, radius,
dark_mode, contrast, accent_color);
if (skrect.isEmpty() || !extra_params.checked) {
return;
}
// Paint the dot.
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(GetAccentOrControlColorForState(
accent_color, kAccentColors, state, dark_mode, contrast, color_provider));
canvas->drawRoundRect(
skrect.makeInset(skrect.width() * 0.2, skrect.height() * 0.2), radius,
radius, flags);
}
void NativeThemeBase::PaintSliderTrack(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const SliderExtraParams& extra_params,
bool dark_mode,
PreferredContrast contrast,
std::optional<SkColor> accent_color) const {
const float track_height = kSliderTrackThickness * extra_params.zoom;
SkRect track_rect = AlignSliderTrack(rect, extra_params, false, track_height);
const float radius = GetBorderRadiusForPart(kSliderTrack, track_rect.width(),
track_rect.height());
// Shrink the track by 1 pixel on each end so the thumb can completely
// cover.
if (extra_params.vertical) {
track_rect.inset(0, 1);
} else {
track_rect.inset(1, 0);
}
// Paint the track.
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(GetControlColorForState(kFillColors, state, dark_mode,
contrast, color_provider));
canvas->drawRoundRect(track_rect, radius, radius, flags);
{
// Paint the value bar, clipped to its extent.
gfx::Canvas gfx_canvas(canvas, 1.0f);
gfx::ScopedCanvas scoped_canvas(&gfx_canvas);
canvas->clipRect(AlignSliderTrack(rect, extra_params, true, track_height),
true);
flags.setColor(GetAccentOrControlColorForState(accent_color, kSliderColors,
state, dark_mode, contrast,
color_provider));
canvas->drawRoundRect(track_rect, radius, radius, flags);
}
// Paint the border.
flags.setStyle(cc::PaintFlags::kStroke_Style);
const float border_width =
AdjustBorderWidthByZoom(kBorderWidth, extra_params.zoom);
flags.setStrokeWidth(border_width);
flags.setColor(GetControlColorForState(kSliderBorderColors, state, dark_mode,
contrast, color_provider));
canvas->drawRoundRect(
track_rect.makeInset(border_width / 2, border_width / 2), radius, radius,
flags);
}
void NativeThemeBase::PaintSliderThumb(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const SliderExtraParams& extra_params,
bool dark_mode,
PreferredContrast contrast,
std::optional<SkColor> accent_color) const {
const SkRect thumb_rect = gfx::RectToSkRect(rect);
const float radius = GetBorderRadiusForPart(kSliderThumb, thumb_rect.width(),
thumb_rect.height());
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(GetAccentOrControlColorForState(
accent_color, kSliderColors, state, dark_mode, contrast, color_provider));
// TODO(pkasting): This inset appears to be a historical accident; consider
// removing it and rebaselining the slider appearance.
canvas->drawRoundRect(thumb_rect.makeInset(0.5f, 0.5f), radius, radius,
flags);
}
void NativeThemeBase::PaintTextField(cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const TextFieldExtraParams& extra_params,
bool dark_mode,
PreferredContrast contrast) const {
SkRect bounds = gfx::RectToSkRect(rect);
const float border_width =
AdjustBorderWidthByZoom(kBorderWidth, extra_params.zoom);
bounds.inset(border_width / 2, border_width / 2);
const float radius = AdjustBorderRadiusByZoom(
kTextField,
GetBorderRadiusForPart(kTextField, bounds.width(), bounds.height()),
extra_params.zoom);
// Paint the background.
const bool paint_autocomplete_background =
extra_params.auto_complete_active && state != kDisabled;
if (paint_autocomplete_background ||
SkColorGetA(extra_params.background_color)) {
PaintLightenLayer(canvas, color_provider, bounds, state, radius, dark_mode,
contrast);
cc::PaintFlags bg_flags;
// TODO(crbug.com/446078854): The current background color seems wrong; it
// was done in
// https://crrev.com/c/1792578/5..7/ui/native_theme/native_theme_aura.cc
// ...due to feedback that the previous version of that patch, which used
// CSS to add a white hover effect, was causing interop problems.
// Unfortunately the new version discarded the passed-in background color
// entirely, likely due to oversight, and affected far more than just the
// hover state.
//
// The disabled branch here contains what pkasting thinks is the correct
// code; however, this may have significant visual effect and possible web
// compat ramifications, and may need to be discussed with Blink forms/a11y
// folks. This might require Blink-side changes and/or WPT updates in
// addition to simple rebaselines.
SkColor default_bg_color;
if constexpr (false) {
// Proposed behavior
default_bg_color = GetAccentOrControlColorForState(
extra_params.background_color,
kFillColors /* Doesn't matter, won't be used */,
// Not certain of the correct disabled state behavior
(state == kDisabled) ? kNormal : state, dark_mode, contrast,
color_provider);
} else {
// Status quo behavior
default_bg_color =
GetControlColorForState(kCheckboxBackgroundColors, state, dark_mode,
contrast, color_provider);
}
bg_flags.setColor(paint_autocomplete_background
? GetControlColor(kAutoCompleteBackground, dark_mode,
contrast, color_provider)
: default_bg_color);
canvas->drawRoundRect(bounds, radius, radius, bg_flags);
}
// Paint the border.
if (extra_params.has_border) {
cc::PaintFlags border_flags;
border_flags.setColor(GetControlColorForState(
kBorderColors, state, dark_mode, contrast, color_provider));
border_flags.setStyle(cc::PaintFlags::kStroke_Style);
border_flags.setStrokeWidth(border_width);
canvas->drawRoundRect(bounds, radius, radius, border_flags);
}
}
SkRect NativeThemeBase::PaintCheckboxRadioCommon(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const ButtonExtraParams& extra_params,
bool is_checkbox,
float border_radius,
bool dark_mode,
PreferredContrast contrast,
std::optional<SkColor> accent_color) const {
// Use the largest square that fits inside the provided rectangle. This
// matches other browsers.
gfx::RectF rect_f(rect);
const float min_side = std::min(rect_f.width(), rect_f.height());
rect_f.ClampToCenteredSize({min_side, min_side});
const SkRect skrect = gfx::RectFToSkRect(rect_f);
const SkColor border_color =
extra_params.checked
? GetAccentOrControlColorForState(accent_color, kAccentColors, state,
dark_mode, contrast, color_provider)
: GetControlColorForState(kBorderColors, state, dark_mode, contrast,
color_provider);
// If the square is too small then paint only a square.
cc::PaintFlags flags;
if (skrect.width() <= 2) {
flags.setColor(border_color);
canvas->drawRect(skrect, flags);
return {}; // Don't draw anything more.
}
// Paint the background.
// Shrink the rect slightly to avoid antialiasing artifacts with the border.
const auto background_rect =
skrect.makeInset(kBorderWidth * 0.2f, kBorderWidth * 0.2f);
PaintLightenLayer(canvas, color_provider, background_rect, state,
border_radius, dark_mode, contrast);
flags.setAntiAlias(true);
flags.setColor(GetControlColorForState(kCheckboxBackgroundColors, state,
dark_mode, contrast, color_provider));
canvas->drawRoundRect(background_rect, border_radius, border_radius, flags);
// Paint the border.
// Indeterminate and checked checkboxes do not draw a border; they will draw
// an accent-colored background instead on the caller side.
if (!is_checkbox || (!extra_params.checked && !extra_params.indeterminate)) {
flags.setColor(border_color);
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(kBorderWidth);
canvas->drawRoundRect(skrect.makeInset(kBorderWidth / 2, kBorderWidth / 2),
border_radius, border_radius, flags);
}
// Let the caller draw any additional decorations.
return skrect;
}
} // namespace ui