blob: 46f658263e0bae880affaa6a809a44187bd02f3f [file] [log] [blame]
// 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/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ui/omnibox/omnibox_theme.h"
#include "chrome/browser/ui/views/harmony/chrome_typography.h"
#include "ui/base/material_design/material_design_controller.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"
namespace {
// Use the primary style for everything. TextStyle sometimes controls color, but
// we use OmniboxTheme for that.
constexpr int kTextStyle = views::style::STYLE_PRIMARY;
// Indicates to use CONTEXT_OMNIBOX_PRIMARY when picking a font size in legacy
// code paths.
constexpr int kInherit = INT_MIN;
struct TextStyle {
OmniboxPart part;
// The legacy size delta, relative to the ui::ResourceBundle BaseFont, or
// kInherit to use CONTEXT_OMNIBOX_PRIMARY, to match the omnibox font.
// Note: the actual font size may differ due to |baseline| altering the size.
int legacy_size_delta = kInherit;
// The size delta from the Touchable chrome spec. This is always relative to
// CONTEXT_OMNIBOX_PRIMARY, which defaults to 15pt under touch. Only negative
// deltas are supported correctly (the line height will not increase to fit).
int touchable_size_delta = 0;
// The baseline shift. Ignored under touch (text is always baseline-aligned).
gfx::BaselineStyle baseline = gfx::NORMAL_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) {
// The size delta for large fonts in the legacy spec (per comment above, the
// result is usually smaller due to the baseline style).
constexpr int kLarge = ui::ResourceBundle::kLargeFontDelta;
// The size delta for the smaller of font size in the touchable style. This
// will always use the same baseline style.
constexpr int kTouchableSmall = -3;
switch (type) {
case SuggestionAnswer::TOP_ALIGNED:
return {OmniboxPart::RESULTS_TEXT_DIMMED, kLarge, kTouchableSmall,
gfx::SUPERIOR};
case SuggestionAnswer::DESCRIPTION_NEGATIVE:
return {OmniboxPart::RESULTS_TEXT_NEGATIVE, kLarge, kTouchableSmall,
gfx::INFERIOR};
case SuggestionAnswer::DESCRIPTION_POSITIVE:
return {OmniboxPart::RESULTS_TEXT_POSITIVE, kLarge, kTouchableSmall,
gfx::INFERIOR};
case SuggestionAnswer::ANSWER_TEXT_MEDIUM:
return {OmniboxPart::RESULTS_TEXT_DIMMED};
case SuggestionAnswer::ANSWER_TEXT_LARGE:
// Note: There is no large font in the touchable spec.
return {OmniboxPart::RESULTS_TEXT_DIMMED, kLarge};
case SuggestionAnswer::SUGGESTION_SECONDARY_TEXT_SMALL:
return {OmniboxPart::RESULTS_TEXT_DIMMED, kLarge, kTouchableSmall,
gfx::INFERIOR};
case SuggestionAnswer::SUGGESTION_SECONDARY_TEXT_MEDIUM:
return {OmniboxPart::RESULTS_TEXT_DIMMED};
case SuggestionAnswer::PERSONALIZED_SUGGESTION:
case SuggestionAnswer::SUGGESTION: // Fall through.
default:
return {OmniboxPart::RESULTS_TEXT_DEFAULT};
}
}
const gfx::FontList& GetFontForType(int text_type) {
const gfx::FontList& omnibox_font =
views::style::GetFont(CONTEXT_OMNIBOX_PRIMARY, kTextStyle);
if (ui::MaterialDesignController::IsTouchOptimizedUiEnabled()) {
int delta = GetTextStyle(text_type).touchable_size_delta;
if (delta == 0)
return omnibox_font;
// Use the cache in ResourceBundle (gfx::FontList::Derive() is slow and
// doesn't return a reference).
return ui::ResourceBundle::GetSharedInstance().GetFontListWithDelta(
omnibox_font.GetFontSize() - gfx::FontList().GetFontSize() + delta);
}
int delta = GetTextStyle(text_type).legacy_size_delta;
if (delta == kInherit)
return omnibox_font;
return ui::ResourceBundle::GetSharedInstance().GetFontListWithDelta(delta);
}
} // namespace
OmniboxTextView::OmniboxTextView(OmniboxResultView* result_view)
: result_view_(result_view), font_height_(0), wrap_text_lines_(false) {}
OmniboxTextView::~OmniboxTextView() {}
gfx::Size OmniboxTextView::CalculatePreferredSize() const {
if (!render_text_)
return gfx::Size();
return render_text_->GetStringSize();
}
bool OmniboxTextView::CanProcessEventsWithinSubtree() const {
return false;
}
const char* OmniboxTextView::GetClassName() const {
return "OmniboxTextView";
}
int OmniboxTextView::GetHeightForWidth(int width) const {
if (!render_text_)
return 0;
// If text wrapping is not called for we can simply return the font height.
if (!wrap_text_lines_) {
return GetLineHeight();
}
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(
views::style::GetFont(CONTEXT_OMNIBOX_PRIMARY, kTextStyle));
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);
OmniboxPart part = OmniboxPart::RESULTS_TEXT_DEFAULT;
if (classifications[i].style & ACMatchClassification::URL) {
part = OmniboxPart::RESULTS_TEXT_URL;
render_text->SetDirectionalityMode(gfx::DIRECTIONALITY_AS_URL);
} else if (classifications[i].style & ACMatchClassification::DIM) {
part = OmniboxPart::RESULTS_TEXT_DIMMED;
} else if (classifications[i].style & ACMatchClassification::INVISIBLE) {
part = OmniboxPart::RESULTS_TEXT_INVISIBLE;
}
render_text->ApplyColor(result_view_->GetColor(part), current_range);
}
return render_text;
}
void OmniboxTextView::OnPaint(gfx::Canvas* canvas) {
View::OnPaint(canvas);
if (!render_text_) {
return;
}
render_text_->SetDisplayRect(GetContentsBounds());
render_text_->Draw(canvas);
}
void OmniboxTextView::Dim() {
render_text_->SetColor(
result_view_->GetColor(OmniboxPart::RESULTS_TEXT_DIMMED));
}
void OmniboxTextView::SetText(const base::string16& text,
const ACMatchClassifications& classifications) {
render_text_.reset();
render_text_ = CreateClassifiedRenderText(text, classifications);
UpdateLineHeight();
}
void OmniboxTextView::SetText(const base::string16& text) {
render_text_.reset();
render_text_ = CreateRenderText(text);
UpdateLineHeight();
}
void OmniboxTextView::SetText(const SuggestionAnswer::ImageLine& line) {
wrap_text_lines_ = line.num_text_lines() > 1;
render_text_.reset();
render_text_ = CreateText(line);
UpdateLineHeight();
}
std::unique_ptr<gfx::RenderText> OmniboxTextView::CreateText(
const SuggestionAnswer::ImageLine& line) const {
std::unique_ptr<gfx::RenderText> destination =
CreateRenderText(base::string16());
// 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.
destination->SetFontList(GetFontForType(line.text_fields()[0].type()));
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 (&lt; 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(result_view_->GetColor(text_style.part), range);
// Baselines are always aligned under the touch UI. Font sizes change instead.
if (!ui::MaterialDesignController::IsTouchOptimizedUiEnabled()) {
destination->ApplyBaselineStyle(text_style.baseline, range);
} else if (text_style.touchable_size_delta != 0) {
destination->ApplyFontSizeOverride(GetFontForType(text_type).GetFontSize(),
range);
}
}
int OmniboxTextView::GetLineHeight() const {
return font_height_;
}
void OmniboxTextView::UpdateLineHeight() {
font_height_ = std::max(render_text_->font_list().GetHeight(),
render_text_->font_list()
.DeriveWithWeight(gfx::Font::Weight::BOLD)
.GetHeight());
}