chromium/ash/assistant/ui/main_stage/ui_element_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/ui_element_container_view.h"

#include <string>

#include "ash/assistant/model/assistant_interaction_model.h"
#include "ash/assistant/model/assistant_response.h"
#include "ash/assistant/model/ui/assistant_ui_element.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/assistant_ui_element_view.h"
#include "ash/assistant/ui/main_stage/assistant_ui_element_view_factory.h"
#include "ash/assistant/ui/main_stage/element_animator.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/assistant/controller/assistant_interaction_controller.h"
#include "ash/public/cpp/style/color_provider.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "cc/base/math_util.h"
#include "chromeos/ash/services/assistant/public/cpp/features.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/aura/window.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/layout/box_layout.h"

namespace ash {

namespace {

// Appearance.
constexpr int kPaddingBottomDip = 8;
constexpr int kScrollIndicatorHeightDip = 1;

// ObservableOverflowIndicator allows a caller to observe visibility change of
// an overflow indicator. Note that we are using this view with setting
// thickness to 0 with ScrollView::SetCustomOverflowIndicator. This view is not
// visible.
class ObservableOverflowIndicator : public views::View {
  METADATA_HEADER(ObservableOverflowIndicator, views::View)

 public:
  explicit ObservableOverflowIndicator(
      UiElementContainerView* ui_element_container_view)
      : ui_element_container_view_(ui_element_container_view) {}

 protected:
  void VisibilityChanged(views::View* starting_from, bool is_visible) override {
    if (starting_from != this)
      return;

    ui_element_container_view_->OnOverflowIndicatorVisibilityChanged(
        is_visible);
  }

 private:
  raw_ptr<UiElementContainerView> ui_element_container_view_ = nullptr;
};

BEGIN_METADATA(ObservableOverflowIndicator)
END_METADATA

// This is views::View. We define InvisibleOverflowIndicator as we can add
// METADATA to this view. We set thickness of this view to 0 with
// ScrollView::SetCustomOverflowIndicator. The background of this view is NOT
// transparent, i.e. it becomes visible if you set thickness larger than 0.
class InvisibleOverflowIndicator : public views::View {
  METADATA_HEADER(InvisibleOverflowIndicator, views::View)
};

BEGIN_METADATA(InvisibleOverflowIndicator)
END_METADATA

}  // namespace

// UiElementContainerView ------------------------------------------------------

UiElementContainerView::UiElementContainerView(AssistantViewDelegate* delegate)
    : AnimatedContainerView(delegate),
      view_factory_(std::make_unique<AssistantUiElementViewFactory>(delegate)) {
  SetID(AssistantViewID::kUiElementContainer);
  InitLayout();
}

UiElementContainerView::~UiElementContainerView() = default;

gfx::Size UiElementContainerView::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  return gfx::Size(INT_MAX, content_view()->GetHeightForWidth(INT_MAX));
}

gfx::Size UiElementContainerView::GetMinimumSize() const {
  // AssistantMainStage uses BoxLayout's flex property to grow/shrink
  // UiElementContainerView to fill available space as needed. When height is
  // shrunk to zero, as is temporarily the case during the initial container
  // growth animation for the first Assistant response, UiElementContainerView
  // will be laid out with zero width. We do not recover from this state until
  // the next layout pass, which causes Assistant cards for the first response
  // to be laid out with zero width. We work around this by imposing a minimum
  // height restriction of 1 dip that is factored into BoxLayout's flex
  // calculations to make sure that our width is never being set to zero.
  return gfx::Size(INT_MAX, 1);
}

void UiElementContainerView::Layout(PassKey) {
  LayoutSuperclass<AnimatedContainerView>(this);

  // Scroll indicator.
  scroll_indicator_->SetBounds(0, height() - kScrollIndicatorHeightDip, width(),
                               kScrollIndicatorHeightDip);
}

void UiElementContainerView::OnContentsPreferredSizeChanged(
    views::View* content_view) {
  const int preferred_height = content_view->GetHeightForWidth(width());
  content_view->SetSize(gfx::Size(width(), preferred_height));
}

void UiElementContainerView::InitLayout() {
  // Content.
  const int horizontal_margin = assistant::ui::kHorizontalMargin;
  content_view()->SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kVertical,
      gfx::Insets::TLBR(0, horizontal_margin, kPaddingBottomDip,
                        horizontal_margin),
      kSpacingDip));

  // Scroll indicator.
  scroll_indicator_ = AddChildView(std::make_unique<views::View>());
  scroll_indicator_->SetID(kOverflowIndicator);
  scroll_indicator_->SetBackground(
      views::CreateSolidBackground(GetOverflowIndicatorBackgroundColor()));

  // The scroll indicator paints to its own layer which is animated in/out using
  // implicit animation settings.
  scroll_indicator_->SetPaintToLayer();
  scroll_indicator_->layer()->SetAnimator(
      ui::LayerAnimator::CreateImplicitAnimator());
  scroll_indicator_->layer()->SetFillsBoundsOpaquely(false);
  scroll_indicator_->layer()->SetOpacity(0.f);

  // We cannot draw |scroll_indicator_| over Assistant cards due to issues w/
  // layer ordering. Because |kScrollIndicatorHeightDip| is sufficiently small,
  // we'll use an empty bottom border to reserve space for |scroll_indicator_|.
  // When |scroll_indicator_| is not visible, this just adds a negligible amount
  // of margin to the bottom of the content. Otherwise, |scroll_indicator_| will
  // occupy this space.
  SetBorder(views::CreateEmptyBorder(
      gfx::Insets::TLBR(0, 0, kScrollIndicatorHeightDip, 0)));

  // We set invisible overflow indicators with thickness=0. But we observe
  // visibility change of the bottom indicator.
  SetDrawOverflowIndicator(true);
  SetCustomOverflowIndicator(
      views::OverflowIndicatorAlignment::kBottom,
      std::make_unique<ObservableOverflowIndicator>(this),
      /*thickness=*/0, /*fills_opaquely=*/true);
  SetCustomOverflowIndicator(views::OverflowIndicatorAlignment::kTop,
                             std::make_unique<InvisibleOverflowIndicator>(),
                             /*thickness=*/0,
                             /*fills_opaquely=*/true);
  SetCustomOverflowIndicator(views::OverflowIndicatorAlignment::kLeft,
                             std::make_unique<InvisibleOverflowIndicator>(),
                             /*thickness=*/0,
                             /*fills_opaquely=*/true);
  SetCustomOverflowIndicator(views::OverflowIndicatorAlignment::kRight,
                             std::make_unique<InvisibleOverflowIndicator>(),
                             /*thickness=*/0,
                             /*fills_opaquely=*/true);
}

void UiElementContainerView::OnCommittedQueryChanged(
    const AssistantQuery& query) {
  // Scroll to the top to play nice with the transition animation.
  ScrollToPosition(vertical_scroll_bar(), 0);
  AnimatedContainerView::OnCommittedQueryChanged(query);
}

void UiElementContainerView::OnThemeChanged() {
  views::View::OnThemeChanged();

  scroll_indicator_->background()->SetNativeControlColor(
      GetOverflowIndicatorBackgroundColor());

  // SetNativeControlColor doesn't trigger a repaint.
  scroll_indicator_->SchedulePaint();
}

std::unique_ptr<ElementAnimator> UiElementContainerView::HandleUiElement(
    const AssistantUiElement* ui_element) {
  // Create a new view for the |ui_element|.
  auto view = view_factory_->Create(ui_element);
  if (!view) {
    return nullptr;
  }

  // If the first UI element is a card, it has a unique margin requirement.
  const bool is_card = ui_element->type() == AssistantUiElementType::kCard;
  const bool is_first_ui_element = content_view()->children().empty();
  if (is_card && is_first_ui_element) {
    constexpr int kMarginTopDip = 24;
    view->SetBorder(
        views::CreateEmptyBorder(gfx::Insets::TLBR(kMarginTopDip, 0, 0, 0)));
  }

  // Add the view to the hierarchy and prepare its animation layer for entry.
  auto* view_ptr = content_view()->AddChildView(std::move(view));

  // If this runs in test, AssistantCardElement can use TestAshWebView. It does
  // not return a native view. We cannot obtain a layer for animation. We want
  // to add it to the UI tree as a test is going to interact with it. But we
  // skip an animation.
  if (is_card && !view_ptr->GetLayerForAnimating())
    return nullptr;

  view_ptr->GetLayerForAnimating()->SetOpacity(0.f);

  // Return the animator that will be used to animate the view.
  return view_ptr->CreateAnimator();
}

void UiElementContainerView::OnAllViewsAnimatedIn() {
  const auto* response =
      AssistantInteractionController::Get()->GetModel()->response();
  DCHECK(response);

  // Let screen reader read the query result. This includes the text response
  // and the card fallback text, but webview result is not included. We don't
  // read when there is TTS to avoid speaking over the server response.
  if (!response->has_tts())
    NotifyAccessibilityEvent(ax::mojom::Event::kAlert, true);
}

void UiElementContainerView::OnOverflowIndicatorVisibilityChanged(
    bool is_visible) {
  const float target_opacity = is_visible ? 1.f : 0.f;

  ui::Layer* layer = scroll_indicator_->layer();
  if (!cc::MathUtil::IsWithinEpsilon(layer->GetTargetOpacity(), target_opacity))
    layer->SetOpacity(target_opacity);
}

SkColor UiElementContainerView::GetOverflowIndicatorBackgroundColor() const {
  return ColorProvider::Get()->GetContentLayerColor(
      ColorProvider::ContentLayerType::kSeparatorColor);
}

BEGIN_METADATA(UiElementContainerView)
END_METADATA

}  // namespace ash