chromium/ash/shelf/shelf_app_button.cc

// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "ash/shelf/shelf_app_button.h"

#include <algorithm>
#include <memory>

#include "ash/constants/ash_features.h"
#include "ash/public/cpp/shelf_config.h"
#include "ash/public/cpp/shelf_model.h"
#include "ash/public/cpp/shelf_types.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shelf/scrollable_shelf_view.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_button_delegate.h"
#include "ash/shelf/shelf_view.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/dot_indicator.h"
#include "ash/style/style_util.h"
#include "ash/system/progress_indicator/progress_indicator.h"
#include "ash/wm/desks/desks_controller.h"
#include "ash/wm/window_util.h"
#include "base/debug/stack_trace.h"
#include "base/functional/bind.h"
#include "base/i18n/rtl.h"
#include "base/metrics/histogram_macros.h"
#include "base/time/time.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/services/app_service/public/cpp/app_shortcut_image.h"
#include "skia/ext/image_operations.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_id.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/screen.h"
#include "ui/gfx/animation/animation_delegate.h"
#include "ui/gfx/animation/throb_animation.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/size_conversions.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/gfx/skbitmap_operations.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/animation/ink_drop_impl.h"
#include "ui/views/animation/square_ink_drop_ripple.h"
#include "ui/views/background.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/painter.h"

namespace {

constexpr int kStatusIndicatorRadiusDip = 2;
constexpr int kStatusIndicatorMaxSize = 10;
constexpr int kStatusIndicatorActiveSizeJellyEnabled = 12;
constexpr int kStatusIndicatorRunningSizeJellyEnabled = 6;
constexpr int kStatusIndicatorThickness = 2;

// The size of the notification indicator circle over the size of the icon.
constexpr float kNotificationIndicatorWidthRatio = 14.0f / 64.0f;

// The size of the notification indicator circle padding over the size of the
// icon.
constexpr float kNotificationIndicatorPaddingRatio = 5.0f / 64.0f;

constexpr SkColor kDefaultIndicatorColor = SK_ColorWHITE;
constexpr SkAlpha kInactiveIndicatorOpacity = 0x80;

// The time threshold before an item can be dragged.
constexpr int kDragTimeThresholdMs = 300;

// The time threshold before the ink drop should activate on a long press.
constexpr int kInkDropRippleActivationTimeMs = 650;

// The drag and drop app icon should get scaled by this factor.
constexpr float kAppIconScale = 1.2f;

// The preferred promise icon size for a placeholder icon. Placeholder icons do
// not change size between states.
constexpr int kPlaceholderIconDimension = 24;

// The preferred promise icon size if the app is currently installing.
constexpr int kPromiseIconDimensionInstalling = 28;

// The preferred promise icon size if the app is currently pending.
constexpr int kPromiseIconDimensionPending = 24;

// The width of the promise app progress ring.
constexpr int kPromiseRingStrokeSize = 2;

// The amount of space between the progress ring and the promise app background
// and icon.
constexpr gfx::Insets kProgressRingMarginInstalling = gfx::Insets(-1);
constexpr gfx::Insets kProgressRingMarginPending = gfx::Insets(-2);

// The drag and drop app icon scaling up or down animation transition duration.
constexpr int kDragDropAppIconScaleTransitionMs = 200;

// Simple AnimationDelegate that owns a single ThrobAnimation instance to
// keep all Draw Attention animations in sync.
class ShelfAppButtonAnimation : public gfx::AnimationDelegate {
 public:
  class Observer {
   public:
    virtual void AnimationProgressed() = 0;

   protected:
    virtual ~Observer() = default;
  };

  ShelfAppButtonAnimation(const ShelfAppButtonAnimation&) = delete;
  ShelfAppButtonAnimation& operator=(const ShelfAppButtonAnimation&) = delete;

  static ShelfAppButtonAnimation* GetInstance() {
    static ShelfAppButtonAnimation* s_instance = new ShelfAppButtonAnimation();
    return s_instance;
  }

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

  void RemoveObserver(Observer* observer) {
    observers_.RemoveObserver(observer);
    if (observers_.empty())
      animation_.Stop();
  }

  bool HasObserver(Observer* observer) const {
    return observers_.HasObserver(observer);
  }

  SkAlpha GetAlpha() {
    return GetThrobAnimation().CurrentValueBetween(SK_AlphaTRANSPARENT,
                                                   SK_AlphaOPAQUE);
  }

  double GetAnimation() { return GetThrobAnimation().GetCurrentValue(); }

 private:
  ShelfAppButtonAnimation() : animation_(this) {
    animation_.SetThrobDuration(base::Milliseconds(800));
    animation_.SetTweenType(gfx::Tween::SMOOTH_IN_OUT);
  }

  ~ShelfAppButtonAnimation() override = default;

  gfx::ThrobAnimation& GetThrobAnimation() {
    if (!animation_.is_animating()) {
      animation_.Reset();
      animation_.StartThrobbing(-1 /*throb indefinitely*/);
    }
    return animation_;
  }

  // gfx::AnimationDelegate
  void AnimationProgressed(const gfx::Animation* animation) override {
    if (animation != &animation_)
      return;
    if (!animation_.is_animating())
      return;
    for (auto& observer : observers_)
      observer.AnimationProgressed();
  }

  gfx::ThrobAnimation animation_;
  base::ObserverList<Observer>::Unchecked observers_;
};

// Draws a circular background for a promise icon view.
class PromiseIconBackground : public views::Background {
 public:
  PromiseIconBackground(ui::ColorId color_id,
                        const gfx::Rect& icon_bounds,
                        const gfx::Insets& insets)
      : color_id_(color_id), icon_bounds_(icon_bounds), insets_(insets) {}

  PromiseIconBackground(const PromiseIconBackground&) = delete;
  PromiseIconBackground& operator=(const PromiseIconBackground&) = delete;
  ~PromiseIconBackground() override = default;

  // views::Background:
  void Paint(gfx::Canvas* canvas, views::View* view) const override {
    gfx::Rect bounds = icon_bounds_;
    bounds.Inset(insets_);

    const float radius =
        std::min(bounds.size().width(), bounds.size().height()) / 2.f;

    cc::PaintFlags flags;
    flags.setAntiAlias(true);
    flags.setColor(get_color());

    canvas->DrawCircle(bounds.CenterPoint(), radius, flags);
  }

  void OnViewThemeChanged(views::View* view) override {
    SetNativeControlColor(view->GetColorProvider()->GetColor(color_id_));
    view->SchedulePaint();
  }

 private:
  const ui::ColorId color_id_;
  const gfx::Rect icon_bounds_;
  const gfx::Insets insets_;
};

}  // namespace

namespace ash {

////////////////////////////////////////////////////////////////////////////////
// ShelfAppButton::AppStatusIndicatorView

class ShelfAppButton::AppStatusIndicatorView
    : public gfx::AnimationDelegate,
      public views::View,
      public ShelfAppButtonAnimation::Observer {
  METADATA_HEADER(AppStatusIndicatorView, views::View)

 public:
  AppStatusIndicatorView() {
    // Make sure the events reach the parent view for handling.
    SetCanProcessEventsWithinSubtree(false);
    status_change_animation_ = std::make_unique<gfx::SlideAnimation>(this);
    status_change_animation_->SetSlideDuration(base::Milliseconds(250));
    status_change_animation_->SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN);
  }

  AppStatusIndicatorView(const AppStatusIndicatorView&) = delete;
  AppStatusIndicatorView& operator=(const AppStatusIndicatorView&) = delete;

  ~AppStatusIndicatorView() override {
    ShelfAppButtonAnimation::GetInstance()->RemoveObserver(this);
  }

  // views::View:
  void OnThemeChanged() override {
    views::View::OnThemeChanged();
    SchedulePaint();
  }

  void OnPaint(gfx::Canvas* canvas) override {
    if (!GetColorProvider()) {
      return;
    }

    gfx::ScopedCanvas scoped(canvas);

    const float dsf = canvas->UndoDeviceScaleFactor();
    gfx::PointF center = gfx::RectF(GetLocalBounds()).CenterPoint();
    cc::PaintFlags flags;
    flags.setColor(GetJellyColor());

    // Active and running indicators look a little different in the new UI.
    flags.setAntiAlias(true);
    flags.setStrokeCap(cc::PaintFlags::Cap::kRound_Cap);
    flags.setStrokeJoin(cc::PaintFlags::Join::kRound_Join);
    flags.setStrokeWidth(kStatusIndicatorThickness);
    flags.setStyle(cc::PaintFlags::kStroke_Style);
    float stroke_length = GetStrokeLength();
    gfx::PointF start;
    gfx::PointF end;
    if (horizontal_shelf_) {
      start = gfx::PointF(center.x() - stroke_length / 2, center.y());
      end = start;
      end.Offset(stroke_length, 0);
    } else {
      start = gfx::PointF(center.x(), center.y() - stroke_length / 2);
      end = start;
      end.Offset(0, stroke_length);
    }
    SkPath path;
    path.moveTo(start.x() * dsf, start.y() * dsf);
    path.lineTo(end.x() * dsf, end.y() * dsf);
    canvas->DrawPath(path, flags);
  }

  float GetStrokeLength() {
    if (status_change_animation_->is_animating()) {
      return status_change_animation_->CurrentValueBetween(
          kStatusIndicatorRunningSizeJellyEnabled,
          kStatusIndicatorActiveSizeJellyEnabled);
    }

    return active_ ? kStatusIndicatorActiveSizeJellyEnabled
                   : kStatusIndicatorRunningSizeJellyEnabled;
  }

  SkColor GetJellyColor() {
    const SkColor active_color =
        GetColorProvider()->GetColor(cros_tokens::kCrosSysOnSurface);
    const SkColor inactive_color =
        GetColorProvider()->GetColor(cros_tokens::kCrosSysSecondary);
    if (show_attention_) {
      if (!ShelfAppButtonAnimation::GetInstance()->HasObserver(this)) {
        return active_color;
      }
      return SkColorSetA(active_color,
                         ShelfAppButtonAnimation::GetInstance()->GetAlpha());
    }

    if (status_change_animation_->is_animating()) {
      return gfx::Tween::ColorValueBetween(
          status_change_animation_->GetCurrentValue(), inactive_color,
          active_color);
    }
    return active_ ? active_color : inactive_color;
  }

  SkAlpha GetAlpha() {
    if (show_attention_) {
      return ShelfAppButtonAnimation::GetInstance()->HasObserver(this)
                 ? ShelfAppButtonAnimation::GetInstance()->GetAlpha()
                 : SK_AlphaOPAQUE;
    }

    if (status_change_animation_->is_animating()) {
      return status_change_animation_->CurrentValueBetween(
          kInactiveIndicatorOpacity, SK_AlphaOPAQUE);
    }

    return active_ ? SK_AlphaOPAQUE : kInactiveIndicatorOpacity;
  }

  // ShelfAppButtonAnimation::Observer
  void AnimationProgressed() override {
    UpdateAnimating();
    SchedulePaint();
  }

  void ShowAttention(bool show) {
    if (show_attention_ == show)
      return;

    show_attention_ = show;

    if (status_change_animation_->is_animating())
      status_change_animation_->End();

    if (show_attention_) {
      animation_end_time_ = base::TimeTicks::Now() + base::Seconds(10);
      ShelfAppButtonAnimation::GetInstance()->AddObserver(this);
    } else {
      ShelfAppButtonAnimation::GetInstance()->RemoveObserver(this);
    }
  }

  // gfx::AnimationDelegate
  void AnimationProgressed(const gfx::Animation* animation) override {
    if (animation != status_change_animation_.get())
      return;
    SchedulePaint();
  }

  void ShowActiveStatus(bool active) {
    if (active_ == active)
      return;
    active_ = active;
    if (active_)
      status_change_animation_->Show();
    else
      status_change_animation_->Hide();
  }

  void SetHorizontalShelf(bool horizontal_shelf) {
    if (horizontal_shelf_ == horizontal_shelf)
      return;
    horizontal_shelf_ = horizontal_shelf;
    SchedulePaint();
  }

 private:
  void UpdateAnimating() {
    if (base::TimeTicks::Now() > animation_end_time_)
      ShelfAppButtonAnimation::GetInstance()->RemoveObserver(this);
  }

  bool show_attention_ = false;
  bool active_ = false;
  bool horizontal_shelf_ = true;
  std::unique_ptr<gfx::SlideAnimation> status_change_animation_;
  base::TimeTicks animation_end_time_;  // For attention throbbing underline.
};

BEGIN_METADATA(ShelfAppButton, AppStatusIndicatorView)
END_METADATA

////////////////////////////////////////////////////////////////////////////////
// ShelfAppButton

// static
bool ShelfAppButton::ShouldHandleEventFromContextMenu(
    const ui::GestureEvent* event) {
  switch (event->type()) {
    case ui::EventType::kGestureEnd:
    case ui::EventType::kGestureTapCancel:
    case ui::EventType::kGestureScrollBegin:
    case ui::EventType::kGestureScrollUpdate:
    case ui::EventType::kGestureScrollEnd:
    case ui::EventType::kScrollFlingStart:
      return true;
    default:
      return false;
  }
}

ShelfAppButton::ShelfAppButton(ShelfView* shelf_view,
                               ShelfButtonDelegate* shelf_button_delegate)
    : ShelfButton(shelf_view->shelf(), shelf_button_delegate),
      shelf_view_(shelf_view),
      indicator_(new AppStatusIndicatorView()) {
  const gfx::ShadowValue kShadows[] = {
      gfx::ShadowValue(gfx::Vector2d(0, 2), 0, SkColorSetARGB(0x1A, 0, 0, 0)),
      gfx::ShadowValue(gfx::Vector2d(0, 3), 1, SkColorSetARGB(0x1A, 0, 0, 0)),
      gfx::ShadowValue(gfx::Vector2d(0, 0), 1, SkColorSetARGB(0x54, 0, 0, 0)),
  };
  icon_shadows_.assign(kShadows, kShadows + std::size(kShadows));

  views::InkDrop::Get(this)->SetMode(
      views::InkDropHost::InkDropMode::ON_NO_GESTURE_HANDLER);
  views::InkDrop::UseInkDropForSquareRipple(views::InkDrop::Get(this),
                                            /*highlight_on_hover=*/false);

  views::InkDrop::Get(this)->SetCreateRippleCallback(base::BindRepeating(
      [](ShelfAppButton* host) -> std::unique_ptr<views::InkDropRipple> {
        const gfx::Rect small_ripple_area = host->CalculateSmallRippleArea();
        const int ripple_size = host->shelf_view_->GetShelfItemRippleSize();

        auto* const ink_drop = views::InkDrop::Get(host);
        const SkColor base_color = ink_drop->GetBaseColor();
        const float base_alpha = SkColorGetA(base_color);
        return std::make_unique<views::SquareInkDropRipple>(
            ink_drop, gfx::Size(ripple_size, ripple_size),
            ink_drop->GetLargeCornerRadius(), small_ripple_area.size(),
            ink_drop->GetSmallCornerRadius(), small_ripple_area.CenterPoint(),
            SkColorSetA(base_color, SK_AlphaOPAQUE),
            (base_alpha / SK_AlphaOPAQUE) * ink_drop->GetVisibleOpacity());
      },
      this));

  // TODO: refactor the layers so each button doesn't require 3.
  // |icon_view_| needs its own layer so it can be scaled up independently of
  // the ink drop ripple.
  icon_view_ = AddChildView(std::make_unique<views::ImageView>());

  icon_view_->SetPaintToLayer();
  icon_view_->layer()->SetFillsBoundsOpaquely(false);
  icon_view_->SetHorizontalAlignment(views::ImageView::Alignment::kCenter);
  icon_view_->SetVerticalAlignment(views::ImageView::Alignment::kLeading);
  // Do not make this interactive, so that events are sent to ShelfView.
  icon_view_->SetCanProcessEventsWithinSubtree(false);

  indicator_->SetPaintToLayer();
  indicator_->layer()->SetFillsBoundsOpaquely(false);

  AddChildView(indicator_.get());

  notification_indicator_ =
      AddChildView(std::make_unique<DotIndicator>(kDefaultIndicatorColor));

  views::InkDrop::Get(this)->GetInkDrop()->AddObserver(this);

  // Do not set a clip, allow the ink drop to burst out.
  views::InstallEmptyHighlightPathGenerator(this);
  SetFocusBehavior(FocusBehavior::ALWAYS);
  SetInstallFocusRingOnFocus(true);
  views::FocusRing::Get(this)->SetOutsetFocusRingDisabled(true);
  views::FocusRing::Get(this)->SetColorId(cros_tokens::kCrosSysFocusRing);
  // The focus ring should have an inset of half the focus border thickness, so
  // the parent view won't clip it.
  views::FocusRing::Get(this)->SetPathGenerator(
      std::make_unique<views::RoundRectHighlightPathGenerator>(
          gfx::Insets::VH(views::FocusRing::kDefaultHaloThickness / 2, 0), 0));

  UpdateAccessibleDescription();
}

ShelfAppButton::~ShelfAppButton() {
  views::InkDrop::Get(this)->GetInkDrop()->RemoveObserver(this);
}

void ShelfAppButton::SetShadowedImage(const gfx::ImageSkia& image) {
  icon_view_->SetImage(gfx::ImageSkiaOperations::CreateImageWithDropShadow(
      image, icon_shadows_));
}

void ShelfAppButton::UpdateMainAndMaybeHostBadgeIconImage() {
  if (is_promise_app_ || progress_indicator_ || has_host_badge_) {
    icon_view_->SetImage(GetIconImage(icon_scale_));
    return;
  }

  SetShadowedImage(GetIconImage(icon_scale_));
}

gfx::ImageSkia ShelfAppButton::GetImage() const {
  return icon_view_->GetImage();
}

gfx::ImageSkia ShelfAppButton::GetIconImage(float icon_scale) const {
  gfx::ImageSkia icon_image;

  bool use_fallback_icon =
      force_fallback_icon_ || !fallback_icon_image_model_.IsEmpty();

  const ui::ImageModel& image_model =
      use_fallback_icon ? fallback_icon_image_model_ : icon_image_model_;

  auto* color_provider = GetColorProvider();
  if (!color_provider) {
    color_provider = shelf_view_->GetColorProvider();
  }
  if (image_model.IsImage()) {
    icon_image = image_model.GetImage().AsImageSkia();
  } else if (image_model.IsVectorIcon()) {
    icon_image = ui::ThemedVectorIcon(image_model.GetVectorIcon())
                     .GetImageSkia(color_provider);
  }
  const gfx::Size preferred_size =
      GetPreferredIconSize(image_model, icon_scale);
  if (icon_image.size() != preferred_size) {
    icon_image = gfx::ImageSkiaOperations::CreateResizedImage(
        icon_image, skia::ImageOperations::RESIZE_BEST, preferred_size);
  }

  if (has_host_badge_ && GetColorProvider()) {
    const int main_icon_radius = std::round(
        icon_scale * shelf_view_->GetShelfShortcutIconContainerSize() / 2.0f);
    const int badge_radius = std::round(
        icon_scale * shelf_view_->GetShelfShortcutHostBadgeContainerSize() /
        2.0f);
    const int teardrop_corner_radius = std::round(
        icon_scale * shelf_view_->GetShelfShortcutTeardropCornerRadiusSize());
    const int badge_icon_size = std::round(
        icon_scale * shelf_view_->GetShelfShortcutHostBadgeIconSize());

    return apps::AppShortcutImage::CreateImageWithBadgeAndTeardropBackground(
        main_icon_radius, teardrop_corner_radius, badge_radius,
        GetColorProvider()->GetColor(cros_tokens::kCrosSysSystemOnBaseOpaque),
        icon_image,
        gfx::ImageSkiaOperations::CreateResizedImage(
            host_badge_image_, skia::ImageOperations::RESIZE_BEST,
            gfx::Size(badge_icon_size, badge_icon_size)));
  }
  return icon_image;
}

gfx::ImageSkia ShelfAppButton::GetBadgeIconImage(float icon_scale) const {
  if (host_badge_image_.isNull()) {
    return gfx::ImageSkia();
  }

  const int background_radius =
      std::round(icon_scale *
                 shelf_view_->GetShelfShortcutHostBadgeContainerSize() / 2.0f);
  const int icon_size =
      std::round(icon_scale * shelf_view_->GetShelfShortcutHostBadgeIconSize());
  return gfx::ImageSkiaOperations::CreateImageWithCircleBackground(
      background_radius,
      GetColorProvider()->GetColor(cros_tokens::kCrosSysSystemOnBaseOpaque),
      gfx::ImageSkiaOperations::CreateResizedImage(
          host_badge_image_, skia::ImageOperations::RESIZE_BEST,
          gfx::Size(icon_size, icon_size)));
}

void ShelfAppButton::SetMainAndMaybeHostBadgeImage(
    const gfx::ImageSkia& main_image,
    bool has_placeholder_icon,
    const gfx::ImageSkia& host_badge_image) {
  has_icon_image_ = !main_image.isNull() && !has_placeholder_icon;
  has_host_badge_ = !host_badge_image.isNull();

  if (has_icon_image_ && !force_fallback_icon_) {
    fallback_icon_image_model_ = ui::ImageModel();
    if (!progress_indicator_) {
      // Clear background set as a result of adding progress indicator.
      SetBackground(nullptr);
    }
    if (has_host_badge_) {
      host_badge_image_ = host_badge_image;
    }
  }

  if (is_promise_app_ && has_placeholder_icon) {
    icon_image_model_ = ui::ImageModel(ui::ImageModel::FromVectorIcon(
        ash::kPlaceholderAppIcon, cros_tokens::kCrosSysPrimary));
  } else {
    icon_image_model_ =
        ui::ImageModel(ui::ImageModel::FromImageSkia(main_image));
  }

  UpdateMainAndMaybeHostBadgeIconImage();
}

void ShelfAppButton::AddState(State state) {
  if (!(state_ & state)) {
    state_ |= state;
    InvalidateLayout();
    if (state & STATE_ATTENTION)
      indicator_->ShowAttention(true);

    if (state & STATE_ACTIVE)
      indicator_->ShowActiveStatus(true);

    if (state & STATE_NOTIFICATION)
      notification_indicator_->SetVisible(true);

    if (state & STATE_DRAGGING)
      ScaleAppIcon(true);
  }
}

void ShelfAppButton::ClearState(State state) {
  if (state_ & state) {
    state_ &= ~state;
    DeprecatedLayoutImmediately();
    if (state & STATE_ATTENTION)
      indicator_->ShowAttention(false);
    if (state & STATE_ACTIVE)
      indicator_->ShowActiveStatus(false);

    if (state & STATE_NOTIFICATION)
      notification_indicator_->SetVisible(false);

    if (state & STATE_DRAGGING)
      ScaleAppIcon(false);
  }
}

void ShelfAppButton::ClearDragStateOnGestureEnd() {
  drag_timer_.Stop();
  ClearState(STATE_HOVERED);
  ClearState(STATE_DRAGGING);
}

gfx::Rect ShelfAppButton::GetIconBounds() const {
  return icon_view_->bounds();
}

gfx::Rect ShelfAppButton::GetIdealIconBounds(const gfx::Size& button_size,
                                             float icon_scale) const {
  return GetIconViewBounds(gfx::Rect(button_size), icon_scale,
                           /*ignore_shadow_insets=*/false);
}

views::InkDrop* ShelfAppButton::GetInkDropForTesting() {
  return views::InkDrop::Get(this)->GetInkDrop();
}

void ShelfAppButton::OnDragStarted(const ui::LocatedEvent* event) {
  views::InkDrop::Get(this)->AnimateToState(views::InkDropState::HIDDEN, event);
}

void ShelfAppButton::OnMenuClosed() {
  views::InkDrop::Get(this)->GetInkDrop()->AnimateToState(
      views::InkDropState::DEACTIVATED);
  context_menu_target_visibility_ = false;
}

void ShelfAppButton::ShowContextMenu(const gfx::Point& p,
                                     ui::MenuSourceType source_type) {
  // Return early if:
  // 1. the context menu controller is not set; or
  // 2. `context_menu_target_visibility_` is already true.
  if (!context_menu_controller() || context_menu_target_visibility_)
    return;

  context_menu_target_visibility_ = true;
  auto weak_this = weak_factory_.GetWeakPtr();

  ShelfButton::ShowContextMenu(p, source_type);

  // This object may have been destroyed by ShowContextMenu.
  if (weak_this) {
    // The menu will not propagate mouse events while it's shown. To address,
    // the hover state gets cleared once the menu was shown (and this was not
    // destroyed). In case context menu is shown target view does not receive
    // OnMouseReleased events and we need to cancel capture manually.
    if (shelf_view_->IsDraggedView(this))
      OnMouseCaptureLost();
    else
      ClearState(STATE_HOVERED);
  }
}

void ShelfAppButton::GetAccessibleNodeData(ui::AXNodeData* node_data) {
  ShelfButton::GetAccessibleNodeData(node_data);
  const std::u16string accessible_name = GetViewAccessibility().GetCachedName();
  node_data->SetName(!accessible_name.empty()
                         ? accessible_name
                         : shelf_view_->GetTitleForView(this));
}

bool ShelfAppButton::ShouldEnterPushedState(const ui::Event& event) {
  if (!shelf_view_->ShouldEventActivateButton(this, event))
    return false;

  return Button::ShouldEnterPushedState(event);
}

void ShelfAppButton::ReflectItemStatus(const ShelfItem& item) {
  if (item.has_notification)
    AddState(ShelfAppButton::STATE_NOTIFICATION);
  else
    ClearState(ShelfAppButton::STATE_NOTIFICATION);

  is_promise_app_ = item.is_promise_app;

  package_id_ = item.package_id;

  // Progress is incremental always by server side implementation. Do not use
  // equal for comparing progress as float point errors may surface.
  if (progress_ < item.progress || app_status_ != item.app_status) {
    progress_ = item.progress;
    app_status_ = item.app_status;
    UpdateProgressRingBounds();
    UpdateAccessibleDescription();
  }

  const ShelfID active_id = shelf_view_->model()->active_shelf_id();
  if (!active_id.IsNull() && item.id == active_id) {
    // The active status trumps all other statuses.
    AddState(ShelfAppButton::STATE_ACTIVE);
    ClearState(ShelfAppButton::STATE_RUNNING);
    ClearState(ShelfAppButton::STATE_ATTENTION);

    // Notify the parent scrollable shelf view to show the current active app.
    shelf_button_delegate()->OnAppButtonActivated(this);
    return;
  }

  ClearState(ShelfAppButton::STATE_ACTIVE);

  switch (item.status) {
    case STATUS_CLOSED:
      ClearState(ShelfAppButton::STATE_RUNNING);
      ClearState(ShelfAppButton::STATE_ATTENTION);
      break;
    case STATUS_RUNNING:
      AddState(ShelfAppButton::STATE_RUNNING);
      ClearState(ShelfAppButton::STATE_ATTENTION);
      break;
    case STATUS_ATTENTION:
      ClearState(ShelfAppButton::STATE_RUNNING);
      AddState(ShelfAppButton::STATE_ATTENTION);
      break;
  }
}

bool ShelfAppButton::IsIconSizeCurrent() {
  gfx::Insets insets_shadows = gfx::ShadowValue::GetMargin(icon_shadows_);
  int icon_width =
      GetIconBounds().width() + insets_shadows.left() + insets_shadows.right();

  return icon_width == shelf_view_->GetButtonIconSize();
}

void ShelfAppButton::AnimateInFromPromiseApp(
    const ui::ImageModel& fallback_icon,
    const base::RepeatingClosure& callback) {
  forced_progress_indicator_value_ = 0.9999f;
  UpdateProgressRingBounds();

  force_fallback_icon_ = true;
  fallback_icon_image_model_ = fallback_icon;
  UpdateMainAndMaybeHostBadgeIconImage();

  gfx::Rect icon_bounds(GetIconViewBounds(GetContentsBounds(), 1.0f,
                                          /*ignore_shadow_insets=*/false));
  const float starting_size =
      fallback_icon_image_model_.IsVectorIcon()
          ? kPlaceholderIconDimension
          : static_cast<float>(kPromiseIconDimensionInstalling);
  // TODO(b/297866814): Shadow insets are ignored for promise apps when
  // calculating icon bounds - make `GetIconViewBounds()` explicitly ignore
  // them.
  gfx::Rect promise_icon_bounds = GetIconViewBounds(
      GetContentsBounds(),
      starting_size / static_cast<float>(shelf_view_->GetButtonIconSize()),
      /*ignore_shadow_insets=*/true);
  icon_view_->layer()->SetTransform(gfx::TransformBetweenRects(
      gfx::RectF(icon_bounds), gfx::RectF(promise_icon_bounds)));

  // Animate the app list view out of the promise app state.
  views::AnimationBuilder animation;
  animation.OnEnded(base::BindOnce(&ShelfAppButton::OnAnimatedInFromPromiseApp,
                                   weak_factory_.GetWeakPtr(), callback));
  animation.OnAborted(
      base::BindOnce(&ShelfAppButton::OnAnimatedInFromPromiseApp,
                     weak_factory_.GetWeakPtr(), callback));
  animation.Once()
      .SetDuration(base::Milliseconds(100))
      .SetOpacity(progress_indicator_->layer(), 0.0f,
                  gfx::Tween::FAST_OUT_LINEAR_IN)
      .SetTransform(icon_view_->layer(), gfx::Transform(),
                    gfx::Tween::FAST_OUT_LINEAR_IN);
}

void ShelfAppButton::OnContextMenuModelRequestCanceled() {
  // The request for the context menu model gets canceled so reset the context
  // menu target visibility.
  context_menu_target_visibility_ = false;
}

bool ShelfAppButton::FireDragTimerForTest() {
  if (!drag_timer_.IsRunning())
    return false;
  drag_timer_.FireNow();
  return true;
}

void ShelfAppButton::FireRippleActivationTimerForTest() {
  ripple_activation_timer_.FireNow();
}

gfx::Rect ShelfAppButton::CalculateSmallRippleArea() const {
  int ink_drop_small_size = shelf_view_->GetButtonSize();
  gfx::Point center_point = GetLocalBounds().CenterPoint();
  const int padding = ShelfConfig::Get()->GetAppIconEndPadding();

  // Add padding to the ink drop for the left-most and right-most app buttons in
  // the shelf when there is a non-zero padding between the app icon and the
  // end of scrollable shelf.
  if (display::Screen::GetScreen()->InTabletMode() && padding > 0) {
    // Note that `current_index` may be nullopt while the button is fading out
    // after it's been removed from the model - for example, see
    // https://crbug.com/1355561.
    const std::optional<size_t> current_index =
        shelf_view_->view_model()->GetIndexOfView(this);
    int left_padding =
        (shelf_view_->visible_views_indices().front() == current_index)
            ? padding
            : 0;
    int right_padding =
        (shelf_view_->visible_views_indices().back() == current_index) ? padding
                                                                       : 0;

    if (base::i18n::IsRTL())
      std::swap(left_padding, right_padding);

    ink_drop_small_size += left_padding + right_padding;

    const int x_offset = (-left_padding / 2) + (right_padding / 2);
    center_point.Offset(x_offset, 0);
  }

  gfx::Rect small_ripple_area(
      gfx::Size(ink_drop_small_size, ink_drop_small_size));
  small_ripple_area.Offset(center_point.x() - ink_drop_small_size / 2,
                           center_point.y() - ink_drop_small_size / 2);
  return small_ripple_area;
}

bool ShelfAppButton::OnMousePressed(const ui::MouseEvent& event) {
  // Clear any closing desks so that the user does not try to interact with an
  // app that is open on a closing desk.
  DesksController::Get()->MaybeCommitPendingDeskRemoval();

  // TODO: This call should probably live somewhere else (such as inside
  // |ShelfView.PointerPressedOnButton|.
  // No need to scale up the app for mouse right click since the app can't be
  // dragged through right button.
  if (!(event.flags() & ui::EF_LEFT_MOUSE_BUTTON)) {
    Button::OnMousePressed(event);
    return true;
  }

  ShelfButton::OnMousePressed(event);
  shelf_view_->PointerPressedOnButton(this, ShelfView::MOUSE, event);

  if (shelf_view_->IsDraggedView(this)) {
    drag_timer_.Start(FROM_HERE, base::Milliseconds(kDragTimeThresholdMs),
                      base::BindOnce(&ShelfAppButton::OnTouchDragTimer,
                                     base::Unretained(this)));
  }
  return true;
}

void ShelfAppButton::OnMouseReleased(const ui::MouseEvent& event) {
  drag_timer_.Stop();
  ClearState(STATE_DRAGGING);
  ShelfButton::OnMouseReleased(event);
  // PointerReleasedOnButton deletes the ShelfAppButton when user drags a pinned
  // running app from shelf.
  shelf_view_->PointerReleasedOnButton(this, ShelfView::MOUSE, false);
  // WARNING: we may have been deleted.
}

void ShelfAppButton::OnMouseCaptureLost() {
  ClearState(STATE_HOVERED);
  shelf_view_->PointerReleasedOnButton(this, ShelfView::MOUSE, true);
  ShelfButton::OnMouseCaptureLost();
}

bool ShelfAppButton::OnMouseDragged(const ui::MouseEvent& event) {
  ShelfButton::OnMouseDragged(event);
  shelf_view_->PointerDraggedOnButton(this, ShelfView::MOUSE, event);
  return true;
}

bool ShelfAppButton::ImageModelHasPlaceholderIcon() const {
  bool use_fallback_icon =
      force_fallback_icon_ || !fallback_icon_image_model_.IsEmpty();
  return use_fallback_icon ? fallback_icon_image_model_.IsVectorIcon()
                           : icon_image_model_.IsVectorIcon();
}

float ShelfAppButton::GetIconDimensionByAppState() const {
  if (is_promise_app_ && features::ArePromiseIconsEnabled()) {
    if (ImageModelHasPlaceholderIcon()) {
      return kPromiseIconDimensionPending;
    }

    switch (app_status_) {
      case AppStatus::kPending:
        return kPromiseIconDimensionPending;
      case AppStatus::kInstalling:
      case AppStatus::kInstallCancelled:
      case AppStatus::kInstallSuccess:
      case AppStatus::kPaused:
        return kPromiseIconDimensionInstalling;
      case AppStatus::kReady:
      case AppStatus::kBlocked:
        return shelf_view_->GetButtonIconSize();
    }
  }

  return shelf_view_->GetButtonIconSize();
}

gfx::Rect ShelfAppButton::GetIconViewBounds(const gfx::Rect& button_bounds,
                                            float icon_scale,
                                            bool ignore_shadow_insets) const {
  const float icon_size =
      (ImageModelHasPlaceholderIcon() ? kPlaceholderIconDimension
                                      : GetIconDimensionByAppState()) *
      icon_scale;
  const float icon_padding = (shelf_view_->GetButtonSize() - icon_size) / 2;

  const Shelf* shelf = shelf_view_->shelf();
  const bool is_horizontal_shelf = shelf->IsHorizontalAlignment();
  float x_offset = is_horizontal_shelf ? 0 : icon_padding;
  float y_offset = is_horizontal_shelf ? icon_padding : 0;

  const float icon_width =
      std::min(icon_size, button_bounds.width() - x_offset);
  const float icon_height =
      std::min(icon_size, button_bounds.height() - y_offset);

  // If on the left or top 'invert' the inset so the constant gap is on
  // the interior (towards the center of display) edge of the shelf.
  if (ShelfAlignment::kLeft == shelf->alignment())
    x_offset = button_bounds.width() - (icon_size + icon_padding);

  // Expand bounds to include shadows.
  // TODO(b/297866814): Promise icon calculation looks off because of the shadow
  // insets. To get a centered icon within the ring, we removed insets for
  // shadows. Consider improving the calculation on UpdateProgressRingBounds()
  // to account for the shadows as well.
  gfx::Insets insets_shadows =
      (is_promise_app_ || has_host_badge_ || ignore_shadow_insets)
          ? gfx::Insets()
          : gfx::ShadowValue::GetMargin(icon_shadows_);
  // Center icon with respect to the secondary axis.
  if (is_horizontal_shelf) {
    x_offset = std::max(0.0f, button_bounds.width() - icon_width) / 2;
  } else {
    y_offset = std::max(0.0f, button_bounds.height() - icon_height) / 2;
  }
  gfx::RectF icon_view_bounds =
      gfx::RectF(button_bounds.x() + x_offset, button_bounds.y() + y_offset,
                 icon_width, icon_height);

  icon_view_bounds.Inset(gfx::InsetsF(insets_shadows));
  // Icon size has been incorrect when running
  // PanelLayoutManagerTest.PanelAlignmentSecondDisplay on valgrind bot, see
  // http://crbug.com/234854.
  DCHECK_LE(icon_width, icon_size);
  DCHECK_LE(icon_height, icon_size);
  return gfx::ToRoundedRect(icon_view_bounds);
}

gfx::Rect ShelfAppButton::GetNotificationIndicatorBounds(float icon_scale) {
  gfx::Rect scaled_icon_view_bounds = GetIconViewBounds(
      GetContentsBounds(), icon_scale, /*ignore_shadow_bounds=*/false);
  float diameter =
      kNotificationIndicatorWidthRatio * scaled_icon_view_bounds.width();
  float padding =
      kNotificationIndicatorPaddingRatio * scaled_icon_view_bounds.width();
  return gfx::ToRoundedRect(
      gfx::RectF(scaled_icon_view_bounds.right() - diameter - padding,
                 scaled_icon_view_bounds.y() + padding, diameter, diameter));
}

void ShelfAppButton::Layout(PassKey) {
  Shelf* shelf = shelf_view_->shelf();
  gfx::Rect icon_view_bounds =
      GetIconViewBounds(GetContentsBounds(), icon_scale_,
                        /*ignore_shadow_bounds=*/false);
  const gfx::Rect button_bounds(GetContentsBounds());
  const int status_indicator_offet_from_shelf_edge =
      ShelfConfig::Get()->status_indicator_offset_from_shelf_edge();
  icon_view_->SetBoundsRect(icon_view_bounds);

  notification_indicator_->SetIndicatorBounds(
      GetNotificationIndicatorBounds(icon_scale_));

  // The indicators should be aligned with the icon, not the icon + shadow.
  // Use 1.0 as icon scale for |indicator_midpoint|, otherwise integer rounding
  // can incorrectly move the midpoint.
  gfx::Point indicator_midpoint =
      GetIconViewBounds(GetContentsBounds(), 1.0,
                        /*ignore_shadow_bounds=*/false)
          .CenterPoint();
  switch (shelf->alignment()) {
    case ShelfAlignment::kBottom:
    case ShelfAlignment::kBottomLocked:
      indicator_midpoint.set_y(button_bounds.bottom() -
                               kStatusIndicatorRadiusDip -
                               status_indicator_offet_from_shelf_edge);
      break;
    case ShelfAlignment::kLeft:
      indicator_midpoint.set_x(button_bounds.x() + kStatusIndicatorRadiusDip +
                               status_indicator_offet_from_shelf_edge);
      break;
    case ShelfAlignment::kRight:
      indicator_midpoint.set_x(button_bounds.right() -
                               kStatusIndicatorRadiusDip -
                               status_indicator_offet_from_shelf_edge);
      break;
  }

  gfx::Rect indicator_bounds(indicator_midpoint, gfx::Size());
  indicator_bounds.Inset(gfx::Insets(-kStatusIndicatorMaxSize));
  indicator_->SetBoundsRect(indicator_bounds);

  UpdateState();
  views::FocusRing::Get(this)->DeprecatedLayoutImmediately();
  UpdateProgressRingBounds();
}

void ShelfAppButton::ChildPreferredSizeChanged(views::View* child) {
  DeprecatedLayoutImmediately();
}

void ShelfAppButton::OnThemeChanged() {
  ShelfButton::OnThemeChanged();

  UpdateMainAndMaybeHostBadgeIconImage();

  // Redraw progress indicator to adjust colors.
  if (progress_indicator_) {
    progress_indicator_->InvalidateLayer();
  }
}

void ShelfAppButton::OnGestureEvent(ui::GestureEvent* event) {
  switch (event->type()) {
    case ui::EventType::kGestureTapDown:
      if (shelf_view_->shelf()->IsVisible()) {
        AddState(STATE_HOVERED);
        drag_timer_.Start(FROM_HERE, base::Milliseconds(kDragTimeThresholdMs),
                          base::BindOnce(&ShelfAppButton::OnTouchDragTimer,
                                         base::Unretained(this)));
        ripple_activation_timer_.Start(
            FROM_HERE, base::Milliseconds(kInkDropRippleActivationTimeMs),
            base::BindOnce(&ShelfAppButton::OnRippleTimer,
                           base::Unretained(this)));
        views::InkDrop::Get(this)->GetInkDrop()->AnimateToState(
            views::InkDropState::ACTION_PENDING);
        event->SetHandled();
      }
      break;
    case ui::EventType::kGestureTap:
      [[fallthrough]];  // Ensure tapped items are not enlarged for drag.
    case ui::EventType::kGestureEnd:
      // If the button is being dragged, or there is an active context menu,
      // for this ShelfAppButton, don't deactivate the ink drop.
      if (!(state_ & STATE_DRAGGING) &&
          !shelf_view_->IsShowingMenuForView(this) &&
          (views::InkDrop::Get(this)->GetInkDrop()->GetTargetInkDropState() ==
           views::InkDropState::ACTIVATED)) {
        views::InkDrop::Get(this)->GetInkDrop()->AnimateToState(
            views::InkDropState::DEACTIVATED);
      } else if (event->type() == ui::EventType::kGestureEnd) {
        // When the gesture ends, we may need to deactivate the button's
        // inkdrop. For example, when a mouse event interputs the gesture press
        // on a shelf app button, the button's inkdrop could be in the pending
        // state while the button's context menu is hidden. In this case, we
        // have to hide the inkdrop explicitly.
        MaybeHideInkDropWhenGestureEnds();
      }

      ClearDragStateOnGestureEnd();
      break;
    case ui::EventType::kGestureScrollBegin:
      if (state_ & STATE_DRAGGING) {
        shelf_view_->PointerPressedOnButton(this, ShelfView::TOUCH, *event);
        event->SetHandled();
      } else {
        // The drag went to the bezel and is about to be passed to
        // ShelfLayoutManager.
        drag_timer_.Stop();
        views::InkDrop::Get(this)->GetInkDrop()->AnimateToState(
            views::InkDropState::HIDDEN);
      }
      break;
    case ui::EventType::kGestureScrollUpdate:
      if ((state_ & STATE_DRAGGING) && shelf_view_->IsDraggedView(this)) {
        shelf_view_->PointerDraggedOnButton(this, ShelfView::TOUCH, *event);
        event->SetHandled();
      }
      break;
    case ui::EventType::kGestureScrollEnd:
    case ui::EventType::kScrollFlingStart:
      if (state_ & STATE_DRAGGING) {
        ClearState(STATE_DRAGGING);
        shelf_view_->PointerReleasedOnButton(this, ShelfView::TOUCH, false);
        event->SetHandled();
      }
      break;
    case ui::EventType::kGestureLongTap:
      views::InkDrop::Get(this)->GetInkDrop()->AnimateToState(
          views::InkDropState::ACTIVATED);

      // The context menu may not show (for example, a mouse click which occurs
      // before the end of gesture could close the context menu). In this case,
      // let the overridden function handles the event to show the context menu
      // (see https://crbug.com/1126491).
      if (shelf_view_->IsShowingMenu()) {
        // Handle LONG_TAP to avoid opening the context menu twice.
        event->SetHandled();
      }
      break;
    case ui::EventType::kGestureTwoFingerTap:
      views::InkDrop::Get(this)->GetInkDrop()->AnimateToState(
          views::InkDropState::ACTIVATED);
      break;
    default:
      break;
  }

  if (!event->handled())
    return Button::OnGestureEvent(event);
}

bool ShelfAppButton::HandleAccessibleAction(
    const ui::AXActionData& action_data) {
  if (notification_indicator_ && notification_indicator_->GetVisible())
    shelf_view_->AnnounceShelfItemNotificationBadge(this);

  if (action_data.action == ax::mojom::Action::kScrollToMakeVisible)
    shelf_button_delegate()->HandleAccessibleActionScrollToMakeVisible(this);

  return views::View::HandleAccessibleAction(action_data);
}

void ShelfAppButton::InkDropAnimationStarted() {
  SetInkDropAnimationStarted(/*started=*/true);
}

void ShelfAppButton::InkDropRippleAnimationEnded(views::InkDropState state) {
  // Notify the host view of the ink drop to be hidden at the end of ink drop
  // animation.
  if (state == views::InkDropState::HIDDEN)
    SetInkDropAnimationStarted(/*started=*/false);
}

void ShelfAppButton::UpdateState() {
  indicator_->SetVisible(!(state_ & STATE_HIDDEN) &&
                         (state_ & STATE_ATTENTION || state_ & STATE_RUNNING ||
                          state_ & STATE_ACTIVE));

  const bool is_horizontal_shelf =
      shelf_view_->shelf()->IsHorizontalAlignment();
  indicator_->SetHorizontalShelf(is_horizontal_shelf);

  icon_view_->SetHorizontalAlignment(
      is_horizontal_shelf ? views::ImageView::Alignment::kCenter
                          : views::ImageView::Alignment::kLeading);
  icon_view_->SetVerticalAlignment(is_horizontal_shelf
                                       ? views::ImageView::Alignment::kLeading
                                       : views::ImageView::Alignment::kCenter);
  SchedulePaint();
}

void ShelfAppButton::OnTouchDragTimer() {
  AddState(STATE_DRAGGING);
}

void ShelfAppButton::OnRippleTimer() {
  if (views::InkDrop::Get(this)->GetInkDrop()->GetTargetInkDropState() !=
      views::InkDropState::ACTION_PENDING) {
    return;
  }
  views::InkDrop::Get(this)->GetInkDrop()->AnimateToState(
      views::InkDropState::ACTIVATED);
}

gfx::Transform ShelfAppButton::GetScaleTransform(float icon_scale) {
  gfx::RectF pre_scaling_bounds(GetMirroredRect(GetIconViewBounds(
      GetContentsBounds(), 1.0f, /*ignore_shadow_bounds=*/false)));
  gfx::RectF target_bounds(GetMirroredRect(GetIconViewBounds(
      GetContentsBounds(), icon_scale, /*ignore_shadow_bounds=*/false)));
  return gfx::TransformBetweenRects(target_bounds, pre_scaling_bounds);
}

gfx::Size ShelfAppButton::GetPreferredIconSize(
    const ui::ImageModel& image_model,
    float icon_scale) const {
  // Placeholder icons do not change base size between states.
  if (image_model.IsVectorIcon() && !has_host_badge_) {
    return gfx::Size(kPlaceholderIconDimension * icon_scale,
                     kPlaceholderIconDimension * icon_scale);
  }

  const int icon_size = has_host_badge_
                            ? shelf_view_->GetShortcutIconSize() * icon_scale
                            : GetIconDimensionByAppState() * icon_scale;

  const gfx::Size current_icon_size = image_model.Size();

  // Resize the image maintaining our aspect ratio.
  float aspect_ratio = static_cast<float>(current_icon_size.width()) /
                       static_cast<float>(current_icon_size.height());
  int height = icon_size;
  int width = static_cast<int>(aspect_ratio * height);
  if (width > icon_size) {
    width = icon_size;
    height = static_cast<int>(width / aspect_ratio);
  }

  return gfx::Size(width, height);
}

void ShelfAppButton::ScaleAppIcon(bool scale_up) {
  StopObservingImplicitAnimations();

  if (scale_up) {
    icon_scale_ = kAppIconScale;
    UpdateMainAndMaybeHostBadgeIconImage();
    icon_view_->layer()->SetTransform(GetScaleTransform(kAppIconScale));
  }
  ui::ScopedLayerAnimationSettings settings(icon_view_->layer()->GetAnimator());
  settings.SetTransitionDuration(
      base::Milliseconds(kDragDropAppIconScaleTransitionMs));
  if (scale_up) {
    icon_view_->layer()->SetTransform(gfx::Transform());
  } else {
    // To avoid poor quality icons, update icon image with the correct scale
    // after the transform animation is completed.
    settings.AddObserver(this);
    icon_view_->layer()->SetTransform(GetScaleTransform(kAppIconScale));
  }

  // Animate the notification indicator alongside the |icon_view_|.
  if (notification_indicator_) {
    gfx::RectF pre_scale(GetMirroredRect(GetNotificationIndicatorBounds(1.0)));
    gfx::RectF post_scale(
        GetMirroredRect(GetNotificationIndicatorBounds(kAppIconScale)));
    gfx::Transform scale_transform =
        gfx::TransformBetweenRects(post_scale, pre_scale);

    if (scale_up)
      notification_indicator_->layer()->SetTransform(scale_transform);
    ui::ScopedLayerAnimationSettings notification_settings(
        notification_indicator_->layer()->GetAnimator());
    notification_settings.SetTransitionDuration(
        base::Milliseconds(kDragDropAppIconScaleTransitionMs));
    notification_indicator_->layer()->SetTransform(scale_up ? gfx::Transform()
                                                            : scale_transform);
  }
}

void ShelfAppButton::OnImplicitAnimationsCompleted() {
  icon_scale_ = 1.0f;
  UpdateMainAndMaybeHostBadgeIconImage();
  icon_view_->layer()->SetTransform(gfx::Transform());
  if (notification_indicator_) {
    notification_indicator_->layer()->SetTransform(gfx::Transform());
  }
}

void ShelfAppButton::SetInkDropAnimationStarted(bool started) {
  if (ink_drop_animation_started_ == started)
    return;

  ink_drop_animation_started_ = started;
  if (started) {
    ink_drop_count_ = shelf_button_delegate()->CreateScopedActiveInkDropCount(
        /*sender=*/this);
  } else {
    ink_drop_count_.reset(nullptr);
  }
}

void ShelfAppButton::SetNotificationBadgeColor(SkColor color) {
  if (notification_indicator_)
    notification_indicator_->SetColor(color);
}

void ShelfAppButton::MaybeHideInkDropWhenGestureEnds() {
  if (context_menu_target_visibility_ ||
      views::InkDrop::Get(this)->GetInkDrop()->GetTargetInkDropState() ==
          views::InkDropState::HIDDEN) {
    // Return early if the shelf app button's context menu should show or
    // the button's inkdrop has already been hidden.
    return;
  }

  views::InkDrop::Get(this)->GetInkDrop()->AnimateToState(
      views::InkDropState::HIDDEN);
}

void ShelfAppButton::UpdateProgressRingBounds() {
  if ((!is_promise_app_ && !forced_progress_indicator_value_) ||
      !features::ArePromiseIconsEnabled()) {
    return;
  }

  if (!progress_indicator_) {
    progress_indicator_ =
        ProgressIndicator::CreateDefaultInstance(base::BindRepeating(
            [](ShelfAppButton* view) -> std::optional<float> {
              if (view->forced_progress_indicator_value_) {
                return *view->forced_progress_indicator_value_;
              }

              if (view->app_status() == AppStatus::kPending) {
                return 0.0f;
              }
              // If download is in-progress, return the progress as a decimal.
              // Otherwise, the progress indicator shouldn't be painted.
              float progress = view->progress();
              return (progress >= 0.f && progress < 1.f)
                         ? progress
                         : ProgressIndicator::kProgressComplete;
            },
            base::Unretained(this)));
    progress_indicator_->SetInnerIconVisible(false);
    progress_indicator_->SetInnerRingVisible(false);
    progress_indicator_->SetOuterRingStrokeWidth(
        static_cast<float>(kPromiseRingStrokeSize));
    SetPaintToLayer();
    layer()->SetFillsBoundsOpaquely(false);
    layer()->Add(progress_indicator_->CreateLayer(base::BindRepeating(
        [](ShelfAppButton* view, ui::ColorId color_id) {
          return view->GetColorProvider()->GetColor(color_id);
        },
        base::Unretained(this))));
  }

  if (app_status() == AppStatus::kPending) {
    progress_indicator_->SetColorId(cros_tokens::kCrosSysHighlightShape);
    progress_indicator_->SetOuterRingTrackVisible(true);
  } else {
    progress_indicator_->SetColorId(cros_tokens::kCrosSysPrimary);
    progress_indicator_->SetOuterRingTrackVisible(false);
  }

  gfx::Rect progress_indicator_bounds = views::View::ConvertRectToTarget(
      icon_view_, this, icon_view_->GetImageBounds());

  const int promise_icon_preferred_dimension = GetIconDimensionByAppState();

  // If the icon is smaller than the expected icon size (e,g for placeholder
  // icons), add padding to ensure the overall size of the promise icon is
  // correct regardless of the image icon size.
  progress_indicator_bounds.Outset(
      gfx::Outsets::VH(std::max(0, (promise_icon_preferred_dimension -
                                    progress_indicator_bounds.width()) /
                                       2),
                       std::max(0, (promise_icon_preferred_dimension -
                                    progress_indicator_bounds.height()) /
                                       2)));

  const gfx::Insets progress_ring_padding =
      ImageModelHasPlaceholderIcon() || app_status() == AppStatus::kPending
          ? kProgressRingMarginPending
          : kProgressRingMarginInstalling;

  progress_indicator_bounds.Inset(progress_ring_padding);

  // The Progress indicator paints the ring within the bounds of the layer, so
  // add padding for the promise ring.
  progress_indicator_bounds.Inset(-gfx::Insets(kPromiseRingStrokeSize));

  // The masked icons include 1px padding.
  progress_indicator_bounds.Inset(1);

  progress_indicator_->layer()->SetBounds(progress_indicator_bounds);
  layer()->StackAtBottom(progress_indicator_->layer());
  progress_indicator_->InvalidateLayer();

  SetBackground(std::make_unique<PromiseIconBackground>(
      cros_tokens::kCrosSysSystemOnBase, progress_indicator_bounds,
      progress_ring_padding));
}

ProgressIndicator* ShelfAppButton::GetProgressIndicatorForTest() const {
  return progress_indicator_.get();
}

void ShelfAppButton::OnAnimatedInFromPromiseApp(
    base::RepeatingClosure callback) {
  forced_progress_indicator_value_.reset();
  if (progress_indicator_) {
    layer()->Remove(progress_indicator_->layer());
  }
  progress_indicator_.reset();
  force_fallback_icon_ = false;

  if (has_icon_image_) {
    fallback_icon_image_model_ = ui::ImageModel();
    // Clear background set as a result of adding progress indicator.
    SetBackground(nullptr);
  }
  UpdateMainAndMaybeHostBadgeIconImage();

  callback.Run();
}

void ShelfAppButton::UpdateAccessibleDescription() {
  switch (app_status_) {
    case AppStatus::kBlocked:
      GetViewAccessibility().SetDescription(
          ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
              IDS_SHELF_ITEM_BLOCKED_APP));
      break;
    case AppStatus::kPaused:
      GetViewAccessibility().SetDescription(
          ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
              IDS_SHELF_ITEM_PAUSED_APP));
      break;
    default:
      GetViewAccessibility().RemoveDescription();
      break;
  }
}

BEGIN_METADATA(ShelfAppButton)
END_METADATA

}  // namespace ash