blob: 297f67fcb86ceb8172ed0866e4457d6b2f4f5add [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/feature_list.h"
#include "base/macros.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/chrome_typography.h"
#include "components/omnibox/browser/omnibox_field_trial.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/color_palette.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;
// The vertical padding to provide each RenderText in addition to the height
// of the font. Where possible, RenderText uses this additional space to
// vertically center the cap height of the font instead of centering the
// entire font.
static constexpr int kVerticalPadding = 3;
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 text_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 (text_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::touch_ui()) {
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);
}
// The new answer layout has separate and different treatment of text styles,
// and as of writing both styling approaches need to be supported. When old
// answer styles are deprecated, the above TextStyle structure and related
// logic can be removed, and this used exclusively. This utility function
// applies new answer text styling for given text_type over range on render_text
// using result_view as a source for omnibox part colors.
void ApplyTextStyleForType(SuggestionAnswer::TextStyle text_style,
OmniboxResultView* result_view,
gfx::RenderText* render_text,
const gfx::Range& range) {
struct TextStyleNewAnswers {
void Apply(gfx::RenderText* render_text, const gfx::Range& range) {
render_text->ApplyWeight(weight, range);
render_text->ApplyBaselineStyle(baseline, range);
render_text->ApplyColor(color, range);
}
SkColor color;
gfx::BaselineStyle baseline = gfx::NORMAL_BASELINE;
gfx::Font::Weight weight = gfx::Font::Weight::NORMAL;
};
TextStyleNewAnswers style;
const SkColor part_color = result_view->GetColor(
(text_style == SuggestionAnswer::TextStyle::NORMAL_DIM)
? OmniboxPart::RESULTS_TEXT_DIMMED
: OmniboxPart::RESULTS_TEXT_DEFAULT);
switch (text_style) {
case SuggestionAnswer::TextStyle::SECONDARY:
style = {gfx::kGoogleGrey600};
break;
case SuggestionAnswer::TextStyle::POSITIVE:
style = {gfx::kGoogleGreen600};
break;
case SuggestionAnswer::TextStyle::NEGATIVE:
style = {gfx::kGoogleRed600};
break;
case SuggestionAnswer::TextStyle::SUPERIOR:
style = {part_color, .baseline = gfx::SUPERIOR};
break;
case SuggestionAnswer::TextStyle::BOLD:
style = {part_color, .baseline = gfx::NORMAL_BASELINE,
.weight = gfx::Font::Weight::BOLD};
break;
case SuggestionAnswer::TextStyle::NORMAL:
case SuggestionAnswer::TextStyle::NORMAL_DIM:
default:
style = {part_color};
break;
}
style.Apply(render_text, range);
}
} // namespace
OmniboxTextView::OmniboxTextView(OmniboxResultView* result_view)
: result_view_(result_view),
font_height_(0),
use_deemphasized_font_(false),
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() + kVerticalPadding;
}
void OmniboxTextView::OnPaint(gfx::Canvas* canvas) {
View::OnPaint(canvas);
if (!render_text_) {
return;
}
render_text_->SetDisplayRect(GetContentsBounds());
render_text_->Draw(canvas);
}
void OmniboxTextView::ApplyTextColor(OmniboxPart part) {
render_text_->SetColor(result_view_->GetColor(part));
}
const base::string16& OmniboxTextView::text() const {
static const base::string16 kEmptyString;
if (!render_text_)
return kEmptyString;
return render_text_->text();
}
void OmniboxTextView::SetText(const base::string16& text, bool deemphasize) {
if (cached_classifications_) {
cached_classifications_.reset();
} else if (render_text_ && render_text_->text() == text &&
deemphasize == use_deemphasized_font_) {
// Only exit early if |cached_classifications_| was empty,
// i.e. the last time text was set was through this method.
return;
}
use_deemphasized_font_ = deemphasize;
render_text_.reset();
render_text_ = CreateRenderText(text);
UpdateLineHeight();
SetPreferredSize(CalculatePreferredSize());
}
void OmniboxTextView::SetText(const base::string16& text,
const ACMatchClassifications& classifications,
bool deemphasize) {
if (render_text_ && render_text_->text() == text && cached_classifications_ &&
classifications == *cached_classifications_ &&
deemphasize == use_deemphasized_font_)
return;
use_deemphasized_font_ = deemphasize;
cached_classifications_ =
std::make_unique<ACMatchClassifications>(classifications);
render_text_ = CreateRenderText(text);
ReapplyStyling();
}
void OmniboxTextView::SetText(const SuggestionAnswer::ImageLine& line,
bool deemphasize) {
use_deemphasized_font_ = deemphasize;
cached_classifications_.reset();
wrap_text_lines_ = line.num_text_lines() > 1;
render_text_.reset();
render_text_ = CreateRenderText(base::string16());
if (!OmniboxFieldTrial::IsNewAnswerLayoutEnabled()) {
// 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_->SetFontList(GetFontForType(line.text_fields()[0].type()));
}
for (const SuggestionAnswer::TextField& text_field : line.text_fields())
AppendText(text_field, base::string16());
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 &&
render_text_->MultilineSupported()) {
render_text_->SetMultiline(true);
render_text_->SetMaxLines(
std::min(kMaxDisplayLines, first_field.num_lines()));
}
}
// Add the "additional" and "status" text from |line|, if any.
// Also updates preferred size.
AppendExtraText(line);
UpdateLineHeight();
}
void OmniboxTextView::AppendExtraText(const SuggestionAnswer::ImageLine& line) {
const base::string16 space(1, base::char16(' '));
const auto* text_field = line.additional_text();
if (text_field) {
AppendText(*text_field, space);
}
text_field = line.status_text();
if (text_field) {
AppendText(*text_field, space);
}
SetPreferredSize(CalculatePreferredSize());
}
int OmniboxTextView::GetLineHeight() const {
return font_height_;
}
void OmniboxTextView::ReapplyStyling() {
const ACMatchClassifications& classifications = *cached_classifications_;
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;
}
render_text_->ApplyColor(result_view_->GetColor(part), current_range);
}
UpdateLineHeight();
SetPreferredSize(CalculatePreferredSize());
}
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);
const gfx::FontList& font = views::style::GetFont(
(use_deemphasized_font_ ? CONTEXT_OMNIBOX_DEEMPHASIZED
: CONTEXT_OMNIBOX_PRIMARY),
kTextStyle);
render_text->SetFontList(font);
render_text->SetText(text);
return render_text;
}
void OmniboxTextView::AppendText(const SuggestionAnswer::TextField& field,
const base::string16& prefix) {
const base::string16& text =
prefix.empty() ? field.text() : (prefix + field.text());
if (text.empty())
return;
int offset = render_text_->text().length();
gfx::Range range(offset, offset + text.length());
render_text_->AppendText(text);
if (OmniboxFieldTrial::IsNewAnswerLayoutEnabled()) {
ApplyTextStyleForType(field.style(), result_view_, render_text_.get(),
range);
} else {
const int text_type = field.type();
const TextStyle& text_style = GetTextStyle(text_type);
// TODO(dschuyler): follow up on the problem of different font sizes within
// one RenderText. Maybe with render_text_->SetFontList(...).
render_text_->ApplyWeight(gfx::Font::Weight::NORMAL, range);
render_text_->ApplyColor(result_view_->GetColor(text_style.part), range);
// Baselines are always aligned under the touch UI. Font sizes change
// instead.
if (!ui::MaterialDesignController::touch_ui()) {
render_text_->ApplyBaselineStyle(text_style.baseline, range);
} else if (text_style.touchable_size_delta != 0) {
render_text_->ApplyFontSizeOverride(
GetFontForType(text_type).GetFontSize(), range);
}
}
}
void OmniboxTextView::UpdateLineHeight() {
const int height_normal = render_text_->font_list().GetHeight();
const int height_bold =
ui::ResourceBundle::GetSharedInstance()
.GetFontListWithDelta(render_text_->font_list().GetFontSize() -
gfx::FontList().GetFontSize(),
gfx::Font::NORMAL, gfx::Font::Weight::BOLD)
.GetHeight();
font_height_ = std::max(height_normal, height_bold);
font_height_ += kVerticalPadding;
}