chromium/ash/app_list/views/assistant/assistant_main_stage.cc

// Copyright 2019 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/app_list/views/assistant/assistant_main_stage.h"

#include "ash/assistant/model/assistant_interaction_model.h"
#include "ash/assistant/model/assistant_query.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/base/stack_layout.h"
#include "ash/assistant/ui/main_stage/assistant_footer_view.h"
#include "ash/assistant/ui/main_stage/assistant_progress_indicator.h"
#include "ash/assistant/ui/main_stage/assistant_query_view.h"
#include "ash/assistant/ui/main_stage/assistant_zero_state_view.h"
#include "ash/assistant/ui/main_stage/ui_element_container_view.h"
#include "ash/assistant/util/animation_util.h"
#include "ash/assistant/util/assistant_util.h"
#include "ash/public/cpp/assistant/controller/assistant_interaction_controller.h"
#include "ash/public/cpp/assistant/controller/assistant_ui_controller.h"
#include "ash/public/cpp/style/color_provider.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/time/time.h"
#include "components/feature_engagement/public/feature_constants.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/color/color_id.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_element.h"
#include "ui/compositor/layer_animator.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/separator.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/layout_manager.h"

namespace ash {

namespace {

using assistant::util::CreateLayerAnimationSequence;
using assistant::util::CreateOpacityElement;
using assistant::util::CreateTransformElement;

// Appearance.
constexpr int kSeparatorThicknessDip = 1;
constexpr int kSeparatorWidthDip = 64;

// Footer entry animation.
constexpr base::TimeDelta kFooterEntryAnimationFadeInDelay =
    base::Milliseconds(283);
constexpr base::TimeDelta kFooterEntryAnimationFadeInDuration =
    base::Milliseconds(167);

// Divider animation.
constexpr base::TimeDelta kDividerAnimationFadeInDelay =
    base::Milliseconds(233);
constexpr base::TimeDelta kDividerAnimationFadeInDuration =
    base::Milliseconds(167);
constexpr base::TimeDelta kDividerAnimationFadeOutDuration =
    base::Milliseconds(83);

// Zero state animation.
constexpr base::TimeDelta kZeroStateAnimationFadeOutDuration =
    base::Milliseconds(83);
constexpr int kZeroStateAnimationTranslationDip = 115;
constexpr base::TimeDelta kZeroStateAnimationFadeInDelay =
    base::Milliseconds(33);
constexpr base::TimeDelta kZeroStateAnimationFadeInDuration =
    base::Milliseconds(167);
constexpr base::TimeDelta kZeroStateAnimationTranslateUpDuration =
    base::Milliseconds(250);

// Helpers ---------------------------------------------------------------------

// These classes exist to solely to provide a class name to UI devtools. They
// don't follow the style guide so they can be shorter.
class ContentContainer : public views::View {
  METADATA_HEADER(ContentContainer, views::View)
};

BEGIN_METADATA(ContentContainer)
END_METADATA

class MainContentContainer : public views::View {
  METADATA_HEADER(MainContentContainer, views::View)
};

BEGIN_METADATA(MainContentContainer)
END_METADATA

class DividerContainer : public views::View {
  METADATA_HEADER(DividerContainer, views::View)
};

BEGIN_METADATA(DividerContainer)
END_METADATA

class FooterContainer : public views::View {
  METADATA_HEADER(FooterContainer, views::View)
};

BEGIN_METADATA(FooterContainer)
END_METADATA

// A view is considered shown when it is visible and not in the process of
// fading out.
bool IsShown(const views::View* view) {
  DCHECK(view->layer());
  bool is_fading_out =
      cc::MathUtil::IsWithinEpsilon(view->layer()->GetTargetOpacity(), 0.f);

  return view->GetVisible() && !is_fading_out;
}

}  // namespace

// AppListAssistantMainStage ---------------------------------------------------

AppListAssistantMainStage::AppListAssistantMainStage(
    AssistantViewDelegate* delegate)
    : delegate_(delegate) {
  SetID(AssistantViewID::kMainStage);
  if (base::FeatureList::IsEnabled(
          feature_engagement::kIPHLauncherSearchHelpUiFeature)) {
    InitLayoutWithIph();
  } else {
    InitLayout();
  }

  assistant_controller_observation_.Observe(AssistantController::Get());
  AssistantInteractionController::Get()->GetModel()->AddObserver(this);
  AssistantUiController::Get()->GetModel()->AddObserver(this);
}

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

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

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

void AppListAssistantMainStage::OnThemeChanged() {
  views::View::OnThemeChanged();
  horizontal_separator_->SetColorId(ui::kColorAshSystemUIMenuSeparator);
}

void AppListAssistantMainStage::OnViewPreferredSizeChanged(views::View* view) {
  PreferredSizeChanged();
  DeprecatedLayoutImmediately();
  SchedulePaint();
}

void AppListAssistantMainStage::InitLayout() {
  // The children of AppListAssistantMainStage will be animated on their own
  // layers and we want them to be clipped by their parent layer.
  SetPaintToLayer();
  layer()->SetFillsBoundsOpaquely(false);
  layer()->SetMasksToBounds(true);

  views::BoxLayout* layout =
      SetLayoutManager(std::make_unique<views::BoxLayout>(
          views::BoxLayout::Orientation::kVertical));
  layout->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kCenter);
  layout->set_cross_axis_alignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);

  layout->SetFlexForView(AddChildView(CreateContentLayoutContainer()), 1);

  AddChildView(CreateFooterLayoutContainer());
}

void AppListAssistantMainStage::InitLayoutWithIph() {
  // The children of AppListAssistantMainStage will be animated on their own
  // layers and we want them to be clipped by their parent layer.
  SetPaintToLayer();
  layer()->SetFillsBoundsOpaquely(false);
  layer()->SetMasksToBounds(true);

  // The layout container stacks two views.
  // On top is a main content container including the line separator, progress
  // indicator query view, `ui_element_container()` and `footer()`.
  // The `zero_state_view_` is laid out above of the main content container. As
  // such, it floats above and does not cause repositioning to any of content
  // layout's underlying views.
  auto* stack_layout = SetLayoutManager(std::make_unique<StackLayout>());

  auto* main_content_layout_container =
      AddChildView(CreateMainContentLayoutContainer());
  // Currently `CreateMainContentLayoutContainer()` is reused for both layouts
  // with/without IPH. So add the footer here separately.
  main_content_layout_container->AddChildView(CreateFooterLayoutContainer());

  // Do not respect height, otherwise bounds will not be set correctly for
  // scrolling.
  stack_layout->SetRespectDimensionForView(
      main_content_layout_container, StackLayout::RespectDimension::kWidth);

  // Zero state, which will be animated on its own layer.
  zero_state_view_ =
      AddChildView(std::make_unique<AssistantZeroStateView>(delegate_));
  zero_state_view_->SetPaintToLayer();
  zero_state_view_->layer()->SetFillsBoundsOpaquely(false);
  // Expand the height of the `zero_state_view_` to the host height.
  stack_layout->SetRespectDimensionForView(
      zero_state_view_, StackLayout::RespectDimension::kWidth);
}

std::unique_ptr<views::View>
AppListAssistantMainStage::CreateContentLayoutContainer() {
  // The content layout container stacks two views.
  // On top is a main content container including the line separator, progress
  // indicator query view and |ui_element_container()|.
  // The |zero_state_view_| is laid out above of the main content container. As
  // such, it floats above and does not cause repositioning to any of content
  // layout's underlying views.
  auto content_layout_container = std::make_unique<ContentContainer>();

  auto* stack_layout = content_layout_container->SetLayoutManager(
      std::make_unique<StackLayout>());

  auto* main_content_layout_container = content_layout_container->AddChildView(
      CreateMainContentLayoutContainer());

  // Do not respect height, otherwise bounds will not be set correctly for
  // scrolling.
  stack_layout->SetRespectDimensionForView(
      main_content_layout_container, StackLayout::RespectDimension::kWidth);

  // Zero state, which will be animated on its own layer.
  zero_state_view_ = content_layout_container->AddChildView(
      std::make_unique<AssistantZeroStateView>(delegate_));
  zero_state_view_->SetPaintToLayer();
  zero_state_view_->layer()->SetFillsBoundsOpaquely(false);
  // Expand the height of the `zero_state_view_` to the host height.
  stack_layout->SetRespectDimensionForView(
      zero_state_view_, StackLayout::RespectDimension::kWidth);

  return content_layout_container;
}

std::unique_ptr<views::View>
AppListAssistantMainStage::CreateMainContentLayoutContainer() {
  auto content_layout_container = std::make_unique<MainContentContainer>();
  views::BoxLayout* content_layout = content_layout_container->SetLayoutManager(
      std::make_unique<views::BoxLayout>(
          views::BoxLayout::Orientation::kVertical));
  content_layout->set_main_axis_alignment(
      views::BoxLayout::MainAxisAlignment::kCenter);
  content_layout->set_cross_axis_alignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);

  content_layout_container->AddChildView(CreateDividerLayoutContainer());

  // Query view. Will be animated on its own layer.
  auto* query_view = content_layout_container->AddChildView(
      std::make_unique<AssistantQueryView>());
  query_view->SetPaintToLayer();
  query_view->layer()->SetFillsBoundsOpaquely(false);
  query_view_observation_.Observe(query_view);

  // UI element container.
  ui_element_container_observation_.Observe(
      content_layout_container->AddChildView(
          std::make_unique<UiElementContainerView>(delegate_)));
  content_layout->SetFlexForView(ui_element_container(), 1,
                                 /*use_min_size=*/true);

  return content_layout_container;
}

std::unique_ptr<views::View>
AppListAssistantMainStage::CreateDividerLayoutContainer() {
  // Dividers: the progress indicator and the horizontal separator will be the
  // separator when querying and showing the results, respectively.
  auto divider_container = std::make_unique<DividerContainer>();
  divider_container->SetLayoutManager(std::make_unique<StackLayout>());

  // Progress indicator, which will be animated on its own layer.
  progress_indicator_ = divider_container->AddChildView(
      std::make_unique<AssistantProgressIndicator>());
  progress_indicator_->SetPaintToLayer();
  progress_indicator_->layer()->SetFillsBoundsOpaquely(false);

  // Horizontal separator, which will be animated on its own layer.
  horizontal_separator_ =
      divider_container->AddChildView(std::make_unique<views::Separator>());
  horizontal_separator_->SetID(kHorizontalSeparator);
  // views::Separator always secure at least 1px even if insets make separator
  // drawable height to 0px.
  int vertical_inset = (progress_indicator_->GetPreferredSize().height() -
                        kSeparatorThicknessDip) /
                       2;
  horizontal_separator_->SetBorder(
      views::CreateEmptyBorder(gfx::Insets::VH(vertical_inset, 0)));
  horizontal_separator_->SetColorId(ui::kColorAshSystemUIMenuSeparator);
  horizontal_separator_->SetPreferredSize(gfx::Size(
      kSeparatorWidthDip, progress_indicator_->GetPreferredSize().height()));
  horizontal_separator_->SetPaintToLayer();
  horizontal_separator_->layer()->SetFillsBoundsOpaquely(false);

  return divider_container;
}

std::unique_ptr<views::View>
AppListAssistantMainStage::CreateFooterLayoutContainer() {
  // Footer.
  // Note that the |footer()| is placed within its own view container so that
  // as its visibility changes, its parent container will still reserve the
  // same layout space. This prevents jank that would otherwise occur due to
  // |ui_element_container()| claiming that empty space.
  auto footer_container = std::make_unique<FooterContainer>();
  footer_container->SetLayoutManager(std::make_unique<views::FillLayout>());

  footer_observation_.Observe(footer_container->AddChildView(
      std::make_unique<AssistantFooterView>(delegate_)));

  // The footer will be animated on its own layer.
  footer()->SetPaintToLayer();
  footer()->layer()->SetFillsBoundsOpaquely(false);

  return footer_container;
}

void AppListAssistantMainStage::AnimateInZeroState() {
  zero_state_view_->layer()->GetAnimator()->StopAnimating();

  // We're going to animate the zero state view up into position so we'll need
  // to apply an initial transformation.
  gfx::Transform transform;
  transform.Translate(0, kZeroStateAnimationTranslationDip);

  // Set up our pre-animation values.
  zero_state_view_->layer()->SetOpacity(0.f);
  zero_state_view_->layer()->SetTransform(transform);
  zero_state_view_->SetVisible(true);

  // Start animating the zero state view.
  zero_state_view_->layer()->GetAnimator()->StartTogether(
      {// Animate the transformation.
       CreateLayerAnimationSequence(CreateTransformElement(
           gfx::Transform(), kZeroStateAnimationTranslateUpDuration,
           gfx::Tween::Type::FAST_OUT_SLOW_IN_2)),
       // Animate the opacity to 100% with delay.
       CreateLayerAnimationSequence(
           ui::LayerAnimationElement::CreatePauseElement(
               ui::LayerAnimationElement::AnimatableProperty::OPACITY,
               kZeroStateAnimationFadeInDelay),
           CreateOpacityElement(1.f, kZeroStateAnimationFadeInDuration))});
}

void AppListAssistantMainStage::AnimateInFooter() {
  // Set up our pre-animation values.
  footer()->layer()->SetOpacity(0.f);
  footer()->SetVisible(true);

  // Animate the footer to 100% opacity with delay.
  footer()->layer()->GetAnimator()->StartAnimation(CreateLayerAnimationSequence(
      ui::LayerAnimationElement::CreatePauseElement(
          ui::LayerAnimationElement::AnimatableProperty::OPACITY,
          kFooterEntryAnimationFadeInDelay),
      CreateOpacityElement(1.f, kFooterEntryAnimationFadeInDuration)));
}

void AppListAssistantMainStage::OnAssistantControllerDestroying() {
  AssistantUiController::Get()->GetModel()->RemoveObserver(this);
  AssistantInteractionController::Get()->GetModel()->RemoveObserver(this);
  DCHECK(assistant_controller_observation_.IsObservingSource(
      AssistantController::Get()));
  assistant_controller_observation_.Reset();
}

void AppListAssistantMainStage::OnCommittedQueryChanged(
    const AssistantQuery& query) {
  // Update the view.
  query_view()->SetQuery(query);

  // If query is empty and we are showing zero state, do not update the Ui.
  if (query.Empty() && IsShown(zero_state_view_))
    return;

  // Hide the horizontal separator.
  horizontal_separator_->layer()->GetAnimator()->StartAnimation(
      CreateLayerAnimationSequence(
          CreateOpacityElement(0.f, kDividerAnimationFadeOutDuration)));

  // Show the progress indicator.
  progress_indicator_->layer()->GetAnimator()->StartAnimation(
      CreateLayerAnimationSequence(
          // Delay...
          ui::LayerAnimationElement::CreatePauseElement(
              ui::LayerAnimationElement::AnimatableProperty::OPACITY,
              kDividerAnimationFadeInDelay),
          // ...then fade in.
          CreateOpacityElement(1.f, kDividerAnimationFadeInDuration)));

  MaybeHideZeroStateAndShowFooter();
}

void AppListAssistantMainStage::OnPendingQueryChanged(
    const AssistantQuery& query) {
  // Update the view.
  query_view()->SetQuery(query);

  if (!IsShown(zero_state_view_))
    return;

  // Animate the opacity to 100% with delay equal to |zero_state_view_| fade out
  // animation duration to avoid the two views displaying at the same time.
  constexpr base::TimeDelta kQueryAnimationFadeInDuration =
      base::Milliseconds(433);
  query_view()->layer()->SetOpacity(0.f);
  query_view()->layer()->GetAnimator()->StartAnimation(
      CreateLayerAnimationSequence(
          ui::LayerAnimationElement::CreatePauseElement(
              ui::LayerAnimationElement::AnimatableProperty::OPACITY,
              kZeroStateAnimationFadeOutDuration),
          CreateOpacityElement(1.f, kQueryAnimationFadeInDuration)));

  if (!query.Empty())
    MaybeHideZeroStateAndShowFooter();
}

void AppListAssistantMainStage::OnPendingQueryCleared(bool due_to_commit) {
  // When a pending query is cleared, it may be because the interaction was
  // cancelled, or because the query was committed. If the query was committed,
  // reseting the query here will have no visible effect. If the interaction was
  // cancelled, we set the query here to restore the previously committed query.
  query_view()->SetQuery(
      AssistantInteractionController::Get()->GetModel()->committed_query());
}

void AppListAssistantMainStage::OnResponseChanged(
    const scoped_refptr<AssistantResponse>& response) {
  MaybeHideZeroStateAndShowFooter();

  // Show the horizontal separator.
  horizontal_separator_->layer()->GetAnimator()->StartAnimation(
      CreateLayerAnimationSequence(
          // Delay...
          ui::LayerAnimationElement::CreatePauseElement(
              ui::LayerAnimationElement::AnimatableProperty::OPACITY,
              kDividerAnimationFadeInDelay),
          // ...then fade in.
          CreateOpacityElement(1.f, kDividerAnimationFadeInDuration)));

  // Hide the progress indicator.
  progress_indicator_->layer()->GetAnimator()->StartAnimation(
      CreateLayerAnimationSequence(
          CreateOpacityElement(0.f, kDividerAnimationFadeOutDuration)));
}

void AppListAssistantMainStage::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)) {
    const bool from_search =
        entry_point == AssistantEntryPoint::kLauncherSearchResult;
    InitializeUIForStartingSession(from_search);
    return;
  }

  query_view()->SetQuery(AssistantNullQuery());
}

void AppListAssistantMainStage::InitializeUIForBubbleView() {
  InitializeUIForStartingSession(/*from_search=*/false);
}

void AppListAssistantMainStage::MaybeHideZeroStateAndShowFooter() {
  if (!IsShown(zero_state_view_))
    return;

  assistant::util::FadeOutAndHide(zero_state_view_,
                                  kZeroStateAnimationFadeOutDuration);

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

void AppListAssistantMainStage::InitializeUIForStartingSession(
    bool from_search) {
  // When Assistant is starting a new session, we animate in the appearance of
  // the zero state view and footer.
  progress_indicator_->layer()->SetOpacity(0.f);
  horizontal_separator_->layer()->SetOpacity(from_search ? 1.f : 0.f);

  footer()->InitializeUIForBubbleView();
  if (from_search) {
    zero_state_view_->SetVisible(false);
    AnimateInFooter();
  } else {
    AnimateInZeroState();

    if (base::FeatureList::IsEnabled(
            feature_engagement::kIPHLauncherSearchHelpUiFeature)) {
      footer()->SetVisible(false);
    } else {
      AnimateInFooter();
    }
  }
}

BEGIN_METADATA(AppListAssistantMainStage)
END_METADATA

}  // namespace ash