// Copyright 2024 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/wm/overview/birch/birch_chip_button.h"
#include "ash/birch/birch_item.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/typography.h"
#include "ash/wm/overview/birch/birch_bar_constants.h"
#include "ash/wm/overview/birch/birch_bar_controller.h"
#include "ash/wm/overview/birch/birch_bar_util.h"
#include "ash/wm/overview/birch/birch_chip_context_menu_model.h"
#include "base/types/cxx23_to_underlying.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/context_menu_controller.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/menu_model_adapter.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/controls/menu/menu_types.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_types.h"
#include "ui/views/layout/layout_types.h"
#include "ui/views/vector_icons.h"
#include "ui/views/view_class_properties.h"
namespace ash {
namespace {
// The color and layout parameters of the chip.
constexpr gfx::Insets kInteriorMarginsNoAddon = gfx::Insets::TLBR(12, 0, 8, 20);
constexpr gfx::Insets kInteriorMarginsWithAddon = gfx::Insets::VH(12, 0);
// The layout parameters of icon.
constexpr gfx::Insets kIconMargins = gfx::Insets::TLBR(0, 12, 0, 8);
constexpr int kMainIconViewSize = 40;
constexpr int kParentIconViewSize = 44;
constexpr int kSecondaryIconViewSize = 20;
constexpr int kSecondaryIconImageSize = 12;
constexpr int kFaviconSize = 32;
constexpr int kFaviconCornerRadius = 8;
constexpr int kAppIconSize = 16;
constexpr int kAppCornerRadius = 20;
constexpr int kIllustrationSize = 40;
constexpr int kIllustrationCornerRadius = 8;
constexpr int kWeatherImageSize = 32;
// The colors of icons.
constexpr ui::ColorId kIconBackgroundColorId =
cros_tokens::kCrosSysSystemOnBase;
constexpr ui::ColorId kSecondaryIconBackgroundColorId =
cros_tokens::kCrosSysSecondaryLight;
constexpr ui::ColorId kSecondaryIconColorId = cros_tokens::kCrosSysOnSecondary;
// The colors and fonts of title and subtitle.
constexpr int kTitleSpacing = 2;
constexpr TypographyToken kTitleFont = TypographyToken::kCrosButton1;
constexpr ui::ColorId kTitleColorId = cros_tokens::kCrosSysOnSurface;
constexpr TypographyToken kSubtitleFont = TypographyToken::kCrosAnnotation1;
constexpr ui::ColorId kSubtitleColorId = cros_tokens::kCrosSysOnSurfaceVariant;
BirchSuggestionType GetSuggestionTypeFromItemType(BirchItemType item_type) {
switch (item_type) {
case BirchItemType::kWeather:
return BirchSuggestionType::kWeather;
case BirchItemType::kCalendar:
return BirchSuggestionType::kCalendar;
// Attachments are considered Drive suggestions in the UI.
case BirchItemType::kAttachment:
case BirchItemType::kFile:
return BirchSuggestionType::kDrive;
// All tab types are "Chrome browser" in the UI.
case BirchItemType::kTab:
case BirchItemType::kLastActive:
case BirchItemType::kMostVisited:
case BirchItemType::kSelfShare:
return BirchSuggestionType::kChromeTab;
case BirchItemType::kLostMedia:
return BirchSuggestionType::kMedia;
case BirchItemType::kReleaseNotes:
return BirchSuggestionType::kExplore;
case BirchItemType::kCoral:
return BirchSuggestionType::kCoral;
default:
return BirchSuggestionType::kUndefined;
}
}
} // namespace
//------------------------------------------------------------------------------
// BirchChipButton::ChipMenuController:
class BirchChipButton::ChipMenuController
: public views::ContextMenuController {
public:
explicit ChipMenuController(BirchChipButton* chip) : chip_(chip) {}
ChipMenuController(const ChipMenuController&) = delete;
ChipMenuController& operator=(const ChipMenuController&) = delete;
~ChipMenuController() override = default;
private:
// views::ContextMenuController:
void ShowContextMenuForViewImpl(views::View* source,
const gfx::Point& point,
ui::MenuSourceType source_type) override {
if (auto* birch_bar_controller_ = BirchBarController::Get()) {
birch_bar_controller_->ShowChipContextMenu(
chip_, GetSuggestionTypeFromItemType(chip_->GetItem()->GetType()),
point, source_type);
}
}
const raw_ptr<BirchChipButton> chip_;
};
//------------------------------------------------------------------------------
// BirchChipButton:
BirchChipButton::BirchChipButton()
: chip_menu_controller_(std::make_unique<ChipMenuController>(this)) {
auto flex_layout = std::make_unique<views::FlexLayout>();
flex_layout_ = flex_layout.get();
flex_layout_->SetOrientation(views::LayoutOrientation::kHorizontal)
.SetMainAxisAlignment(views::LayoutAlignment::kStart)
.SetCrossAxisAlignment(views::LayoutAlignment::kCenter)
.SetInteriorMargin(kInteriorMarginsNoAddon);
raw_ptr<views::BoxLayoutView> titles_container = nullptr;
// Build up the chip's contents.
views::Builder<BirchChipButtonBase>(this)
.SetLayoutManager(std::move(flex_layout))
.AddChildren(
// Icon parent.
views::Builder<views::View>()
.CopyAddressTo(&icon_parent_view_)
.SetPreferredSize(
gfx::Size(kParentIconViewSize, kParentIconViewSize))
.SetProperty(views::kMarginsKey, kIconMargins)
.SetVisible(true)
.AddChildren(
// Main icon.
views::Builder<views::ImageView>().CopyAddressTo(
&primary_icon_view_),
// Secondary icon.
views::Builder<views::ImageView>().CopyAddressTo(
&secondary_icon_view_)),
views::Builder<views::BoxLayoutView>()
.CopyAddressTo(&titles_container)
.SetProperty(views::kFlexBehaviorKey,
views::FlexSpecification(
views::MinimumFlexSizeRule::kScaleToZero,
views::MaximumFlexSizeRule::kUnbounded))
.SetOrientation(views::BoxLayout::Orientation::kVertical)
.SetBetweenChildSpacing(kTitleSpacing)
.AddChildren(views::Builder<views::Label>()
.CopyAddressTo(&title_)
.SetAutoColorReadabilityEnabled(false)
.SetEnabledColorId(kTitleColorId)
.SetHorizontalAlignment(gfx::ALIGN_LEFT),
views::Builder<views::Label>()
.CopyAddressTo(&subtitle_)
.SetAutoColorReadabilityEnabled(false)
.SetEnabledColorId(kSubtitleColorId)
.SetHorizontalAlignment(gfx::ALIGN_LEFT)))
.BuildChildren();
// Stylize the titles.
auto* typography_provider = TypographyProvider::Get();
typography_provider->StyleLabel(kTitleFont, *title_);
typography_provider->StyleLabel(kSubtitleFont, *subtitle_);
// Add removal chip panel.
set_context_menu_controller(chip_menu_controller_.get());
}
BirchChipButton::~BirchChipButton() = default;
void BirchChipButton::Init(BirchItem* item) {
item_ = item;
title_->SetText(item_->title());
subtitle_->SetText(item_->subtitle());
SetCallback(
base::BindRepeating(&BirchItem::PerformAction, base::Unretained(item_)));
const auto addon_type = item_->GetAddonType();
// Add add-ons according to the add-on type.
switch (addon_type) {
case BirchAddonType::kButton: {
auto button = birch_bar_util::CreateAddonButton(
base::BindRepeating(&BirchItem::PerformAddonAction,
base::Unretained(item_)),
*item_->addon_label());
button->SetTooltipText(item->GetAddonAccessibleName());
SetAddon(std::move(button));
break;
}
case BirchAddonType::kWeatherTempLabelC:
case BirchAddonType::kWeatherTempLabelF:
SetAddon(birch_bar_util::CreateWeatherTemperatureView(
*item_->addon_label(),
addon_type == BirchAddonType::kWeatherTempLabelF));
break;
case BirchAddonType::kNone:
break;
}
item_->LoadIcon(base::BindOnce(&BirchChipButton::SetIconImage,
weak_factory_.GetWeakPtr()));
SetAccessibleName(item_->GetAccessibleName());
}
const BirchItem* BirchChipButton::GetItem() const {
return item_.get();
}
BirchItem* BirchChipButton::GetItem() {
return item_.get();
}
void BirchChipButton::Shutdown() {
item_ = nullptr;
// Invalidate all weakptrs to avoid previously triggered callbacks from using
// `item_`.
weak_factory_.InvalidateWeakPtrs();
}
void BirchChipButton::StylizeIconForItemType(
BirchItemType type,
SecondaryIconType secondary_icon_type,
bool use_smaller_dimension) {
int icon_size;
int rounded_corners;
std::optional<ui::ColorId> background_color_id;
switch (type) {
case BirchItemType::kTest:
case BirchItemType::kCalendar:
case BirchItemType::kAttachment:
case BirchItemType::kFile:
icon_size = kAppIconSize;
rounded_corners = kAppCornerRadius;
background_color_id = kIconBackgroundColorId;
break;
case BirchItemType::kWeather:
icon_size = kWeatherImageSize;
break;
case BirchItemType::kReleaseNotes:
icon_size = kIllustrationSize;
rounded_corners = kIllustrationCornerRadius;
background_color_id = kIconBackgroundColorId;
break;
case BirchItemType::kTab:
case BirchItemType::kSelfShare:
case BirchItemType::kMostVisited:
case BirchItemType::kLastActive:
case BirchItemType::kLostMedia:
case BirchItemType::kCoral:
// When `use_smaller_dimension` is true, we use the smaller app icon sizes
// because we have access only to smaller icons.
use_smaller_dimension ? icon_size = kAppIconSize
: icon_size = kFaviconSize;
rounded_corners = kFaviconCornerRadius;
background_color_id = kIconBackgroundColorId;
break;
}
primary_icon_view_->SetImageSize(gfx::Size(icon_size, icon_size));
primary_icon_view_->SetBounds(0, 0, kMainIconViewSize, kMainIconViewSize);
primary_icon_view_->SetBorder(views::CreateEmptyBorder(
gfx::Insets((kMainIconViewSize - icon_size) / 2)));
if (background_color_id) {
primary_icon_view_->SetBackground(views::CreateThemedRoundedRectBackground(
background_color_id.value(), rounded_corners));
}
if (secondary_icon_type == SecondaryIconType::kNoIcon) {
secondary_icon_view_->SetVisible(false);
return;
}
secondary_icon_view_->SetImageSize(
gfx::Size(kSecondaryIconImageSize, kSecondaryIconImageSize));
secondary_icon_view_->SetBounds(24, 24, kSecondaryIconViewSize,
kSecondaryIconViewSize);
secondary_icon_view_->SetBackground(views::CreateThemedRoundedRectBackground(
kSecondaryIconBackgroundColorId, kSecondaryIconViewSize / 2));
secondary_icon_view_->SetBorder(views::CreateThemedRoundedRectBorder(
1, kSecondaryIconViewSize / 2, cros_tokens::kCrosSysSystemOnBaseOpaque));
}
void BirchChipButton::SetIconImage(const ui::ImageModel& icon_image,
SecondaryIconType secondary_icon_type) {
primary_icon_view_->SetImage(icon_image);
if (secondary_icon_type != SecondaryIconType::kNoIcon) {
ui::ImageModel secondary_icon_image;
switch (secondary_icon_type) {
case SecondaryIconType::kTabFromDesktop:
secondary_icon_image = ui::ImageModel::FromVectorIcon(
kBirchSecondaryIconDesktopIcon, kSecondaryIconColorId);
break;
case SecondaryIconType::kTabFromPhone:
secondary_icon_image = ui::ImageModel::FromVectorIcon(
kBirchSecondaryIconPortraitIcon, kSecondaryIconColorId);
break;
case SecondaryIconType::kTabFromTablet:
secondary_icon_image = ui::ImageModel::FromVectorIcon(
kBirchSecondaryIconLandscapeIcon, kSecondaryIconColorId);
break;
case SecondaryIconType::kTabFromUnknown:
secondary_icon_image = ui::ImageModel::FromVectorIcon(
kBirchSecondaryIconUnknownIcon, kSecondaryIconColorId);
break;
case SecondaryIconType::kLostMediaAudio:
secondary_icon_image = ui::ImageModel::FromVectorIcon(
kBirchSecondaryIconAudioIcon, kSecondaryIconColorId);
break;
case SecondaryIconType::kLostMediaVideo:
secondary_icon_image = ui::ImageModel::FromVectorIcon(
kBirchSecondaryIconVideoIcon, kSecondaryIconColorId);
break;
case SecondaryIconType::kLostMediaVideoConference:
secondary_icon_image = ui::ImageModel::FromVectorIcon(
kBirchSecondaryIconVideoConferenceIcon, kSecondaryIconColorId);
break;
case SecondaryIconType::kNoIcon:
break;
}
secondary_icon_view_->SetImage(secondary_icon_image);
}
bool use_smaller_dimension = icon_image.Size().width() <= kAppIconSize ||
icon_image.Size().height() <= kAppIconSize;
StylizeIconForItemType(item_->GetType(), secondary_icon_type,
use_smaller_dimension);
}
void BirchChipButton::ExecuteCommand(int command_id, int event_flags) {
auto* birch_bar_controller = BirchBarController::Get();
CHECK(birch_bar_controller);
using CommandId = BirchChipContextMenuModel::CommandId;
switch (command_id) {
case base::to_underlying(CommandId::kHideSuggestion):
birch_bar_controller->OnItemHiddenByUser(item_);
break;
case base::to_underlying(CommandId::kHideWeatherSuggestions):
birch_bar_controller->SetShowSuggestionType(BirchSuggestionType::kWeather,
/*show=*/false);
break;
case base::to_underlying(CommandId::kToggleTemperatureUnits):
birch_bar_controller->ToggleTemperatureUnits();
break;
case base::to_underlying(CommandId::kHideCalendarSuggestions):
birch_bar_controller->SetShowSuggestionType(
BirchSuggestionType::kCalendar,
/*show=*/false);
break;
case base::to_underlying(CommandId::kHideDriveSuggestions):
birch_bar_controller->SetShowSuggestionType(BirchSuggestionType::kDrive,
/*show=*/false);
break;
case base::to_underlying(CommandId::kHideChromeTabSuggestions):
birch_bar_controller->SetShowSuggestionType(
BirchSuggestionType::kChromeTab,
/*show=*/false);
break;
case base::to_underlying(CommandId::kHideMediaSuggestions):
birch_bar_controller->SetShowSuggestionType(BirchSuggestionType::kMedia,
/*show=*/false);
break;
case base::to_underlying(CommandId::kHideCoralSuggestions):
birch_bar_controller->SetShowSuggestionType(BirchSuggestionType::kCoral,
/*show=*/false);
break;
default:
birch_bar_controller->ExecuteMenuCommand(command_id, /*from_chip=*/true);
}
}
void BirchChipButton::SetAddon(std::unique_ptr<views::View> addon_view) {
if (addon_view_) {
RemoveChildViewT(addon_view_);
} else {
flex_layout_->SetInteriorMargin(kInteriorMarginsWithAddon);
}
addon_view_ = AddChildView(std::move(addon_view));
}
BEGIN_METADATA(BirchChipButton)
END_METADATA
} // namespace ash