chromium/ash/app_list/views/app_list_toast_view.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/app_list/views/app_list_toast_view.h"

#include <algorithm>
#include <memory>
#include <utility>

#include "ash/app_list/app_list_controller_impl.h"
#include "ash/app_list/app_list_view_delegate.h"
#include "ash/bubble/bubble_utils.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/ash_color_provider.h"
#include "ash/style/icon_button.h"
#include "ash/style/pill_button.h"
#include "ash/style/typography.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/vector_icons/vector_icons.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/compositor/layer.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/vector_icon_utils.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/highlight_border.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"

namespace ash {
namespace {

constexpr int kCornerRadius = 16;
constexpr auto kInteriorMargin = gfx::Insets::TLBR(8, 8, 8, 16);
constexpr auto kTitleContainerMargin = gfx::Insets::TLBR(0, 16, 0, 24);
constexpr auto kCloseButtonMargin = gfx::Insets::TLBR(0, 8, 0, 0);

constexpr int kToastMinimumHeight = 32;
constexpr int kToastMaximumWidth = 640;
constexpr int kToastMinimumWidth = 288;

constexpr int kIconCornerRadius = 8;

class IconImageWithBackground : public views::ImageView {
 public:
  IconImageWithBackground() = default;
  IconImageWithBackground(const IconImageWithBackground&) = delete;
  IconImageWithBackground& operator=(const IconImageWithBackground&) = delete;
  ~IconImageWithBackground() override = default;

 private:
  // views::ImageView:
  void OnPaint(gfx::Canvas* canvas) override {
    if (GetImage().isNull())
      return;

    cc::PaintFlags flags;
    flags.setStyle(cc::PaintFlags::kFill_Style);
    flags.setColor(
        GetColorProvider()->GetColor(cros_tokens::kCrosSysSystemOnBase));
    canvas->DrawRoundRect(GetContentsBounds(), kIconCornerRadius, flags);
    SkPath mask;
    mask.addRoundRect(gfx::RectToSkRect(GetContentsBounds()), kIconCornerRadius,
                      kIconCornerRadius);
    canvas->ClipPath(mask, true);
    views::ImageView::OnPaint(canvas);
  }
};

}  // namespace

AppListToastView::Builder::Builder(const std::u16string title)
    : title_(title) {}

AppListToastView::Builder::~Builder() = default;

std::unique_ptr<AppListToastView> AppListToastView::Builder::Build() {
  std::unique_ptr<AppListToastView> toast =
      std::make_unique<AppListToastView>(title_, style_for_tablet_mode_);

  if (view_delegate_)
    toast->SetViewDelegate(view_delegate_);

  if (icon_) {
    toast->SetIcon(*icon_);
  }

  if (icon_size_)
    toast->SetIconSize(*icon_size_);

  if (has_icon_background_)
    toast->AddIconBackground();

  if (button_callback_)
    toast->SetButton(*button_text_, std::move(button_callback_));

  if (close_button_callback_)
    toast->SetCloseButton(std::move(close_button_callback_));

  if (subtitle_)
    toast->SetSubtitle(*subtitle_);

  if (subtitle_ && is_subtitle_multiline_) {
    toast->SetSubtitleMultiline(is_subtitle_multiline_);
  }

  return toast;
}

AppListToastView::Builder& AppListToastView::Builder::SetIcon(
    const ui::ImageModel& icon) {
  icon_ = icon;
  return *this;
}

AppListToastView::Builder& AppListToastView::Builder::SetSubtitle(
    const std::u16string subtitle) {
  subtitle_ = subtitle;
  return *this;
}

AppListToastView::Builder& AppListToastView::Builder::SetSubtitleMultiline(
    bool multiline) {
  is_subtitle_multiline_ = multiline;
  return *this;
}

AppListToastView::Builder& AppListToastView::Builder::SetIconSize(
    int icon_size) {
  icon_size_ = icon_size;
  return *this;
}

AppListToastView::Builder& AppListToastView::Builder::SetButton(
    const std::u16string button_text,
    views::Button::PressedCallback button_callback) {
  DCHECK(button_callback);

  button_text_ = button_text;
  button_callback_ = std::move(button_callback);
  return *this;
}

AppListToastView::Builder& AppListToastView::Builder::SetCloseButton(
    views::Button::PressedCallback close_button_callback) {
  DCHECK(close_button_callback);

  close_button_callback_ = std::move(close_button_callback);
  return *this;
}

AppListToastView::Builder& AppListToastView::Builder::SetStyleForTabletMode(
    bool style_for_tablet_mode) {
  style_for_tablet_mode_ = style_for_tablet_mode;
  return *this;
}

AppListToastView::Builder& AppListToastView::Builder::SetViewDelegate(
    AppListViewDelegate* delegate) {
  view_delegate_ = delegate;
  return *this;
}

void AppListToastView::SetViewDelegate(AppListViewDelegate* delegate) {
  view_delegate_ = delegate;
}

AppListToastView::Builder& AppListToastView::Builder::SetIconBackground(
    bool has_icon_background) {
  has_icon_background_ = has_icon_background;
  return *this;
}

bool AppListToastView::IsToastButton(views::View* view) {
  return views::IsViewClass<ToastPillButton>(view);
}

AppListToastView::AppListToastView(const std::u16string title,
                                   bool style_for_tablet_mode) {
  layout_manager_ = SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kHorizontal, kInteriorMargin));
  layout_manager_->set_cross_axis_alignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);

  label_container_ = AddChildView(std::make_unique<views::View>());
  label_container_->SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kVertical));
  label_container_->SetProperty(views::kMarginsKey, kTitleContainerMargin);

  title_label_ =
      label_container_->AddChildView(std::make_unique<views::Label>(title));

  const ui::ColorId title_color_id = cros_tokens::kCrosSysOnSurface;
  bubble_utils::ApplyStyle(title_label_,
                           style_for_tablet_mode ? TypographyToken::kCrosBody1
                                                 : TypographyToken::kCrosBody2,
                           title_color_id);

  title_label_->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
  title_label_->SetMultiLine(true);

  layout_manager_->SetFlexForView(label_container_, 1);

  if (style_for_tablet_mode) {
    SetPaintToLayer();
    layer()->SetFillsBoundsOpaquely(false);
    layer()->SetBackgroundBlur(ColorProvider::kBackgroundBlurSigma);
    layer()->SetBackdropFilterQuality(ColorProvider::kBackgroundBlurQuality);
    layer()->SetRoundedCornerRadius(gfx::RoundedCornersF(kCornerRadius));

    const ui::ColorId background_color_id =
        cros_tokens::kCrosSysSystemBaseElevated;
    SetBackground(views::CreateThemedRoundedRectBackground(background_color_id,
                                                           kCornerRadius));
    SetBorder(std::make_unique<views::HighlightBorder>(
        kCornerRadius, views::HighlightBorder::Type::kHighlightBorderNoShadow));
  } else {
    const ui::ColorId background_color_id = cros_tokens::kCrosSysSystemOnBase;
    SetBackground(views::CreateThemedRoundedRectBackground(background_color_id,
                                                           kCornerRadius));
  }
}

AppListToastView::~AppListToastView() = default;

void AppListToastView::SetButton(
    std::u16string button_text,
    views::Button::PressedCallback button_callback) {
  DCHECK(button_callback);

  toast_button_ =
      AddChildView(std::make_unique<AppListToastView::ToastPillButton>(
          view_delegate_, std::move(button_callback), button_text,
          PillButton::Type::kDefaultWithoutIcon,
          /*icon=*/nullptr));
  toast_button_->SetBorder(views::NullBorder());
}

void AppListToastView::SetCloseButton(
    views::Button::PressedCallback close_button_callback) {
  DCHECK(close_button_callback);

  close_button_ = AddChildView(std::make_unique<IconButton>(
      std::move(close_button_callback), IconButton::Type::kMediumFloating,
      &vector_icons::kCloseIcon,
      IDS_ASH_LAUNCHER_CLOSE_SORT_TOAST_BUTTON_SPOKEN_TEXT));
  close_button_->SetProperty(views::kMarginsKey, kCloseButtonMargin);
}

void AppListToastView::SetTitle(const std::u16string title) {
  title_label_->SetText(title);
}

void AppListToastView::SetSubtitle(const std::u16string subtitle) {
  if (subtitle_label_) {
    subtitle_label_->SetText(subtitle);
    return;
  }

  subtitle_label_ =
      label_container_->AddChildView(std::make_unique<views::Label>(subtitle));
  const ui::ColorId label_color_id = cros_tokens::kCrosSysOnSurfaceVariant;
  bubble_utils::ApplyStyle(subtitle_label_, TypographyToken::kCrosAnnotation1,
                           label_color_id);
  subtitle_label_->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
}

void AppListToastView::SetSubtitleMultiline(bool multiline) {
  if (!subtitle_label_) {
    return;
  }

  subtitle_label_->SetMultiLine(multiline);
}

void AppListToastView::SetIcon(const ui::ImageModel& icon) {
  CreateIconView();

  default_icon_ = icon;
  UpdateIconImage();
}

void AppListToastView::SetIconSize(int icon_size) {
  icon_size_ = icon_size;
  if (icon_)
    UpdateIconImage();
}

void AppListToastView::AddIconBackground() {
  if (icon_) {
    RemoveChildViewT(icon_.get());
    icon_ = nullptr;
  }

  has_icon_background_ = true;
  CreateIconView();
  UpdateIconImage();
}

void AppListToastView::SetAvailableWidth(int width) {
  if (available_width_ == width) {
    return;
  }
  available_width_ = width;
}

gfx::Size AppListToastView::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  const int available_width = std::min(
      kToastMaximumWidth, available_width_.value_or(kToastMaximumWidth));

  // Ensure that the toast can accommodate text in the label container.
  const int available_label_container_width =
      GetLabelWidthForToastWidth(available_width);
  // Adjust the label container width so it fits as much of the text per line,
  // but still fits within the available width for the toast labels.
  const int preferred_label_container_width =
      GetMaxLabelContainerWidth(available_label_container_width);
  const int min_height_for_labels =
      layout_manager_->inside_border_insets().height() +
      kTitleContainerMargin.height() +
      label_container_->GetHeightForWidth(preferred_label_container_width);
  // `available_width` would leave `available_label_container_width` space for
  // labels. Reduce `available_width` by the difference in available and
  // preferred width for label container to get ideal width for the toast.
  const int ideal_width = available_width - (available_label_container_width -
                                             preferred_label_container_width);
  return gfx::Size(
      std::max(kToastMinimumWidth, ideal_width),
      std::max(std::max(kToastMinimumHeight, min_height_for_labels),
               GetLayoutManager()->GetPreferredSize(this).height()));
}

void AppListToastView::Layout(PassKey) {
  // Make sure that labels are sized so the text fits the available width, logic
  // in `GetPreferredSize()` should ensure the toast is large enough for the
  // text to be visible within the UI.
  const int label_width = GetLabelWidthForToastWidth(width());
  title_label_->SetSize(
      gfx::Size(label_width, title_label_->GetHeightForWidth(label_width)));
  if (subtitle_label_) {
    subtitle_label_->SetSize(gfx::Size(
        label_width, subtitle_label_->GetHeightForWidth(label_width)));
  }

  LayoutSuperclass<views::View>(this);
}

void AppListToastView::UpdateInteriorMargins(const gfx::Insets& margin) {
  layout_manager_->set_inside_border_insets(margin);
}

AppListToastView::ToastPillButton::ToastPillButton(
    AppListViewDelegate* view_delegate,
    PressedCallback callback,
    const std::u16string& text,
    Type type,
    const gfx::VectorIcon* icon)
    : PillButton(std::move(callback), text, type, icon),
      view_delegate_(view_delegate) {
  views::FocusRing::Get(this)->SetHasFocusPredicate(
      base::BindRepeating([](const View* view) {
        const auto* v = views::AsViewClass<ToastPillButton>(view);
        CHECK(v);
        // With a `view_delegate_` present, focus ring should only show when
        // button is focused and keyboard traversal is engaged.
        return (!v->view_delegate_ ||
                v->view_delegate_->KeyboardTraversalEngaged()) &&
               v->HasFocus();
      }));
}

void AppListToastView::ToastPillButton::OnFocus() {
  PillButton::OnFocus();
  views::FocusRing::Get(this)->SchedulePaint();
}

void AppListToastView::ToastPillButton::OnBlur() {
  PillButton::OnBlur();
  views::FocusRing::Get(this)->SchedulePaint();
}

BEGIN_METADATA(AppListToastView, ToastPillButton)
END_METADATA

void AppListToastView::UpdateIconImage() {
  if (!icon_)
    return;

  if (!default_icon_) {
    return;
  }

  icon_->SetImage(*default_icon_);
}

void AppListToastView::CreateIconView() {
  if (icon_)
    return;

  icon_ = AddChildViewAt(has_icon_background_
                             ? std::make_unique<IconImageWithBackground>()
                             : std::make_unique<views::ImageView>(),
                         0);
  icon_->SetVerticalAlignment(views::ImageView::Alignment::kCenter);
  icon_->SetHorizontalAlignment(views::ImageView::Alignment::kCenter);
}

int AppListToastView::GetLabelWidthForToastWidth(int toast_width) const {
  int available_space = toast_width -
                        layout_manager_->inside_border_insets().width() -
                        kTitleContainerMargin.width();
  for (const auto& child : children()) {
    if (child->GetVisible() && child != label_container_) {
      // Reserve space for a label container sibling.
      available_space -= child->GetPreferredSize().width();

      // Reserve space for a label container siblings' margins.
      // NOTE: This assumes that the children margins are not collapsed,
      // otherwise margin overlaps would potentially get counted twice.
      CHECK(!layout_manager_->GetCollapseMarginsSpacing());
      const gfx::Insets* margins = child->GetProperty(views::kMarginsKey);
      if (margins) {
        available_space -= margins->width();
      }
    }
  }

  return available_space;
}

int AppListToastView::GetMaxLabelContainerWidth(int available_width) const {
  int labels_width =
      title_label_->CalculatePreferredSize({available_width, 0}).width();
  if (subtitle_label_) {
    labels_width = std::max(
        labels_width,
        subtitle_label_->CalculatePreferredSize({available_width, 0}).width());
  }
  return labels_width;
}

BEGIN_METADATA(AppListToastView)
END_METADATA

}  // namespace ash