Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / ui / views / omnibox / omnibox_result_view.cc
blobfb513d0dcedf3ad7db82c47c889a3c67cafa5e6b
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 // For WinDDK ATL compatibility, these ATL headers must come first.
6 #include "build/build_config.h"
7 #if defined(OS_WIN)
8 #include <atlbase.h> // NOLINT
9 #include <atlwin.h> // NOLINT
10 #endif
12 #include "chrome/browser/ui/views/omnibox/omnibox_result_view.h"
14 #include <algorithm> // NOLINT
16 #include "base/i18n/bidi_line_iterator.h"
17 #include "base/strings/string_number_conversions.h"
18 #include "base/strings/string_util.h"
19 #include "chrome/browser/ui/views/layout_constants.h"
20 #include "chrome/browser/ui/views/location_bar/location_bar_view.h"
21 #include "chrome/browser/ui/views/omnibox/omnibox_popup_contents_view.h"
22 #include "chrome/grit/generated_resources.h"
23 #include "components/omnibox/browser/omnibox_popup_model.h"
24 #include "grit/components_scaled_resources.h"
25 #include "grit/theme_resources.h"
26 #include "third_party/skia/include/core/SkColor.h"
27 #include "ui/base/l10n/l10n_util.h"
28 #include "ui/base/resource/material_design/material_design_controller.h"
29 #include "ui/base/resource/resource_bundle.h"
30 #include "ui/base/theme_provider.h"
31 #include "ui/gfx/canvas.h"
32 #include "ui/gfx/color_utils.h"
33 #include "ui/gfx/image/image.h"
34 #include "ui/gfx/range/range.h"
35 #include "ui/gfx/render_text.h"
36 #include "ui/gfx/text_utils.h"
38 using ui::NativeTheme;
40 namespace {
42 // A mapping from OmniboxResultView's ResultViewState/ColorKind types to
43 // NativeTheme colors.
44 struct TranslationTable {
45 ui::NativeTheme::ColorId id;
46 OmniboxResultView::ResultViewState state;
47 OmniboxResultView::ColorKind kind;
48 } static const kTranslationTable[] = {
49 { NativeTheme::kColorId_ResultsTableNormalBackground,
50 OmniboxResultView::NORMAL, OmniboxResultView::BACKGROUND },
51 { NativeTheme::kColorId_ResultsTableHoveredBackground,
52 OmniboxResultView::HOVERED, OmniboxResultView::BACKGROUND },
53 { NativeTheme::kColorId_ResultsTableSelectedBackground,
54 OmniboxResultView::SELECTED, OmniboxResultView::BACKGROUND },
55 { NativeTheme::kColorId_ResultsTableNormalText,
56 OmniboxResultView::NORMAL, OmniboxResultView::TEXT },
57 { NativeTheme::kColorId_ResultsTableHoveredText,
58 OmniboxResultView::HOVERED, OmniboxResultView::TEXT },
59 { NativeTheme::kColorId_ResultsTableSelectedText,
60 OmniboxResultView::SELECTED, OmniboxResultView::TEXT },
61 { NativeTheme::kColorId_ResultsTableNormalDimmedText,
62 OmniboxResultView::NORMAL, OmniboxResultView::DIMMED_TEXT },
63 { NativeTheme::kColorId_ResultsTableHoveredDimmedText,
64 OmniboxResultView::HOVERED, OmniboxResultView::DIMMED_TEXT },
65 { NativeTheme::kColorId_ResultsTableSelectedDimmedText,
66 OmniboxResultView::SELECTED, OmniboxResultView::DIMMED_TEXT },
67 { NativeTheme::kColorId_ResultsTableNormalUrl,
68 OmniboxResultView::NORMAL, OmniboxResultView::URL },
69 { NativeTheme::kColorId_ResultsTableHoveredUrl,
70 OmniboxResultView::HOVERED, OmniboxResultView::URL },
71 { NativeTheme::kColorId_ResultsTableSelectedUrl,
72 OmniboxResultView::SELECTED, OmniboxResultView::URL },
73 { NativeTheme::kColorId_ResultsTableNormalDivider,
74 OmniboxResultView::NORMAL, OmniboxResultView::DIVIDER },
75 { NativeTheme::kColorId_ResultsTableHoveredDivider,
76 OmniboxResultView::HOVERED, OmniboxResultView::DIVIDER },
77 { NativeTheme::kColorId_ResultsTableSelectedDivider,
78 OmniboxResultView::SELECTED, OmniboxResultView::DIVIDER },
81 struct TextStyle {
82 ui::ResourceBundle::FontStyle font;
83 ui::NativeTheme::ColorId colors[OmniboxResultView::NUM_STATES];
84 gfx::BaselineStyle baseline;
85 } const kTextStyles[] = {
86 // 1 ANSWER_TEXT
87 {ui::ResourceBundle::LargeFont,
88 {NativeTheme::kColorId_ResultsTableNormalText,
89 NativeTheme::kColorId_ResultsTableHoveredText,
90 NativeTheme::kColorId_ResultsTableSelectedText},
91 gfx::NORMAL_BASELINE},
92 // 2 HEADLINE_TEXT
93 {ui::ResourceBundle::LargeFont,
94 {NativeTheme::kColorId_ResultsTableNormalDimmedText,
95 NativeTheme::kColorId_ResultsTableHoveredDimmedText,
96 NativeTheme::kColorId_ResultsTableSelectedDimmedText},
97 gfx::NORMAL_BASELINE},
98 // 3 TOP_ALIGNED_TEXT
99 {ui::ResourceBundle::LargeFont,
100 {NativeTheme::kColorId_ResultsTableNormalDimmedText,
101 NativeTheme::kColorId_ResultsTableHoveredDimmedText,
102 NativeTheme::kColorId_ResultsTableSelectedDimmedText},
103 gfx::SUPERIOR},
104 // 4 DESCRIPTION_TEXT
105 {ui::ResourceBundle::BaseFont,
106 {NativeTheme::kColorId_ResultsTableNormalDimmedText,
107 NativeTheme::kColorId_ResultsTableHoveredDimmedText,
108 NativeTheme::kColorId_ResultsTableSelectedDimmedText},
109 gfx::NORMAL_BASELINE},
110 // 5 DESCRIPTION_TEXT_NEGATIVE
111 {ui::ResourceBundle::LargeFont,
112 {NativeTheme::kColorId_ResultsTableNegativeText,
113 NativeTheme::kColorId_ResultsTableNegativeHoveredText,
114 NativeTheme::kColorId_ResultsTableNegativeSelectedText},
115 gfx::INFERIOR},
116 // 6 DESCRIPTION_TEXT_POSITIVE
117 {ui::ResourceBundle::LargeFont,
118 {NativeTheme::kColorId_ResultsTablePositiveText,
119 NativeTheme::kColorId_ResultsTablePositiveHoveredText,
120 NativeTheme::kColorId_ResultsTablePositiveSelectedText},
121 gfx::INFERIOR},
122 // 7 MORE_INFO_TEXT
123 {ui::ResourceBundle::BaseFont,
124 {NativeTheme::kColorId_ResultsTableNormalDimmedText,
125 NativeTheme::kColorId_ResultsTableHoveredDimmedText,
126 NativeTheme::kColorId_ResultsTableSelectedDimmedText},
127 gfx::INFERIOR},
128 // 8 SUGGESTION_TEXT
129 {ui::ResourceBundle::BaseFont,
130 {NativeTheme::kColorId_ResultsTableNormalText,
131 NativeTheme::kColorId_ResultsTableHoveredText,
132 NativeTheme::kColorId_ResultsTableSelectedText},
133 gfx::NORMAL_BASELINE},
134 // 9 SUGGESTION_TEXT_POSITIVE
135 {ui::ResourceBundle::BaseFont,
136 {NativeTheme::kColorId_ResultsTablePositiveText,
137 NativeTheme::kColorId_ResultsTablePositiveHoveredText,
138 NativeTheme::kColorId_ResultsTablePositiveSelectedText},
139 gfx::NORMAL_BASELINE},
140 // 10 SUGGESTION_TEXT_NEGATIVE
141 {ui::ResourceBundle::BaseFont,
142 {NativeTheme::kColorId_ResultsTableNegativeText,
143 NativeTheme::kColorId_ResultsTableNegativeHoveredText,
144 NativeTheme::kColorId_ResultsTableNegativeSelectedText},
145 gfx::NORMAL_BASELINE},
146 // 11 SUGGESTION_LINK_COLOR
147 {ui::ResourceBundle::BaseFont,
148 {NativeTheme::kColorId_ResultsTableNormalUrl,
149 NativeTheme::kColorId_ResultsTableHoveredUrl,
150 NativeTheme::kColorId_ResultsTableSelectedUrl},
151 gfx::NORMAL_BASELINE},
152 // 12 STATUS_TEXT
153 {ui::ResourceBundle::LargeFont,
154 {NativeTheme::kColorId_ResultsTableNormalDimmedText,
155 NativeTheme::kColorId_ResultsTableHoveredDimmedText,
156 NativeTheme::kColorId_ResultsTableSelectedDimmedText},
157 gfx::INFERIOR},
158 // 13 PERSONALIZED_SUGGESTION_TEXT
159 {ui::ResourceBundle::BaseFont,
160 {NativeTheme::kColorId_ResultsTableNormalText,
161 NativeTheme::kColorId_ResultsTableHoveredText,
162 NativeTheme::kColorId_ResultsTableSelectedText},
163 gfx::NORMAL_BASELINE},
166 const TextStyle& GetTextStyle(int type) {
167 if (type < 1 || static_cast<size_t>(type) > arraysize(kTextStyles))
168 type = 1;
169 // Subtract one because the types are one based (not zero based).
170 return kTextStyles[type - 1];
173 } // namespace
175 ////////////////////////////////////////////////////////////////////////////////
176 // OmniboxResultView, public:
178 // This class is a utility class for calculations affected by whether the result
179 // view is horizontally mirrored. The drawing functions can be written as if
180 // all drawing occurs left-to-right, and then use this class to get the actual
181 // coordinates to begin drawing onscreen.
182 class OmniboxResultView::MirroringContext {
183 public:
184 MirroringContext() : center_(0), right_(0) {}
186 // Tells the mirroring context to use the provided range as the physical
187 // bounds of the drawing region. When coordinate mirroring is needed, the
188 // mirror point will be the center of this range.
189 void Initialize(int x, int width) {
190 center_ = x + width / 2;
191 right_ = x + width;
194 // Given a logical range within the drawing region, returns the coordinate of
195 // the possibly-mirrored "left" side. (This functions exactly like
196 // View::MirroredLeftPointForRect().)
197 int mirrored_left_coord(int left, int right) const {
198 return base::i18n::IsRTL() ? (center_ + (center_ - right)) : left;
201 // Given a logical coordinate within the drawing region, returns the remaining
202 // width available.
203 int remaining_width(int x) const {
204 return right_ - x;
207 private:
208 int center_;
209 int right_;
211 DISALLOW_COPY_AND_ASSIGN(MirroringContext);
214 OmniboxResultView::OmniboxResultView(OmniboxPopupContentsView* model,
215 int model_index,
216 LocationBarView* location_bar_view,
217 const gfx::FontList& font_list)
218 : model_(model),
219 model_index_(model_index),
220 location_bar_view_(location_bar_view),
221 font_list_(font_list),
222 font_height_(
223 std::max(font_list.GetHeight(),
224 font_list.DeriveWithStyle(gfx::Font::BOLD).GetHeight())),
225 mirroring_context_(new MirroringContext()),
226 keyword_icon_(new views::ImageView()),
227 animation_(new gfx::SlideAnimation(this)) {
228 CHECK_GE(model_index, 0);
229 if (default_icon_size_ == 0) {
230 default_icon_size_ =
231 location_bar_view_->GetThemeProvider()->GetImageSkiaNamed(
232 AutocompleteMatch::TypeToIcon(
233 AutocompleteMatchType::URL_WHAT_YOU_TYPED))->width();
235 keyword_icon_->set_owned_by_client();
236 keyword_icon_->EnableCanvasFlippingForRTLUI(true);
237 keyword_icon_->SetImage(GetKeywordIcon());
238 keyword_icon_->SizeToPreferredSize();
241 OmniboxResultView::~OmniboxResultView() {
244 SkColor OmniboxResultView::GetColor(
245 ResultViewState state,
246 ColorKind kind) const {
247 for (size_t i = 0; i < arraysize(kTranslationTable); ++i) {
248 if (kTranslationTable[i].state == state &&
249 kTranslationTable[i].kind == kind) {
250 return GetNativeTheme()->GetSystemColor(kTranslationTable[i].id);
254 NOTREACHED();
255 return SK_ColorRED;
258 void OmniboxResultView::SetMatch(const AutocompleteMatch& match) {
259 match_ = match;
260 match_.PossiblySwapContentsAndDescriptionForDisplay();
261 ResetRenderTexts();
262 animation_->Reset();
263 answer_image_ = gfx::ImageSkia();
265 AutocompleteMatch* associated_keyword_match = match_.associated_keyword.get();
266 if (associated_keyword_match) {
267 keyword_icon_->SetImage(GetKeywordIcon());
268 if (!keyword_icon_->parent())
269 AddChildView(keyword_icon_.get());
270 } else if (keyword_icon_->parent()) {
271 RemoveChildView(keyword_icon_.get());
274 if (GetWidget())
275 Layout();
278 void OmniboxResultView::ShowKeyword(bool show_keyword) {
279 if (show_keyword)
280 animation_->Show();
281 else
282 animation_->Hide();
285 void OmniboxResultView::Invalidate() {
286 keyword_icon_->SetImage(GetKeywordIcon());
287 // While the text in the RenderTexts may not have changed, the styling
288 // (color/bold) may need to change. So we reset them to cause them to be
289 // recomputed in OnPaint().
290 ResetRenderTexts();
291 SchedulePaint();
294 gfx::Size OmniboxResultView::GetPreferredSize() const {
295 if (!match_.answer)
296 return gfx::Size(0, GetContentLineHeight());
297 // An answer implies a match and a description in a large font.
298 return gfx::Size(0, GetContentLineHeight() + GetAnswerLineHeight());
301 ////////////////////////////////////////////////////////////////////////////////
302 // OmniboxResultView, protected:
304 OmniboxResultView::ResultViewState OmniboxResultView::GetState() const {
305 if (model_->IsSelectedIndex(model_index_))
306 return SELECTED;
307 return model_->IsHoveredIndex(model_index_) ? HOVERED : NORMAL;
310 int OmniboxResultView::GetTextHeight() const {
311 return font_height_;
314 void OmniboxResultView::PaintMatch(const AutocompleteMatch& match,
315 gfx::RenderText* contents,
316 gfx::RenderText* description,
317 gfx::Canvas* canvas,
318 int x) const {
319 int y = text_bounds_.y();
321 if (!separator_rendertext_) {
322 const base::string16& separator =
323 l10n_util::GetStringUTF16(IDS_AUTOCOMPLETE_MATCH_DESCRIPTION_SEPARATOR);
324 separator_rendertext_.reset(CreateRenderText(separator).release());
325 separator_rendertext_->SetColor(GetColor(GetState(), DIMMED_TEXT));
326 separator_width_ = separator_rendertext_->GetContentWidth();
329 contents->SetDisplayRect(gfx::Rect(gfx::Size(INT_MAX, 0)));
330 if (description)
331 description->SetDisplayRect(gfx::Rect(gfx::Size(INT_MAX, 0)));
332 int contents_max_width, description_max_width;
333 OmniboxPopupModel::ComputeMatchMaxWidths(
334 contents->GetContentWidth(),
335 separator_width_,
336 description ? description->GetContentWidth() : 0,
337 mirroring_context_->remaining_width(x),
338 !AutocompleteMatch::IsSearchType(match.type),
339 &contents_max_width,
340 &description_max_width);
342 int after_contents_x =
343 DrawRenderText(match, contents, true, canvas, x, y, contents_max_width);
345 if (description_max_width != 0) {
346 if (match.answer) {
347 y += GetContentLineHeight();
348 if (!answer_image_.isNull()) {
349 int answer_icon_size = GetAnswerLineHeight();
350 canvas->DrawImageInt(
351 answer_image_,
352 0, 0, answer_image_.width(), answer_image_.height(),
353 GetMirroredXInView(x), y, answer_icon_size, answer_icon_size, true);
354 // See TODO in Layout().
355 x += answer_icon_size +
356 GetLayoutConstant(ICON_LABEL_VIEW_TRAILING_PADDING);
358 } else {
359 x = DrawRenderText(match, separator_rendertext_.get(), false, canvas,
360 after_contents_x, y, separator_width_);
363 DrawRenderText(match, description, false, canvas, x, y,
364 description_max_width);
368 int OmniboxResultView::DrawRenderText(
369 const AutocompleteMatch& match,
370 gfx::RenderText* render_text,
371 bool contents,
372 gfx::Canvas* canvas,
373 int x,
374 int y,
375 int max_width) const {
376 DCHECK(!render_text->text().empty());
378 const int remaining_width = mirroring_context_->remaining_width(x);
379 int right_x = x + max_width;
381 // Infinite suggestions should appear with the leading ellipses vertically
382 // stacked.
383 if (contents &&
384 (match.type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL)) {
385 // When the directionality of suggestion doesn't match the UI, we try to
386 // vertically stack the ellipsis by restricting the end edge (right_x).
387 const bool is_ui_rtl = base::i18n::IsRTL();
388 const bool is_match_contents_rtl =
389 (render_text->GetDisplayTextDirection() == base::i18n::RIGHT_TO_LEFT);
390 const int offset =
391 GetDisplayOffset(match, is_ui_rtl, is_match_contents_rtl);
393 scoped_ptr<gfx::RenderText> prefix_render_text(
394 CreateRenderText(base::UTF8ToUTF16(
395 match.GetAdditionalInfo(kACMatchPropertyContentsPrefix))));
396 const int prefix_width = prefix_render_text->GetContentWidth();
397 int prefix_x = x;
399 const int max_match_contents_width = model_->max_match_contents_width();
401 if (is_ui_rtl != is_match_contents_rtl) {
402 // RTL infinite suggestions appear near the left edge in LTR UI, while LTR
403 // infinite suggestions appear near the right edge in RTL UI. This is
404 // against the natural horizontal alignment of the text. We reduce the
405 // width of the box for suggestion display, so that the suggestions appear
406 // in correct confines. This reduced width allows us to modify the text
407 // alignment (see below).
408 right_x = x + std::min(remaining_width - prefix_width,
409 std::max(offset, max_match_contents_width));
410 prefix_x = right_x;
411 // We explicitly set the horizontal alignment so that when LTR suggestions
412 // show in RTL UI (or vice versa), their ellipses appear stacked in a
413 // single column.
414 render_text->SetHorizontalAlignment(
415 is_match_contents_rtl ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT);
416 } else {
417 // If the dropdown is wide enough, place the ellipsis at the position
418 // where the omitted text would have ended. Otherwise reduce the offset of
419 // the ellipsis such that the widest suggestion reaches the end of the
420 // dropdown.
421 const int start_offset = std::max(prefix_width,
422 std::min(remaining_width - max_match_contents_width, offset));
423 right_x = x + std::min(remaining_width, start_offset + max_width);
424 x += start_offset;
425 prefix_x = x - prefix_width;
427 prefix_render_text->SetDirectionalityMode(is_match_contents_rtl ?
428 gfx::DIRECTIONALITY_FORCE_RTL : gfx::DIRECTIONALITY_FORCE_LTR);
429 prefix_render_text->SetHorizontalAlignment(
430 is_match_contents_rtl ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT);
431 prefix_render_text->SetDisplayRect(
432 gfx::Rect(mirroring_context_->mirrored_left_coord(
433 prefix_x, prefix_x + prefix_width),
434 y, prefix_width, GetContentLineHeight()));
435 prefix_render_text->Draw(canvas);
438 // Set the display rect to trigger eliding.
439 render_text->SetDisplayRect(
440 gfx::Rect(mirroring_context_->mirrored_left_coord(x, right_x), y,
441 right_x - x, GetContentLineHeight()));
442 render_text->Draw(canvas);
443 return right_x;
446 scoped_ptr<gfx::RenderText> OmniboxResultView::CreateRenderText(
447 const base::string16& text) const {
448 scoped_ptr<gfx::RenderText> render_text(gfx::RenderText::CreateInstance());
449 render_text->SetDisplayRect(gfx::Rect(gfx::Size(INT_MAX, 0)));
450 render_text->SetCursorEnabled(false);
451 render_text->SetElideBehavior(gfx::ELIDE_TAIL);
452 render_text->SetFontList(font_list_);
453 render_text->SetText(text);
454 return render_text.Pass();
457 scoped_ptr<gfx::RenderText> OmniboxResultView::CreateClassifiedRenderText(
458 const base::string16& text,
459 const ACMatchClassifications& classifications,
460 bool force_dim) const {
461 scoped_ptr<gfx::RenderText> render_text(CreateRenderText(text));
462 const size_t text_length = render_text->text().length();
463 for (size_t i = 0; i < classifications.size(); ++i) {
464 const size_t text_start = classifications[i].offset;
465 if (text_start >= text_length)
466 break;
468 const size_t text_end = (i < (classifications.size() - 1)) ?
469 std::min(classifications[i + 1].offset, text_length) :
470 text_length;
471 const gfx::Range current_range(text_start, text_end);
473 // Calculate style-related data.
474 if (classifications[i].style & ACMatchClassification::MATCH)
475 render_text->ApplyStyle(gfx::BOLD, true, current_range);
477 ColorKind color_kind = TEXT;
478 if (classifications[i].style & ACMatchClassification::URL) {
479 color_kind = URL;
480 // Consider logical string for domain "ABC.com×™/hello" where ABC are
481 // Hebrew (RTL) characters. This string should ideally show as
482 // "CBA.com/hello". If we do not force LTR on URL, it will appear as
483 // "com/hello.CBA".
484 // With IDN and RTL TLDs, it might be okay to allow RTL rendering of URLs,
485 // but it still has some pitfalls like :
486 // ABC.COM/abc-pqr/xyz/FGH will appear as HGF/abc-pqr/xyz/MOC.CBA which
487 // really confuses the path hierarchy of the URL.
488 // Also, if the URL supports https, the appearance will change into LTR
489 // directionality.
490 // In conclusion, LTR rendering of URL is probably the safest bet.
491 render_text->SetDirectionalityMode(gfx::DIRECTIONALITY_FORCE_LTR);
492 } else if (force_dim ||
493 (classifications[i].style & ACMatchClassification::DIM)) {
494 color_kind = DIMMED_TEXT;
496 render_text->ApplyColor(GetColor(GetState(), color_kind), current_range);
498 return render_text.Pass();
501 int OmniboxResultView::GetMatchContentsWidth() const {
502 InitContentsRenderTextIfNecessary();
503 contents_rendertext_->SetDisplayRect(gfx::Rect(gfx::Size(INT_MAX, 0)));
504 return contents_rendertext_->GetContentWidth();
507 void OmniboxResultView::SetAnswerImage(const gfx::ImageSkia& image) {
508 answer_image_ = image;
509 SchedulePaint();
512 // TODO(skanuj): This is probably identical across all OmniboxResultView rows in
513 // the omnibox dropdown. Consider sharing the result.
514 int OmniboxResultView::GetDisplayOffset(
515 const AutocompleteMatch& match,
516 bool is_ui_rtl,
517 bool is_match_contents_rtl) const {
518 if (match.type != AutocompleteMatchType::SEARCH_SUGGEST_TAIL)
519 return 0;
521 const base::string16& input_text =
522 base::UTF8ToUTF16(match.GetAdditionalInfo(kACMatchPropertyInputText));
523 int contents_start_index = 0;
524 base::StringToInt(match.GetAdditionalInfo(kACMatchPropertyContentsStartIndex),
525 &contents_start_index);
527 scoped_ptr<gfx::RenderText> input_render_text(CreateRenderText(input_text));
528 const gfx::Range& glyph_bounds =
529 input_render_text->GetGlyphBounds(contents_start_index);
530 const int start_padding = is_match_contents_rtl ?
531 std::max(glyph_bounds.start(), glyph_bounds.end()) :
532 std::min(glyph_bounds.start(), glyph_bounds.end());
534 return is_ui_rtl ?
535 (input_render_text->GetContentWidth() - start_padding) : start_padding;
538 // static
539 int OmniboxResultView::default_icon_size_ = 0;
541 const char* OmniboxResultView::GetClassName() const {
542 return "OmniboxResultView";
545 gfx::ImageSkia OmniboxResultView::GetIcon() const {
546 const gfx::Image image = model_->GetIconIfExtensionMatch(model_index_);
547 if (!image.IsEmpty())
548 return image.AsImageSkia();
550 int icon = model_->IsStarredMatch(match_) ?
551 IDR_OMNIBOX_STAR : AutocompleteMatch::TypeToIcon(match_.type);
552 if (GetState() == SELECTED &&
553 !ui::MaterialDesignController::IsModeMaterial()) {
554 switch (icon) {
555 case IDR_OMNIBOX_CALCULATOR:
556 icon = IDR_OMNIBOX_CALCULATOR_SELECTED;
557 break;
558 case IDR_OMNIBOX_EXTENSION_APP:
559 icon = IDR_OMNIBOX_EXTENSION_APP_SELECTED;
560 break;
561 case IDR_OMNIBOX_HTTP:
562 icon = IDR_OMNIBOX_HTTP_SELECTED;
563 break;
564 case IDR_OMNIBOX_SEARCH:
565 icon = IDR_OMNIBOX_SEARCH_SELECTED;
566 break;
567 case IDR_OMNIBOX_STAR:
568 icon = IDR_OMNIBOX_STAR_SELECTED;
569 break;
570 default:
571 NOTREACHED();
572 break;
575 return *(location_bar_view_->GetThemeProvider()->GetImageSkiaNamed(icon));
578 const gfx::ImageSkia* OmniboxResultView::GetKeywordIcon() const {
579 // NOTE: If we ever begin returning icons of varying size, then callers need
580 // to ensure that |keyword_icon_| is resized each time its image is reset.
581 int icon = IDR_OMNIBOX_TTS;
582 if (GetState() == SELECTED && !ui::MaterialDesignController::IsModeMaterial())
583 icon = IDR_OMNIBOX_TTS_SELECTED;
585 return location_bar_view_->GetThemeProvider()->GetImageSkiaNamed(icon);
588 bool OmniboxResultView::ShowOnlyKeywordMatch() const {
589 return match_.associated_keyword &&
590 (keyword_icon_->x() <= icon_bounds_.right());
593 void OmniboxResultView::ResetRenderTexts() const {
594 contents_rendertext_.reset();
595 description_rendertext_.reset();
596 separator_rendertext_.reset();
597 keyword_contents_rendertext_.reset();
598 keyword_description_rendertext_.reset();
601 void OmniboxResultView::InitContentsRenderTextIfNecessary() const {
602 if (!contents_rendertext_) {
603 contents_rendertext_.reset(
604 CreateClassifiedRenderText(
605 match_.contents, match_.contents_class, false).release());
609 void OmniboxResultView::Layout() {
610 const int horizontal_padding =
611 GetLayoutConstant(LOCATION_BAR_HORIZONTAL_PADDING);
612 const int start_x = StartMargin() + horizontal_padding;
613 const int end_x = width() - EndMargin() - horizontal_padding;
615 const gfx::ImageSkia icon = GetIcon();
616 icon_bounds_.SetRect(
617 start_x + ((icon.width() == default_icon_size_) ?
618 0 : GetLayoutConstant(ICON_LABEL_VIEW_TRAILING_PADDING)),
619 (GetContentLineHeight() - icon.height()) / 2,
620 icon.width(), icon.height());
622 const int text_x = start_x + default_icon_size_ + horizontal_padding;
623 int text_width = end_x - text_x;
625 if (match_.associated_keyword.get()) {
626 const int max_kw_x = end_x - keyword_icon_->width();
627 const int kw_x = animation_->CurrentValueBetween(max_kw_x, start_x);
628 const int kw_text_x = kw_x + keyword_icon_->width() + horizontal_padding;
630 text_width = kw_x - text_x - horizontal_padding;
631 keyword_text_bounds_.SetRect(
632 kw_text_x, 0, std::max(end_x - kw_text_x, 0), height());
633 keyword_icon_->SetPosition(
634 gfx::Point(kw_x, (height() - keyword_icon_->height()) / 2));
637 text_bounds_.SetRect(text_x, 0, std::max(text_width, 0), height());
640 void OmniboxResultView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
641 animation_->SetSlideDuration((width() - StartMargin() - EndMargin()) / 4);
644 void OmniboxResultView::OnPaint(gfx::Canvas* canvas) {
645 const ResultViewState state = GetState();
646 if (state != NORMAL)
647 canvas->DrawColor(GetColor(state, BACKGROUND));
649 // NOTE: While animating the keyword match, both matches may be visible.
651 if (!ShowOnlyKeywordMatch()) {
652 canvas->DrawImageInt(GetIcon(), GetMirroredXForRect(icon_bounds_),
653 icon_bounds_.y());
654 int x = GetMirroredXForRect(text_bounds_);
655 mirroring_context_->Initialize(x, text_bounds_.width());
656 InitContentsRenderTextIfNecessary();
658 if (!description_rendertext_) {
659 if (match_.answer) {
660 contents_rendertext_ =
661 CreateAnswerLine(match_.answer->first_line(), font_list_);
662 description_rendertext_ = CreateAnswerLine(
663 match_.answer->second_line(),
664 ui::ResourceBundle::GetSharedInstance().GetFontList(
665 ui::ResourceBundle::LargeFont));
666 } else if (!match_.description.empty()) {
667 description_rendertext_ = CreateClassifiedRenderText(
668 match_.description, match_.description_class, true);
671 PaintMatch(match_, contents_rendertext_.get(),
672 description_rendertext_.get(), canvas, x);
675 AutocompleteMatch* keyword_match = match_.associated_keyword.get();
676 if (keyword_match) {
677 int x = GetMirroredXForRect(keyword_text_bounds_);
678 mirroring_context_->Initialize(x, keyword_text_bounds_.width());
679 if (!keyword_contents_rendertext_) {
680 keyword_contents_rendertext_.reset(
681 CreateClassifiedRenderText(keyword_match->contents,
682 keyword_match->contents_class,
683 false).release());
685 if (!keyword_description_rendertext_ &&
686 !keyword_match->description.empty()) {
687 keyword_description_rendertext_.reset(
688 CreateClassifiedRenderText(keyword_match->description,
689 keyword_match->description_class,
690 true).release());
692 PaintMatch(*keyword_match, keyword_contents_rendertext_.get(),
693 keyword_description_rendertext_.get(), canvas, x);
697 void OmniboxResultView::AnimationProgressed(const gfx::Animation* animation) {
698 Layout();
699 SchedulePaint();
702 int OmniboxResultView::GetAnswerLineHeight() const {
703 // GetTextStyle(1) is the largest font used and so defines the boundary that
704 // all the other answer styles fit within.
705 return ui::ResourceBundle::GetSharedInstance()
706 .GetFontList(GetTextStyle(1).font)
707 .GetHeight();
710 int OmniboxResultView::GetContentLineHeight() const {
711 return std::max(
712 default_icon_size_ + GetLayoutInsets(OMNIBOX_DROPDOWN_ICON).height(),
713 GetTextHeight() + GetLayoutInsets(OMNIBOX_DROPDOWN_TEXT).height());
716 scoped_ptr<gfx::RenderText> OmniboxResultView::CreateAnswerLine(
717 const SuggestionAnswer::ImageLine& line,
718 gfx::FontList font_list) {
719 scoped_ptr<gfx::RenderText> destination = CreateRenderText(base::string16());
720 destination->SetFontList(font_list);
722 for (const SuggestionAnswer::TextField& text_field : line.text_fields())
723 AppendAnswerText(destination.get(), text_field.text(), text_field.type());
724 const base::char16 space(' ');
725 const auto* text_field = line.additional_text();
726 if (text_field) {
727 AppendAnswerText(destination.get(), space + text_field->text(),
728 text_field->type());
730 text_field = line.status_text();
731 if (text_field) {
732 AppendAnswerText(destination.get(), space + text_field->text(),
733 text_field->type());
735 return destination.Pass();
738 void OmniboxResultView::AppendAnswerText(gfx::RenderText* destination,
739 const base::string16& text,
740 int text_type) {
741 // TODO(dschuyler): make this better. Right now this only supports unnested
742 // bold tags. In the future we'll need to flag unexpected tags while adding
743 // support for b, i, u, sub, and sup. We'll also need to support HTML
744 // entities (&lt; for '<', etc.).
745 const base::string16 begin_tag = base::ASCIIToUTF16("<b>");
746 const base::string16 end_tag = base::ASCIIToUTF16("</b>");
747 size_t begin = 0;
748 while (true) {
749 size_t end = text.find(begin_tag, begin);
750 if (end == base::string16::npos) {
751 AppendAnswerTextHelper(destination, text.substr(begin), text_type, false);
752 break;
754 AppendAnswerTextHelper(destination, text.substr(begin, end - begin),
755 text_type, false);
756 begin = end + begin_tag.length();
757 end = text.find(end_tag, begin);
758 if (end == base::string16::npos)
759 break;
760 AppendAnswerTextHelper(destination, text.substr(begin, end - begin),
761 text_type, true);
762 begin = end + end_tag.length();
766 void OmniboxResultView::AppendAnswerTextHelper(gfx::RenderText* destination,
767 const base::string16& text,
768 int text_type,
769 bool is_bold) {
770 if (text.empty())
771 return;
772 int offset = destination->text().length();
773 gfx::Range range(offset, offset + text.length());
774 destination->AppendText(text);
775 const TextStyle& text_style = GetTextStyle(text_type);
776 // TODO(dschuyler): follow up on the problem of different font sizes within
777 // one RenderText. Maybe with destination->SetFontList(...).
778 destination->ApplyStyle(gfx::BOLD, is_bold, range);
779 destination->ApplyColor(
780 GetNativeTheme()->GetSystemColor(text_style.colors[GetState()]), range);
781 destination->ApplyBaselineStyle(text_style.baseline, range);
784 int OmniboxResultView::StartMargin() const {
785 return ui::MaterialDesignController::IsModeMaterial() ?
786 model_->start_margin() : 0;
789 int OmniboxResultView::EndMargin() const {
790 return ui::MaterialDesignController::IsModeMaterial() ?
791 model_->end_margin() : 0;