// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/style/pill_button.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/style/color_provider.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/blurred_background_shield.h"
#include "ash/style/color_util.h"
#include "ash/style/style_util.h"
#include "ash/style/typography.h"
#include "chromeos/constants/chromeos_features.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/background.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
namespace ash {
namespace {
// The height of default size button, mainly used for button types other than
// kIconLarge.
constexpr int kPillButtonHeight = 32;
// The height of large size button, used for button type kIconLarge.
constexpr int kPillButtonLargeHeight = 36;
constexpr int kPillButtonMinimumWidth = 56;
constexpr int kIconSize = 20;
constexpr int kIconPillButtonImageLabelSpacingDp = 8;
// Including the thickness and inset of the focus ring in order to keep 2px
// padding between the focus ring and content of the button.
constexpr int kFocusRingPadding = 2 + views::FocusRing::kDefaultHaloThickness +
views::FocusRing::kDefaultHaloInset;
// The type mask of button color variant.
// TODO(crbug.com/1355517): Remove `kAccent` from color variant when CrosNext is
// fully launched.
constexpr PillButton::TypeFlag kButtonColorVariant =
PillButton::kDefault | PillButton::kDefaultElevated | PillButton::kPrimary |
PillButton::kSecondary | PillButton::kFloating | PillButton::kAlert |
PillButton::kAccent;
// Returns true it is a floating type of PillButton, which is a type of
// PillButton without a background.
bool IsFloatingPillButton(PillButton::Type type) {
return type & PillButton::kFloating;
}
// Returns true if the button has an icon.
bool IsIconPillButton(PillButton::Type type) {
return type & (PillButton::kIconLeading | PillButton::kIconFollowing);
}
// Returns the button height according to the given type.
int GetButtonHeight(PillButton::Type type) {
return (type & PillButton::kLarge) ? kPillButtonLargeHeight
: kPillButtonHeight;
}
// Checks if the color variant is assigned a color/color ID.
bool IsAssignedColorVariant(PillButton::ColorVariant color_variant) {
// The color variant is assigned as long as it is not equal to
// `gfx::kPlaceholderColor`.
return !(absl::holds_alternative<SkColor>(color_variant) &&
absl::get<SkColor>(color_variant) == gfx::kPlaceholderColor);
}
// Updates the target color variant with given color variant if they are not
// equal.
bool MaybeUpdateColorVariant(PillButton::ColorVariant& target_color_variant,
PillButton::ColorVariant color_variant) {
if (target_color_variant == color_variant) {
return false;
}
target_color_variant = color_variant;
return true;
}
std::optional<ui::ColorId> GetDefaultBackgroundColorId(PillButton::Type type) {
std::optional<ui::ColorId> color_id;
const bool is_jellyroll_enabled = chromeos::features::IsJellyrollEnabled();
switch (type & kButtonColorVariant) {
case PillButton::kDefault:
color_id = is_jellyroll_enabled
? cros_tokens::kCrosSysSystemOnBase
: static_cast<ui::ColorId>(
kColorAshControlBackgroundColorInactive);
break;
case PillButton::kDefaultElevated:
color_id = cros_tokens::kCrosSysSystemBaseElevated;
break;
case PillButton::kPrimary:
color_id =
is_jellyroll_enabled
? cros_tokens::kCrosSysPrimary
: static_cast<ui::ColorId>(kColorAshControlBackgroundColorActive);
break;
case PillButton::kSecondary:
color_id = kColorAshSecondaryButtonBackgroundColor;
break;
case PillButton::kAlert:
color_id =
is_jellyroll_enabled
? cros_tokens::kCrosSysError
: static_cast<ui::ColorId>(kColorAshControlBackgroundColorAlert);
break;
case PillButton::kAccent:
color_id = kColorAshControlBackgroundColorInactive;
break;
default:
NOTREACHED() << "Invalid and floating pill button type: " << type;
}
return color_id;
}
std::optional<ui::ColorId> GetDefaultButtonTextIconColorId(
PillButton::Type type) {
std::optional<ui::ColorId> color_id;
const bool is_jellyroll_enabled = chromeos::features::IsJellyrollEnabled();
switch (type & kButtonColorVariant) {
case PillButton::kDefault:
color_id = is_jellyroll_enabled
? cros_tokens::kCrosSysOnSurface
: static_cast<ui::ColorId>(kColorAshButtonLabelColor);
break;
case PillButton::kDefaultElevated:
color_id = cros_tokens::kCrosSysOnSurface;
break;
case PillButton::kPrimary:
color_id =
is_jellyroll_enabled
? cros_tokens::kCrosSysOnPrimary
: static_cast<ui::ColorId>(kColorAshButtonLabelColorPrimary);
break;
case PillButton::kSecondary:
color_id = cros_tokens::kCrosSysOnSecondaryContainer;
break;
case PillButton::kFloating:
color_id = is_jellyroll_enabled
? cros_tokens::kCrosSysPrimary
: static_cast<ui::ColorId>(kColorAshButtonLabelColor);
break;
case PillButton::kAlert:
color_id =
is_jellyroll_enabled
? cros_tokens::kCrosSysOnError
: static_cast<ui::ColorId>(kColorAshButtonLabelColorPrimary);
break;
case PillButton::kAccent:
case PillButton::kAccent | PillButton::kFloating:
color_id = kColorAshButtonLabelColorBlue;
break;
default:
NOTREACHED() << "Invalid pill button type: " << type;
}
return color_id;
}
} // namespace
PillButton::PillButton(PressedCallback callback,
const std::u16string& text,
PillButton::Type type,
const gfx::VectorIcon* icon,
int horizontal_spacing,
int padding_reduction_for_icon)
: views::LabelButton(std::move(callback), text),
type_(type),
icon_(icon),
horizontal_spacing_(horizontal_spacing),
padding_reduction_for_icon_(padding_reduction_for_icon) {
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
label()->SetSubpixelRenderingEnabled(false);
TypographyProvider::Get()->StyleLabel(TypographyToken::kLegacyButton2,
*label());
StyleUtil::SetUpInkDropForButton(this, gfx::Insets(),
/*highlight_on_hover=*/false,
/*highlight_on_focus=*/false,
/*background_color=*/
gfx::kPlaceholderColor);
auto* focus_ring = views::FocusRing::Get(this);
focus_ring->SetOutsetFocusRingDisabled(true);
focus_ring->SetColorId(ui::kColorAshFocusRing);
// Initialize image and icon spacing.
SetImageLabelSpacing(kIconPillButtonImageLabelSpacingDp);
Init();
enabled_changed_subscription_ = AddEnabledChangedCallback(base::BindRepeating(
&PillButton::UpdateBackgroundColor, base::Unretained(this)));
}
PillButton::~PillButton() = default;
gfx::Size PillButton::CalculatePreferredSize(
const views::SizeBounds& available_size) const {
int button_width =
label()
->GetPreferredSize(views::SizeBounds(label()->width(), {}))
.width();
if (IsIconPillButton(type_)) {
// Add the padding on two sides.
button_width += horizontal_spacing_ + GetHorizontalSpacingWithIcon();
// Add the icon width and the spacing between the icon and the text.
button_width += kIconSize + GetImageLabelSpacing();
} else {
button_width += 2 * horizontal_spacing_;
}
const int height = GetButtonHeight(type_);
gfx::Size size(button_width, height);
size.SetToMax(gfx::Size(kPillButtonMinimumWidth, height));
return size;
}
gfx::Insets PillButton::GetInsets() const {
const int vertical_spacing = (GetButtonHeight(type_) - kIconSize) / 2;
const int icon_padding = IsIconPillButton(type_)
? GetHorizontalSpacingWithIcon()
: horizontal_spacing_;
if (type_ & kIconFollowing) {
return gfx::Insets::TLBR(vertical_spacing, horizontal_spacing_,
vertical_spacing, icon_padding);
}
return gfx::Insets::TLBR(vertical_spacing, icon_padding, vertical_spacing,
horizontal_spacing_);
}
void PillButton::UpdateBackgroundColor() {
if (IsFloatingPillButton(type_)) {
return;
}
// Resolve the expected background color.
ColorVariant background_color;
if (!GetEnabled()) {
background_color = cros_tokens::kCrosSysDisabledContainer;
} else if (IsAssignedColorVariant(background_color_)) {
background_color = background_color_;
} else {
auto default_color_id = GetDefaultBackgroundColorId(type_);
DCHECK(default_color_id);
background_color = default_color_id.value();
}
// Replace the background with blurred background shield if the background
// blur is enabled. Otherwise, remove the blurred background shield.
const float corner_radius = GetButtonHeight(type_) / 2.0f;
if (enable_background_blur_) {
if (background()) {
SetBackground(nullptr);
}
if (!blurred_background_) {
blurred_background_ = std::make_unique<BlurredBackgroundShield>(
this, background_color, ColorProvider::kBackgroundBlurSigma,
gfx::RoundedCornersF(corner_radius),
/*add_layer_to_region=*/false);
return;
}
} else if (blurred_background_) {
blurred_background_.reset();
}
// Create the background with expected color or update the colors of blurred
// background shield.
if (absl::holds_alternative<SkColor>(background_color)) {
SkColor color_value = absl::get<SkColor>(background_color);
if (enable_background_blur_) {
blurred_background_->SetColor(color_value);
} else {
SetBackground(
views::CreateRoundedRectBackground(color_value, corner_radius));
}
} else {
ui::ColorId color_id = absl::get<ui::ColorId>(background_color);
if (enable_background_blur_) {
blurred_background_->SetColorId(color_id);
} else {
SetBackground(
views::CreateThemedRoundedRectBackground(color_id, corner_radius));
}
}
}
views::PropertyEffects PillButton::UpdateStyleToIndicateDefaultStatus() {
// Override the method defined in LabelButton to avoid style changes when the
// `is_default_` flag is updated.
return views::kPropertyEffectsNone;
}
std::u16string PillButton::GetTooltipText(const gfx::Point& p) const {
const auto& tooltip = views::LabelButton::GetTooltipText(p);
if (use_label_as_default_tooltip_ && tooltip.empty()) {
return GetText();
}
return tooltip;
}
void PillButton::SetBackgroundColor(const SkColor background_color) {
if (MaybeUpdateColorVariant(background_color_, background_color)) {
UpdateBackgroundColor();
}
}
void PillButton::SetBackgroundColorId(ui::ColorId background_color_id) {
if (MaybeUpdateColorVariant(background_color_, background_color_id)) {
UpdateBackgroundColor();
}
}
void PillButton::SetButtonTextColor(const SkColor text_color) {
if (MaybeUpdateColorVariant(text_color_, text_color)) {
UpdateTextColor();
}
}
void PillButton::SetButtonTextColorId(ui::ColorId text_color_id) {
if (MaybeUpdateColorVariant(text_color_, text_color_id)) {
UpdateTextColor();
}
}
void PillButton::SetIconColor(const SkColor icon_color) {
if (MaybeUpdateColorVariant(icon_color_, icon_color)) {
UpdateIconColor();
}
}
void PillButton::SetIconColorId(ui::ColorId icon_color_id) {
if (MaybeUpdateColorVariant(icon_color_, icon_color_id)) {
UpdateIconColor();
}
}
void PillButton::SetPillButtonType(Type type) {
if (type_ == type)
return;
type_ = type;
Init();
}
void PillButton::SetUseDefaultLabelFont() {
label()->SetFontList(TypographyProvider::Get()->ResolveTypographyToken(
TypographyToken::kLegacyBody2));
}
void PillButton::SetEnableBackgroundBlur(bool enable) {
if (enable_background_blur_ == enable) {
return;
}
enable_background_blur_ = enable;
UpdateBackgroundColor();
}
void PillButton::SetTextWithStringId(int message_id) {
SetText(l10n_util::GetStringUTF16(message_id));
}
void PillButton::SetUseLabelAsDefaultTooltip(
bool use_label_as_default_tooltip) {
use_label_as_default_tooltip_ = use_label_as_default_tooltip;
}
void PillButton::Init() {
if (type_ & kIconFollowing) {
SetHorizontalAlignment(gfx::ALIGN_RIGHT);
} else {
SetHorizontalAlignment(gfx::ALIGN_CENTER);
}
const int height = GetButtonHeight(type_);
views::InstallRoundRectHighlightPathGenerator(this, gfx::Insets(),
height / 2.f);
if (chromeos::features::IsJellyrollEnabled() ||
(type_ & kButtonColorVariant) == kPrimary) {
// Add padding around focus highlight only.
views::FocusRing::Get(this)->SetPathGenerator(
std::make_unique<views::RoundRectHighlightPathGenerator>(
gfx::Insets(-kFocusRingPadding), height / 2.f + kFocusRingPadding));
}
// TODO(b/290639214): We no longer need this after deprecating
// SetPillButtonType since the whether using background should be settled on
// initialization. For now, we should remove the background if the client
// changes from non-floating type button to floating type button.
if (IsFloatingPillButton(type_)) {
SetBackground(nullptr);
}
UpdateBackgroundColor();
UpdateTextColor();
UpdateIconColor();
PreferredSizeChanged();
}
void PillButton::UpdateTextColor() {
SetTextColorId(views::Button::STATE_DISABLED, cros_tokens::kCrosSysDisabled);
// If custom text color is set, use it to set text color.
if (IsAssignedColorVariant(text_color_)) {
if (absl::holds_alternative<SkColor>(text_color_)) {
SetEnabledTextColors(absl::get<SkColor>(text_color_));
} else {
SetEnabledTextColorIds(absl::get<ui::ColorId>(text_color_));
}
} else {
// Otherwise, use default color ID to set text color.
auto default_color_id = GetDefaultButtonTextIconColorId(type_);
DCHECK(default_color_id);
SetEnabledTextColorIds(default_color_id.value());
}
}
void PillButton::UpdateIconColor() {
if (!IsIconPillButton(type_))
return;
if (!icon_) {
return;
}
SetImageModel(views::Button::STATE_DISABLED,
ui::ImageModel::FromVectorIcon(
*icon_, cros_tokens::kCrosSysDisabled, kIconSize));
// If custom icon color is set, use it to set icon color.
if (IsAssignedColorVariant(icon_color_)) {
if (absl::holds_alternative<SkColor>(icon_color_)) {
SetImageModel(views::Button::STATE_NORMAL,
ui::ImageModel::FromVectorIcon(
*icon_, absl::get<SkColor>(icon_color_), kIconSize));
} else {
SetImageModel(
views::Button::STATE_NORMAL,
ui::ImageModel::FromVectorIcon(
*icon_, absl::get<ui::ColorId>(icon_color_), kIconSize));
}
} else {
// Otherwise, use default color ID to set icon color.
auto default_color_id = GetDefaultButtonTextIconColorId(type_);
DCHECK(default_color_id);
SetImageModel(views::Button::STATE_NORMAL,
ui::ImageModel::FromVectorIcon(
*icon_, default_color_id.value(), kIconSize));
}
}
int PillButton::GetHorizontalSpacingWithIcon() const {
return std::max(horizontal_spacing_ - padding_reduction_for_icon_, 0);
}
BEGIN_METADATA(PillButton)
END_METADATA
} // namespace ash