// 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/style/counter_expand_button.h"
#include <string>
#include "ash/public/cpp/metrics_util.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_provider.h"
#include "ash/style/typography.h"
#include "ash/system/notification_center/message_center_constants.h"
#include "ash/system/notification_center/message_center_utils.h"
#include "ash/system/tray/tray_constants.h"
#include "base/metrics/histogram_functions.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/animation_throughput_reporter.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/animation/tween.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/view_class_properties.h"
namespace ash {
namespace {
constexpr gfx::Insets kFocusInsets(2);
constexpr gfx::Insets kImageInsets(2);
constexpr auto kLabelInsets = gfx::Insets::TLBR(0, 8, 0, 0);
constexpr int kCornerRadius = 12;
constexpr int kChevronIconSize = 16;
constexpr int kJellyChevronIconSize = 20;
constexpr int kLabelFontSize = 12;
} // namespace
CounterExpandButton::CounterExpandButton() {
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal));
auto label = std::make_unique<views::Label>();
label->SetPaintToLayer();
label->layer()->SetFillsBoundsOpaquely(false);
label->SetFontList(gfx::FontList({kGoogleSansFont}, gfx::Font::NORMAL,
kLabelFontSize, gfx::Font::Weight::MEDIUM));
label->SetProperty(views::kMarginsKey, kLabelInsets);
label->SetElideBehavior(gfx::ElideBehavior::NO_ELIDE);
label->SetText(base::NumberToString16(counter_));
label->SetVisible(ShouldShowLabel());
label_ = AddChildView(std::move(label));
if (chromeos::features::IsJellyEnabled()) {
ash::TypographyProvider::Get()->StyleLabel(
ash::TypographyToken::kCrosAnnotation1, *label_);
}
auto image = std::make_unique<views::ImageView>();
image->SetPaintToLayer();
image->layer()->SetFillsBoundsOpaquely(false);
image->SetProperty(views::kMarginsKey, kImageInsets);
image_ = AddChildView(std::move(image));
UpdateTooltip();
views::InstallRoundRectHighlightPathGenerator(this, kFocusInsets,
kCornerRadius);
views::FocusRing::Get(this)->SetColorId(ui::kColorAshFocusRing);
views::FocusRing::Get(this)->SetOutsetFocusRingDisabled(true);
SetPaintToLayer(ui::LAYER_SOLID_COLOR);
layer()->SetFillsBoundsOpaquely(false);
layer()->SetRoundedCornerRadius(gfx::RoundedCornersF{kTrayItemCornerRadius});
layer()->SetIsFastRoundedCorner(true);
}
CounterExpandButton::~CounterExpandButton() = default;
void CounterExpandButton::SetExpanded(bool expanded) {
if (expanded_ == expanded) {
return;
}
previous_bounds_ = GetContentsBounds();
expanded_ = expanded;
label_->SetText(base::NumberToString16(counter_));
label_->SetVisible(ShouldShowLabel());
image_->SetImage(expanded_ ? expanded_image_ : collapsed_image_);
UpdateTooltip();
}
bool CounterExpandButton::ShouldShowLabel() const {
return !expanded_ && counter_;
}
void CounterExpandButton::UpdateCounter(int count) {
counter_ = count;
label_->SetText(base::NumberToString16(counter_));
label_->SetVisible(ShouldShowLabel());
}
void CounterExpandButton::UpdateIcons() {
SkColor icon_color =
GetColorProvider()->GetColor(cros_tokens::kCrosSysOnSurface);
int icon_size = chromeos::features::IsJellyEnabled() ? kJellyChevronIconSize
: kChevronIconSize;
expanded_image_ =
gfx::CreateVectorIcon(kChevronUpSmallIcon, icon_size, icon_color);
collapsed_image_ =
gfx::CreateVectorIcon(kChevronDownSmallIcon, icon_size, icon_color);
image_->SetImage(expanded_ ? expanded_image_ : collapsed_image_);
}
void CounterExpandButton::UpdateTooltip() {
std::u16string tooltip_text = expanded_ ? GetExpandedStateTooltipText()
: GetCollapsedStateTooltipText();
SetTooltipText(tooltip_text);
GetViewAccessibility().SetName(
tooltip_text, tooltip_text.empty()
? ax::mojom::NameFrom::kAttributeExplicitlyEmpty
: ax::mojom::NameFrom::kAttribute);
}
void CounterExpandButton::AnimateExpandCollapse() {
// If there is no child to expand/collapse, there's no animation to perform
// here.
if (!counter_) {
return;
}
int bounds_animation_duration;
gfx::Tween::Type bounds_animation_tween_type;
if (label()->GetVisible()) {
if (label()->layer()->GetAnimator()->is_animating()) {
// Label's fade out animation might still be running. If that's the case,
// we need to abort this and reset visibility for fade in animation.
label()->layer()->GetAnimator()->AbortAllAnimations();
label()->SetVisible(true);
}
// Fade in animation when label is visible.
// TODO(b/336646488): Move `message_center_utils` functions and variables
// used in this file to ash/style.
message_center_utils::FadeInView(
label(), kExpandButtonFadeInLabelDelayMs,
kExpandButtonFadeInLabelDurationMs, gfx::Tween::LINEAR,
GetAnimationHistogramName(AnimationType::kFadeInLabel));
bounds_animation_duration = kExpandButtonShowLabelBoundsChangeDurationMs;
bounds_animation_tween_type = gfx::Tween::LINEAR_OUT_SLOW_IN;
} else {
// In this case, `counter_` is not zero and label is not visible.
// This means the label switch from visible to invisible and we should do
// fade out animation.
label_fading_out_ = true;
// TODO(b/336646488): Move `message_center_utils` functions and variables
// used in this file to ash/style.
message_center_utils::FadeOutView(
label(),
base::BindRepeating(
[](base::WeakPtr<CounterExpandButton> parent, views::Label* label) {
if (parent) {
label->layer()->SetOpacity(1.0f);
label->SetVisible(false);
parent->set_label_fading_out(false);
}
},
weak_factory_.GetWeakPtr(), label()),
0, kExpandButtonFadeOutLabelDurationMs, gfx::Tween::LINEAR,
GetAnimationHistogramName(AnimationType::kFadeOutLabel));
bounds_animation_duration = kExpandButtonHideLabelBoundsChangeDurationMs;
bounds_animation_tween_type = gfx::Tween::ACCEL_20_DECEL_100;
}
AnimateBoundsChange(bounds_animation_duration, bounds_animation_tween_type,
GetAnimationHistogramName(AnimationType::kBoundsChange));
}
const std::string CounterExpandButton::GetAnimationHistogramName(
AnimationType type) {
return "";
}
void CounterExpandButton::OnThemeChanged() {
views::Button::OnThemeChanged();
UpdateIcons();
UpdateBackgroundColor();
}
gfx::Size CounterExpandButton::CalculatePreferredSize(
const views::SizeBounds& available_size) const {
gfx::Size size = Button::CalculatePreferredSize(available_size);
// When label is fading out, it is still visible but we should not consider
// its size in our calculation here, so that size change animation can be
// performed correctly.
if (label_fading_out_) {
return gfx::Size(
size.width() -
label_->GetPreferredSize(views::SizeBounds(label_->width(), {}))
.width() -
kLabelInsets.width(),
size.height());
}
return size;
}
void CounterExpandButton::AnimateBoundsChange(
int duration_in_ms,
gfx::Tween::Type tween_type,
const std::string& animation_histogram_name) {
// Perform size change animation with layer bounds animation, setting the
// bounds to its previous state and then animating to current state. At the
// same time, we move `image_` in the opposite direction so that it appears to
// stay in the same location when the parent's bounds is moving.
const gfx::Rect target_bounds = layer()->GetTargetBounds();
const gfx::Rect image_target_bounds = image_->layer()->GetTargetBounds();
// This value is used to add extra width to the view's bounds. We will animate
// the view with this extra width to its target state.
int extra_width = previous_bounds_.width() - target_bounds.width();
ui::AnimationThroughputReporter reporter(
layer()->GetAnimator(),
metrics_util::ForSmoothnessV3(base::BindRepeating(
[](const std::string& animation_histogram_name, int smoothness) {
base::UmaHistogramPercentage(animation_histogram_name, smoothness);
},
animation_histogram_name)));
layer()->SetBounds(
gfx::Rect(target_bounds.x() - extra_width, target_bounds.y(),
target_bounds.width() + extra_width, target_bounds.height()));
image_->layer()->SetBounds(
gfx::Rect(image_target_bounds.x() + extra_width, image_target_bounds.y(),
image_target_bounds.width(), image_target_bounds.height()));
views::AnimationBuilder()
.Once()
.SetDuration(base::Milliseconds(duration_in_ms))
.SetBounds(this, target_bounds, tween_type)
.SetBounds(image_, image_target_bounds, tween_type);
}
std::u16string CounterExpandButton::GetExpandedStateTooltipText() const {
return u"";
}
std::u16string CounterExpandButton::GetCollapsedStateTooltipText() const {
return u"";
}
void CounterExpandButton::UpdateBackgroundColor() {
layer()->SetColor(
GetColorProvider()->GetColor(cros_tokens::kCrosSysSystemOnBase1));
}
BEGIN_METADATA(CounterExpandButton)
END_METADATA
} // namespace ash