chromium/ash/system/tray/tray_item_view.cc

// Copyright 2012 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/system/tray/tray_item_view.h"

#include <optional>

#include "ash/constants/ash_features.h"
#include "ash/public/cpp/shelf_types.h"
#include "ash/shelf/shelf.h"
#include "ash/system/status_area_animation_controller.h"
#include "ash/system/tray/tray_constants.h"
#include "base/metrics/histogram_functions.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/compositor.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/compositor/throughput_tracker.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/widget/widget.h"

namespace ash {

namespace {

// Animating in will start (after resize stage) when animation value is greater
// than this value.
constexpr double kAnimatingInStartValue = 0.5;

// Animating out will end (before resize stage) when animation value is less
// than this value.
constexpr double kAnimatingOutEndValue = 0.5;

constexpr char kShowAnimationSmoothnessHistogramName[] =
    "Ash.StatusArea.TrayItemView.Show";

constexpr char kHideAnimationSmoothnessHistogramName[] =
    "Ash.StatusArea.TrayItemView.Hide";

void RecordAnimationSmoothness(const std::string& histogram_name,
                               int smoothness) {
  DCHECK(0 <= smoothness && smoothness <= 100);
  base::UmaHistogramPercentage(histogram_name, smoothness);
}

void SetupThroughputTrackerForAnimationSmoothness(
    views::Widget* widget,
    std::optional<ui::ThroughputTracker>& tracker,
    const char* histogram_name) {
  // Return if `tracker` is already running; `widget` may not exist in tests.
  if (tracker || !widget)
    return;

  tracker.emplace(widget->GetCompositor()->RequestNewThroughputTracker());
  tracker->Start(ash::metrics_util::ForSmoothnessV3(
      base::BindRepeating(&RecordAnimationSmoothness, histogram_name)));
}

}  // namespace

void IconizedLabel::SetCustomAccessibleName(const std::u16string& name) {
  custom_accessible_name_ = name;

  UpdateAccessibleRole();
  GetViewAccessibility().SetName(custom_accessible_name_);
}

void IconizedLabel::AdjustAccessibleName(std::u16string& new_name,
                                         ax::mojom::NameFrom& name_from) {
  if (!custom_accessible_name_.empty()) {
    new_name = custom_accessible_name_;
    name_from = ax::mojom::NameFrom::kAttribute;
  } else {
    views::Label::AdjustAccessibleName(new_name, name_from);
  }
}

void IconizedLabel::UpdateAccessibleRole() {
  if (!custom_accessible_name_.empty()) {
    GetViewAccessibility().SetRole(ax::mojom::Role::kStaticText);
  } else {
    GetViewAccessibility().SetRole(GetTextContext() ==
                                           views::style::CONTEXT_DIALOG_TITLE
                                       ? ax::mojom::Role::kTitleBar
                                       : ax::mojom::Role::kStaticText);
  }
}

BEGIN_METADATA(IconizedLabel)
END_METADATA

TrayItemView::TrayItemView(Shelf* shelf)
    : views::AnimationDelegateViews(this), shelf_(shelf) {
  DCHECK(shelf_);
  SetPaintToLayer();
  layer()->SetFillsBoundsOpaquely(false);
  SetLayoutManager(std::make_unique<views::FillLayout>());
}

TrayItemView::~TrayItemView() = default;

void TrayItemView::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void TrayItemView::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

void TrayItemView::CreateLabel() {
  label_ = new IconizedLabel;
  AddChildView(label_.get());
  PreferredSizeChanged();
}

void TrayItemView::CreateImageView() {
  image_view_ = new views::ImageView;
  AddChildView(image_view_.get());
  PreferredSizeChanged();
}

void TrayItemView::DestroyLabel() {
  if (!label_)
    return;

  RemoveChildViewT(label_.get());
  label_ = nullptr;
}

void TrayItemView::DestroyImageView() {
  if (!image_view_)
    return;

  RemoveChildViewT(image_view_.get());
  image_view_ = nullptr;
}

void TrayItemView::UpdateLabelOrImageViewColor(bool active) {
  is_active_ = active;
}

base::ScopedClosureRunner TrayItemView::DisableAnimation() {
  if (layer()->GetAnimator()->is_animating()) {
    layer()->GetAnimator()->StopAnimating();
  }
  ++disable_animation_count_;
  return base::ScopedClosureRunner(base::BindOnce(
      [](const base::WeakPtr<TrayItemView>& ptr) {
        if (ptr) {
          --ptr->disable_animation_count_;
        }
      },
      weak_factory_.GetWeakPtr()));
}

void TrayItemView::SetAnimationIdleClosureForTest(base::OnceClosure closure) {
  animation_idle_closure_ = std::move(closure);
}

bool TrayItemView::IsAnimating() {
  return animation_ && animation_->is_animating();
}

void TrayItemView::SetVisible(bool visible) {
  // Do not invoke animation when the current visibility is already at the
  // target visibility.
  if (visible == target_visible_) {
    return;
  }
  target_visible_ = visible;
  for (auto& observer : observers_) {
    observer.OnTrayItemVisibilityAboutToChange(target_visible_);
  }
  views::View::SetVisible(visible);
  // During startup TrayItemViews are often SetVisible(false) before they are
  // attached to a widget. Don't bother constructing animations for them.
  if (!GetWidget()) {
    return;
  }
  PerformVisibilityAnimation(visible);
}

bool TrayItemView::IsHorizontalAlignment() const {
  return shelf_->IsHorizontalAlignment();
}

void TrayItemView::PerformVisibilityAnimation(bool visible) {
  // Set the view visible to show both show/hide animation.
  views::View::SetVisible(true);

  if (!animation_) {
    animation_ = std::make_unique<gfx::SlideAnimation>(this);
    animation_->SetTweenType(gfx::Tween::LINEAR);
    animation_->Reset(target_visible_ ? 0.0 : 1.0);
  }

  // Immediately progress to the end of the animation if animation is disabled.
  // NOTE: `ScreenRotationAnimator` can set animations to ZERO_DURATION.
  if (!ShouldVisibilityChangeBeAnimated() ||
      ui::ScopedAnimationDurationScaleMode::duration_multiplier() ==
          ui::ScopedAnimationDurationScaleMode::ZERO_DURATION) {
    // Tray items need to stay visible if the notification center tray's hide
    // animation is going to run, so don't hide the tray item here.
    // `StatusAreaAnimationController` will call `ImmediatelyUpdateVisibility()`
    // once the hide animation is over to ensure that all tray items are given a
    // chance to properly update their visibilities.
    if (!target_visible_ && shelf_->status_area_widget()
                                ->animation_controller()
                                ->is_hide_animation_scheduled()) {
      return;
    }
    animation_->SetSlideDuration(base::TimeDelta());
    target_visible_ ? animation_->Show() : animation_->Hide();
    return;
  }

  if (target_visible_) {
    SetupThroughputTrackerForAnimationSmoothness(
        GetWidget(), show_throughput_tracker_,
        kShowAnimationSmoothnessHistogramName);
    animation_->SetSlideDuration(base::Milliseconds(400));
    animation_->Show();
    AnimationProgressed(animation_.get());
  } else {
    SetupThroughputTrackerForAnimationSmoothness(
        GetWidget(), hide_throughput_tracker_,
        kHideAnimationSmoothnessHistogramName);
    animation_->SetSlideDuration(base::Milliseconds(100));
    animation_->Hide();
    AnimationProgressed(animation_.get());
  }
}

void TrayItemView::ImmediatelyUpdateVisibility() {
  // Reset the animation to the end state according to `target_visible_` so that
  // future visibility changes can animate properly.
  if (animation_) {
    animation_->Reset(target_visible_ ? 1.0 : 0.0);
  }
  layer()->SetTransform(gfx::Transform());
  layer()->SetOpacity(target_visible_ ? 1.0 : 0.0);
  views::View::SetVisible(target_visible_);
}

gfx::Size TrayItemView::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  DCHECK_EQ(1u, children().size());
  gfx::Size size = views::View::CalculatePreferredSize(available_size);
  if (image_view_) {
    size = gfx::Size(kUnifiedTrayIconSize, kUnifiedTrayIconSize);
    // Some TrayItemViews have slightly larger icons (e.g. Ethernet with VPN
    // badge).
    size.SetToMax(image_view_->CalculatePreferredSize({}));
  }

  if (!animation_.get() || !animation_->is_animating() ||
      !InResizeAnimation(animation_->GetCurrentValue())) {
    return size;
  }

  double progress = gfx::Tween::CalculateValue(
      gfx::Tween::FAST_OUT_SLOW_IN,
      GetResizeProgressFromAnimationProgress(animation_->GetCurrentValue()));
  if (shelf_->IsHorizontalAlignment()) {
    size.set_width(std::max(1, static_cast<int>(size.width() * progress)));
  } else {
    size.set_height(std::max(1, static_cast<int>(size.height() * progress)));
  }
  return size;
}

void TrayItemView::ChildPreferredSizeChanged(views::View* child) {
  PreferredSizeChanged();
}

void TrayItemView::AnimationProgressed(const gfx::Animation* animation) {
  // Should not animate during resize stage.
  if (InResizeAnimation(animation->GetCurrentValue())) {
    // Ensure we are not visible during resize stage.
    if (layer()->opacity() > 0.0) {
      layer()->SetOpacity(0.0);
    }
    PreferredSizeChanged();
    return;
  }

  double scale_progress =
      GetItemScaleProgressFromAnimationProgress(animation->GetCurrentValue());
  layer()->SetOpacity(scale_progress);

  // Only scale when animating icon in.
  if (target_visible_ && use_scale_in_animation_) {
    scale_progress = gfx::Tween::CalculateValue(gfx::Tween::LINEAR_OUT_SLOW_IN,
                                                scale_progress);
    gfx::Transform transform;
    transform.Translate(
        gfx::Tween::DoubleValueBetween(scale_progress,
                                       static_cast<double>(width()) / 2, 0.),
        gfx::Tween::DoubleValueBetween(scale_progress,
                                       static_cast<double>(height()) / 2, 0.));
    transform.Scale(scale_progress, scale_progress);
    layer()->SetTransform(transform);
  }

  // Container size might not fully transition to full size (the resize progress
  // value converted from animation progress might not be 1 after resize
  // animation). This call makes sure that it is fully resized.
  PreferredSizeChanged();
}

void TrayItemView::AnimationEnded(const gfx::Animation* animation) {
  views::View::SetVisible(target_visible_);
  layer()->SetOpacity(target_visible_ ? 1.0 : 0.0);

  if (show_throughput_tracker_) {
    // Reset `show_throughput_tracker_` to reset animation metrics recording.
    show_throughput_tracker_->Stop();
    show_throughput_tracker_.reset();
  }

  if (hide_throughput_tracker_) {
    // Reset `hide_throughput_tracker_` to reset animation metrics recording.
    hide_throughput_tracker_->Stop();
    hide_throughput_tracker_.reset();
  }

  if (animation_idle_closure_) {
    std::move(animation_idle_closure_).Run();
  }
}

void TrayItemView::AnimationCanceled(const gfx::Animation* animation) {
  AnimationEnded(animation);
}

bool TrayItemView::InResizeAnimation(double animation_value) const {
  // Animation should be delayed for the first part of animating in and last
  // part of animating out, allowing item resize happen before item animating in
  // and after item animating out.
  return ((target_visible_ && animation_value <= kAnimatingInStartValue) ||
          (!target_visible_ && animation_value <= kAnimatingOutEndValue));
}

double TrayItemView::GetResizeProgressFromAnimationProgress(
    double animation_value) const {
  DCHECK(InResizeAnimation(animation_value));
  // When animating in, convert value from [0,kAnimatingInStartValue] to [0,1].
  if (target_visible_)
    return animation_value * (1 / kAnimatingInStartValue);

  // When animating out, convert value from [kAnimatingOutEndValue,0] to [1,0].
  return animation_value * (1 / kAnimatingOutEndValue);
}

double TrayItemView::GetItemScaleProgressFromAnimationProgress(
    double animation_value) const {
  DCHECK(!InResizeAnimation(animation_value));
  // When animating in, convert value from [kAnimatingInStartValue,1] to [0,1].
  if (target_visible_) {
    return (animation_value - kAnimatingInStartValue) *
           (1 / (1 - kAnimatingInStartValue));
  }

  // When animating out, convert value from [1,kAnimatingOutEndValue] to [1,0].
  return (animation_value - kAnimatingOutEndValue) *
         (1 / (1 - kAnimatingOutEndValue));
}

BEGIN_METADATA(TrayItemView)
END_METADATA

}  // namespace ash