blob: 43e6008492a043472d2ed5df073e84aef57de910 [file] [log] [blame]
// Copyright (c) 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 "chrome/browser/ui/views/omnibox/omnibox_match_cell_view.h"
#include <algorithm>
#include "base/macros.h"
#include "base/metrics/field_trial_params.h"
#include "base/optional.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/omnibox/omnibox_theme.h"
#include "chrome/browser/ui/views/omnibox/omnibox_popup_contents_view.h"
#include "chrome/browser/ui/views/omnibox/omnibox_text_view.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h"
#include "components/omnibox/browser/vector_icons.h"
#include "extensions/common/image_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/pointer/touch_ui_controller.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/image/canvas_image_source.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/render_text.h"
#include "ui/views/border.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/layout/layout_provider.h"
namespace {
// The diameter of the answer layout images.
static constexpr int kAnswerImageSize = 24;
// The edge length of the entity suggestions images.
static constexpr int kEntityImageSize = 32;
////////////////////////////////////////////////////////////////////////////////
// PlaceholderImageSource:
class PlaceholderImageSource : public gfx::CanvasImageSource {
public:
PlaceholderImageSource(const gfx::Size& canvas_size, SkColor color);
~PlaceholderImageSource() override = default;
// gfx::CanvasImageSource:
void Draw(gfx::Canvas* canvas) override;
private:
const SkColor color_;
DISALLOW_COPY_AND_ASSIGN(PlaceholderImageSource);
};
PlaceholderImageSource::PlaceholderImageSource(const gfx::Size& canvas_size,
SkColor color)
: gfx::CanvasImageSource(canvas_size), color_(color) {}
void PlaceholderImageSource::Draw(gfx::Canvas* canvas) {
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kStrokeAndFill_Style);
flags.setColor(color_);
const int corner_radius = views::LayoutProvider::Get()->GetCornerRadiusMetric(
views::EMPHASIS_MEDIUM);
canvas->sk_canvas()->drawRoundRect(gfx::RectToSkRect(gfx::Rect(size())),
corner_radius, corner_radius, flags);
}
////////////////////////////////////////////////////////////////////////////////
// EncircledImageSource:
class EncircledImageSource : public gfx::CanvasImageSource {
public:
EncircledImageSource(int radius, SkColor color, const gfx::ImageSkia& image);
~EncircledImageSource() override = default;
// gfx::CanvasImageSource:
void Draw(gfx::Canvas* canvas) override;
private:
const int radius_;
const SkColor color_;
const gfx::ImageSkia image_;
DISALLOW_COPY_AND_ASSIGN(EncircledImageSource);
};
EncircledImageSource::EncircledImageSource(int radius,
SkColor color,
const gfx::ImageSkia& image)
: gfx::CanvasImageSource(gfx::Size(radius * 2, radius * 2)),
radius_(radius),
color_(color),
image_(image) {}
void EncircledImageSource::Draw(gfx::Canvas* canvas) {
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kStrokeAndFill_Style);
flags.setColor(color_);
canvas->DrawCircle(gfx::Point(radius_, radius_), radius_, flags);
const int x = radius_ - image_.width() / 2;
const int y = radius_ - image_.height() / 2;
canvas->DrawImageInt(image_, x, y);
}
////////////////////////////////////////////////////////////////////////////////
// RoundedCornerImageView:
class RoundedCornerImageView : public views::ImageView {
public:
RoundedCornerImageView() = default;
// views::ImageView:
bool CanProcessEventsWithinSubtree() const override { return false; }
protected:
// views::ImageView:
void OnPaint(gfx::Canvas* canvas) override;
private:
DISALLOW_COPY_AND_ASSIGN(RoundedCornerImageView);
};
void RoundedCornerImageView::OnPaint(gfx::Canvas* canvas) {
SkPath mask;
const int corner_radius = views::LayoutProvider::Get()->GetCornerRadiusMetric(
views::EMPHASIS_MEDIUM);
mask.addRoundRect(gfx::RectToSkRect(GetImageBounds()), corner_radius,
corner_radius);
canvas->ClipPath(mask, true);
ImageView::OnPaint(canvas);
}
} // namespace
////////////////////////////////////////////////////////////////////////////////
// OmniboxMatchCellView:
OmniboxMatchCellView::OmniboxMatchCellView(OmniboxResultView* result_view) {
icon_view_ = AddChildView(std::make_unique<views::ImageView>());
answer_image_view_ = AddChildView(std::make_unique<RoundedCornerImageView>());
content_view_ = AddChildView(std::make_unique<OmniboxTextView>(result_view));
description_view_ =
AddChildView(std::make_unique<OmniboxTextView>(result_view));
separator_view_ =
AddChildView(std::make_unique<OmniboxTextView>(result_view));
separator_view_->SetText(
l10n_util::GetStringUTF16(IDS_AUTOCOMPLETE_MATCH_DESCRIPTION_SEPARATOR));
}
OmniboxMatchCellView::~OmniboxMatchCellView() = default;
// static
int OmniboxMatchCellView::GetTextIndent() {
return ui::TouchUiController::Get()->touch_ui() ? 51 : 47;
}
void OmniboxMatchCellView::OnMatchUpdate(const OmniboxResultView* result_view,
const AutocompleteMatch& match) {
is_rich_suggestion_ = match.answer ||
match.type == AutocompleteMatchType::CALCULATOR ||
!match.image_url.is_empty();
is_search_type_ = AutocompleteMatch::IsSearchType(match.type);
// Decide layout style once before Layout, while match data is available.
const bool two_line =
is_rich_suggestion_ || match.ShouldShowTabMatchButton() || match.pedal;
layout_style_ = two_line ? LayoutStyle::TWO_LINE_SUGGESTION
: LayoutStyle::ONE_LINE_SUGGESTION;
// Set up the separator.
separator_view_->SetSize(two_line ? gfx::Size()
: separator_view_->GetPreferredSize());
// Set up the small icon.
icon_view_->SetSize(is_rich_suggestion_ ? gfx::Size()
: icon_view_->GetPreferredSize());
const auto apply_vector_icon = [=](const gfx::VectorIcon& vector_icon) {
const auto& icon = gfx::CreateVectorIcon(vector_icon, SK_ColorWHITE);
answer_image_view_->SetImageSize(
gfx::Size(kAnswerImageSize, kAnswerImageSize));
answer_image_view_->SetImage(
gfx::CanvasImageSource::MakeImageSkia<EncircledImageSource>(
kAnswerImageSize / 2, gfx::kGoogleBlue600, icon));
};
if (match.type == AutocompleteMatchType::CALCULATOR) {
apply_vector_icon(omnibox::kAnswerCalculatorIcon);
} else if (!is_rich_suggestion_) {
answer_image_view_->SetImage(gfx::ImageSkia());
answer_image_view_->SetSize(gfx::Size());
} else {
// Determine if we have a local icon (or else it will be downloaded).
if (match.answer) {
switch (match.answer->type()) {
case SuggestionAnswer::ANSWER_TYPE_CURRENCY:
apply_vector_icon(omnibox::kAnswerCurrencyIcon);
break;
case SuggestionAnswer::ANSWER_TYPE_DICTIONARY:
apply_vector_icon(omnibox::kAnswerDictionaryIcon);
break;
case SuggestionAnswer::ANSWER_TYPE_FINANCE:
apply_vector_icon(omnibox::kAnswerFinanceIcon);
break;
case SuggestionAnswer::ANSWER_TYPE_SUNRISE:
apply_vector_icon(omnibox::kAnswerSunriseIcon);
break;
case SuggestionAnswer::ANSWER_TYPE_TRANSLATION:
apply_vector_icon(omnibox::kAnswerTranslationIcon);
break;
case SuggestionAnswer::ANSWER_TYPE_WEATHER:
// Weather icons are downloaded. We just need to set the correct size.
answer_image_view_->SetImageSize(
gfx::Size(kAnswerImageSize, kAnswerImageSize));
break;
case SuggestionAnswer::ANSWER_TYPE_WHEN_IS:
apply_vector_icon(omnibox::kAnswerWhenIsIcon);
break;
default:
apply_vector_icon(omnibox::kAnswerDefaultIcon);
break;
}
} else {
SkColor color = result_view->GetColor(OmniboxPart::RESULTS_BACKGROUND);
extensions::image_util::ParseHexColorString(match.image_dominant_color,
&color);
color = SkColorSetA(color, 0x40); // 25% transparency (arbitrary).
constexpr gfx::Size size(kEntityImageSize, kEntityImageSize);
answer_image_view_->SetImageSize(size);
answer_image_view_->SetImage(
gfx::CanvasImageSource::MakeImageSkia<PlaceholderImageSource>(size,
color));
}
}
SetTailSuggestCommonPrefixWidth(
(match.type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL)
? match.tail_suggest_common_prefix // Used for indent calculation.
: base::string16());
}
void OmniboxMatchCellView::SetImage(const gfx::ImageSkia& image) {
answer_image_view_->SetImage(image);
// Usually, answer images are square. But if that's not the case, setting
// answer_image_view_ size proportional to the image size preserves
// the aspect ratio.
int width = image.width();
int height = image.height();
if (width == height)
return;
const int max = std::max(width, height);
width = kEntityImageSize * width / max;
height = kEntityImageSize * height / max;
answer_image_view_->SetImageSize(gfx::Size(width, height));
}
const char* OmniboxMatchCellView::GetClassName() const {
return "OmniboxMatchCellView";
}
gfx::Insets OmniboxMatchCellView::GetInsets() const {
const bool single_line = layout_style_ == LayoutStyle::ONE_LINE_SUGGESTION;
const int vertical_margin = single_line ? 8 : 4;
return gfx::Insets(vertical_margin, 4, vertical_margin,
OmniboxMatchCellView::kMarginRight);
}
void OmniboxMatchCellView::Layout() {
views::View::Layout();
const bool two_line = layout_style_ == LayoutStyle::TWO_LINE_SUGGESTION;
const gfx::Rect child_area = GetContentsBounds();
int x = child_area.x();
int y = child_area.y();
const int row_height = child_area.height();
views::ImageView* const image_view =
(two_line && is_rich_suggestion_) ? answer_image_view_ : icon_view_;
image_view->SetBounds(x, y, 40, row_height);
const int text_indent = GetTextIndent() + tail_suggest_common_prefix_width_;
x += text_indent;
const int text_width = child_area.width() - text_indent;
if (two_line) {
if (description_view_->text().empty()) {
// This vertically centers content in the rare case that no description is
// provided.
content_view_->SetBounds(x, y, text_width, row_height);
description_view_->SetSize(gfx::Size());
} else {
content_view_->SetBounds(x, y, text_width,
content_view_->GetLineHeight());
description_view_->SetBounds(
x, content_view_->bounds().bottom(), text_width,
description_view_->GetHeightForWidth(text_width));
}
} else {
int content_width = content_view_->GetPreferredSize().width();
int description_width = description_view_->GetPreferredSize().width();
const gfx::Size separator_size = separator_view_->GetPreferredSize();
OmniboxPopupModel::ComputeMatchMaxWidths(
content_width, separator_size.width(), description_width, text_width,
/*description_on_separate_line=*/false, !is_search_type_,
&content_width, &description_width);
content_view_->SetBounds(x, y, content_width, row_height);
if (description_width) {
x += content_view_->width();
separator_view_->SetSize(separator_size);
separator_view_->SetBounds(x, y, separator_view_->width(), row_height);
x += separator_view_->width();
description_view_->SetBounds(x, y, description_width, row_height);
} else {
separator_view_->SetSize(gfx::Size());
description_view_->SetSize(gfx::Size());
}
}
}
bool OmniboxMatchCellView::CanProcessEventsWithinSubtree() const {
return false;
}
gfx::Size OmniboxMatchCellView::CalculatePreferredSize() const {
int height = content_view_->GetLineHeight() + GetInsets().height();
if (layout_style_ == LayoutStyle::TWO_LINE_SUGGESTION)
height += description_view_->GetHeightForWidth(width() - GetTextIndent());
// Width is not calculated because it's not needed by current callers.
return gfx::Size(0, height);
}
void OmniboxMatchCellView::SetTailSuggestCommonPrefixWidth(
const base::string16& common_prefix) {
InvalidateLayout();
if (common_prefix.empty()) {
tail_suggest_common_prefix_width_ = 0;
return;
}
std::unique_ptr<gfx::RenderText> render_text =
content_view_->CreateRenderText(common_prefix);
tail_suggest_common_prefix_width_ = render_text->GetStringSize().width();
// Only calculate fixed string width once.
if (!ellipsis_width_) {
render_text->SetText(base::ASCIIToUTF16(AutocompleteMatch::kEllipsis));
ellipsis_width_ = render_text->GetStringSize().width();
}
// Indent text by prefix, but come back by width of ellipsis.
tail_suggest_common_prefix_width_ -= ellipsis_width_;
}