chromium/ash/assistant/ui/main_stage/suggestion_container_view.cc

// Copyright 2018 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/assistant/ui/main_stage/suggestion_container_view.h"

#include <algorithm>
#include <memory>
#include <utility>

#include "ash/assistant/model/assistant_response.h"
#include "ash/assistant/model/assistant_suggestions_model.h"
#include "ash/assistant/model/assistant_ui_model.h"
#include "ash/assistant/ui/assistant_ui_constants.h"
#include "ash/assistant/ui/assistant_view_delegate.h"
#include "ash/assistant/ui/assistant_view_ids.h"
#include "ash/assistant/ui/main_stage/animated_container_view.h"
#include "ash/assistant/ui/main_stage/element_animator.h"
#include "ash/assistant/util/animation_util.h"
#include "ash/assistant/util/assistant_util.h"
#include "ash/public/cpp/assistant/assistant_state.h"
#include "ash/public/cpp/assistant/controller/assistant_suggestions_controller.h"
#include "ash/public/cpp/assistant/controller/assistant_ui_controller.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "components/feature_engagement/public/feature_constants.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/callback_layer_animation_observer.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_element.h"
#include "ui/views/layout/box_layout.h"

namespace ash {

namespace {

using assistant::util::CreateLayerAnimationSequence;
using assistant::util::CreateOpacityElement;
using assistant::util::StartLayerAnimationSequence;

// Animation.
constexpr base::TimeDelta kChipFadeInDuration = base::Milliseconds(250);
constexpr base::TimeDelta kChipFadeOutDuration = base::Milliseconds(200);

// Metrics.
constexpr char kAssistantSuggestionChipHistogram[] =
    "Ash.Assistant.AnimationSmoothness.SuggestionChip";

constexpr int kPreferredHeightDip = 64;

}  // namespace

// SuggestionChipAnimator -----------------------------------------------------

class SuggestionChipAnimator : public ElementAnimator {
 public:
  SuggestionChipAnimator(SuggestionChipView* chip,
                         const SuggestionContainerView* parent)
      : ElementAnimator(chip), parent_(parent) {}

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

  ~SuggestionChipAnimator() override = default;

  void AnimateIn(ui::CallbackLayerAnimationObserver* observer) override {
    StartLayerAnimationSequence(
        layer()->GetAnimator(), CreateAnimateInAnimation(), observer,
        base::BindRepeating<void(const std::string&, int)>(
            base::UmaHistogramPercentage, kAssistantSuggestionChipHistogram));
  }

  void AnimateOut(ui::CallbackLayerAnimationObserver* observer) override {
    StartLayerAnimationSequence(
        layer()->GetAnimator(), CreateAnimateOutAnimation(), observer,
        base::BindRepeating<void(const std::string&, int)>(
            base::UmaHistogramPercentage, kAssistantSuggestionChipHistogram));
  }

  void FadeOut(ui::CallbackLayerAnimationObserver* observer) override {
    // If the user pressed a chip we do not fade it out.
    if (!IsSelectedChip())
      ElementAnimator::FadeOut(observer);
  }

 private:
  bool IsSelectedChip() const { return view() == parent_->selected_chip(); }

  ui::LayerAnimationSequence* CreateAnimateInAnimation() const {
    return CreateLayerAnimationSequence(CreateOpacityElement(
        1.f, kChipFadeInDuration, gfx::Tween::Type::FAST_OUT_SLOW_IN));
  }

  ui::LayerAnimationSequence* CreateAnimateOutAnimation() const {
    return CreateLayerAnimationSequence(CreateOpacityElement(
        0.f, kChipFadeOutDuration, gfx::Tween::Type::FAST_OUT_SLOW_IN));
  }

  const raw_ptr<const SuggestionContainerView>
      parent_;  // |parent_| owns |this|.
};

// SuggestionContainerView -----------------------------------------------------

SuggestionContainerView::SuggestionContainerView(
    AssistantViewDelegate* delegate)
    : AnimatedContainerView(delegate) {
  SetID(AssistantViewID::kSuggestionContainer);
  InitLayout();

  AssistantSuggestionsController::Get()->GetModel()->AddObserver(this);
  AssistantUiController::Get()->GetModel()->AddObserver(this);
}

SuggestionContainerView::~SuggestionContainerView() {
  if (AssistantUiController::Get())
    AssistantUiController::Get()->GetModel()->RemoveObserver(this);

  if (AssistantSuggestionsController::Get())
    AssistantSuggestionsController::Get()->GetModel()->RemoveObserver(this);
}

gfx::Size SuggestionContainerView::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  return gfx::Size(INT_MAX, kPreferredHeightDip);
}

void SuggestionContainerView::OnContentsPreferredSizeChanged(
    views::View* content_view) {
  // Our contents should never be smaller than our container width because when
  // showing conversation starters we will be center aligned.
  const int width =
      std::max(content_view->GetPreferredSize().width(), this->width());
  content_view->SetSize(gfx::Size(width, kPreferredHeightDip));
}

void SuggestionContainerView::OnAssistantControllerDestroying() {
  AnimatedContainerView::OnAssistantControllerDestroying();

  AssistantUiController::Get()->GetModel()->RemoveObserver(this);
  AssistantSuggestionsController::Get()->GetModel()->RemoveObserver(this);
}

void SuggestionContainerView::OnCommittedQueryChanged(
    const AssistantQuery& query) {
  AnimatedContainerView::OnCommittedQueryChanged(query);

  // Cache the fact that a query has been committed in this Assistant session so
  // that we know to stop handling conversation starter updates.
  has_committed_query_ = true;
}

void SuggestionContainerView::InitLayout() {
  layout_manager_ =
      content_view()->SetLayoutManager(std::make_unique<views::BoxLayout>(
          views::BoxLayout::Orientation::kHorizontal,
          gfx::Insets::VH(0, assistant::ui::kHorizontalPadding),
          /*between_child_spacing=*/kSpacingDip));

  layout_manager_->set_cross_axis_alignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);

  // We center align when showing conversation starters.
  layout_manager_->set_main_axis_alignment(
      views::BoxLayout::MainAxisAlignment::kCenter);
}

void SuggestionContainerView::OnConversationStartersChanged(
    const std::vector<AssistantSuggestion>& conversation_starters) {
  // We don't show conversation starters when showing onboarding since the
  // onboarding experience already provides the user w/ suggestions.
  if (delegate()->ShouldShowOnboarding()) {
    return;
  }

  if (base::FeatureList::IsEnabled(
          feature_engagement::kIPHLauncherSearchHelpUiFeature)) {
    return;
  }

  // If we've committed a query we should ignore changes to the cache of
  // conversation starters as we are past the state in which they should be
  // presented. To present them now could incorrectly associate the conversation
  // starters with a response.
  if (has_committed_query_) {
    return;
  }

  RemoveAllViews();
  OnSuggestionsAdded(conversation_starters);
}

std::unique_ptr<ElementAnimator> SuggestionContainerView::HandleSuggestion(
    const AssistantSuggestion& suggestion) {
  // When no longer showing conversation starters, we start align our content.
  layout_manager_->set_main_axis_alignment(
      has_committed_query_ ? views::BoxLayout::MainAxisAlignment::kStart
                           : views::BoxLayout::MainAxisAlignment::kCenter);

  return AddSuggestionChip(suggestion);
}

void SuggestionContainerView::OnAllViewsRemoved() {
  // Clear the selected button.
  selected_chip_ = nullptr;
}

std::unique_ptr<ElementAnimator> SuggestionContainerView::AddSuggestionChip(
    const AssistantSuggestion& suggestion) {
  auto suggestion_chip_view =
      std::make_unique<SuggestionChipView>(delegate(), suggestion);
  suggestion_chip_view->SetCallback(base::BindRepeating(
      &SuggestionContainerView::OnButtonPressed, base::Unretained(this),
      base::Unretained(suggestion_chip_view.get())));

  // The chip will be animated on its own layer.
  suggestion_chip_view->SetPaintToLayer();
  suggestion_chip_view->layer()->SetFillsBoundsOpaquely(false);
  suggestion_chip_view->layer()->SetOpacity(0.f);

  // Add to the view hierarchy and return the animator for the suggestion chip.
  return std::make_unique<SuggestionChipAnimator>(
      contents()->AddChildView(std::move(suggestion_chip_view)), this);
}

void SuggestionContainerView::OnUiVisibilityChanged(
    AssistantVisibility new_visibility,
    AssistantVisibility old_visibility,
    std::optional<AssistantEntryPoint> entry_point,
    std::optional<AssistantExitPoint> exit_point) {
  if (assistant::util::IsStartingSession(new_visibility, old_visibility) &&
      entry_point.value() != AssistantEntryPoint::kLauncherSearchResult) {
    // Show conversation starters at the start of a new Assistant session except
    // when the user already started a query in Launcher quick search box (QSB).
    OnConversationStartersChanged(AssistantSuggestionsController::Get()
                                      ->GetModel()
                                      ->GetConversationStarters());
    return;
  }

  if (!assistant::util::IsFinishingSession(new_visibility))
    return;

  // When Assistant is finishing a session, we need to reset state.
  has_committed_query_ = false;

  // When we start a new session we will be showing conversation starters so
  // we need to center align our content.
  layout_manager_->set_main_axis_alignment(
      views::BoxLayout::MainAxisAlignment::kCenter);
}

void SuggestionContainerView::InitializeUIForBubbleView() {
  OnConversationStartersChanged(AssistantSuggestionsController::Get()
                                    ->GetModel()
                                    ->GetConversationStarters());
}

void SuggestionContainerView::OnButtonPressed(SuggestionChipView* chip_view) {
  // Remember which chip was selected, so we can give it a special animation.
  selected_chip_ = chip_view;
  delegate()->OnSuggestionPressed(selected_chip_->suggestion_id());
}

BEGIN_METADATA(SuggestionContainerView)
END_METADATA

}  // namespace ash