| // 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 "base/strings/string_number_conversions.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/gfx/paint_vector_icon.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/animation/animation_builder.h" |
| #include "ui/views/background.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 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)); |
| 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); |
| |
| SetBackground(views::CreateLayerBasedRoundedBackground( |
| cros_tokens::kCrosSysSystemOnBase1, |
| gfx::RoundedCornersF{kTrayItemCornerRadius})); |
| } |
| |
| 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(ui::ImageModel::FromImageSkia(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() { |
| const SkColor icon_color = |
| GetColorProvider()->GetColor(cros_tokens::kCrosSysOnSurface); |
| const int icon_size = kJellyChevronIconSize; |
| |
| expanded_image_ = |
| gfx::CreateVectorIcon(kChevronUpSmallIcon, icon_size, icon_color); |
| |
| collapsed_image_ = |
| gfx::CreateVectorIcon(kChevronDownSmallIcon, icon_size, icon_color); |
| |
| image_->SetImage(ui::ImageModel::FromImageSkia(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(); |
| } |
| |
| 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""; |
| } |
| |
| BEGIN_METADATA(CounterExpandButton) |
| END_METADATA |
| |
| } // namespace ash |