chromium/chrome/browser/ui/quick_answers/ui/rich_answers_view.cc

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

#include "chrome/browser/ui/quick_answers/ui/rich_answers_view.h"

#include "base/functional/bind.h"
#include "chrome/browser/ui/quick_answers/quick_answers_ui_controller.h"
#include "chrome/browser/ui/quick_answers/ui/quick_answers_text_label.h"
#include "chrome/browser/ui/quick_answers/ui/quick_answers_util.h"
#include "chrome/browser/ui/quick_answers/ui/quick_answers_view.h"
#include "chrome/browser/ui/quick_answers/ui/rich_answers_definition_view.h"
#include "chrome/browser/ui/quick_answers/ui/rich_answers_translation_view.h"
#include "chrome/browser/ui/quick_answers/ui/rich_answers_unit_conversion_view.h"
#include "chromeos/components/quick_answers/public/cpp/controller/quick_answers_controller.h"
#include "chromeos/components/quick_answers/quick_answers_model.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/themed_vector_icon.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/compositor/layer.h"
#include "ui/display/screen.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/button_controller.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/link.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_view.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "url/gurl.h"

namespace {

using quick_answers::QuickAnswer;
using quick_answers::QuickAnswerResultText;
using quick_answers::ResultType;

// Rich card dimensions.
constexpr int kDefaultRichCardWidth = 360;
constexpr int kMinimumRichCardHeight = 120;
constexpr int kMaximumRichCardHeight = 464;

// View dimensions.
constexpr auto kMainViewInsets = gfx::Insets::TLBR(20, 20, 16, 20);

// Buttons view.
constexpr int kSettingsButtonSizeDip = 20;

// Border corner radius.
constexpr int kRoundedCornerRadius = 12;

// Google search link.
constexpr auto kSearchLinkViewInsets = gfx::Insets::TLBR(0, 60, 20, 20);

}  // namespace

namespace quick_answers {

// RichAnswersView -----------------------------------------------------------

RichAnswersView::RichAnswersView(
    const gfx::Rect& anchor_view_bounds,
    base::WeakPtr<QuickAnswersUiController> controller,
    const ResultType result_type)
    : anchor_view_bounds_(anchor_view_bounds),
      controller_(std::move(controller)),
      result_type_(result_type) {
  InitLayout();

  SetFocusBehavior(views::View::FocusBehavior::ALWAYS);

  GetViewAccessibility().SetRole(ax::mojom::Role::kDialog);
  GetViewAccessibility().SetName(
      l10n_util::GetStringUTF8(IDS_RICH_ANSWERS_VIEW_A11Y_NAME_TEXT));
}

RichAnswersView::~RichAnswersView() = default;

views::UniqueWidgetPtr RichAnswersView::CreateWidget(
    const gfx::Rect& anchor_view_bounds,
    base::WeakPtr<QuickAnswersUiController> controller,
    const QuickAnswer& quick_answer,
    const StructuredResult& result) {
  // Create the correct rich card child view depending on the result type.
  std::unique_ptr<RichAnswersView> child_view = nullptr;
  switch (quick_answer.result_type) {
    case ResultType::kDefinitionResult: {
      child_view = std::make_unique<RichAnswersDefinitionView>(
          anchor_view_bounds, controller, *result.definition_result.get());
      break;
    }
    case ResultType::kTranslationResult: {
      child_view = std::make_unique<RichAnswersTranslationView>(
          anchor_view_bounds, controller, *result.translation_result.get());
      break;
    }
    case ResultType::kUnitConversionResult: {
      child_view = std::make_unique<RichAnswersUnitConversionView>(
          anchor_view_bounds, controller, *result.unit_conversion_result.get());
      break;
    }
    case ResultType::kNoResult: {
      return views::UniqueWidgetPtr();
    }
  }

  CHECK(child_view);

  views::Widget::InitParams params(
      views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
      views::Widget::InitParams::TYPE_POPUP);
  params.activatable = views::Widget::InitParams::Activatable::kYes;
  params.shadow_elevation = 2;
  params.shadow_type = views::Widget::InitParams::ShadowType::kDrop;
  params.z_order = ui::ZOrderLevel::kFloatingUIElement;
  params.corner_radius = kRoundedCornerRadius;
  params.name = kWidgetName;
  views::UniqueWidgetPtr widget =
      std::make_unique<views::Widget>(std::move(params));

  RichAnswersView* rich_answers_view =
      widget->SetContentsView(std::move(child_view));
  rich_answers_view->UpdateBounds();
  rich_answers_view->SetPaintToLayer();
  rich_answers_view->layer()->SetRoundedCornerRadius(
      gfx::RoundedCornersF(kRoundedCornerRadius));
  rich_answers_view->layer()->SetIsFastRoundedCorner(true);

  return widget;
}

void RichAnswersView::AddedToWidget() {
  widget_observation_.Observe(GetWidget());
}

void RichAnswersView::OnWidgetDestroying(views::Widget* widget) {
  widget_observation_.Reset();
}

void RichAnswersView::OnKeyEvent(ui::KeyEvent* event) {
  // TODO(b/283135347): Track rich card interaction types for metrics.
  if (event->type() != ui::EventType::kKeyPressed) {
    return;
  }

  if (event->key_code() == ui::VKEY_ESCAPE) {
    QuickAnswersController::Get()->DismissQuickAnswers(
        quick_answers::QuickAnswersExitPoint::kUnspecified);
  }
}

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

  search_link_label_->SetEnabledColor(
      GetColorProvider()->GetColor(ui::kColorSysPrimary));
}

void RichAnswersView::OnWidgetActivationChanged(views::Widget* widget,
                                                bool active) {
  // TODO(b/283135347): Track rich card interaction types for metrics.
  if (!active && widget->IsVisible()) {
    QuickAnswersController::Get()->DismissQuickAnswers(
        quick_answers::QuickAnswersExitPoint::kUnspecified);
  }
}

ui::ImageModel RichAnswersView::GetIconImageModelForTesting() {
  return vector_icon_ ? vector_icon_->GetImageModel() : ui::ImageModel();
}

void RichAnswersView::InitLayout() {
  SetLayoutManager(std::make_unique<views::FillLayout>());

  // Set up the scrollable base view that contains all the rich card components.
  SetUpBaseView();

  // Set up the main view that contains the icon and content view.
  SetUpMainView();

  // Add icon that corresponds to the quick answer result type.
  AddResultTypeIcon();

  // Set up the content view that will be populated by the rich card subclasses.
  SetUpContentView();

  // Add google search link label at the bottom of the base view.
  AddGoogleSearchLink();
}

void RichAnswersView::SetUpBaseView() {
  views::ScrollView* scroll_view = AddChildView(
      views::Builder<views::ScrollView>()
          .ClipHeightTo(kMinimumRichCardHeight, kMaximumRichCardHeight)
          .SetBackgroundThemeColorId(ui::kColorPrimaryBackground)
          .SetHorizontalScrollBarMode(
              views::ScrollView::ScrollBarMode::kDisabled)
          .SetDrawOverflowIndicator(false)
          .SetAllowKeyboardScrolling(true)
          .Build());

  base_view_ = scroll_view->SetContents(std::make_unique<views::View>());
  auto* base_layout =
      base_view_->SetLayoutManager(std::make_unique<views::FlexLayout>());
  base_layout->SetOrientation(views::LayoutOrientation::kVertical)
      .SetCrossAxisAlignment(views::LayoutAlignment::kStretch);
}

void RichAnswersView::SetUpMainView() {
  // This box layout will have the view flex values as:
  // - result type icon (flex=0): no resize
  // - content_view_ (flex=1): resize (either shrink or expand as necessary)
  main_view_ = base_view_->AddChildView(
      views::Builder<views::BoxLayoutView>()
          .SetOrientation(views::BoxLayout::Orientation::kHorizontal)
          .SetCrossAxisAlignment(views::BoxLayout::CrossAxisAlignment::kStart)
          .SetInsideBorderInsets(kMainViewInsets)
          .SetBetweenChildSpacing(kContentDoubleSpacing)
          .Build());
}

void RichAnswersView::SetUpContentView() {
  content_view_ = main_view_->AddChildView(CreateVerticalBoxLayoutView());
  content_view_->SetMinimumCrossAxisSize(kContentTextWidth);

  main_view_->SetFlexForView(content_view_, /*flex=*/1);
}

void RichAnswersView::AddResultTypeIcon() {
  // Add the icon representing the quick answers result type as well as
  // a circle background behind the icon.
  auto* vector_icon_container =
      main_view_->AddChildView(std::make_unique<views::FlexLayoutView>());
  vector_icon_container->SetBackground(views::CreateThemedRoundedRectBackground(
      ui::kColorSysPrimary, kRichAnswersIconContainerRadius));
  vector_icon_container->SetBorder(
      views::CreateEmptyBorder(kRichAnswersIconBorderDip));

  vector_icon_ =
      vector_icon_container->AddChildView(std::make_unique<views::ImageView>());
  vector_icon_->SetImage(ui::ImageModel::FromVectorIcon(
      GetResultTypeIcon(result_type_), ui::kColorSysBaseContainerElevated,
      /*icon_size=*/kRichAnswersIconSizeDip));

  main_view_->SetFlexForView(vector_icon_container, /*flex=*/0);
}

views::View* RichAnswersView::AddSettingsButtonTo(views::View* container_view) {
  CHECK(container_view);

  settings_button_ = container_view->AddChildView(
      std::make_unique<views::ImageButton>(base::BindRepeating(
          &QuickAnswersUiController::OnSettingsButtonPressed, controller_)));
  settings_button_->SetImageModel(
      views::Button::ButtonState::STATE_NORMAL,
      ui::ImageModel::FromVectorIcon(vector_icons::kSettingsOutlineIcon,
                                     ui::kColorSysOnSurface,
                                     /*icon_size=*/kSettingsButtonSizeDip));
  settings_button_->SetTooltipText(l10n_util::GetStringUTF16(
      IDS_RICH_ANSWERS_VIEW_SETTINGS_BUTTON_A11Y_NAME_TEXT));

  return settings_button_;
}

void RichAnswersView::AddHeaderViewsTo(views::View* container_view,
                                       const std::string& header_text) {
  // This box layout will have the view flex values as:
  // - header_label (flex=1): resize (either shrink or expand as necessary)
  // - settings_button_view (flex=0): no resize
  views::BoxLayoutView* box_layout_view =
      container_view->AddChildView(CreateHorizontalBoxLayoutView());
  box_layout_view->SetCrossAxisAlignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);

  auto* header_label =
      box_layout_view->AddChildView(QuickAnswersTextLabel::CreateLabelWithStyle(
          header_text, GetFontList(TypographyToken::kCrosButton2),
          kContentHeaderWidth,
          /*is_multi_line=*/false, ui::kColorSysSecondary));

  views::View* settings_button_view = AddSettingsButtonTo(box_layout_view);

  box_layout_view->SetFlexForView(header_label, /*flex=*/1);
  box_layout_view->SetFlexForView(settings_button_view, /*flex=*/0);
}

void RichAnswersView::AddGoogleSearchLink() {
  auto* search_link_view = base_view_->AddChildView(
      views::Builder<views::FlexLayoutView>()
          .SetOrientation(views::LayoutOrientation::kHorizontal)
          .SetInteriorMargin(kSearchLinkViewInsets)
          .Build());

  search_link_label_ = search_link_view->AddChildView(
      std::make_unique<views::Link>(l10n_util::GetStringUTF16(
          IDS_RICH_ANSWERS_VIEW_SEARCH_LINK_LABEL_TEXT)));
  search_link_label_->SetCallback(base::BindRepeating(
      &RichAnswersView::OnGoogleSearchLinkClicked, weak_factory_.GetWeakPtr()));
  search_link_label_->SetFontList(GetFontList(TypographyToken::kCrosButton2));
  search_link_label_->SetForceUnderline(false);
}

void RichAnswersView::OnGoogleSearchLinkClicked() {
  CHECK(controller_);
  controller_->OnGoogleSearchLabelPressed();
}

void RichAnswersView::UpdateBounds() {
  auto display_bounds = display::Screen::GetScreen()
                            ->GetDisplayMatching(anchor_view_bounds_)
                            .work_area();
  int preferred_height = GetPreferredSize().height();
  gfx::Rect bounds = {
      {anchor_view_bounds_.x(), anchor_view_bounds_.y() - preferred_height / 2},
      {kDefaultRichCardWidth, preferred_height}};
  bounds.AdjustToFit(display_bounds);

#if BUILDFLAG(IS_CHROMEOS_ASH)
  // For Ash, convert the position relative to the screen.
  // For Lacros, `bounds` is already relative to the top-level window and
  // the position will be calculated on server side.
  wm::ConvertRectFromScreen(GetWidget()->GetNativeWindow()->parent(), &bounds);
#endif

  GetWidget()->SetBounds(bounds);
}

views::View* RichAnswersView::GetContentView() {
  CHECK(content_view_);

  return content_view_;
}

BEGIN_METADATA(RichAnswersView)
END_METADATA

}  // namespace quick_answers