blob: c3e0449cbfb60ac302c3c4be7ba26a8fbceab227 [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 <limits>
#include <memory>
#include "base/check.h"
#include "base/command_line.h"
#include "base/containers/fixed_flat_set.h"
#include "base/notreached.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "cc/paint/paint_flags.h"
#include "cc/paint/paint_shader.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/core/SkRRect.h"
#include "third_party/skia/include/effects/SkGradientShader.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/resource/resource_scale_factor.h"
#include "ui/base/ui_base_features.h"
#include "ui/base/ui_base_switches.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/color_utils.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/image/image_skia.h"
#include "ui/native_theme/common_theme.h"
#include "ui/native_theme/native_theme.h"
namespace {
// These are the default dimensions of radio buttons and checkboxes.
const int kCheckboxAndRadioWidth = 13;
const int kCheckboxAndRadioHeight = 13;
// Color constant pairs for light/default and dark color-schemes below.
constexpr SkColor kThumbActiveColor[2] = {SkColorSetRGB(0xF4, 0xF4, 0xF4),
gfx::kPlaceholderColor};
constexpr SkColor kThumbInactiveColor[2] = {SkColorSetRGB(0xEA, 0xEA, 0xEA),
SK_ColorWHITE};
constexpr SkColor kTrackColor[2] = {SkColorSetRGB(0xD3, 0xD3, 0xD3),
gfx::kPlaceholderColor};
// We are currently only painting kMenuPopupBackground with the kDefault
// scheme. If that changes, we need to replace gfx::kPlaceholderColor with an
// appropriate dark scheme color. See the DCHECK in PaintMenuPopupBackground().
constexpr SkColor kMenuPopupBackgroundColor[2] = {SkColorSetRGB(210, 225, 246),
gfx::kPlaceholderColor};
constexpr SkColor kArrowDisabledColor[2] = {SK_ColorBLACK, SK_ColorWHITE};
// The "dash" is 8x2 px by default (the checkbox is 13x13 px).
const SkScalar kIndeterminateInsetWidthRatio = (13 - 8) / 2.0f / 13;
const SkScalar kIndeterminateInsetHeightRatio = (13 - 2) / 2.0f / 13;
const SkScalar kBorderWidth = 1.f;
const SkScalar kSliderTrackHeight = 8.f;
const SkScalar kSliderThumbBorderWidth = 1.f;
const SkScalar kSliderThumbBorderHoveredWidth = 1.f;
// Default block size for progress is 16px and the track is 8px.
const SkScalar kTrackBlockRatio = 8.0f / 16;
const SkScalar kMenuListArrowStrokeWidth = 2.f;
const int kSliderThumbSize = 16;
// This value was created with the following steps:
// 1. Take the SkColors returned by GetControlColor for kAccent and
// kHoveredAccent.
// 2. use color_utils::SkColorToHSL to convert those colors to HSL.
// 3. Take the difference of the luminance component of the HSL between those
// two colors.
// 4. Round to the nearest two decimal points.
//
// This is used to emulate the changes in color used for hover and pressed
// states when a custom accent-color is used to draw form controls. It just so
// happens that the luminance difference is the same for hover and press, and it
// also happens that GetDarkModeControlColor has very close values when you run
// these steps, which makes it work well for forced color-scheme for contrast
// with certain accent-colors.
const double kAccentLuminanceAdjust = 0.11;
// Get a color constant based on color-scheme
// TODO(crbug.com/40242489): Move colors defined above to the color pipeline and
// remove this function.
SkColor GetColor(const SkColor colors[2],
ui::NativeTheme::ColorScheme color_scheme) {
return colors[color_scheme == ui::NativeTheme::ColorScheme::kDark ? 1 : 0];
}
SkColor AdjustLuminance(const SkColor& color, double luminance) {
color_utils::HSL hsl;
color_utils::SkColorToHSL(color, &hsl);
hsl.l = std::clamp(hsl.l + luminance, 0., 1.);
return color_utils::HSLToSkColor(hsl, SkColorGetA(color));
}
SkColor CustomAccentColorForState(
const SkColor& accent_color,
ui::NativeTheme::State state,
const ui::NativeTheme::ColorScheme& color_scheme) {
bool make_lighter = false;
bool is_dark_mode = color_scheme == ui::NativeTheme::ColorScheme::kDark;
switch (state) {
case ui::NativeTheme::kHovered:
make_lighter = is_dark_mode;
break;
case ui::NativeTheme::kPressed:
make_lighter = !is_dark_mode;
break;
default:
return accent_color;
}
return AdjustLuminance(accent_color,
(make_lighter ? 1 : -1) * kAccentLuminanceAdjust);
}
} // namespace
namespace ui {
gfx::Size NativeThemeBase::GetPartSize(Part part,
State state,
const ExtraParams& extra) const {
switch (part) {
// Please keep these in the order of NativeTheme::Part.
case kCheckbox:
return gfx::Size(kCheckboxAndRadioWidth, kCheckboxAndRadioHeight);
case kInnerSpinButton:
return gfx::Size(scrollbar_width_, 0);
case kMenuList:
return gfx::Size(); // No default size.
case kMenuPopupBackground:
return gfx::Size(); // No default size.
case kMenuItemBackground:
case kProgressBar:
case kPushButton:
return gfx::Size(); // No default size.
case kRadio:
return gfx::Size(kCheckboxAndRadioWidth, kCheckboxAndRadioHeight);
case kScrollbarDownArrow:
case kScrollbarUpArrow:
return gfx::Size(scrollbar_width_, scrollbar_button_length_);
case kScrollbarLeftArrow:
case kScrollbarRightArrow:
return gfx::Size(scrollbar_button_length_, scrollbar_width_);
case kScrollbarHorizontalThumb:
// This matches Firefox on Linux.
return gfx::Size(2 * scrollbar_width_, scrollbar_width_);
case kScrollbarVerticalThumb:
// This matches Firefox on Linux.
return gfx::Size(scrollbar_width_, 2 * scrollbar_width_);
case kScrollbarHorizontalTrack:
return gfx::Size(0, scrollbar_width_);
case kScrollbarVerticalTrack:
return gfx::Size(scrollbar_width_, 0);
case kScrollbarHorizontalGripper:
case kScrollbarVerticalGripper:
NOTIMPLEMENTED();
break;
case kSliderTrack:
return gfx::Size(); // No default size.
case kSliderThumb:
// These sizes match the sizes in Chromium Win.
return gfx::Size(kSliderThumbSize, kSliderThumbSize);
case kTabPanelBackground:
NOTIMPLEMENTED();
break;
case kTextField:
return gfx::Size(); // No default size.
case kTrackbarThumb:
case kTrackbarTrack:
case kWindowResizeGripper:
NOTIMPLEMENTED();
break;
default:
NOTREACHED() << "Unknown theme part: " << part;
break;
}
return gfx::Size();
}
float NativeThemeBase::GetBorderRadiusForPart(Part part,
float width,
float height) const {
switch (part) {
case kCheckbox:
return 2.f;
case kPushButton:
case kTextField:
return 2.f;
case kRadio:
return std::max(width, height) * 0.5;
case kProgressBar:
case kSliderTrack:
// default border radius for progress and range is 40px.
return 40.f;
case kSliderThumb:
return std::max(width, height) * 0.5;
default:
break;
}
return 0;
}
void NativeThemeBase::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;
canvas->save();
canvas->clipRect(gfx::RectToSkRect(rect));
// Form control accents shouldn't be drawn with any transparency.
std::optional<SkColor> accent_color_opaque;
if (accent_color) {
accent_color_opaque = SkColorSetA(accent_color.value(), SK_AlphaOPAQUE);
}
switch (part) {
// Please keep these in the order of NativeTheme::Part.
case kCheckbox:
PaintCheckbox(canvas, color_provider, state, rect,
absl::get<ButtonExtraParams>(extra), color_scheme,
accent_color_opaque);
break;
// TODO(crbug.com/40118868): Revisit the macro expression once build flag switch
// of lacros-chrome is complete.
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS)
case kFrameTopArea:
PaintFrameTopArea(canvas, state, rect,
absl::get<FrameTopAreaExtraParams>(extra),
color_scheme);
break;
#endif
case kInnerSpinButton:
PaintInnerSpinButton(canvas, color_provider, state, rect,
absl::get<InnerSpinButtonExtraParams>(extra),
color_scheme, in_forced_colors);
break;
case kMenuList:
PaintMenuList(canvas, color_provider, state, rect,
absl::get<MenuListExtraParams>(extra), color_scheme);
break;
case kMenuPopupBackground:
PaintMenuPopupBackground(canvas, color_provider, rect.size(),
absl::get<MenuBackgroundExtraParams>(extra),
color_scheme);
break;
case kMenuPopupSeparator:
PaintMenuSeparator(canvas, color_provider, state, rect,
absl::get<MenuSeparatorExtraParams>(extra));
break;
case kMenuItemBackground:
PaintMenuItemBackground(canvas, color_provider, state, rect,
absl::get<MenuItemExtraParams>(extra),
color_scheme);
break;
case kProgressBar:
PaintProgressBar(canvas, color_provider, state, rect,
absl::get<ProgressBarExtraParams>(extra), color_scheme,
accent_color_opaque);
break;
case kPushButton:
PaintButton(canvas, color_provider, state, rect,
absl::get<ButtonExtraParams>(extra), color_scheme);
break;
case kRadio:
PaintRadio(canvas, color_provider, state, rect,
absl::get<ButtonExtraParams>(extra), color_scheme,
accent_color_opaque);
break;
case kScrollbarDownArrow:
case kScrollbarUpArrow:
case kScrollbarLeftArrow:
case kScrollbarRightArrow:
if (scrollbar_button_length_ > 0)
PaintArrowButton(canvas, color_provider, rect, part, state,
color_scheme, in_forced_colors,
absl::get<ScrollbarArrowExtraParams>(extra));
break;
case kScrollbarHorizontalThumb:
case kScrollbarVerticalThumb:
PaintScrollbarThumb(canvas, color_provider, part, state, rect,
absl::get<ScrollbarThumbExtraParams>(extra),
color_scheme);
break;
case kScrollbarHorizontalTrack:
case kScrollbarVerticalTrack:
PaintScrollbarTrack(canvas, color_provider, part, state,
absl::get<ScrollbarTrackExtraParams>(extra), rect,
color_scheme, in_forced_colors);
break;
case kScrollbarHorizontalGripper:
case kScrollbarVerticalGripper:
// Invoked by views scrollbar code, don't care about for non-win
// implementations, so no NOTIMPLEMENTED.
break;
case kScrollbarCorner:
PaintScrollbarCorner(canvas, color_provider, state, rect,
absl::get<ScrollbarTrackExtraParams>(extra),
color_scheme);
break;
case kSliderTrack:
PaintSliderTrack(canvas, color_provider, state, rect,
absl::get<SliderExtraParams>(extra), color_scheme,
accent_color_opaque);
break;
case kSliderThumb:
PaintSliderThumb(canvas, color_provider, state, rect,
absl::get<SliderExtraParams>(extra), color_scheme,
accent_color_opaque);
break;
case kTabPanelBackground:
NOTIMPLEMENTED();
break;
case kTextField:
PaintTextField(canvas, color_provider, state, rect,
absl::get<TextFieldExtraParams>(extra), color_scheme);
break;
case kTrackbarThumb:
case kTrackbarTrack:
case kWindowResizeGripper:
NOTIMPLEMENTED();
break;
default:
NOTREACHED() << "Unknown theme part: " << part;
break;
}
canvas->restore();
}
bool NativeThemeBase::SupportsNinePatch(Part part) const {
return false;
}
gfx::Size NativeThemeBase::GetNinePatchCanvasSize(Part part) const {
NOTREACHED() << "NativeThemeBase doesn't support nine-patch resources.";
return gfx::Size();
}
gfx::Rect NativeThemeBase::GetNinePatchAperture(Part part) const {
NOTREACHED() << "NativeThemeBase doesn't support nine-patch resources.";
return gfx::Rect();
}
NativeThemeBase::NativeThemeBase() : NativeThemeBase(false) {}
NativeThemeBase::NativeThemeBase(bool should_only_use_dark_colors,
ui::SystemTheme system_theme)
: NativeTheme(should_only_use_dark_colors, system_theme) {}
NativeThemeBase::~NativeThemeBase() = default;
void NativeThemeBase::PaintArrowButton(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
const gfx::Rect& rect,
Part direction,
State state,
ColorScheme color_scheme,
bool in_forced_colors,
const ScrollbarArrowExtraParams& extra_params) const {
cc::PaintFlags flags;
// Calculate button color.
SkScalar track_hsv[3];
SkColorToHSV(
extra_params.track_color.value_or(GetColor(kTrackColor, color_scheme)),
track_hsv);
SkColor button_color = SaturateAndBrighten(track_hsv, 0, 0.2f);
SkColor background_color = button_color;
if (state == kPressed) {
SkScalar button_hsv[3];
SkColorToHSV(button_color, button_hsv);
button_color = SaturateAndBrighten(button_hsv, 0, -0.1f);
} else if (state == kHovered) {
SkScalar button_hsv[3];
SkColorToHSV(button_color, button_hsv);
button_color = SaturateAndBrighten(button_hsv, 0, 0.05f);
}
SkIRect skrect;
skrect.setXYWH(rect.x(), rect.y(), rect.width(), rect.height());
// Paint the background (the area visible behind the rounded corners).
flags.setColor(background_color);
canvas->drawIRect(skrect, flags);
// Paint the button's outline and fill the middle
SkPath outline;
switch (direction) {
case kScrollbarUpArrow:
outline.moveTo(rect.x() + 0.5, rect.y() + rect.height() + 0.5);
outline.rLineTo(0, -(rect.height() - 2));
outline.rLineTo(2, -2);
outline.rLineTo(rect.width() - 5, 0);
outline.rLineTo(2, 2);
outline.rLineTo(0, rect.height() - 2);
break;
case kScrollbarDownArrow:
outline.moveTo(rect.x() + 0.5, rect.y() - 0.5);
outline.rLineTo(0, rect.height() - 2);
outline.rLineTo(2, 2);
outline.rLineTo(rect.width() - 5, 0);
outline.rLineTo(2, -2);
outline.rLineTo(0, -(rect.height() - 2));
break;
case kScrollbarRightArrow:
outline.moveTo(rect.x() - 0.5, rect.y() + 0.5);
outline.rLineTo(rect.width() - 2, 0);
outline.rLineTo(2, 2);
outline.rLineTo(0, rect.height() - 5);
outline.rLineTo(-2, 2);
outline.rLineTo(-(rect.width() - 2), 0);
break;
case kScrollbarLeftArrow:
outline.moveTo(rect.x() + rect.width() + 0.5, rect.y() + 0.5);
outline.rLineTo(-(rect.width() - 2), 0);
outline.rLineTo(-2, 2);
outline.rLineTo(0, rect.height() - 5);
outline.rLineTo(2, 2);
outline.rLineTo(rect.width() - 2, 0);
break;
default:
break;
}
outline.close();
flags.setStyle(cc::PaintFlags::kFill_Style);
flags.setColor(button_color);
canvas->drawPath(outline, flags);
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kStroke_Style);
SkScalar thumb_hsv[3];
SkColorToHSV(GetColor(kThumbInactiveColor, color_scheme), thumb_hsv);
flags.setColor(OutlineColor(track_hsv, thumb_hsv));
canvas->drawPath(outline, flags);
// TODO(crbug.com/40596569): Adjust thumb_color based on `state`.
const SkColor arrow_color =
extra_params.thumb_color.has_value()
? extra_params.thumb_color.value()
: GetArrowColor(state, color_scheme, color_provider);
PaintArrow(canvas, rect, direction, arrow_color);
}
void NativeThemeBase::PaintArrow(cc::PaintCanvas* gc,
const gfx::Rect& rect,
Part direction,
SkColor color) const {
cc::PaintFlags flags;
flags.setColor(color);
SkPath path = PathForArrow(BoundingRectForArrow(rect), direction);
gc->drawPath(path, flags);
}
SkPath NativeThemeBase::PathForArrow(const gfx::Rect& bounding_rect,
Part direction) const {
const gfx::PointF center = gfx::RectF(bounding_rect).CenterPoint();
SkPath path;
SkMatrix transform;
transform.setIdentity();
if (direction == kScrollbarUpArrow || direction == kScrollbarDownArrow) {
int arrow_altitude = bounding_rect.height() / 2 + 1;
path.moveTo(bounding_rect.x(), bounding_rect.bottom());
path.rLineTo(bounding_rect.width(), 0);
path.rLineTo(-bounding_rect.width() / 2.0f, -arrow_altitude);
path.close();
path.offset(0, -arrow_altitude / 2 + 1);
if (direction == kScrollbarDownArrow) {
transform.setScale(1, -1, center.x(), center.y());
}
} else {
int arrow_altitude = bounding_rect.width() / 2 + 1;
path.moveTo(bounding_rect.x(), bounding_rect.y());
path.rLineTo(0, bounding_rect.height());
path.rLineTo(arrow_altitude, -bounding_rect.height() / 2.0f);
path.close();
path.offset(arrow_altitude / 2, 0);
if (direction == kScrollbarLeftArrow) {
transform.setScale(-1, 1, center.x(), center.y());
}
}
path.transform(transform);
return path;
}
gfx::Rect NativeThemeBase::BoundingRectForArrow(const gfx::Rect& rect) const {
const std::pair<int, int> rect_sides =
std::minmax(rect.width(), rect.height());
const int side_length_inset = 2 * std::ceil(rect_sides.second / 4.f);
const int side_length =
std::min(rect_sides.first, rect_sides.second - side_length_inset);
// When there are an odd number of pixels, put the extra on the top/left.
return gfx::Rect(rect.x() + (rect.width() - side_length + 1) / 2,
rect.y() + (rect.height() - side_length + 1) / 2,
side_length, side_length);
}
void NativeThemeBase::PaintScrollbarTrack(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
Part part,
State state,
const ScrollbarTrackExtraParams& extra_params,
const gfx::Rect& rect,
ColorScheme color_scheme,
bool in_forced_colors) const {
cc::PaintFlags flags;
SkIRect skrect;
skrect.setLTRB(rect.x(), rect.y(), rect.right(), rect.bottom());
SkScalar track_hsv[3];
SkColorToHSV(GetColor(kTrackColor, color_scheme), track_hsv);
flags.setColor(SaturateAndBrighten(track_hsv, 0, 0));
canvas->drawIRect(skrect, flags);
SkScalar thumb_hsv[3];
SkColorToHSV(GetColor(kThumbInactiveColor, color_scheme), thumb_hsv);
flags.setColor(OutlineColor(track_hsv, thumb_hsv));
DrawBox(canvas, rect, flags);
}
void NativeThemeBase::PaintScrollbarThumb(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
Part part,
State state,
const gfx::Rect& rect,
const ScrollbarThumbExtraParams& extra_params,
ColorScheme color_scheme) const {
const bool hovered = state == kHovered;
const int midx = rect.x() + rect.width() / 2;
const int midy = rect.y() + rect.height() / 2;
const bool vertical = part == kScrollbarVerticalThumb;
SkScalar thumb[3];
SkColorToHSV(
GetColor(hovered ? kThumbActiveColor : kThumbInactiveColor, color_scheme),
thumb);
cc::PaintFlags flags;
flags.setColor(SaturateAndBrighten(thumb, 0, 0.02f));
SkIRect skrect;
if (vertical)
skrect.setLTRB(rect.x(), rect.y(), midx + 1, rect.y() + rect.height());
else
skrect.setLTRB(rect.x(), rect.y(), rect.x() + rect.width(), midy + 1);
canvas->drawIRect(skrect, flags);
flags.setColor(SaturateAndBrighten(thumb, 0, -0.02f));
if (vertical) {
skrect.setLTRB(midx + 1, rect.y(), rect.x() + rect.width(),
rect.y() + rect.height());
} else {
skrect.setLTRB(rect.x(), midy + 1, rect.x() + rect.width(),
rect.y() + rect.height());
}
canvas->drawIRect(skrect, flags);
SkScalar track[3];
SkColorToHSV(GetColor(kTrackColor, color_scheme), track);
flags.setColor(OutlineColor(track, thumb));
DrawBox(canvas, rect, flags);
if (rect.height() > 10 && rect.width() > 10) {
const int grippy_half_width = 2;
const int inter_grippy_offset = 3;
if (vertical) {
DrawHorizLine(canvas, midx - grippy_half_width, midx + grippy_half_width,
midy - inter_grippy_offset, flags);
DrawHorizLine(canvas, midx - grippy_half_width, midx + grippy_half_width,
midy, flags);
DrawHorizLine(canvas, midx - grippy_half_width, midx + grippy_half_width,
midy + inter_grippy_offset, flags);
} else {
DrawVertLine(canvas, midx - inter_grippy_offset, midy - grippy_half_width,
midy + grippy_half_width, flags);
DrawVertLine(canvas, midx, midy - grippy_half_width,
midy + grippy_half_width, flags);
DrawVertLine(canvas, midx + inter_grippy_offset, midy - grippy_half_width,
midy + grippy_half_width, flags);
}
}
}
void NativeThemeBase::PaintScrollbarCorner(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const ScrollbarTrackExtraParams& extra_params,
ColorScheme color_scheme) const {}
void NativeThemeBase::PaintCheckbox(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const ButtonExtraParams& button,
ColorScheme color_scheme,
const std::optional<SkColor>& accent_color) const {
const float border_radius =
GetBorderRadiusForPart(kCheckbox, rect.width(), rect.height());
SkRect skrect =
PaintCheckboxRadioCommon(canvas, color_provider, state, rect, button,
true, border_radius, color_scheme, accent_color);
if (!skrect.isEmpty()) {
cc::PaintFlags flags;
flags.setAntiAlias(true);
if (button.indeterminate) {
// Draw the dash.
flags.setColor(
ControlsBorderColorForState(state, color_scheme, color_provider));
const auto indeterminate =
skrect.makeInset(skrect.width() * kIndeterminateInsetWidthRatio,
skrect.height() * kIndeterminateInsetHeightRatio);
flags.setStyle(cc::PaintFlags::kFill_Style);
canvas->drawRoundRect(indeterminate, border_radius, border_radius, flags);
} else if (button.checked) {
// Draw the accent background.
flags.setStyle(cc::PaintFlags::kFill_Style);
if (accent_color && state != kDisabled) {
flags.setColor(
CustomAccentColorForState(*accent_color, state, color_scheme));
} else {
flags.setColor(
ControlsAccentColorForState(state, color_scheme, color_provider));
}
canvas->drawRoundRect(skrect, border_radius, border_radius, flags);
// Draw the checkmark.
SkPath check;
check.moveTo(skrect.x() + skrect.width() * 0.2, skrect.centerY());
check.rLineTo(skrect.width() * 0.2, skrect.height() * 0.2);
check.lineTo(skrect.right() - skrect.width() * 0.2,
skrect.y() + skrect.height() * 0.2);
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(SkFloatToScalar(skrect.height() * 0.16));
SkColor checkmark_color =
ControlsBackgroundColorForState(state, color_scheme, color_provider);
flags.setColor(checkmark_color);
canvas->drawPath(check, flags);
}
}
}
// Draws the common elements of checkboxes and radio buttons.
// Returns the rectangle within which any additional decorations should be
// drawn, or empty if none.
SkRect NativeThemeBase::PaintCheckboxRadioCommon(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const ButtonExtraParams& button,
bool is_checkbox,
const SkScalar border_radius,
ColorScheme color_scheme,
const std::optional<SkColor>& accent_color) const {
SkRect skrect = gfx::RectToSkRect(rect);
// Use the largest square that fits inside the provided rectangle.
// No other browser seems to support non-square widget, so accidentally
// having non-square sizes is common (eg. amazon and webkit dev tools).
if (skrect.width() != skrect.height()) {
SkScalar size = std::min(skrect.width(), skrect.height());
skrect.inset((skrect.width() - size) / 2, (skrect.height() - size) / 2);
}
// If the rectangle is too small then paint only a rectangle. We don't want
// to have to worry about '- 1' and '+ 1' calculations below having overflow
// or underflow.
if (skrect.width() <= 2) {
cc::PaintFlags flags;
if (accent_color && state != kDisabled) {
flags.setColor(
CustomAccentColorForState(*accent_color, state, color_scheme));
} else {
flags.setColor(GetControlColor(kBorder, color_scheme, color_provider));
}
flags.setStyle(cc::PaintFlags::kFill_Style);
canvas->drawRect(skrect, flags);
// Too small to draw anything more.
return SkRect::MakeEmpty();
}
cc::PaintFlags flags;
flags.setAntiAlias(true);
// Paint the background (is not visible behind the rounded corners).
// Note we need to shrink the rect for background a little bit so we don't
// see artifacts introduced by antialiasing between the border and the
// background near the rounded corners of checkbox.
const auto background_rect =
skrect.makeInset(kBorderWidth * 0.2f, kBorderWidth * 0.2f);
PaintLightenLayer(canvas, color_provider, background_rect, state,
border_radius, color_scheme);
flags.setColor(
ControlsBackgroundColorForState(state, color_scheme, color_provider));
flags.setStyle(cc::PaintFlags::kFill_Style);
canvas->drawRoundRect(background_rect, border_radius, border_radius, flags);
// For checkbox the border is drawn only when it is unchecked or
// indeterminate. For radio the border is always drawn.
if (!(is_checkbox && button.checked && !button.indeterminate)) {
// Shrink half border width so the final pixels of the border will be
// within the rectangle.
const auto border_rect =
skrect.makeInset(kBorderWidth / 2, kBorderWidth / 2);
SkColor border_color;
if (button.checked && !button.indeterminate) {
if (accent_color && state != kDisabled) {
border_color =
CustomAccentColorForState(*accent_color, state, color_scheme);
} else {
border_color =
ControlsAccentColorForState(state, color_scheme, color_provider);
}
} else {
border_color =
ControlsBorderColorForState(state, color_scheme, color_provider);
}
flags.setColor(border_color);
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(kBorderWidth);
canvas->drawRoundRect(border_rect, border_radius, border_radius, flags);
}
// Return the rectangle for drawing any additional decorations.
return skrect;
}
void NativeThemeBase::PaintRadio(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const ButtonExtraParams& button,
ColorScheme color_scheme,
const std::optional<SkColor>& accent_color) const {
// Most of a radio button is the same as a checkbox, except the the rounded
// square is a circle (i.e. border radius >= 100%).
const float border_radius =
GetBorderRadiusForPart(kRadio, rect.width(), rect.height());
SkRect skrect = PaintCheckboxRadioCommon(canvas, color_provider, state, rect,
button, false, border_radius,
color_scheme, accent_color);
if (!skrect.isEmpty() && button.checked) {
// Draw the dot.
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kFill_Style);
if (accent_color && state != kDisabled) {
flags.setColor(
CustomAccentColorForState(*accent_color, state, color_scheme));
} else {
flags.setColor(
ControlsAccentColorForState(state, color_scheme, color_provider));
}
skrect.inset(skrect.width() * 0.2, skrect.height() * 0.2);
// Use drawRoundedRect instead of drawOval to be completely consistent
// with the border in PaintCheckboxRadioNewCommon.
canvas->drawRoundRect(skrect, border_radius, border_radius, flags);
}
}
void NativeThemeBase::PaintButton(cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const ButtonExtraParams& button,
ColorScheme color_scheme) const {
cc::PaintFlags flags;
SkRect skrect = gfx::RectToSkRect(rect);
float border_width = AdjustBorderWidthByZoom(kBorderWidth, button.zoom);
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kFill_Style);
// If the button is too small, fallback to drawing a single, solid color.
if (rect.width() < 5 || rect.height() < 5) {
flags.setColor(
ButtonFillColorForState(state, color_scheme, color_provider));
canvas->drawRect(skrect, flags);
return;
}
float border_radius =
GetBorderRadiusForPart(kPushButton, rect.width(), rect.height());
border_radius =
AdjustBorderRadiusByZoom(kPushButton, border_radius, button.zoom);
// Paint the background (is not visible behind the rounded corners).
skrect.inset(border_width / 2, border_width / 2);
PaintLightenLayer(canvas, color_provider, skrect, state, border_radius,
color_scheme);
flags.setColor(ButtonFillColorForState(state, color_scheme, color_provider));
canvas->drawRoundRect(skrect, border_radius, border_radius, flags);
// Paint the border: 1px solid.
if (button.has_border) {
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(border_width);
flags.setColor(
ButtonBorderColorForState(state, color_scheme, color_provider));
canvas->drawRoundRect(skrect, border_radius, border_radius, flags);
}
}
void NativeThemeBase::PaintTextField(cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const TextFieldExtraParams& text,
ColorScheme color_scheme) const {
SkRect bounds = gfx::RectToSkRect(rect);
float border_radius =
GetBorderRadiusForPart(kTextField, rect.width(), rect.height());
border_radius =
AdjustBorderRadiusByZoom(kTextField, border_radius, text.zoom);
float border_width = AdjustBorderWidthByZoom(kBorderWidth, text.zoom);
// Paint the background (is not visible behind the rounded corners).
bounds.inset(border_width / 2, border_width / 2);
cc::PaintFlags fill_flags;
fill_flags.setStyle(cc::PaintFlags::kFill_Style);
if (text.background_color != 0) {
PaintLightenLayer(canvas, color_provider, bounds, state, border_radius,
color_scheme);
SkColor text_field_background_color =
ControlsBackgroundColorForState(state, color_scheme, color_provider);
if (text.auto_complete_active && state != kDisabled) {
text_field_background_color = GetControlColor(
kAutoCompleteBackground, color_scheme, color_provider);
}
fill_flags.setColor(text_field_background_color);
canvas->drawRoundRect(bounds, border_radius, border_radius, fill_flags);
}
// Paint the border: 1px solid.
if (text.has_border) {
cc::PaintFlags stroke_flags;
stroke_flags.setColor(
ControlsBorderColorForState(state, color_scheme, color_provider));
stroke_flags.setStyle(cc::PaintFlags::kStroke_Style);
stroke_flags.setStrokeWidth(border_width);
canvas->drawRoundRect(bounds, border_radius, border_radius, stroke_flags);
}
}
void NativeThemeBase::PaintMenuList(cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const MenuListExtraParams& menu_list,
ColorScheme color_scheme) const {
// If a border radius is specified paint the background and the border of
// the menulist, otherwise let the non-theming code paint the background
// and the border of the control. The arrow (menulist button) is always
// painted by the theming code.
if (!menu_list.has_border_radius) {
TextFieldExtraParams text_field;
text_field.background_color = menu_list.background_color;
text_field.has_border = menu_list.has_border;
text_field.zoom = menu_list.zoom;
PaintTextField(canvas, color_provider, state, rect, text_field,
color_scheme);
}
// Paint the arrow.
cc::PaintFlags flags;
flags.setColor(menu_list.arrow_color);
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(kMenuListArrowStrokeWidth);
if (menu_list.arrow_direction == ui::NativeTheme::ArrowDirection::kDown) {
float arrow_width = menu_list.arrow_size;
int arrow_height = arrow_width * 0.5;
gfx::Rect arrow(menu_list.arrow_x, menu_list.arrow_y - (arrow_height / 2),
arrow_width, arrow_height);
arrow.Intersect(rect);
if (arrow_width != arrow.width() || arrow_height != arrow.height()) {
// The arrow is clipped after being constrained to the paint rect so we
// need to recalculate its size.
int height_clip = arrow_height - arrow.height();
int width_clip = arrow_width - arrow.width();
if (height_clip > width_clip) {
arrow.set_width(arrow.height() * 1.6);
} else {
arrow.set_height(arrow.width() * 0.6);
}
arrow.set_y(menu_list.arrow_y - (arrow.height() / 2));
}
SkPath path;
path.moveTo(arrow.x(), arrow.y());
path.lineTo(arrow.x() + arrow.width() / 2, arrow.y() + arrow.height());
path.lineTo(arrow.x() + arrow.width(), arrow.y());
canvas->drawPath(path, flags);
} else {
// Arrow direction is either left or right
float arrow_height = menu_list.arrow_size;
int arrow_width = arrow_height * 0.5;
gfx::Rect arrow(menu_list.arrow_x - (arrow_width / 2), menu_list.arrow_y,
arrow_width, arrow_height);
arrow.Intersect(rect);
if (arrow_width != arrow.width() || arrow_height != arrow.height()) {
// The arrow is clipped after being constrained to the paint rect so we
// need to recalculate its size.
int height_clip = arrow_height - arrow.height();
int width_clip = arrow_width - arrow.width();
if (height_clip > width_clip) {
arrow.set_width(arrow.height() * 0.6);
} else {
arrow.set_height(arrow.width() * 1.6);
}
arrow.set_x(menu_list.arrow_x - (arrow.width() / 2));
}
SkPath path;
if (menu_list.arrow_direction == ui::NativeTheme::ArrowDirection::kLeft) {
path.moveTo(arrow.x() + arrow.width(), arrow.y());
path.lineTo(arrow.x(), arrow.y() + arrow.height() / 2);
path.lineTo(arrow.x() + arrow.width(), arrow.y() + arrow.height());
} else {
// Arrow direction is right
path.moveTo(arrow.x(), arrow.y());
path.lineTo(arrow.x() + arrow.width(), arrow.y() + arrow.height() / 2);
path.lineTo(arrow.x(), arrow.y() + arrow.height());
}
canvas->drawPath(path, flags);
}
}
void NativeThemeBase::PaintMenuPopupBackground(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
const gfx::Size& size,
const MenuBackgroundExtraParams& menu_background,
ColorScheme color_scheme) const {
// We are currently only painting kMenuPopupBackground with the kDefault
// scheme. If that changes, we need to add an appropriate dark scheme color to
// kMenuPopupBackgroundColor.
DCHECK(color_scheme == ColorScheme::kDefault);
// TODO(crbug.com/40219248): Remove FromColor and make all SkColor4f.
canvas->drawColor(
SkColor4f::FromColor(GetColor(kMenuPopupBackgroundColor, color_scheme)),
SkBlendMode::kSrc);
}
void NativeThemeBase::PaintMenuItemBackground(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const MenuItemExtraParams& menu_item,
ColorScheme color_scheme) const {
// By default don't draw anything over the normal background.
}
void NativeThemeBase::PaintMenuSeparator(
cc::PaintCanvas* canvas,
const ui::ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const MenuSeparatorExtraParams& menu_separator) const {
DCHECK(color_provider);
cc::PaintFlags flags;
flags.setColor(color_provider->GetColor(kColorMenuSeparator));
canvas->drawRect(gfx::RectToSkRect(*menu_separator.paint_rect), flags);
}
void NativeThemeBase::PaintSliderTrack(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const SliderExtraParams& slider,
ColorScheme color_scheme,
const std::optional<SkColor>& accent_color) const {
// Paint the entire slider track.
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(
ControlsFillColorForState(state, color_scheme, color_provider));
const float track_height = kSliderTrackHeight * slider.zoom;
SkRect track_rect = AlignSliderTrack(rect, slider, false, track_height);
float border_width = AdjustBorderWidthByZoom(kBorderWidth, slider.zoom);
// Shrink the track by 1 pixel so the thumb can completely cover the track
// on both ends.
if (slider.vertical)
track_rect.inset(0, 1);
else
track_rect.inset(1, 0);
float border_radius =
GetBorderRadiusForPart(kSliderTrack, rect.width(), rect.height());
canvas->drawRoundRect(track_rect, border_radius, border_radius, flags);
// Set the clip to the extent of the value bar.
SkRect value_rect = AlignSliderTrack(rect, slider, true, track_height);
canvas->save();
canvas->clipRect(value_rect, SkClipOp::kIntersect, true);
// Draw the full value bar, clipped to its extent.
if (accent_color && state != kDisabled) {
flags.setColor(
CustomAccentColorForState(*accent_color, state, color_scheme));
} else {
flags.setColor(
ControlsSliderColorForState(state, color_scheme, color_provider));
}
SkRRect rounded_rect;
rounded_rect.setRectXY(track_rect, border_radius, border_radius);
canvas->drawRRect(rounded_rect, flags);
canvas->restore();
// Paint the border.
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(border_width);
SkColor border_color =
ControlsBorderColorForState(state, color_scheme, color_provider);
if (!UserHasContrastPreference() && state != kDisabled &&
color_scheme != ColorScheme::kDark)
border_color = SkColorSetA(border_color, 0x80);
flags.setColor(border_color);
track_rect.inset(border_width / 2, border_width / 2);
canvas->drawRoundRect(track_rect, border_radius, border_radius, flags);
}
void NativeThemeBase::PaintSliderThumb(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const SliderExtraParams& slider,
ColorScheme color_scheme,
const std::optional<SkColor>& accent_color) const {
const float radius =
GetBorderRadiusForPart(kSliderThumb, rect.width(), rect.height());
SkRect thumb_rect = gfx::RectToSkRect(rect);
cc::PaintFlags flags;
flags.setAntiAlias(true);
SkScalar border_width = kSliderThumbBorderWidth;
if (state == kHovered || state == kPressed) {
border_width = kSliderThumbBorderHoveredWidth;
}
// Paint the background (is not visible behind the rounded corners).
thumb_rect.inset(border_width / 2, border_width / 2);
if (accent_color && state != kDisabled) {
flags.setColor(
CustomAccentColorForState(*accent_color, state, color_scheme));
} else {
flags.setColor(
ControlsSliderColorForState(state, color_scheme, color_provider));
}
flags.setStyle(cc::PaintFlags::kFill_Style);
canvas->drawRoundRect(thumb_rect, radius, radius, flags);
}
void NativeThemeBase::PaintInnerSpinButton(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const InnerSpinButtonExtraParams& spin_button,
ColorScheme color_scheme,
bool in_forced_colors) const {
if (spin_button.read_only)
state = kDisabled;
State north_state = state;
State south_state = state;
if (spin_button.spin_up)
south_state = south_state != kDisabled ? kNormal : kDisabled;
else
north_state = north_state != kDisabled ? kNormal : kDisabled;
gfx::Rect half = rect;
ScrollbarArrowExtraParams arrow = ScrollbarArrowExtraParams();
arrow.zoom = 1.0;
if (spin_button.spin_arrows_direction ==
ui::NativeTheme::SpinArrowsDirection::kUpDown) {
half.set_height(rect.height() / 2);
PaintArrowButton(canvas, color_provider, half, kScrollbarUpArrow,
north_state, color_scheme, in_forced_colors, arrow);
half.set_y(rect.y() + rect.height() / 2);
PaintArrowButton(canvas, color_provider, half, kScrollbarDownArrow,
south_state, color_scheme, in_forced_colors, arrow);
} else {
half.set_width(rect.width() / 2);
PaintArrowButton(canvas, color_provider, half, kScrollbarLeftArrow,
south_state, color_scheme, in_forced_colors, arrow);
half.set_x(rect.x() + rect.width() / 2);
PaintArrowButton(canvas, color_provider, half, kScrollbarRightArrow,
north_state, color_scheme, in_forced_colors, arrow);
}
}
void NativeThemeBase::PaintProgressBar(
cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
State state,
const gfx::Rect& rect,
const ProgressBarExtraParams& progress_bar,
ColorScheme color_scheme,
const std::optional<SkColor>& accent_color) const {
DCHECK(!rect.IsEmpty());
// Paint the track.
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kFill_Style);
flags.setColor(GetControlColor(kFill, color_scheme, color_provider));
SliderExtraParams slider;
float track_block_thickness = rect.height();
if (progress_bar.is_horizontal) {
slider.vertical = false;
track_block_thickness = rect.height() * kTrackBlockRatio;
} else {
slider.vertical = true;
track_block_thickness = rect.width() * kTrackBlockRatio;
}
SkRect track_rect =
AlignSliderTrack(rect, slider, false, track_block_thickness);
float border_radius =
GetBorderRadiusForPart(kProgressBar, rect.width(), rect.height());
canvas->drawRoundRect(track_rect, border_radius, border_radius, flags);
// Clip the track to create rounded corners for the value bar.
SkRRect rounded_rect;
rounded_rect.setRectXY(track_rect, border_radius, border_radius);
canvas->clipRRect(rounded_rect, SkClipOp::kIntersect, true);
// Paint the progress value bar.
const SkScalar kMinimumProgressInlineValue = 2;
SkScalar adjusted_height = progress_bar.value_rect_height;
SkScalar adjusted_width = progress_bar.value_rect_width;
// If adjusted thickness is not zero, make sure it is equal or larger than
// kMinimumProgressInlineValue.
if (slider.vertical) {
if (adjusted_height > 0)
adjusted_height = std::max(kMinimumProgressInlineValue, adjusted_height);
} else {
if (adjusted_width > 0)
adjusted_width = std::max(kMinimumProgressInlineValue, adjusted_width);
}
gfx::Rect original_value_rect(progress_bar.value_rect_x,
progress_bar.value_rect_y, adjusted_width,
adjusted_height);
SkRect value_rect = AlignSliderTrack(original_value_rect, slider, false,
track_block_thickness);
if (accent_color) {
flags.setColor(*accent_color);
} else {
flags.setColor(GetControlColor(kAccent, color_scheme, color_provider));
}
if (progress_bar.determinate) {
canvas->drawRect(value_rect, flags);
} else {
canvas->drawRoundRect(value_rect, border_radius, border_radius, flags);
}
// Paint the border.
float border_width = AdjustBorderWidthByZoom(kBorderWidth, progress_bar.zoom);
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(border_width);
SkColor border_color = GetControlColor(kBorder, color_scheme, color_provider);
if (!UserHasContrastPreference() && color_scheme != ColorScheme::kDark)
border_color = SkColorSetA(border_color, 0x80);
flags.setColor(border_color);
track_rect.inset(border_width / 2, border_width / 2);
canvas->drawRoundRect(track_rect, border_radius, border_radius, flags);
}
void NativeThemeBase::PaintFrameTopArea(
cc::PaintCanvas* canvas,
State state,
const gfx::Rect& rect,
const FrameTopAreaExtraParams& frame_top_area,
ColorScheme color_scheme) const {
cc::PaintFlags flags;
flags.setColor(frame_top_area.default_background_color);
canvas->drawRect(gfx::RectToSkRect(rect), flags);
}
void NativeThemeBase::AdjustCheckboxRadioRectForPadding(SkRect* rect) const {
// By default we only take 1px from right and bottom for the drop shadow.
rect->setLTRB(static_cast<int>(rect->x()), static_cast<int>(rect->y()),
static_cast<int>(rect->right()) - 1,
static_cast<int>(rect->bottom()) - 1);
}
SkColor NativeThemeBase::SaturateAndBrighten(SkScalar* hsv,
SkScalar saturate_amount,
SkScalar brighten_amount) const {
SkScalar color[3];
color[0] = hsv[0];
color[1] = std::clamp(hsv[1] + saturate_amount, SkScalar{0}, SK_Scalar1);
color[2] = std::clamp(hsv[2] + brighten_amount, SkScalar{0}, SK_Scalar1);
return SkHSVToColor(color);
}
SkColor NativeThemeBase::GetArrowColor(
State state,
ColorScheme color_scheme,
const ColorProvider* color_provider) const {
if (state != kDisabled)
return GetColor(kArrowDisabledColor, color_scheme);
SkScalar track_hsv[3];
SkColorToHSV(GetColor(kTrackColor, color_scheme), track_hsv);
SkScalar thumb_hsv[3];
SkColorToHSV(
GetControlColor(kScrollbarThumbInactive, color_scheme, color_provider),
thumb_hsv);
return OutlineColor(track_hsv, thumb_hsv);
}
void NativeThemeBase::DrawVertLine(cc::PaintCanvas* canvas,
int x,
int y1,
int y2,
const cc::PaintFlags& flags) const {
SkIRect skrect;
skrect.setLTRB(x, y1, x + 1, y2 + 1);
canvas->drawIRect(skrect, flags);
}
void NativeThemeBase::DrawHorizLine(cc::PaintCanvas* canvas,
int x1,
int x2,
int y,
const cc::PaintFlags& flags) const {
SkIRect skrect;
skrect.setLTRB(x1, y, x2 + 1, y + 1);
canvas->drawIRect(skrect, flags);
}
void NativeThemeBase::DrawBox(cc::PaintCanvas* canvas,
const gfx::Rect& rect,
const cc::PaintFlags& flags) const {
const int right = rect.x() + rect.width() - 1;
const int bottom = rect.y() + rect.height() - 1;
DrawHorizLine(canvas, rect.x(), right, rect.y(), flags);
DrawVertLine(canvas, right, rect.y(), bottom, flags);
DrawHorizLine(canvas, rect.x(), right, bottom, flags);
DrawVertLine(canvas, rect.x(), rect.y(), bottom, flags);
}
SkColor NativeThemeBase::OutlineColor(SkScalar* hsv1, SkScalar* hsv2) const {
// GTK Theme engines have way too much control over the layout of
// the scrollbar. We might be able to more closely approximate its
// look-and-feel, if we sent whole images instead of just colors
// from the browser to the renderer. But even then, some themes
// would just break.
//
// So, instead, we don't even try to 100% replicate the look of
// the native scrollbar. We render our own version, but we make
// sure to pick colors that blend in nicely with the system GTK
// theme. In most cases, we can just sample a couple of pixels
// from the system scrollbar and use those colors to draw our
// scrollbar.
//
// This works fine for the track color and the overall thumb
// color. But it fails spectacularly for the outline color used
// around the thumb piece. Not all themes have a clearly defined
// outline. For some of them it is partially transparent, and for
// others the thickness is very unpredictable.
//
// So, instead of trying to approximate the system theme, we
// instead try to compute a reasonable looking choice based on the
// known color of the track and the thumb piece. This is difficult
// when trying to deal both with high- and low-contrast themes,
// and both with positive and inverted themes.
//
// The following code has been tested to look OK with all of the
// default GTK themes.
SkScalar min_diff = std::clamp((hsv1[1] + hsv2[1]) * 1.2f, 0.28f, 0.5f);
SkScalar diff = std::clamp(fabsf(hsv1[2] - hsv2[2]) / 2, min_diff, 0.5f);
if (hsv1[2] + hsv2[2] > 1.0)
diff = -diff;
return SaturateAndBrighten(hsv2, -0.2f, diff);
}
SkColor NativeThemeBase::ControlsAccentColorForState(
State state,
ColorScheme color_scheme,
const ColorProvider* color_provider) const {
ControlColorId color_id;
if (state == kHovered) {
color_id = kHoveredAccent;
} else if (state == kPressed) {
color_id = kPressedAccent;
} else if (state == kDisabled) {
color_id = kDisabledAccent;
} else {
color_id = kAccent;
}
return GetControlColor(color_id, color_scheme, color_provider);
}
SkColor NativeThemeBase::ControlsSliderColorForState(
State state,
ColorScheme color_scheme,
const ColorProvider* color_provider) const {
ControlColorId color_id;
if (state == kHovered) {
color_id = kHoveredSlider;
} else if (state == kPressed) {
color_id = kPressedSlider;
} else if (state == kDisabled) {
color_id = kDisabledSlider;
} else {
color_id = kSlider;
}
return GetControlColor(color_id, color_scheme, color_provider);
}
SkColor NativeThemeBase::ControlsBorderColorForState(
State state,
ColorScheme color_scheme,
const ColorProvider* color_provider) const {
ControlColorId color_id;
if (state == kHovered) {
color_id = kHoveredBorder;
} else if (state == kPressed) {
color_id = kPressedBorder;
} else if (state == kDisabled) {
color_id = kDisabledBorder;
} else {
color_id = kBorder;
}
return GetControlColor(color_id, color_scheme, color_provider);
}
SkColor NativeThemeBase::ButtonBorderColorForState(
State state,
ColorScheme color_scheme,
const ColorProvider* color_provider) const {
ControlColorId color_id;
if (state == kHovered) {
color_id = kButtonHoveredBorder;
} else if (state == kPressed) {
color_id = kButtonPressedBorder;
} else if (state == kDisabled) {
color_id = kButtonDisabledBorder;
} else {
color_id = kButtonBorder;
}
return GetControlColor(color_id, color_scheme, color_provider);
}
SkColor NativeThemeBase::ControlsFillColorForState(
State state,
ColorScheme color_scheme,
const ColorProvider* color_provider) const {
ControlColorId color_id;
if (state == kHovered) {
color_id = kHoveredFill;
} else if (state == kPressed) {
color_id = kPressedFill;
} else if (state == kDisabled) {
color_id = kDisabledFill;
} else {
color_id = kFill;
}
return GetControlColor(color_id, color_scheme, color_provider);
}
SkColor NativeThemeBase::ButtonFillColorForState(
State state,
ColorScheme color_scheme,
const ColorProvider* color_provider) const {
ControlColorId color_id;
if (state == kHovered) {
color_id = kButtonHoveredFill;
} else if (state == kPressed) {
color_id = kButtonPressedFill;
} else if (state == kDisabled) {
color_id = kButtonDisabledFill;
} else {
color_id = kButtonFill;
}
return GetControlColor(color_id, color_scheme, color_provider);
}
SkColor NativeThemeBase::ControlsBackgroundColorForState(
State state,
ColorScheme color_scheme,
const ColorProvider* color_provider) const {
ControlColorId color_id;
if (state == kDisabled) {
color_id = kDisabledBackground;
} else {
color_id = kBackground;
}
return GetControlColor(color_id, color_scheme, color_provider);
}
SkColor NativeThemeBase::GetControlColor(
ControlColorId color_id,
ColorScheme color_scheme,
const ColorProvider* color_provider) const {
if (IsColorPipelineSupportedForControlColorId(color_provider, color_id))
return GetControlColorFromColorProvider(color_id, color_provider);
if (color_scheme == ColorScheme::kDark)
return GetDarkModeControlColor(color_id);
switch (color_id) {
case kBorder:
case kButtonBorder:
return SkColorSetRGB(0x76, 0x76, 0x76);
case kHoveredBorder:
case kButtonHoveredBorder:
return SkColorSetRGB(0x4F, 0x4F, 0x4F);
case kPressedBorder:
case kButtonPressedBorder:
return SkColorSetRGB(0x8D, 0x8D, 0x8D);
case kDisabledBorder:
case kButtonDisabledBorder:
return SkColorSetARGB(0x4D, 0x76, 0x76, 0x76);
case kAccent:
return SkColorSetRGB(0x00, 0x75, 0xFF);
case kHoveredAccent:
return SkColorSetRGB(0x00, 0x5C, 0xC8);
case kPressedAccent:
return SkColorSetRGB(0x37, 0x93, 0xFF);
case kDisabledAccent:
return SkColorSetARGB(0x4D, 0x76, 0x76, 0x76);
case kBackground:
return SK_ColorWHITE;
case kDisabledBackground:
return SkColorSetA(SK_ColorWHITE, 0x99);
case kFill:
case kButtonFill:
return SkColorSetRGB(0xEF, 0xEF, 0xEF);
case kHoveredFill:
case kButtonHoveredFill:
return SkColorSetRGB(0xE5, 0xE5, 0xE5);
case kPressedFill:
case kButtonPressedFill:
return SkColorSetRGB(0xF5, 0xF5, 0xF5);
case kDisabledFill:
case kButtonDisabledFill:
return SkColorSetARGB(0x4D, 0xEF, 0xEF, 0xEF);
case kLightenLayer:
return SkColorSetARGB(0x33, 0xA9, 0xA9, 0xA9);
case kProgressValue:
return SkColorSetRGB(0x00, 0x75, 0xFF);
case kSlider:
return SkColorSetRGB(0x00, 0x75, 0xFF);
case kHoveredSlider:
return SkColorSetRGB(0x00, 0x5C, 0xC8);
case kPressedSlider:
return SkColorSetRGB(0x37, 0x93, 0xFF);
case kDisabledSlider:
return SkColorSetRGB(0xCB, 0xCB, 0xCB);
case kAutoCompleteBackground:
return SkColorSetRGB(0xE8, 0xF0, 0xFE);
case kScrollbarArrowBackground:
case kScrollbarTrack:
return SkColorSetRGB(0xF1, 0xF1, 0xF1);
case kScrollbarArrowBackgroundHovered:
return SkColorSetRGB(0xD2, 0xD2, 0xD2);
case kScrollbarArrowBackgroundPressed:
return SkColorSetRGB(0x78, 0x78, 0x78);
case kScrollbarArrowHovered:
case kScrollbarArrow:
return SkColorSetRGB(0x50, 0x50, 0x50);
case kScrollbarArrowPressed:
return SK_ColorWHITE;
case kScrollbarCornerControlColorId:
return SkColorSetRGB(0xDC, 0xDC, 0xDC);
case kScrollbarThumbInactive:
return SkColorSetRGB(0xEA, 0xEA, 0xEA);
case kScrollbarThumbHovered:
return SkColorSetA(SK_ColorBLACK, 0x4D);
case kScrollbarThumbPressed:
return SkColorSetA(SK_ColorBLACK, 0x80);
case kScrollbarThumb:
return SkColorSetA(SK_ColorBLACK, 0x33);
}
NOTREACHED();
return gfx::kPlaceholderColor;
}
SkColor NativeThemeBase::GetDarkModeControlColor(
ControlColorId color_id) const {
switch (color_id) {
case kAccent:
return SkColorSetRGB(0x99, 0xC8, 0xFF);
case kHoveredAccent:
return SkColorSetRGB(0xD1, 0xE6, 0xFF);
case kPressedAccent:
return SkColorSetRGB(0x61, 0xA9, 0xFF);
case kDisabledAccent:
return SkColorSetRGB(0x75, 0x75, 0x75);
case kProgressValue:
return SkColorSetRGB(0x63, 0xAD, 0xE5);
case kFill:
return SkColorSetRGB(0x3B, 0x3B, 0x3B);
case kButtonBorder:
case kButtonFill:
return SkColorSetRGB(0x6B, 0x6B, 0x6B);
case kAutoCompleteBackground:
return SkColorSetARGB(0x66, 0x46, 0x5a, 0x7e);
case kLightenLayer:
case kBackground:
return SkColorSetRGB(0x3B, 0x3B, 0x3B);
case kBorder:
return SkColorSetRGB(0x85, 0x85, 0x85);
case kSlider:
return SkColorSetRGB(0x99, 0xC8, 0xFF);
case kHoveredSlider:
return SkColorSetRGB(0xD1, 0xE6, 0xFF);
case kPressedSlider:
return SkColorSetRGB(0x61, 0xA9, 0xFF);
case kDisabledSlider:
return SkColorSetRGB(0x75, 0x75, 0x75);
case kDisabledBackground:
return SkColorSetRGB(0x3B, 0x3B, 0x3B);
case kHoveredBorder:
return SkColorSetRGB(0xAC, 0xAC, 0xAC);
case kPressedBorder:
return SkColorSetRGB(0x6E, 0x6E, 0x6E);
case kDisabledBorder:
return SkColorSetRGB(0x62, 0x62, 0x62);
case kHoveredFill:
return SkColorSetRGB(0x3B, 0x3B, 0x3B);
case kButtonHoveredBorder:
case kButtonHoveredFill:
return SkColorSetRGB(0x7B, 0x7B, 0x7B);
case kPressedFill:
return SkColorSetRGB(0x3B, 0x3B, 0x3B);
case kButtonPressedBorder:
case kButtonPressedFill:
return SkColorSetRGB(0x61, 0x61, 0x61);
case kDisabledFill:
case kButtonDisabledBorder:
case kButtonDisabledFill:
return SkColorSetRGB(0x36, 0x36, 0x36);
case kScrollbarArrowBackground:
return SkColorSetRGB(0x42, 0x42, 0x42);
case kScrollbarArrowBackgroundHovered:
return SkColorSetRGB(0x4F, 0x4F, 0x4F);
case kScrollbarArrowBackgroundPressed:
return SkColorSetRGB(0xB1, 0xB1, 0xB1);
case kScrollbarArrowHovered:
case kScrollbarArrow:
return SK_ColorWHITE;
case kScrollbarArrowPressed:
return SK_ColorBLACK;
case kScrollbarCornerControlColorId:
return SkColorSetRGB(0x12, 0x12, 0x12);
case kScrollbarTrack:
return SkColorSetRGB(0x42, 0x42, 0x42);
case kScrollbarThumbInactive:
return SK_ColorWHITE;
case kScrollbarThumbHovered:
return SkColorSetA(SK_ColorWHITE, 0x4D);
case kScrollbarThumbPressed:
return SkColorSetA(SK_ColorWHITE, 0x80);
case kScrollbarThumb:
return SkColorSetA(SK_ColorWHITE, 0x33);
}
NOTREACHED();
return gfx::kPlaceholderColor;
}
SkColor NativeThemeBase::GetControlColorFromColorProvider(
ControlColorId color_id,
const ColorProvider* color_provider) const {
DCHECK(IsColorPipelineSupportedForControlColorId(color_provider, color_id));
switch (color_id) {
case kBorder:
return color_provider->GetColor(kColorWebNativeControlBorder);
case kDisabledBorder:
return color_provider->GetColor(kColorWebNativeControlBorderDisabled);
case kHoveredBorder:
return color_provider->GetColor(kColorWebNativeControlBorderHovered);
case kPressedBorder:
return color_provider->GetColor(kColorWebNativeControlBorderPressed);
case kAccent:
return color_provider->GetColor(kColorWebNativeControlAccent);
case kDisabledAccent:
return color_provider->GetColor(kColorWebNativeControlAccentDisabled);
case kHoveredAccent:
return color_provider->GetColor(kColorWebNativeControlAccentHovered);
case kPressedAccent:
return color_provider->GetColor(kColorWebNativeControlAccentPressed);
case kBackground:
return color_provider->GetColor(kColorWebNativeControlBackground);
case kDisabledBackground:
return color_provider->GetColor(kColorWebNativeControlBackgroundDisabled);
case kFill:
return color_provider->GetColor(kColorWebNativeControlFill);
case kDisabledFill:
return color_provider->GetColor(kColorWebNativeControlFillDisabled);
case kHoveredFill:
return color_provider->GetColor(kColorWebNativeControlFillHovered);
case kPressedFill:
return color_provider->GetColor(kColorWebNativeControlFillPressed);
case kLightenLayer:
return color_provider->GetColor(kColorWebNativeControlLightenLayer);
case kProgressValue:
return color_provider->GetColor(kColorWebNativeControlProgressValue);
case kSlider:
return color_provider->GetColor(kColorWebNativeControlSlider);
case kDisabledSlider:
return color_provider->GetColor(kColorWebNativeControlSliderDisabled);
case kHoveredSlider:
return color_provider->GetColor(kColorWebNativeControlSliderHovered);
case kPressedSlider:
return color_provider->GetColor(kColorWebNativeControlSliderPressed);
case kAutoCompleteBackground:
return color_provider->GetColor(
kColorWebNativeControlAutoCompleteBackground);
case kScrollbarArrowBackground:
case kScrollbarTrack:
return color_provider->GetColor(kColorWebNativeControlScrollbarTrack);
case kScrollbarArrowBackgroundHovered:
return color_provider->GetColor(
kColorWebNativeControlScrollbarArrowBackgroundHovered);
case kScrollbarArrowBackgroundPressed:
return color_provider->GetColor(
kColorWebNativeControlScrollbarArrowBackgroundPressed);
case kScrollbarArrow:
case kScrollbarArrowHovered:
return color_provider->GetColor(
kColorWebNativeControlScrollbarArrowForeground);
case kScrollbarArrowPressed:
return color_provider->GetColor(
kColorWebNativeControlScrollbarArrowForegroundPressed);
case kScrollbarCornerControlColorId:
return color_provider->GetColor(kColorWebNativeControlScrollbarCorner);
case kScrollbarThumb:
return color_provider->GetColor(kColorWebNativeControlScrollbarThumb);
case kScrollbarThumbHovered:
return color_provider->GetColor(
kColorWebNativeControlScrollbarThumbHovered);
case kScrollbarThumbInactive:
return color_provider->GetColor(
kColorWebNativeControlScrollbarThumbInactive);
case kScrollbarThumbPressed:
return color_provider->GetColor(
kColorWebNativeControlScrollbarThumbPressed);
case kButtonBorder:
return color_provider->GetColor(kColorWebNativeControlButtonBorder);
case kButtonDisabledBorder:
return color_provider->GetColor(
kColorWebNativeControlButtonBorderDisabled);
case kButtonHoveredBorder:
return color_provider->GetColor(
kColorWebNativeControlButtonBorderHovered);
case kButtonPressedBorder:
return color_provider->GetColor(
kColorWebNativeControlButtonBorderPressed);
case kButtonFill:
return color_provider->GetColor(kColorWebNativeControlButtonFill);
case kButtonDisabledFill:
return color_provider->GetColor(kColorWebNativeControlButtonFillDisabled);
case kButtonHoveredFill:
return color_provider->GetColor(kColorWebNativeControlButtonFillHovered);
case kButtonPressedFill:
return color_provider->GetColor(kColorWebNativeControlButtonFillPressed);
default:
break;
}
NOTREACHED_NORETURN();
}
void NativeThemeBase::PaintLightenLayer(cc::PaintCanvas* canvas,
const ColorProvider* color_provider,
SkRect skrect,
State state,
SkScalar border_radius,
ColorScheme color_scheme) const {
if (state == kDisabled) {
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kFill_Style);
// Draw the lighten layer to lighten the background so the translucent
// disabled color works regardless of what it's over.
flags.setColor(
GetControlColor(kLightenLayer, color_scheme, color_provider));
canvas->drawRoundRect(skrect, border_radius, border_radius, flags);
}
}
SkRect NativeThemeBase::AlignSliderTrack(
const gfx::Rect& slider_rect,
const NativeTheme::SliderExtraParams& slider,
bool is_value,
float track_block_thickness) const {
const float kAlignment = track_block_thickness / 2;
const float mid_x = slider_rect.x() + slider_rect.width() / 2.0f;
const float mid_y = slider_rect.y() + slider_rect.height() / 2.0f;
SkRect aligned_rect;
if (slider.vertical) {
const float top = is_value && slider.right_to_left
? slider_rect.y() + slider.thumb_y + kAlignment
: slider_rect.y();
const float bottom = is_value && !slider.right_to_left
? slider_rect.y() + slider.thumb_y + kAlignment
: slider_rect.bottom();
aligned_rect.setLTRB(
std::max(float(slider_rect.x()), mid_x - kAlignment), top,
std::min(float(slider_rect.right()), mid_x + kAlignment), bottom);
} else {
const float right = is_value && !slider.right_to_left
? slider_rect.x() + slider.thumb_x + kAlignment
: slider_rect.right();
const float left = is_value && slider.right_to_left
? slider_rect.x() + slider.thumb_x + kAlignment
: slider_rect.x();
aligned_rect.setLTRB(
left, std::max(float(slider_rect.y()), mid_y - kAlignment), right,
std::min(float(slider_rect.bottom()), mid_y + kAlignment));
}
return aligned_rect;
}
bool NativeThemeBase::IsColorPipelineSupportedForControlColorId(
const ColorProvider* color_provider,
ControlColorId color_id) const {
// Color providers are not yet supported on Android so we need to check that
// the color_provider is not null here.
if (!color_provider || !color_provider->HasMixers()) {
return false;
}
static constexpr auto kControlColorIdsSet =
base::MakeFixedFlatSet<ControlColorId>({kBorder,
kDisabledBorder,
kHoveredBorder,
kPressedBorder,
kAccent,
kDisabledAccent,
kHoveredAccent,
kPressedAccent,
kBackground,
kDisabledBackground,
kFill,
kDisabledFill,
kHoveredFill,
kPressedFill,
kLightenLayer,
kProgressValue,
kSlider,
kDisabledSlider,
kHoveredSlider,
kPressedSlider,
kAutoCompleteBackground,
kScrollbarArrowBackground,
kScrollbarArrowBackgroundHovered,
kScrollbarArrowBackgroundPressed,
kScrollbarArrow,
kScrollbarArrowHovered,
kScrollbarArrowPressed,
kScrollbarCornerControlColorId,
kScrollbarTrack,
kScrollbarThumb,
kScrollbarThumbHovered,
kScrollbarThumbPressed,
kScrollbarThumbInactive,
kButtonBorder,
kButtonDisabledBorder,
kButtonHoveredBorder,
kButtonPressedBorder,
kButtonFill,
kButtonDisabledFill,
kButtonHoveredFill,
kButtonPressedFill});
return kControlColorIdsSet.contains(color_id);
}
} // namespace ui