chromium/ash/style/pill_button.cc

// 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