Infobar material design refresh: layout
[chromium-blink-merge.git] / chrome / browser / ui / views / omnibox / omnibox_result_view.cc
blobdfbbda5d0ad64d164ba8534490eb833a04becd99
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/themes/theme_properties.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 location_bar_view_->GetThemeProvider()->GetDisplayProperty(
357 ThemeProperties::PROPERTY_ICON_LABEL_VIEW_TRAILING_PADDING);
359 } else {
360 x = DrawRenderText(match, separator_rendertext_.get(), false, canvas,
361 after_contents_x, y, separator_width_);
364 DrawRenderText(match, description, false, canvas, x, y,
365 description_max_width);
369 int OmniboxResultView::DrawRenderText(
370 const AutocompleteMatch& match,
371 gfx::RenderText* render_text,
372 bool contents,
373 gfx::Canvas* canvas,
374 int x,
375 int y,
376 int max_width) const {
377 DCHECK(!render_text->text().empty());
379 const int remaining_width = mirroring_context_->remaining_width(x);
380 int right_x = x + max_width;
382 // Infinite suggestions should appear with the leading ellipses vertically
383 // stacked.
384 if (contents &&
385 (match.type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL)) {
386 // When the directionality of suggestion doesn't match the UI, we try to
387 // vertically stack the ellipsis by restricting the end edge (right_x).
388 const bool is_ui_rtl = base::i18n::IsRTL();
389 const bool is_match_contents_rtl =
390 (render_text->GetDisplayTextDirection() == base::i18n::RIGHT_TO_LEFT);
391 const int offset =
392 GetDisplayOffset(match, is_ui_rtl, is_match_contents_rtl);
394 scoped_ptr<gfx::RenderText> prefix_render_text(
395 CreateRenderText(base::UTF8ToUTF16(
396 match.GetAdditionalInfo(kACMatchPropertyContentsPrefix))));
397 const int prefix_width = prefix_render_text->GetContentWidth();
398 int prefix_x = x;
400 const int max_match_contents_width = model_->max_match_contents_width();
402 if (is_ui_rtl != is_match_contents_rtl) {
403 // RTL infinite suggestions appear near the left edge in LTR UI, while LTR
404 // infinite suggestions appear near the right edge in RTL UI. This is
405 // against the natural horizontal alignment of the text. We reduce the
406 // width of the box for suggestion display, so that the suggestions appear
407 // in correct confines. This reduced width allows us to modify the text
408 // alignment (see below).
409 right_x = x + std::min(remaining_width - prefix_width,
410 std::max(offset, max_match_contents_width));
411 prefix_x = right_x;
412 // We explicitly set the horizontal alignment so that when LTR suggestions
413 // show in RTL UI (or vice versa), their ellipses appear stacked in a
414 // single column.
415 render_text->SetHorizontalAlignment(
416 is_match_contents_rtl ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT);
417 } else {
418 // If the dropdown is wide enough, place the ellipsis at the position
419 // where the omitted text would have ended. Otherwise reduce the offset of
420 // the ellipsis such that the widest suggestion reaches the end of the
421 // dropdown.
422 const int start_offset = std::max(prefix_width,
423 std::min(remaining_width - max_match_contents_width, offset));
424 right_x = x + std::min(remaining_width, start_offset + max_width);
425 x += start_offset;
426 prefix_x = x - prefix_width;
428 prefix_render_text->SetDirectionalityMode(is_match_contents_rtl ?
429 gfx::DIRECTIONALITY_FORCE_RTL : gfx::DIRECTIONALITY_FORCE_LTR);
430 prefix_render_text->SetHorizontalAlignment(
431 is_match_contents_rtl ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT);
432 prefix_render_text->SetDisplayRect(
433 gfx::Rect(mirroring_context_->mirrored_left_coord(
434 prefix_x, prefix_x + prefix_width),
435 y, prefix_width, GetContentLineHeight()));
436 prefix_render_text->Draw(canvas);
439 // Set the display rect to trigger eliding.
440 render_text->SetDisplayRect(
441 gfx::Rect(mirroring_context_->mirrored_left_coord(x, right_x), y,
442 right_x - x, GetContentLineHeight()));
443 render_text->Draw(canvas);
444 return right_x;
447 scoped_ptr<gfx::RenderText> OmniboxResultView::CreateRenderText(
448 const base::string16& text) const {
449 scoped_ptr<gfx::RenderText> render_text(gfx::RenderText::CreateInstance());
450 render_text->SetDisplayRect(gfx::Rect(gfx::Size(INT_MAX, 0)));
451 render_text->SetCursorEnabled(false);
452 render_text->SetElideBehavior(gfx::ELIDE_TAIL);
453 render_text->SetFontList(font_list_);
454 render_text->SetText(text);
455 return render_text.Pass();
458 scoped_ptr<gfx::RenderText> OmniboxResultView::CreateClassifiedRenderText(
459 const base::string16& text,
460 const ACMatchClassifications& classifications,
461 bool force_dim) const {
462 scoped_ptr<gfx::RenderText> render_text(CreateRenderText(text));
463 const size_t text_length = render_text->text().length();
464 for (size_t i = 0; i < classifications.size(); ++i) {
465 const size_t text_start = classifications[i].offset;
466 if (text_start >= text_length)
467 break;
469 const size_t text_end = (i < (classifications.size() - 1)) ?
470 std::min(classifications[i + 1].offset, text_length) :
471 text_length;
472 const gfx::Range current_range(text_start, text_end);
474 // Calculate style-related data.
475 if (classifications[i].style & ACMatchClassification::MATCH)
476 render_text->ApplyStyle(gfx::BOLD, true, current_range);
478 ColorKind color_kind = TEXT;
479 if (classifications[i].style & ACMatchClassification::URL) {
480 color_kind = URL;
481 // Consider logical string for domain "ABC.com×™/hello" where ABC are
482 // Hebrew (RTL) characters. This string should ideally show as
483 // "CBA.com/hello". If we do not force LTR on URL, it will appear as
484 // "com/hello.CBA".
485 // With IDN and RTL TLDs, it might be okay to allow RTL rendering of URLs,
486 // but it still has some pitfalls like :
487 // ABC.COM/abc-pqr/xyz/FGH will appear as HGF/abc-pqr/xyz/MOC.CBA which
488 // really confuses the path hierarchy of the URL.
489 // Also, if the URL supports https, the appearance will change into LTR
490 // directionality.
491 // In conclusion, LTR rendering of URL is probably the safest bet.
492 render_text->SetDirectionalityMode(gfx::DIRECTIONALITY_FORCE_LTR);
493 } else if (force_dim ||
494 (classifications[i].style & ACMatchClassification::DIM)) {
495 color_kind = DIMMED_TEXT;
497 render_text->ApplyColor(GetColor(GetState(), color_kind), current_range);
499 return render_text.Pass();
502 int OmniboxResultView::GetMatchContentsWidth() const {
503 InitContentsRenderTextIfNecessary();
504 contents_rendertext_->SetDisplayRect(gfx::Rect(gfx::Size(INT_MAX, 0)));
505 return contents_rendertext_->GetContentWidth();
508 void OmniboxResultView::SetAnswerImage(const gfx::ImageSkia& image) {
509 answer_image_ = image;
510 SchedulePaint();
513 // TODO(skanuj): This is probably identical across all OmniboxResultView rows in
514 // the omnibox dropdown. Consider sharing the result.
515 int OmniboxResultView::GetDisplayOffset(
516 const AutocompleteMatch& match,
517 bool is_ui_rtl,
518 bool is_match_contents_rtl) const {
519 if (match.type != AutocompleteMatchType::SEARCH_SUGGEST_TAIL)
520 return 0;
522 const base::string16& input_text =
523 base::UTF8ToUTF16(match.GetAdditionalInfo(kACMatchPropertyInputText));
524 int contents_start_index = 0;
525 base::StringToInt(match.GetAdditionalInfo(kACMatchPropertyContentsStartIndex),
526 &contents_start_index);
528 scoped_ptr<gfx::RenderText> input_render_text(CreateRenderText(input_text));
529 const gfx::Range& glyph_bounds =
530 input_render_text->GetGlyphBounds(contents_start_index);
531 const int start_padding = is_match_contents_rtl ?
532 std::max(glyph_bounds.start(), glyph_bounds.end()) :
533 std::min(glyph_bounds.start(), glyph_bounds.end());
535 return is_ui_rtl ?
536 (input_render_text->GetContentWidth() - start_padding) : start_padding;
539 // static
540 int OmniboxResultView::default_icon_size_ = 0;
542 const char* OmniboxResultView::GetClassName() const {
543 return "OmniboxResultView";
546 gfx::ImageSkia OmniboxResultView::GetIcon() const {
547 const gfx::Image image = model_->GetIconIfExtensionMatch(model_index_);
548 if (!image.IsEmpty())
549 return image.AsImageSkia();
551 int icon = model_->IsStarredMatch(match_) ?
552 IDR_OMNIBOX_STAR : AutocompleteMatch::TypeToIcon(match_.type);
553 if (GetState() == SELECTED &&
554 !ui::MaterialDesignController::IsModeMaterial()) {
555 switch (icon) {
556 case IDR_OMNIBOX_CALCULATOR:
557 icon = IDR_OMNIBOX_CALCULATOR_SELECTED;
558 break;
559 case IDR_OMNIBOX_EXTENSION_APP:
560 icon = IDR_OMNIBOX_EXTENSION_APP_SELECTED;
561 break;
562 case IDR_OMNIBOX_HTTP:
563 icon = IDR_OMNIBOX_HTTP_SELECTED;
564 break;
565 case IDR_OMNIBOX_SEARCH:
566 icon = IDR_OMNIBOX_SEARCH_SELECTED;
567 break;
568 case IDR_OMNIBOX_STAR:
569 icon = IDR_OMNIBOX_STAR_SELECTED;
570 break;
571 default:
572 NOTREACHED();
573 break;
576 return *(location_bar_view_->GetThemeProvider()->GetImageSkiaNamed(icon));
579 const gfx::ImageSkia* OmniboxResultView::GetKeywordIcon() const {
580 // NOTE: If we ever begin returning icons of varying size, then callers need
581 // to ensure that |keyword_icon_| is resized each time its image is reset.
582 int icon = IDR_OMNIBOX_TTS;
583 if (GetState() == SELECTED && !ui::MaterialDesignController::IsModeMaterial())
584 icon = IDR_OMNIBOX_TTS_SELECTED;
586 return location_bar_view_->GetThemeProvider()->GetImageSkiaNamed(icon);
589 bool OmniboxResultView::ShowOnlyKeywordMatch() const {
590 return match_.associated_keyword &&
591 (keyword_icon_->x() <= icon_bounds_.right());
594 void OmniboxResultView::ResetRenderTexts() const {
595 contents_rendertext_.reset();
596 description_rendertext_.reset();
597 separator_rendertext_.reset();
598 keyword_contents_rendertext_.reset();
599 keyword_description_rendertext_.reset();
602 void OmniboxResultView::InitContentsRenderTextIfNecessary() const {
603 if (!contents_rendertext_) {
604 contents_rendertext_.reset(
605 CreateClassifiedRenderText(
606 match_.contents, match_.contents_class, false).release());
610 void OmniboxResultView::Layout() {
611 const gfx::ImageSkia icon = GetIcon();
612 // TODO(jonross): Currently |location_bar_view_| provides the correct
613 // ThemeProvider, as it is loaded on the BrowserFrame widget. The root widget
614 // for OmniboxResultView is AutocompletePopupWidget, which is not loading the
615 // theme. We should update the omnibox code to also track its own
616 // ThemeProvider in order to reduce dependancy on LocationBarView.
617 ui::ThemeProvider* theme_provider = location_bar_view_->GetThemeProvider();
618 // |theme_provider| can be null when animations are running during shutdown,
619 // after OmniboxResultView has been removed from the tree of Views.
620 if (!theme_provider)
621 return;
622 const int horizontal_padding = theme_provider->GetDisplayProperty(
623 ThemeProperties::PROPERTY_LOCATION_BAR_HORIZONTAL_PADDING);
624 const int trailing_padding = theme_provider->GetDisplayProperty(
625 ThemeProperties::PROPERTY_ICON_LABEL_VIEW_TRAILING_PADDING);
627 const int start_x = StartMargin() + horizontal_padding;
628 const int end_x = width() - EndMargin() - horizontal_padding;
630 icon_bounds_.SetRect(
631 start_x + ((icon.width() == default_icon_size_) ? 0 : trailing_padding),
632 (GetContentLineHeight() - icon.height()) / 2,
633 icon.width(), icon.height());
635 const int text_x = start_x + default_icon_size_ + horizontal_padding;
636 int text_width = end_x - text_x;
638 if (match_.associated_keyword.get()) {
639 const int max_kw_x = end_x - keyword_icon_->width();
640 const int kw_x = animation_->CurrentValueBetween(max_kw_x, start_x);
641 const int kw_text_x = kw_x + keyword_icon_->width() + horizontal_padding;
643 text_width = kw_x - text_x - horizontal_padding;
644 keyword_text_bounds_.SetRect(
645 kw_text_x, 0, std::max(end_x - kw_text_x, 0), height());
646 keyword_icon_->SetPosition(
647 gfx::Point(kw_x, (height() - keyword_icon_->height()) / 2));
650 text_bounds_.SetRect(text_x, 0, std::max(text_width, 0), height());
653 void OmniboxResultView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
654 animation_->SetSlideDuration((width() - StartMargin() - EndMargin()) / 4);
657 void OmniboxResultView::OnPaint(gfx::Canvas* canvas) {
658 const ResultViewState state = GetState();
659 if (state != NORMAL)
660 canvas->DrawColor(GetColor(state, BACKGROUND));
662 // NOTE: While animating the keyword match, both matches may be visible.
664 if (!ShowOnlyKeywordMatch()) {
665 canvas->DrawImageInt(GetIcon(), GetMirroredXForRect(icon_bounds_),
666 icon_bounds_.y());
667 int x = GetMirroredXForRect(text_bounds_);
668 mirroring_context_->Initialize(x, text_bounds_.width());
669 InitContentsRenderTextIfNecessary();
671 if (!description_rendertext_) {
672 if (match_.answer) {
673 contents_rendertext_ =
674 CreateAnswerLine(match_.answer->first_line(), font_list_);
675 description_rendertext_ = CreateAnswerLine(
676 match_.answer->second_line(),
677 ui::ResourceBundle::GetSharedInstance().GetFontList(
678 ui::ResourceBundle::LargeFont));
679 } else if (!match_.description.empty()) {
680 description_rendertext_ = CreateClassifiedRenderText(
681 match_.description, match_.description_class, true);
684 PaintMatch(match_, contents_rendertext_.get(),
685 description_rendertext_.get(), canvas, x);
688 AutocompleteMatch* keyword_match = match_.associated_keyword.get();
689 if (keyword_match) {
690 int x = GetMirroredXForRect(keyword_text_bounds_);
691 mirroring_context_->Initialize(x, keyword_text_bounds_.width());
692 if (!keyword_contents_rendertext_) {
693 keyword_contents_rendertext_.reset(
694 CreateClassifiedRenderText(keyword_match->contents,
695 keyword_match->contents_class,
696 false).release());
698 if (!keyword_description_rendertext_ &&
699 !keyword_match->description.empty()) {
700 keyword_description_rendertext_.reset(
701 CreateClassifiedRenderText(keyword_match->description,
702 keyword_match->description_class,
703 true).release());
705 PaintMatch(*keyword_match, keyword_contents_rendertext_.get(),
706 keyword_description_rendertext_.get(), canvas, x);
710 void OmniboxResultView::AnimationProgressed(const gfx::Animation* animation) {
711 Layout();
712 SchedulePaint();
715 int OmniboxResultView::GetAnswerLineHeight() const {
716 // GetTextStyle(1) is the largest font used and so defines the boundary that
717 // all the other answer styles fit within.
718 return ui::ResourceBundle::GetSharedInstance()
719 .GetFontList(GetTextStyle(1).font)
720 .GetHeight();
723 int OmniboxResultView::GetContentLineHeight() const {
724 ui::ThemeProvider* theme_provider = location_bar_view_->GetThemeProvider();
725 const int min_icon_vertical_padding = theme_provider->GetDisplayProperty(
726 ThemeProperties::PROPERTY_OMNIBOX_DROPDOWN_MIN_ICON_VERTICAL_PADDING);
727 const int min_text_vertical_padding = theme_provider->GetDisplayProperty(
728 ThemeProperties::PROPERTY_OMNIBOX_DROPDOWN_MIN_TEXT_VERTICAL_PADDING);
730 return std::max(default_icon_size_ + (min_icon_vertical_padding * 2),
731 GetTextHeight() + (min_text_vertical_padding * 2));
734 scoped_ptr<gfx::RenderText> OmniboxResultView::CreateAnswerLine(
735 const SuggestionAnswer::ImageLine& line,
736 gfx::FontList font_list) {
737 scoped_ptr<gfx::RenderText> destination = CreateRenderText(base::string16());
738 destination->SetFontList(font_list);
740 for (const SuggestionAnswer::TextField& text_field : line.text_fields())
741 AppendAnswerText(destination.get(), text_field.text(), text_field.type());
742 const base::char16 space(' ');
743 const auto* text_field = line.additional_text();
744 if (text_field) {
745 AppendAnswerText(destination.get(), space + text_field->text(),
746 text_field->type());
748 text_field = line.status_text();
749 if (text_field) {
750 AppendAnswerText(destination.get(), space + text_field->text(),
751 text_field->type());
753 return destination.Pass();
756 void OmniboxResultView::AppendAnswerText(gfx::RenderText* destination,
757 const base::string16& text,
758 int text_type) {
759 // TODO(dschuyler): make this better. Right now this only supports unnested
760 // bold tags. In the future we'll need to flag unexpected tags while adding
761 // support for b, i, u, sub, and sup. We'll also need to support HTML
762 // entities (&lt; for '<', etc.).
763 const base::string16 begin_tag = base::ASCIIToUTF16("<b>");
764 const base::string16 end_tag = base::ASCIIToUTF16("</b>");
765 size_t begin = 0;
766 while (true) {
767 size_t end = text.find(begin_tag, begin);
768 if (end == base::string16::npos) {
769 AppendAnswerTextHelper(destination, text.substr(begin), text_type, false);
770 break;
772 AppendAnswerTextHelper(destination, text.substr(begin, end - begin),
773 text_type, false);
774 begin = end + begin_tag.length();
775 end = text.find(end_tag, begin);
776 if (end == base::string16::npos)
777 break;
778 AppendAnswerTextHelper(destination, text.substr(begin, end - begin),
779 text_type, true);
780 begin = end + end_tag.length();
784 void OmniboxResultView::AppendAnswerTextHelper(gfx::RenderText* destination,
785 const base::string16& text,
786 int text_type,
787 bool is_bold) {
788 if (text.empty())
789 return;
790 int offset = destination->text().length();
791 gfx::Range range(offset, offset + text.length());
792 destination->AppendText(text);
793 const TextStyle& text_style = GetTextStyle(text_type);
794 // TODO(dschuyler): follow up on the problem of different font sizes within
795 // one RenderText. Maybe with destination->SetFontList(...).
796 destination->ApplyStyle(gfx::BOLD, is_bold, range);
797 destination->ApplyColor(
798 GetNativeTheme()->GetSystemColor(text_style.colors[GetState()]), range);
799 destination->ApplyBaselineStyle(text_style.baseline, range);
802 int OmniboxResultView::StartMargin() const {
803 return ui::MaterialDesignController::IsModeMaterial() ?
804 model_->start_margin() : 0;
807 int OmniboxResultView::EndMargin() const {
808 return ui::MaterialDesignController::IsModeMaterial() ?
809 model_->end_margin() : 0;