chromium/chrome/browser/ui/quick_answers/ui/rich_answers_definition_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_definition_view.h"

#include "base/functional/bind.h"
#include "chrome/browser/profiles/profile_manager.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 "chromeos/components/quick_answers/quick_answers_model.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/color/color_id.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/webview/webview.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/layout/flex_layout_view.h"

namespace {

// The space available to show text in the definition header view.
// The width of the phonetics audio button needs to be subtracted
// from the total header width.
constexpr int kDefinitionHeaderTextWidth =
    quick_answers::kContentHeaderWidth -
    (quick_answers::kContentSingleSpacing +
     quick_answers::kRichAnswersIconContainerRadius);

constexpr char kBulletSymbol[] = " \u2022 ";

}  // namespace

namespace quick_answers {

// RichAnswersDefinitionView
// -----------------------------------------------------------

RichAnswersDefinitionView::RichAnswersDefinitionView(
    const gfx::Rect& anchor_view_bounds,
    base::WeakPtr<QuickAnswersUiController> controller,
    const DefinitionResult& definition_result)
    : RichAnswersView(anchor_view_bounds,
                      controller,
                      ResultType::kDefinitionResult),
      definition_result_(definition_result) {
  InitLayout();

  // TODO (b/274184670): Add custom focus behavior according to
  // approved greenlines.
}

RichAnswersDefinitionView::~RichAnswersDefinitionView() = default;

void RichAnswersDefinitionView::InitLayout() {
  content_view_ = GetContentView();

  // Word and phonetics.
  AddHeaderViews();

  AddWordClass();

  // Set up the subcontent view that contains all the definition info other than
  // the word, phonetics info, and word class.
  SetUpSubContentView();

  AddDefinition(subcontent_view_, definition_result_.sense,
                kSubContentTextWidth);

  MaybeAddSampleSentence(subcontent_view_, definition_result_.sense,
                         kSubContentTextWidth);

  MaybeAddSynonyms(subcontent_view_, definition_result_.sense,
                   kSubContentTextWidth);

  MaybeAddAdditionalDefinitions();
}

void RichAnswersDefinitionView::AddHeaderViews() {
  // This box layout will have the view flex values as:
  // - header_view (flex=1): resize (either shrink or expand as necessary)
  // - settings_button_view (flex=0): no resize
  views::BoxLayoutView* box_layout_view =
      content_view_->AddChildView(CreateHorizontalBoxLayoutView());
  box_layout_view->SetCrossAxisAlignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);
  views::FlexLayoutView* header_view =
      box_layout_view->AddChildView(CreateHorizontalFlexLayoutView());

  QuickAnswersTextLabel* word_label =
      header_view->AddChildView(QuickAnswersTextLabel::CreateLabelWithStyle(
          definition_result_.word, GetFontList(TypographyToken::kCrosTitle1),
          kDefinitionHeaderTextWidth,
          /*is_multi_line=*/false));

  // The phonetics text label is an optional child view in the header.
  // Check that the phonetics text is not empty.
  if (!definition_result_.phonetics_info.text.empty()) {
    std::unique_ptr<QuickAnswersTextLabel> phonetics_label =
        QuickAnswersTextLabel::CreateLabelWithStyle(
            "/" + definition_result_.phonetics_info.text + "/",
            GetFontList(TypographyToken::kCrosBody2), kContentTextWidth,
            /*is_multi_line=*/false);

    // Display the phonetics label in the header view if it fits, otherwise show
    // it in a subheader view below.
    int header_space_available =
        kDefinitionHeaderTextWidth - word_label->GetPreferredSize().width();
    int phonetics_label_width =
        phonetics_label->GetPreferredSize().width() + kContentSingleSpacing;
    if (phonetics_label_width <= header_space_available) {
      header_view->AddChildView(std::move(phonetics_label));
    } else {
      content_view_->AddChildView(std::move(phonetics_label));
    }
  }

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

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

void RichAnswersDefinitionView::AddPhoneticsAudioButtonTo(
    views::View* container_view) {
  // Setup an invisible web view to play TTS audio.
  tts_audio_web_view_ = container_view->AddChildView(
      std::make_unique<views::WebView>(ProfileManager::GetActiveUserProfile()));
  tts_audio_web_view_->SetVisible(false);

  base::RepeatingClosure phonetics_audio_button_closure = base::BindRepeating(
      &RichAnswersDefinitionView::OnPhoneticsAudioButtonPressed,
      weak_factory_.GetWeakPtr());
  ui::ImageModel phonetics_audio_button_closure_image_model =
      ui::ImageModel::FromVectorIcon(vector_icons::kVolumeUpIcon,
                                     ui::kColorSysOnSurface,
                                     kRichAnswersIconSizeDip);
  views::ImageButton* button_view =
      container_view->AddChildView(CreateImageButtonView(
          phonetics_audio_button_closure,
          phonetics_audio_button_closure_image_model,
          ui::kColorSysStateHoverOnSubtle,
          l10n_util::GetStringUTF16(
              IDS_RICH_ANSWERS_VIEW_PHONETICS_BUTTON_A11Y_NAME_TEXT)));
  button_view->SetMinimumImageSize(
      gfx::Size(kRichAnswersIconSizeDip, kRichAnswersIconSizeDip));
}

void RichAnswersDefinitionView::OnPhoneticsAudioButtonPressed() {
  PhoneticsInfo phonetics_info = definition_result_.phonetics_info;
  // Use the phonetics audio URL if provided.
  if (!phonetics_info.phonetics_audio.is_empty()) {
    tts_audio_web_view_->LoadInitialURL(phonetics_info.phonetics_audio);
    return;
  }

  GenerateTTSAudio(tts_audio_web_view_->GetBrowserContext(),
                   phonetics_info.query_text, phonetics_info.locale);
}

void RichAnswersDefinitionView::AddWordClass() {
  content_view_->AddChildView(QuickAnswersTextLabel::CreateLabelWithStyle(
      definition_result_.word_class,
      GetFontList(TypographyToken::kCrosBody2Italic), kContentTextWidth,
      /*is_multi_line=*/false, ui::kColorSysSecondary));
}

void RichAnswersDefinitionView::SetUpSubContentView() {
  subcontent_view_ = content_view_->AddChildView(CreateVerticalBoxLayoutView());
  subcontent_view_->SetMinimumCrossAxisSize(kSubContentTextWidth);
  subcontent_view_->SetInsideBorderInsets(kSubContentViewInsets);
}

void RichAnswersDefinitionView::AddDefinition(views::View* container_view,
                                              const Sense& sense,
                                              int label_width) {
  container_view->AddChildView(QuickAnswersTextLabel::CreateLabelWithStyle(
      sense.definition, GetFontList(TypographyToken::kCrosBody2), label_width,
      /*is_multi_line=*/true, ui::kColorSysOnSurface));
}

void RichAnswersDefinitionView::MaybeAddSampleSentence(
    views::View* container_view,
    const Sense& sense,
    int label_width) {
  if (!sense.sample_sentence) {
    return;
  }

  container_view->AddChildView(QuickAnswersTextLabel::CreateLabelWithStyle(
      "\"" + sense.sample_sentence.value() + "\"",
      GetFontList(TypographyToken::kCrosBody2), label_width,
      /*is_multi_line=*/true, ui::kColorSysSecondary));
}

void RichAnswersDefinitionView::MaybeAddSynonyms(views::View* container_view,
                                                 const Sense& sense,
                                                 int label_width) {
  if (!sense.synonyms_list) {
    return;
  }

  // This box layout will have the view flex values as:
  // - similar_label (flex=0): no resize
  // - synonyms_label (flex=1): resize (either shrink or expand as necessary)
  views::BoxLayoutView* box_layout_view =
      container_view->AddChildView(CreateHorizontalBoxLayoutView());
  box_layout_view->SetCrossAxisAlignment(
      views::BoxLayout::CrossAxisAlignment::kStart);

  QuickAnswersTextLabel* similar_label =
      box_layout_view->AddChildView(QuickAnswersTextLabel::CreateLabelWithStyle(
          l10n_util::GetStringUTF8(
              IDS_RICH_ANSWERS_VIEW_DEFINITION_SYNONYMS_LABEL_TEXT),
          GetFontList(TypographyToken::kCrosBody2), label_width,
          /*is_multi_line=*/true, ui::kColorCrosSysPositive));
  std::string synonyms_text =
      base::JoinString(sense.synonyms_list.value(), ", ");
  int synonyms_label_width = label_width -
                             similar_label->GetPreferredSize().width() -
                             kContentSingleSpacing;
  QuickAnswersTextLabel* synonyms_label =
      box_layout_view->AddChildView(QuickAnswersTextLabel::CreateLabelWithStyle(
          synonyms_text, GetFontList(TypographyToken::kCrosBody2),
          synonyms_label_width,
          /*is_multi_line=*/true, ui::kColorSysSecondary));

  box_layout_view->SetFlexForView(similar_label, /*flex=*/0);
  box_layout_view->SetFlexForView(synonyms_label, /*flex=*/1);
}

void RichAnswersDefinitionView::MaybeAddAdditionalDefinitions() {
  if (!definition_result_.subsenses_list) {
    return;
  }

  for (const Sense& subsense : *definition_result_.subsenses_list) {
    AddSubsense(subsense);
  }
}

void RichAnswersDefinitionView::AddSubsense(const Sense& subsense) {
  // This box layout will have the view flex values as:
  // - bullet_label (flex=0): no resize
  // - subsense_view (flex=1): resize (either shrink or expand as necessary)
  views::BoxLayoutView* box_layout_view =
      subcontent_view_->AddChildView(CreateHorizontalBoxLayoutView());
  box_layout_view->SetCrossAxisAlignment(
      views::BoxLayout::CrossAxisAlignment::kStart);

  QuickAnswersTextLabel* bullet_label =
      box_layout_view->AddChildView(QuickAnswersTextLabel::CreateLabelWithStyle(
          kBulletSymbol, GetFontList(TypographyToken::kCrosBody2),
          kSubContentTextWidth,
          /*is_multi_line=*/false, ui::kColorSysOnSurface));

  views::BoxLayoutView* subsense_view =
      box_layout_view->AddChildView(CreateVerticalBoxLayoutView());
  int subsense_labels_width = kSubContentTextWidth -
                              bullet_label->GetPreferredSize().width() -
                              kContentSingleSpacing;
  subsense_view->SetMinimumCrossAxisSize(subsense_labels_width);
  AddDefinition(subsense_view, subsense, subsense_labels_width);
  MaybeAddSampleSentence(subsense_view, subsense, subsense_labels_width);
  MaybeAddSynonyms(subsense_view, subsense, subsense_labels_width);

  box_layout_view->SetFlexForView(bullet_label, /*flex=*/0);
  box_layout_view->SetFlexForView(subsense_view, /*flex=*/1);
}

BEGIN_METADATA(RichAnswersDefinitionView)
END_METADATA

}  // namespace quick_answers