blob: be63ca20b27b584fa7695ab3f0dbe99c9908fdf9 [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/icon_button.h"
#include "ash/public/cpp/style/color_provider.h"
#include "ash/style/blurred_background_shield.h"
#include "ash/style/style_util.h"
#include "base/notreached.h"
#include "chromeos/utils/haptics_util.h"
#include "ui/accessibility/ax_node_data.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/color/color_id.h"
#include "ui/events/devices/haptic_touchpad_effects.h"
#include "ui/events/event.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/highlight_path_generator.h"
namespace ash {
namespace {
constexpr int kXSmallButtonSize = 20;
constexpr int kSmallButtonSize = 24;
constexpr int kMediumButtonSize = 32;
constexpr int kLargeButtonSize = 36;
// Icon size of the small, medium and large size buttons.
constexpr int kIconSize = 20;
// Icon size of the extra small size button.
constexpr int kXSmallIconSize = 16;
// The gap between the focus ring and the button's content.
constexpr int kFocusRingPadding = 2;
// The default toggled background and icon color IDs.
constexpr ui::ColorId kDefaultToggledBackgroundColorId =
cros_tokens::kCrosSysSystemPrimaryContainer;
constexpr ui::ColorId kDefaultToggledIconColorId =
cros_tokens::kCrosSysSystemOnPrimaryContainer;
int GetButtonSizeOnType(IconButton::Type type) {
switch (type) {
case IconButton::Type::kXSmall:
case IconButton::Type::kXSmallProminent:
case IconButton::Type::kXSmallFloating:
case IconButton::Type::kXSmallProminentFloating:
return kXSmallButtonSize;
case IconButton::Type::kSmall:
case IconButton::Type::kSmallProminent:
case IconButton::Type::kSmallFloating:
case IconButton::Type::kSmallProminentFloating:
return kSmallButtonSize;
case IconButton::Type::kMedium:
case IconButton::Type::kMediumProminent:
case IconButton::Type::kMediumFloating:
case IconButton::Type::kMediumProminentFloating:
return kMediumButtonSize;
case IconButton::Type::kLarge:
case IconButton::Type::kLargeProminent:
case IconButton::Type::kLargeFloating:
case IconButton::Type::kLargeProminentFloating:
return kLargeButtonSize;
}
}
std::optional<ui::ColorId> GetDefaultBackgroundColorId(IconButton::Type type) {
switch (type) {
case IconButton::Type::kXSmall:
case IconButton::Type::kSmall:
case IconButton::Type::kMedium:
case IconButton::Type::kLarge:
return cros_tokens::kCrosSysSystemOnBase;
case IconButton::Type::kXSmallProminent:
case IconButton::Type::kSmallProminent:
case IconButton::Type::kMediumProminent:
case IconButton::Type::kLargeProminent:
return cros_tokens::kCrosSysSystemPrimaryContainer;
default:
NOTREACHED() << "Floating type button does not have a background";
return std::nullopt;
}
}
ui::ColorId GetDefaultIconColorId(IconButton::Type type, bool focused) {
switch (type) {
case IconButton::Type::kXSmall:
case IconButton::Type::kXSmallFloating:
case IconButton::Type::kSmall:
case IconButton::Type::kSmallFloating:
case IconButton::Type::kMedium:
case IconButton::Type::kMediumFloating:
case IconButton::Type::kLarge:
case IconButton::Type::kLargeFloating:
return cros_tokens::kCrosSysOnSurface;
case IconButton::Type::kXSmallProminent:
case IconButton::Type::kSmallProminent:
case IconButton::Type::kMediumProminent:
case IconButton::Type::kLargeProminent:
return cros_tokens::kCrosSysSystemOnPrimaryContainer;
case IconButton::Type::kXSmallProminentFloating:
case IconButton::Type::kSmallProminentFloating:
case IconButton::Type::kMediumProminentFloating:
case IconButton::Type::kLargeProminentFloating:
return focused ? cros_tokens::kCrosSysPrimary
: cros_tokens::kCrosSysSecondary;
}
}
int GetIconSizeOnType(IconButton::Type type) {
if (type == IconButton::Type::kXSmall ||
type == IconButton::Type::kXSmallFloating ||
type == IconButton::Type::kXSmallProminent ||
type == IconButton::Type::kXSmallProminentFloating) {
return kXSmallIconSize;
}
return kIconSize;
}
bool IsFloatingIconButton(IconButton::Type type) {
switch (type) {
case IconButton::Type::kXSmallFloating:
case IconButton::Type::kXSmallProminentFloating:
case IconButton::Type::kSmallFloating:
case IconButton::Type::kSmallProminentFloating:
case IconButton::Type::kMediumFloating:
case IconButton::Type::kMediumProminentFloating:
case IconButton::Type::kLargeFloating:
case IconButton::Type::kLargeProminentFloating:
return true;
default:
break;
}
return false;
}
bool IsProminentFloatingType(IconButton::Type type) {
switch (type) {
case IconButton::Type::kXSmallProminentFloating:
case IconButton::Type::kSmallProminentFloating:
case IconButton::Type::kMediumProminentFloating:
case IconButton::Type::kLargeProminentFloating:
return true;
default:
break;
}
return false;
}
// Create a themed fully rounded rect background for icon button.
std::unique_ptr<views::Background> CreateThemedBackground(
ui::ColorId color_id,
IconButton::Type type) {
return views::CreateThemedRoundedRectBackground(
color_id, GetButtonSizeOnType(type) / 2);
}
// Create a solid color fully rounded rect background for icon button.
std::unique_ptr<views::Background> CreateSolidBackground(
SkColor color,
IconButton::Type type) {
return views::CreateRoundedRectBackground(color,
GetButtonSizeOnType(type) / 2);
}
} // namespace
IconButton::IconButton(PressedCallback callback,
IconButton::Type type,
const gfx::VectorIcon* icon,
int accessible_name_id)
: IconButton(std::move(callback),
type,
icon,
accessible_name_id,
/*is_togglable=*/false,
/*has_border=*/false) {}
IconButton::IconButton(PressedCallback callback,
IconButton::Type type,
const gfx::VectorIcon* icon,
bool is_togglable,
bool has_border)
: views::ImageButton(std::move(callback)),
type_(type),
icon_(icon),
is_togglable_(is_togglable),
background_toggled_color_(kDefaultToggledBackgroundColorId),
icon_color_(GetDefaultIconColorId(type, /*focused=*/false)),
icon_toggled_color_(kDefaultToggledIconColorId) {
const int button_size = GetButtonSizeOnType(type);
SetPreferredSize(gfx::Size(button_size, button_size));
SetImageHorizontalAlignment(ALIGN_CENTER);
SetImageVerticalAlignment(ALIGN_MIDDLE);
StyleUtil::SetUpInkDropForButton(this, gfx::Insets(),
/*highlight_on_hover=*/false,
/*highlight_on_focus=*/false);
if (!IsFloatingIconButton(type)) {
background_color_ = GetDefaultBackgroundColorId(type).value();
}
UpdateBackground();
UpdateVectorIcon();
auto* focus_ring = views::FocusRing::Get(this);
focus_ring->SetOutsetFocusRingDisabled(true);
focus_ring->SetColorId(cros_tokens::kCrosSysFocusRing);
if (has_border) {
// The focus ring will have the outline padding with the bounds of the
// buttons.
focus_ring->SetPathGenerator(
std::make_unique<views::CircleHighlightPathGenerator>(-gfx::Insets(
focus_ring->GetHaloThickness() / 2 + kFocusRingPadding)));
}
views::InstallCircleHighlightPathGenerator(this);
enabled_changed_subscription_ = AddEnabledChangedCallback(base::BindRepeating(
&IconButton::OnEnabledStateChanged, base::Unretained(this)));
}
IconButton::IconButton(PressedCallback callback,
IconButton::Type type,
const gfx::VectorIcon* icon,
const std::u16string& accessible_name,
bool is_togglable,
bool has_border)
: IconButton(std::move(callback), type, icon, is_togglable, has_border) {
SetTooltipText(accessible_name);
}
IconButton::IconButton(PressedCallback callback,
IconButton::Type type,
const gfx::VectorIcon* icon,
int accessible_name_id,
bool is_togglable,
bool has_border)
: IconButton(std::move(callback),
type,
icon,
l10n_util::GetStringUTF16(accessible_name_id),
is_togglable,
has_border) {}
IconButton::~IconButton() = default;
void IconButton::SetButtonBehavior(DisabledButtonBehavior button_behavior) {
if(button_behavior_ == button_behavior) {
return;
}
button_behavior_ = button_behavior;
// Change button behavior may impact the toggled state.
if(toggled_ && !GetEnabled()) {
UpdateVectorIcon();
}
}
void IconButton::SetVectorIcon(const gfx::VectorIcon& icon) {
icon_ = &icon;
if (!IsToggledOn()) {
UpdateVectorIcon();
}
}
void IconButton::SetToggledVectorIcon(const gfx::VectorIcon& icon) {
toggled_icon_ = &icon;
if (IsToggledOn()) {
UpdateVectorIcon();
}
}
void IconButton::SetBackgroundColor(ColorVariant background_color) {
if (background_color_ == background_color) {
return;
}
background_color_ = background_color;
if (GetEnabled() && !IsToggledOn()) {
UpdateBackground();
}
}
void IconButton::SetBackgroundToggledColor(
ColorVariant background_toggled_color) {
if (!is_togglable_ || background_toggled_color == background_toggled_color_) {
return;
}
background_toggled_color_ = background_toggled_color;
if (GetEnabled() && IsToggledOn()) {
UpdateBackground();
}
}
void IconButton::SetBackgroundImage(const gfx::ImageSkia& background_image) {
background_image_ = gfx::ImageSkiaOperations::CreateResizedImage(
background_image, skia::ImageOperations::RESIZE_BEST, GetPreferredSize());
SchedulePaint();
}
void IconButton::SetIconColor(ColorVariant icon_color) {
if (icon_color_ == icon_color) {
return;
}
icon_color_ = icon_color;
if (!IsToggledOn()) {
UpdateVectorIcon(/*color_changes_only=*/true);
}
}
void IconButton::SetIconToggledColor(ColorVariant icon_toggled_color) {
if (!is_togglable_ || icon_toggled_color == icon_toggled_color_) {
return;
}
icon_toggled_color_ = icon_toggled_color;
if (IsToggledOn()) {
UpdateVectorIcon(/*color_changes_only=*/true);
}
}
void IconButton::SetIconSize(int size) {
if (icon_size_ == size) {
return;
}
icon_size_ = size;
UpdateVectorIcon();
}
void IconButton::SetToggled(bool toggled) {
if (!is_togglable_ || toggled_ == toggled) {
return;
}
toggled_ = toggled;
if (GetEnabled()) {
UpdateBackground();
}
// If toggle state is changed with `toggled_`, update the icon.
if (GetEnabled() ||
button_behavior_ ==
DisabledButtonBehavior::kCanDisplayDisabledToggleValue) {
UpdateVectorIcon();
}
}
void IconButton::SetEnableBlurredBackgroundShield(bool enable) {
if (blurred_background_shield_enabled_ == enable) {
return;
}
blurred_background_shield_enabled_ = enable;
if (blurred_background_shield_enabled_) {
SetBackground(nullptr);
} else {
blurred_background_shield_.reset();
}
UpdateBackground();
}
void IconButton::OnFocus() {
// Update prominent floating type button's icon color on focus.
if (IsProminentFloatingType(type_) && !IsToggledOn()) {
// If prominent floating button is still using default colors, updates its
// icon color on focus.
if (absl::holds_alternative<ui::ColorId>(icon_color_) &&
absl::get<ui::ColorId>(icon_color_) ==
GetDefaultIconColorId(type_, /*focused=*/false)) {
icon_color_ = GetDefaultIconColorId(type_, /*focused=*/true);
UpdateVectorIcon(/*color_changes_only=*/true);
}
}
}
void IconButton::OnBlur() {
// Update prominent floating type button's icon color on blur.
if (IsProminentFloatingType(type_) && !IsToggledOn()) {
// If prominent floating button is still using default colors, updates its
// icon color on focus.
if (absl::holds_alternative<ui::ColorId>(icon_color_) &&
absl::get<ui::ColorId>(icon_color_) ==
GetDefaultIconColorId(type_, /*focused=*/true)) {
icon_color_ = GetDefaultIconColorId(type_, /*focused=*/false);
UpdateVectorIcon(/*color_changes_only=*/true);
}
}
}
void IconButton::PaintButtonContents(gfx::Canvas* canvas) {
if (!IsFloatingIconButton(type_) || IsToggledOn()) {
// Apply the background image. This is painted on top of the |color|.
if (!background_image_.isNull()) {
const gfx::Rect rect(GetContentsBounds());
cc::PaintFlags flags;
flags.setAntiAlias(true);
SkPath mask;
mask.addCircle(rect.CenterPoint().x(), rect.CenterPoint().y(),
rect.width() / 2);
canvas->ClipPath(mask, true);
canvas->DrawImageInt(background_image_, 0, 0, flags);
}
}
views::ImageButton::PaintButtonContents(canvas);
}
void IconButton::GetAccessibleNodeData(ui::AXNodeData* node_data) {
views::ImageButton::GetAccessibleNodeData(node_data);
if (is_togglable_) {
node_data->role = ax::mojom::Role::kToggleButton;
node_data->SetCheckedState(toggled_ ? ax::mojom::CheckedState::kTrue
: ax::mojom::CheckedState::kFalse);
} else {
node_data->role = ax::mojom::Role::kButton;
}
}
void IconButton::NotifyClick(const ui::Event& event) {
if (is_togglable_) {
chromeos::haptics_util::PlayHapticToggleEffect(
!toggled_, ui::HapticTouchpadEffectStrength::kMedium);
}
views::Button::NotifyClick(event);
}
void IconButton::UpdateBackground() {
if (blurred_background_shield_enabled_) {
UpdateBlurredBackgroundShield();
return;
}
// The untoggled floating button does not have a background.
const bool is_toggled = IsToggledOn();
if (IsFloatingIconButton(type_) && !is_toggled) {
SetBackground(nullptr);
return;
}
// Create a themed rounded rect background when the button is disabled.
if (!GetEnabled()) {
SetBackground(
CreateThemedBackground(cros_tokens::kCrosSysDisabledContainer, type_));
return;
}
// Create a background according to the toggled state.
ColorVariant color_variant =
is_toggled ? background_toggled_color_ : background_color_;
if (absl::holds_alternative<SkColor>(color_variant)) {
SetBackground(CreateSolidBackground(absl::get<SkColor>(color_variant), type_));
} else {
SetBackground(CreateThemedBackground(absl::get<ui::ColorId>(color_variant), type_));
}
}
void IconButton::UpdateBlurredBackgroundShield() {
CHECK(blurred_background_shield_enabled_);
const bool is_toggled = IsToggledOn();
if (IsFloatingIconButton(type_) && !is_toggled) {
blurred_background_shield_.reset();
return;
}
// Create a new blurred background shield if needed.
if (!blurred_background_shield_) {
blurred_background_shield_ = std::make_unique<BlurredBackgroundShield>(
this, background_color_, ColorProvider::kBackgroundBlurSigma,
gfx::RoundedCornersF(GetButtonSizeOnType(type_) / 2));
}
ColorVariant color_variant =
GetEnabled()
? (is_toggled ? background_toggled_color_ : background_color_)
: ColorVariant(cros_tokens::kCrosSysDisabledContainer);
if (absl::holds_alternative<SkColor>(color_variant)) {
blurred_background_shield_->SetColor(absl::get<SkColor>(color_variant));
} else {
blurred_background_shield_->SetColorId(absl::get<ui::ColorId>(color_variant));
}
}
void IconButton::UpdateVectorIcon(bool color_changes_only) {
const bool is_toggled = IsToggledOn();
const gfx::VectorIcon* icon =
is_toggled && toggled_icon_ ? toggled_icon_.get() : icon_.get();
if (!icon) {
return;
}
const int icon_size = icon_size_.value_or(GetIconSizeOnType(type_));
ui::ImageModel new_normal_image_model;
ColorVariant color_variant = is_toggled ? icon_toggled_color_ : icon_color_;
if (absl::holds_alternative<SkColor>(color_variant)) {
new_normal_image_model = ui::ImageModel::FromVectorIcon(
*icon, absl::get<SkColor>(color_variant), icon_size);
} else {
new_normal_image_model = ui::ImageModel::FromVectorIcon(
*icon, absl::get<ui::ColorId>(color_variant), icon_size);
}
if (GetWidget()) {
// Skip repainting if the incoming icon is the same as the current icon. If
// the icon has been painted before, |gfx::CreateVectorIcon()| will simply
// grab the ImageSkia from a cache, so it will be cheap. Note that this
// assumes that toggled/disabled images changes at the same time as the
// normal image, which it currently does.
const gfx::ImageSkia new_normal_image =
new_normal_image_model.Rasterize(GetColorProvider());
const gfx::ImageSkia& old_normal_image =
GetImage(views::Button::STATE_NORMAL);
if (!new_normal_image.isNull() && !old_normal_image.isNull() &&
new_normal_image.BackedBySameObjectAs(old_normal_image)) {
return;
}
}
SetImageModel(views::Button::STATE_NORMAL, new_normal_image_model);
if (!color_changes_only) {
SetImageModel(views::Button::STATE_DISABLED,
ui::ImageModel::FromVectorIcon(
*icon, cros_tokens::kCrosSysDisabled, icon_size));
}
}
void IconButton::OnEnabledStateChanged() {
// Enabled state change may cause toggled state change.
if (toggled_ && button_behavior_ !=
DisabledButtonBehavior::kCanDisplayDisabledToggleValue) {
UpdateVectorIcon();
}
UpdateBackground();
}
SkColor IconButton::GetBackgroundColor() const {
DCHECK(background());
return background()->get_color();
}
bool IconButton::IsToggledOn() const {
return toggled_ &&
(GetEnabled() ||
button_behavior_ ==
DisabledButtonBehavior::kCanDisplayDisabledToggleValue);
}
BEGIN_METADATA(IconButton, views::ImageButton)
END_METADATA
} // namespace ash