blob: fa12af94869e63514b35370e62f353f28f75be1f [file] [log] [blame]
// 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/style/ash_color_id.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/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;
}
absl::optional<ui::ColorId> GetDefaultBackgroundColorId(PillButton::Type type) {
absl::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;
}
absl::optional<ui::ColorId> GetDefaultButtonTextIconColorId(
PillButton::Type type) {
absl::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);
views::FocusRing::Get(this)->SetColorId(ui::kColorAshFocusRing);
SetTooltipText(text);
// Initialize image and icon spacing.
SetImageLabelSpacing(kIconPillButtonImageLabelSpacingDp);
enabled_changed_subscription_ = AddEnabledChangedCallback(base::BindRepeating(
&PillButton::UpdateBackgroundColor, base::Unretained(this)));
}
PillButton::~PillButton() = default;
void PillButton::AddedToWidget() {
// Only initialize the button after the button is added to a widget.
Init();
}
gfx::Size PillButton::CalculatePreferredSize() const {
int button_width = label()->GetPreferredSize().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;
}
int PillButton::GetHeightForWidth(int width) const {
return GetButtonHeight(type_);
}
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;
const int height = GetButtonHeight(type_);
if (!GetEnabled()) {
SetBackground(views::CreateThemedRoundedRectBackground(
cros_tokens::kCrosSysDisabledContainer, height / 2.f));
return;
}
// If custom color is set, use it to create a solid background.
if (background_color_) {
SetBackground(views::CreateRoundedRectBackground(background_color_.value(),
height / 2.f));
return;
}
// Otherwise, use custom ID if set or default color ID to create a themed
// background.
auto default_color_id = GetDefaultBackgroundColorId(type_);
DCHECK(default_color_id);
SetBackground(views::CreateThemedRoundedRectBackground(
background_color_id_.value_or(default_color_id.value()), height / 2.f));
}
views::PropertyEffects PillButton::UpdateStyleToIndicateDefaultStatus() {
// Override the method defined in LabelButton to avoid style changes when the
// `is_default_` flag is updated.
return views::kPropertyEffectsNone;
}
void PillButton::SetBackgroundColor(const SkColor background_color) {
if (background_color_ && background_color_.value() == background_color) {
return;
}
background_color_ = background_color;
background_color_id_ = absl::nullopt;
UpdateBackgroundColor();
}
void PillButton::SetBackgroundColorId(ui::ColorId background_color_id) {
if (background_color_id_ &&
background_color_id_.value() == background_color_id) {
return;
}
background_color_id_ = background_color_id;
background_color_ = absl::nullopt;
UpdateBackgroundColor();
}
void PillButton::SetButtonTextColor(const SkColor text_color) {
if (text_color_ && text_color_.value() == text_color) {
return;
}
text_color_ = text_color;
text_color_id_ = absl::nullopt;
UpdateTextColor();
}
void PillButton::SetButtonTextColorId(ui::ColorId text_color_id) {
if (text_color_id_ && text_color_id_.value() == text_color_id) {
return;
}
text_color_id_ = text_color_id;
text_color_ = absl::nullopt;
UpdateTextColor();
}
void PillButton::SetIconColor(const SkColor icon_color) {
if (icon_color_ && icon_color_.value() == icon_color) {
return;
}
icon_color_ = icon_color;
icon_color_id_ = absl::nullopt;
UpdateIconColor();
}
void PillButton::SetIconColorId(ui::ColorId icon_color_id) {
if (icon_color_id_ && icon_color_id_.value() == icon_color_id) {
return;
}
icon_color_id_ = icon_color_id;
icon_color_ = absl::nullopt;
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::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));
}
UpdateBackgroundColor();
UpdateTextColor();
UpdateIconColor();
PreferredSizeChanged();
}
void PillButton::UpdateTextColor() {
// Only update text color when the button is added to a widget.
if (!GetWidget())
return;
SetTextColorId(views::Button::STATE_DISABLED, cros_tokens::kCrosSysDisabled);
// If custom text color is set, use it to set text color.
if (text_color_) {
SetEnabledTextColors(text_color_.value());
return;
}
// Otherwise, use custom color ID if set or default color ID to set text
// color.
auto default_color_id = GetDefaultButtonTextIconColorId(type_);
DCHECK(default_color_id);
SetEnabledTextColorIds(text_color_id_.value_or(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 (icon_color_) {
SetImage(views::Button::STATE_NORMAL,
gfx::CreateVectorIcon(*icon_, kIconSize, icon_color_.value()));
return;
}
// Otherwise, use custom color ID if set or 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_, icon_color_id_.value_or(default_color_id.value()),
kIconSize));
}
int PillButton::GetHorizontalSpacingWithIcon() const {
return std::max(horizontal_spacing_ - padding_reduction_for_icon_, 0);
}
BEGIN_METADATA(PillButton, views::LabelButton)
END_METADATA
} // namespace ash