| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <limits.h> |
| |
| #include <algorithm> |
| #include <memory> |
| |
| #include "chrome/browser/ui/views/omnibox/omnibox_text_view.h" |
| |
| #include "base/macros.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/range/range.h" |
| #include "ui/gfx/render_text.h" |
| #include "ui/native_theme/native_theme.h" |
| |
| using ui::NativeTheme; |
| |
| namespace { |
| |
| struct TextStyle { |
| ui::ResourceBundle::FontStyle font; |
| ui::NativeTheme::ColorId |
| colors[OmniboxResultView::ResultViewState::NUM_STATES]; |
| gfx::BaselineStyle baseline; |
| }; |
| |
| // Returns the styles that should be applied to the specified answer text type. |
| // |
| // Note that the font value is only consulted for the first text type that |
| // appears on an answer line, because RenderText does not yet support multiple |
| // font sizes. Subsequent text types on the same line will share the text size |
| // of the first type, while the color and baseline styles specified here will |
| // always apply. The gfx::INFERIOR baseline style is used as a workaround to |
| // produce smaller text on the same line. The way this is used in the current |
| // set of answers is that the small types (TOP_ALIGNED, DESCRIPTION_NEGATIVE, |
| // DESCRIPTION_POSITIVE and SUGGESTION_SECONDARY_TEXT_SMALL) only ever appear |
| // following LargeFont text, so for consistency they specify LargeFont for the |
| // first value even though this is not actually used (since they're not the |
| // first value). |
| TextStyle GetTextStyle(int type) { |
| switch (type) { |
| case SuggestionAnswer::TOP_ALIGNED: |
| return {ui::ResourceBundle::LargeFont, |
| {NativeTheme::kColorId_ResultsTableNormalDimmedText, |
| NativeTheme::kColorId_ResultsTableHoveredDimmedText, |
| NativeTheme::kColorId_ResultsTableSelectedDimmedText}, |
| gfx::SUPERIOR}; |
| case SuggestionAnswer::DESCRIPTION_NEGATIVE: |
| return {ui::ResourceBundle::LargeFont, |
| {NativeTheme::kColorId_ResultsTableNegativeText, |
| NativeTheme::kColorId_ResultsTableNegativeHoveredText, |
| NativeTheme::kColorId_ResultsTableNegativeSelectedText}, |
| gfx::INFERIOR}; |
| case SuggestionAnswer::DESCRIPTION_POSITIVE: |
| return {ui::ResourceBundle::LargeFont, |
| {NativeTheme::kColorId_ResultsTablePositiveText, |
| NativeTheme::kColorId_ResultsTablePositiveHoveredText, |
| NativeTheme::kColorId_ResultsTablePositiveSelectedText}, |
| gfx::INFERIOR}; |
| case SuggestionAnswer::PERSONALIZED_SUGGESTION: |
| return {ui::ResourceBundle::BaseFont, |
| {NativeTheme::kColorId_ResultsTableNormalText, |
| NativeTheme::kColorId_ResultsTableHoveredText, |
| NativeTheme::kColorId_ResultsTableSelectedText}, |
| gfx::NORMAL_BASELINE}; |
| case SuggestionAnswer::ANSWER_TEXT_MEDIUM: |
| return {ui::ResourceBundle::BaseFont, |
| {NativeTheme::kColorId_ResultsTableNormalDimmedText, |
| NativeTheme::kColorId_ResultsTableHoveredDimmedText, |
| NativeTheme::kColorId_ResultsTableSelectedDimmedText}, |
| gfx::NORMAL_BASELINE}; |
| case SuggestionAnswer::ANSWER_TEXT_LARGE: |
| return {ui::ResourceBundle::LargeFont, |
| {NativeTheme::kColorId_ResultsTableNormalDimmedText, |
| NativeTheme::kColorId_ResultsTableHoveredDimmedText, |
| NativeTheme::kColorId_ResultsTableSelectedDimmedText}, |
| gfx::NORMAL_BASELINE}; |
| case SuggestionAnswer::SUGGESTION_SECONDARY_TEXT_SMALL: |
| return {ui::ResourceBundle::LargeFont, |
| {NativeTheme::kColorId_ResultsTableNormalDimmedText, |
| NativeTheme::kColorId_ResultsTableHoveredDimmedText, |
| NativeTheme::kColorId_ResultsTableSelectedDimmedText}, |
| gfx::INFERIOR}; |
| case SuggestionAnswer::SUGGESTION_SECONDARY_TEXT_MEDIUM: |
| return {ui::ResourceBundle::BaseFont, |
| {NativeTheme::kColorId_ResultsTableNormalDimmedText, |
| NativeTheme::kColorId_ResultsTableHoveredDimmedText, |
| NativeTheme::kColorId_ResultsTableSelectedDimmedText}, |
| gfx::NORMAL_BASELINE}; |
| case SuggestionAnswer::SUGGESTION: // Fall through. |
| default: |
| return {ui::ResourceBundle::BaseFont, |
| {NativeTheme::kColorId_ResultsTableNormalText, |
| NativeTheme::kColorId_ResultsTableHoveredText, |
| NativeTheme::kColorId_ResultsTableSelectedText}, |
| gfx::NORMAL_BASELINE}; |
| } |
| } |
| |
| } // namespace |
| |
| OmniboxTextView::OmniboxTextView(OmniboxResultView* result_view, |
| const gfx::FontList& font_list) |
| : result_view_(result_view), |
| font_list_(font_list), |
| font_height_(std::max( |
| font_list.GetHeight(), |
| font_list.DeriveWithWeight(gfx::Font::Weight::BOLD).GetHeight())) {} |
| |
| OmniboxTextView::~OmniboxTextView() {} |
| |
| gfx::Size OmniboxTextView::CalculatePreferredSize() const { |
| if (!render_text_) |
| return gfx::Size(); |
| return render_text_->GetStringSize(); |
| } |
| |
| const char* OmniboxTextView::GetClassName() const { |
| return "OmniboxTextView"; |
| } |
| |
| int OmniboxTextView::GetHeightForWidth(int width) const { |
| if (!render_text_) |
| return 0; |
| render_text_->SetDisplayRect(gfx::Rect(width, 0)); |
| gfx::Size string_size = render_text_->GetStringSize(); |
| return string_size.height(); |
| } |
| |
| std::unique_ptr<gfx::RenderText> OmniboxTextView::CreateRenderText( |
| const base::string16& text) const { |
| auto render_text = gfx::RenderText::CreateHarfBuzzInstance(); |
| render_text->SetDisplayRect(gfx::Rect(gfx::Size(INT_MAX, 0))); |
| render_text->SetCursorEnabled(false); |
| render_text->SetElideBehavior(gfx::ELIDE_TAIL); |
| render_text->SetFontList(font_list_); |
| render_text->SetText(text); |
| return render_text; |
| } |
| |
| std::unique_ptr<gfx::RenderText> OmniboxTextView::CreateClassifiedRenderText( |
| const base::string16& text, |
| const ACMatchClassifications& classifications) const { |
| std::unique_ptr<gfx::RenderText> render_text(CreateRenderText(text)); |
| const size_t text_length = render_text->text().length(); |
| for (size_t i = 0; i < classifications.size(); ++i) { |
| const size_t text_start = classifications[i].offset; |
| if (text_start >= text_length) |
| break; |
| |
| const size_t text_end = |
| (i < (classifications.size() - 1)) |
| ? std::min(classifications[i + 1].offset, text_length) |
| : text_length; |
| const gfx::Range current_range(text_start, text_end); |
| |
| // Calculate style-related data. |
| if (classifications[i].style & ACMatchClassification::MATCH) |
| render_text->ApplyWeight(gfx::Font::Weight::BOLD, current_range); |
| |
| OmniboxResultView::ColorKind color_kind = OmniboxResultView::TEXT; |
| if (classifications[i].style & ACMatchClassification::URL) { |
| color_kind = OmniboxResultView::URL; |
| render_text->SetDirectionalityMode(gfx::DIRECTIONALITY_AS_URL); |
| } else if (classifications[i].style & ACMatchClassification::DIM) { |
| color_kind = OmniboxResultView::DIMMED_TEXT; |
| } else if (classifications[i].style & ACMatchClassification::INVISIBLE) { |
| color_kind = OmniboxResultView::INVISIBLE_TEXT; |
| } |
| render_text->ApplyColor( |
| result_view_->GetColor(result_view_->GetState(), color_kind), |
| current_range); |
| } |
| return render_text; |
| } |
| |
| void OmniboxTextView::OnPaint(gfx::Canvas* canvas) { |
| View::OnPaint(canvas); |
| |
| render_text_->SetDisplayRect(GetContentsBounds()); |
| render_text_->Draw(canvas); |
| } |
| |
| void OmniboxTextView::Dim() { |
| render_text_->SetColor(result_view_->GetColor( |
| result_view_->GetState(), OmniboxResultView::DIMMED_TEXT)); |
| } |
| |
| void OmniboxTextView::SetText(const base::string16& text, |
| const ACMatchClassifications& classifications) { |
| render_text_.reset(); |
| render_text_ = CreateClassifiedRenderText(text, classifications); |
| SizeToPreferredSize(); |
| } |
| |
| void OmniboxTextView::SetText(const base::string16& text) { |
| render_text_.reset(); |
| render_text_ = CreateRenderText(text); |
| SizeToPreferredSize(); |
| } |
| |
| void OmniboxTextView::SetText(const SuggestionAnswer::ImageLine& line) { |
| // This assumes that the first text type in the line can be used to specify |
| // the font for all the text fields in the line. For now this works but |
| // eventually it may be necessary to get RenderText to support multiple font |
| // sizes or use multiple RenderTexts. |
| render_text_.reset(); |
| render_text_ = CreateText(line, GetFontForType(line.text_fields()[0].type())); |
| SizeToPreferredSize(); |
| } |
| |
| std::unique_ptr<gfx::RenderText> OmniboxTextView::CreateText( |
| const SuggestionAnswer::ImageLine& line, |
| const gfx::FontList& font_list) const { |
| std::unique_ptr<gfx::RenderText> destination = |
| CreateRenderText(base::string16()); |
| destination->SetFontList(font_list); |
| |
| for (const SuggestionAnswer::TextField& text_field : line.text_fields()) |
| AppendText(destination.get(), text_field.text(), text_field.type()); |
| if (!line.text_fields().empty()) { |
| constexpr int kMaxDisplayLines = 3; |
| const SuggestionAnswer::TextField& first_field = line.text_fields().front(); |
| if (first_field.has_num_lines() && first_field.num_lines() > 1 && |
| destination->MultilineSupported()) { |
| destination->SetMultiline(true); |
| destination->SetMaxLines( |
| std::min(kMaxDisplayLines, first_field.num_lines())); |
| } |
| } |
| const base::char16 space(' '); |
| const auto* text_field = line.additional_text(); |
| if (text_field) { |
| AppendText(destination.get(), space + text_field->text(), |
| text_field->type()); |
| } |
| text_field = line.status_text(); |
| if (text_field) { |
| AppendText(destination.get(), space + text_field->text(), |
| text_field->type()); |
| } |
| return destination; |
| } |
| |
| void OmniboxTextView::AppendText(gfx::RenderText* destination, |
| const base::string16& text, |
| int text_type) const { |
| // TODO(dschuyler): make this better. Right now this only supports unnested |
| // bold tags. In the future we'll need to flag unexpected tags while adding |
| // support for b, i, u, sub, and sup. We'll also need to support HTML |
| // entities (< for '<', etc.). |
| const base::string16 begin_tag = base::ASCIIToUTF16("<b>"); |
| const base::string16 end_tag = base::ASCIIToUTF16("</b>"); |
| size_t begin = 0; |
| while (true) { |
| size_t end = text.find(begin_tag, begin); |
| if (end == base::string16::npos) { |
| AppendTextHelper(destination, text.substr(begin), text_type, false); |
| break; |
| } |
| AppendTextHelper(destination, text.substr(begin, end - begin), text_type, |
| false); |
| begin = end + begin_tag.length(); |
| end = text.find(end_tag, begin); |
| if (end == base::string16::npos) |
| break; |
| AppendTextHelper(destination, text.substr(begin, end - begin), text_type, |
| true); |
| begin = end + end_tag.length(); |
| } |
| } |
| |
| void OmniboxTextView::AppendTextHelper(gfx::RenderText* destination, |
| const base::string16& text, |
| int text_type, |
| bool is_bold) const { |
| if (text.empty()) |
| return; |
| int offset = destination->text().length(); |
| gfx::Range range(offset, offset + text.length()); |
| destination->AppendText(text); |
| const TextStyle& text_style = GetTextStyle(text_type); |
| // TODO(dschuyler): follow up on the problem of different font sizes within |
| // one RenderText. Maybe with destination->SetFontList(...). |
| destination->ApplyWeight( |
| is_bold ? gfx::Font::Weight::BOLD : gfx::Font::Weight::NORMAL, range); |
| destination->ApplyColor(GetNativeTheme()->GetSystemColor( |
| text_style.colors[result_view_->GetState()]), |
| range); |
| destination->ApplyBaselineStyle(text_style.baseline, range); |
| } |
| |
| int OmniboxTextView::GetLineHeight() const { |
| return font_height_; |
| } |
| |
| const gfx::FontList& OmniboxTextView::GetFontForType(int text_type) const { |
| // When BaseFont is specified, reuse font_list_, which may have had size |
| // adjustments from BaseFont before it was provided to this class. Otherwise, |
| // get the standard font list for the specified style. |
| ui::ResourceBundle::FontStyle font_style = GetTextStyle(text_type).font; |
| const gfx::FontList& font_list = |
| (font_style == ui::ResourceBundle::BaseFont) |
| ? font_list_ |
| : ui::ResourceBundle::GetSharedInstance().GetFontList(font_style); |
| font_height_ = font_list.GetHeight(); |
| return font_list; |
| } |