omnibox_result_view.cc revision 58537e28ecd584eab876aee8be7156509866d23a
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.
4
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
11
12#include "chrome/browser/ui/views/omnibox/omnibox_result_view.h"
13
14#include <algorithm>  // NOLINT
15
16#include "base/i18n/bidi_line_iterator.h"
17#include "base/memory/scoped_vector.h"
18#include "chrome/browser/ui/omnibox/omnibox_popup_model.h"
19#include "chrome/browser/ui/views/location_bar/location_bar_view.h"
20#include "chrome/browser/ui/views/omnibox/omnibox_result_view_model.h"
21#include "grit/generated_resources.h"
22#include "grit/theme_resources.h"
23#include "ui/base/l10n/l10n_util.h"
24#include "ui/base/theme_provider.h"
25#include "ui/gfx/canvas.h"
26#include "ui/gfx/color_utils.h"
27#include "ui/gfx/image/image.h"
28#include "ui/gfx/render_text.h"
29#include "ui/gfx/text_elider.h"
30#include "ui/native_theme/native_theme.h"
31
32#if defined(OS_WIN)
33#include "ui/native_theme/native_theme_win.h"
34#endif
35
36#if defined(USE_AURA)
37#include "ui/native_theme/native_theme_aura.h"
38#endif
39
40namespace {
41
42const char16 kEllipsis[] = { 0x2026, 0x0 };
43
44// The minimum distance between the top and bottom of the {icon|text} and the
45// top or bottom of the row.
46const int kMinimumIconVerticalPadding = 2;
47const int kMinimumTextVerticalPadding = 3;
48
49}  // namespace
50
51////////////////////////////////////////////////////////////////////////////////
52// OmniboxResultView, public:
53
54// Precalculated data used to draw a complete visual run within the match.
55// This will include all or part of at least one, and possibly several,
56// classifications.
57struct OmniboxResultView::RunData {
58  RunData() : run_start(0), visual_order(0), is_rtl(false), pixel_width(0) {}
59
60  size_t run_start;  // Offset within the match text where this run begins.
61  int visual_order;  // Where this run occurs in visual order.  The earliest
62  // run drawn is run 0.
63  bool is_rtl;
64  int pixel_width;
65
66  // Styled text classification pieces within this run, in logical order.
67  Classifications classifications;
68};
69
70// This class is a utility class for calculations affected by whether the result
71// view is horizontally mirrored.  The drawing functions can be written as if
72// all drawing occurs left-to-right, and then use this class to get the actual
73// coordinates to begin drawing onscreen.
74class OmniboxResultView::MirroringContext {
75 public:
76  MirroringContext() : center_(0), right_(0) {}
77
78  // Tells the mirroring context to use the provided range as the physical
79  // bounds of the drawing region.  When coordinate mirroring is needed, the
80  // mirror point will be the center of this range.
81  void Initialize(int x, int width) {
82    center_ = x + width / 2;
83    right_ = x + width;
84  }
85
86  // Given a logical range within the drawing region, returns the coordinate of
87  // the possibly-mirrored "left" side.  (This functions exactly like
88  // View::MirroredLeftPointForRect().)
89  int mirrored_left_coord(int left, int right) const {
90    return base::i18n::IsRTL() ? (center_ + (center_ - right)) : left;
91  }
92
93  // Given a logical coordinate within the drawing region, returns the remaining
94  // width available.
95  int remaining_width(int x) const {
96    return right_ - x;
97  }
98
99 private:
100  int center_;
101  int right_;
102
103  DISALLOW_COPY_AND_ASSIGN(MirroringContext);
104};
105
106OmniboxResultView::OmniboxResultView(
107    OmniboxResultViewModel* model,
108    int model_index,
109    LocationBarView* location_bar_view,
110    const gfx::FontList& font_list)
111    : edge_item_padding_(LocationBarView::GetItemPadding()),
112      item_padding_(LocationBarView::GetItemPadding()),
113      minimum_text_vertical_padding_(kMinimumTextVerticalPadding),
114      model_(model),
115      model_index_(model_index),
116      location_bar_view_(location_bar_view),
117      font_list_(font_list),
118      font_height_(std::max(font_list.GetHeight(),
119                            font_list.DeriveFontList(gfx::BOLD).GetHeight())),
120      ellipsis_width_(font_list.GetPrimaryFont().GetStringWidth(
121          string16(kEllipsis))),
122      mirroring_context_(new MirroringContext()),
123      keyword_icon_(new views::ImageView()),
124      animation_(new ui::SlideAnimation(this)) {
125  CHECK_GE(model_index, 0);
126  if (default_icon_size_ == 0) {
127    default_icon_size_ =
128        location_bar_view_->GetThemeProvider()->GetImageSkiaNamed(
129            AutocompleteMatch::TypeToIcon(
130                AutocompleteMatchType::URL_WHAT_YOU_TYPED))->width();
131  }
132  keyword_icon_->set_owned_by_client();
133  keyword_icon_->EnableCanvasFlippingForRTLUI(true);
134  keyword_icon_->SetImage(GetKeywordIcon());
135  keyword_icon_->SizeToPreferredSize();
136}
137
138OmniboxResultView::~OmniboxResultView() {
139}
140
141SkColor OmniboxResultView::GetColor(
142    ResultViewState state,
143    ColorKind kind) const {
144  const ui::NativeTheme* theme = GetNativeTheme();
145#if defined(OS_WIN)
146  if (theme == ui::NativeThemeWin::instance()) {
147    static bool win_initialized = false;
148    static SkColor win_colors[NUM_STATES][NUM_KINDS];
149    if (!win_initialized) {
150      win_colors[NORMAL][BACKGROUND] = color_utils::GetSysSkColor(COLOR_WINDOW);
151      win_colors[SELECTED][BACKGROUND] =
152          color_utils::GetSysSkColor(COLOR_HIGHLIGHT);
153      win_colors[NORMAL][TEXT] = color_utils::GetSysSkColor(COLOR_WINDOWTEXT);
154      win_colors[SELECTED][TEXT] =
155          color_utils::GetSysSkColor(COLOR_HIGHLIGHTTEXT);
156      CommonInitColors(theme, win_colors);
157      win_initialized = true;
158    }
159    return win_colors[state][kind];
160  }
161#endif
162  static bool initialized = false;
163  static SkColor colors[NUM_STATES][NUM_KINDS];
164  if (!initialized) {
165    colors[NORMAL][BACKGROUND] = theme->GetSystemColor(
166        ui::NativeTheme::kColorId_TextfieldDefaultBackground);
167    colors[NORMAL][TEXT] = theme->GetSystemColor(
168        ui::NativeTheme::kColorId_TextfieldDefaultColor);
169    colors[NORMAL][URL] = SkColorSetARGB(0xff, 0x00, 0x99, 0x33);
170    colors[SELECTED][BACKGROUND] = theme->GetSystemColor(
171        ui::NativeTheme::kColorId_TextfieldSelectionBackgroundFocused);
172    colors[SELECTED][TEXT] = theme->GetSystemColor(
173        ui::NativeTheme::kColorId_TextfieldSelectionColor);
174    colors[SELECTED][URL] = SkColorSetARGB(0xff, 0x00, 0x66, 0x22);
175    colors[HOVERED][URL] = SkColorSetARGB(0xff, 0x00, 0x66, 0x22);
176    CommonInitColors(theme, colors);
177    initialized = true;
178  }
179  return colors[state][kind];
180}
181
182void OmniboxResultView::SetMatch(const AutocompleteMatch& match) {
183  match_ = match;
184  animation_->Reset();
185
186  if (match.associated_keyword.get()) {
187    keyword_icon_->SetImage(GetKeywordIcon());
188
189    if (!keyword_icon_->parent())
190      AddChildView(keyword_icon_.get());
191  } else if (keyword_icon_->parent()) {
192    RemoveChildView(keyword_icon_.get());
193  }
194
195  Layout();
196}
197
198void OmniboxResultView::ShowKeyword(bool show_keyword) {
199  if (show_keyword)
200    animation_->Show();
201  else
202    animation_->Hide();
203}
204
205void OmniboxResultView::Invalidate() {
206  keyword_icon_->SetImage(GetKeywordIcon());
207  SchedulePaint();
208}
209
210gfx::Size OmniboxResultView::GetPreferredSize() {
211  return gfx::Size(0, std::max(
212      default_icon_size_ + (kMinimumIconVerticalPadding * 2),
213      GetTextHeight() + (minimum_text_vertical_padding_ * 2)));
214}
215
216////////////////////////////////////////////////////////////////////////////////
217// OmniboxResultView, protected:
218
219OmniboxResultView::ResultViewState OmniboxResultView::GetState() const {
220  if (model_->IsSelectedIndex(model_index_))
221    return SELECTED;
222  return model_->IsHoveredIndex(model_index_) ? HOVERED : NORMAL;
223}
224
225int OmniboxResultView::GetTextHeight() const {
226  return font_height_;
227}
228
229void OmniboxResultView::PaintMatch(gfx::Canvas* canvas,
230                                   const AutocompleteMatch& match,
231                                   int x) {
232  x = DrawString(canvas, match.contents, match.contents_class, false, x,
233                 text_bounds_.y());
234
235  // Paint the description.
236  // TODO(pkasting): Because we paint in multiple separate pieces, we can wind
237  // up with no space even for an ellipsis for one or both of these pieces.
238  // Instead, we should paint the entire match as a single long string.  This
239  // would also let us use a more properly-localizable string than we get with
240  // just the IDS_AUTOCOMPLETE_MATCH_DESCRIPTION_SEPARATOR.
241  if (!match.description.empty()) {
242    string16 separator =
243        l10n_util::GetStringUTF16(IDS_AUTOCOMPLETE_MATCH_DESCRIPTION_SEPARATOR);
244    ACMatchClassifications classifications;
245    classifications.push_back(
246        ACMatchClassification(0, ACMatchClassification::NONE));
247    x = DrawString(canvas, separator, classifications, true, x,
248                   text_bounds_.y());
249
250    DrawString(canvas, match.description, match.description_class, true, x,
251               text_bounds_.y());
252  }
253}
254
255// static
256void OmniboxResultView::CommonInitColors(const ui::NativeTheme* theme,
257                                         SkColor colors[][NUM_KINDS]) {
258  colors[HOVERED][BACKGROUND] =
259      color_utils::AlphaBlend(colors[SELECTED][BACKGROUND],
260                              colors[NORMAL][BACKGROUND], 64);
261  colors[HOVERED][TEXT] = colors[NORMAL][TEXT];
262#if defined(USE_AURA)
263  const bool is_aura = theme == ui::NativeThemeAura::instance();
264#else
265  const bool is_aura = false;
266#endif
267  for (int i = 0; i < NUM_STATES; ++i) {
268    if (is_aura) {
269      colors[i][TEXT] =
270          color_utils::AlphaBlend(SK_ColorBLACK, colors[i][BACKGROUND], 0xdd);
271      colors[i][DIMMED_TEXT] =
272          color_utils::AlphaBlend(SK_ColorBLACK, colors[i][BACKGROUND], 0xbb);
273    } else {
274      colors[i][DIMMED_TEXT] =
275          color_utils::AlphaBlend(colors[i][TEXT], colors[i][BACKGROUND], 128);
276      colors[i][URL] = color_utils::GetReadableColor(SkColorSetRGB(0, 128, 0),
277                                                     colors[i][BACKGROUND]);
278    }
279
280    // TODO(joi): Programmatically draw the dropdown border using
281    // this color as well. (Right now it's drawn as black with 25%
282    // alpha.)
283    colors[i][DIVIDER] =
284        color_utils::AlphaBlend(colors[i][TEXT], colors[i][BACKGROUND], 0x34);
285  }
286}
287
288// static
289bool OmniboxResultView::SortRunsLogically(const RunData& lhs,
290                                          const RunData& rhs) {
291  return lhs.run_start < rhs.run_start;
292}
293
294// static
295bool OmniboxResultView::SortRunsVisually(const RunData& lhs,
296                                         const RunData& rhs) {
297  return lhs.visual_order < rhs.visual_order;
298}
299
300// static
301int OmniboxResultView::default_icon_size_ = 0;
302
303gfx::ImageSkia OmniboxResultView::GetIcon() const {
304  const gfx::Image image = model_->GetIconIfExtensionMatch(model_index_);
305  if (!image.IsEmpty())
306    return image.AsImageSkia();
307
308  int icon = match_.starred ?
309      IDR_OMNIBOX_STAR : AutocompleteMatch::TypeToIcon(match_.type);
310  if (GetState() == SELECTED) {
311    switch (icon) {
312      case IDR_OMNIBOX_EXTENSION_APP:
313        icon = IDR_OMNIBOX_EXTENSION_APP_SELECTED;
314        break;
315      case IDR_OMNIBOX_HTTP:
316        icon = IDR_OMNIBOX_HTTP_SELECTED;
317        break;
318      case IDR_OMNIBOX_SEARCH:
319        icon = IDR_OMNIBOX_SEARCH_SELECTED;
320        break;
321      case IDR_OMNIBOX_STAR:
322        icon = IDR_OMNIBOX_STAR_SELECTED;
323        break;
324      default:
325        NOTREACHED();
326        break;
327    }
328  }
329  return *(location_bar_view_->GetThemeProvider()->GetImageSkiaNamed(icon));
330}
331
332const gfx::ImageSkia* OmniboxResultView::GetKeywordIcon() const {
333  // NOTE: If we ever begin returning icons of varying size, then callers need
334  // to ensure that |keyword_icon_| is resized each time its image is reset.
335  return location_bar_view_->GetThemeProvider()->GetImageSkiaNamed(
336      (GetState() == SELECTED) ? IDR_OMNIBOX_TTS_SELECTED : IDR_OMNIBOX_TTS);
337}
338
339int OmniboxResultView::DrawString(
340    gfx::Canvas* canvas,
341    const string16& text,
342    const ACMatchClassifications& classifications,
343    bool force_dim,
344    int x,
345    int y) {
346  if (text.empty())
347    return x;
348
349  // Check whether or not this text is a URL.  URLs are always displayed LTR
350  // regardless of locale.
351  bool is_url = true;
352  for (ACMatchClassifications::const_iterator i(classifications.begin());
353       i != classifications.end(); ++i) {
354    if (!(i->style & ACMatchClassification::URL)) {
355      is_url = false;
356      break;
357    }
358  }
359
360  // Split the text into visual runs.  We do this first so that we don't need to
361  // worry about whether our eliding might change the visual display in
362  // unintended ways, e.g. by removing directional markings or by adding an
363  // ellipsis that's not enclosed in appropriate markings.
364  base::i18n::BiDiLineIterator bidi_line;
365  if (!bidi_line.Open(text, base::i18n::IsRTL(), is_url))
366    return x;
367  const int num_runs = bidi_line.CountRuns();
368  ScopedVector<gfx::RenderText> render_texts;
369  Runs runs;
370  for (int run = 0; run < num_runs; ++run) {
371    int run_start_int = 0, run_length_int = 0;
372    // The index we pass to GetVisualRun corresponds to the position of the run
373    // in the displayed text. For example, the string "Google in HEBREW" (where
374    // HEBREW is text in the Hebrew language) has two runs: "Google in " which
375    // is an LTR run, and "HEBREW" which is an RTL run. In an LTR context, the
376    // run "Google in " has the index 0 (since it is the leftmost run
377    // displayed). In an RTL context, the same run has the index 1 because it
378    // is the rightmost run. This is why the order in which we traverse the
379    // runs is different depending on the locale direction.
380    const UBiDiDirection run_direction = bidi_line.GetVisualRun(
381        (base::i18n::IsRTL() && !is_url) ? (num_runs - run - 1) : run,
382        &run_start_int, &run_length_int);
383    DCHECK_GT(run_length_int, 0);
384    runs.push_back(RunData());
385    RunData* current_run = &runs.back();
386    current_run->run_start = run_start_int;
387    const size_t run_end = current_run->run_start + run_length_int;
388    current_run->visual_order = run;
389    current_run->is_rtl = !is_url && (run_direction == UBIDI_RTL);
390
391    // Compute classifications for this run.
392    for (size_t i = 0; i < classifications.size(); ++i) {
393      const size_t text_start =
394          std::max(classifications[i].offset, current_run->run_start);
395      if (text_start >= run_end)
396        break;  // We're past the last classification in the run.
397
398      const size_t text_end = (i < (classifications.size() - 1)) ?
399          std::min(classifications[i + 1].offset, run_end) : run_end;
400      if (text_end <= current_run->run_start)
401        continue;  // We haven't reached the first classification in the run.
402
403      render_texts.push_back(gfx::RenderText::CreateInstance());
404      gfx::RenderText* render_text = render_texts.back();
405      current_run->classifications.push_back(render_text);
406      render_text->SetText(text.substr(text_start, text_end - text_start));
407      render_text->SetFontList(font_list_);
408
409      // Calculate style-related data.
410      if (classifications[i].style & ACMatchClassification::MATCH)
411        render_text->SetStyle(gfx::BOLD, true);
412      const ResultViewState state = GetState();
413      if (classifications[i].style & ACMatchClassification::URL)
414        render_text->SetColor(GetColor(state, URL));
415      else if (classifications[i].style & ACMatchClassification::DIM)
416        render_text->SetColor(GetColor(state, DIMMED_TEXT));
417      else
418        render_text->SetColor(GetColor(state, force_dim ? DIMMED_TEXT : TEXT));
419
420      current_run->pixel_width += render_text->GetStringSize().width();
421    }
422    DCHECK(!current_run->classifications.empty());
423  }
424  DCHECK(!runs.empty());
425
426  // Sort into logical order so we can elide logically.
427  std::sort(runs.begin(), runs.end(), &SortRunsLogically);
428
429  // Now determine what to elide, if anything.  Several subtle points:
430  //   * Because we have the run data, we can get edge cases correct, like
431  //     whether to place an ellipsis before or after the end of a run when the
432  //     text needs to be elided at the run boundary.
433  //   * The "or one before it" comments below refer to cases where an earlier
434  //     classification fits completely, but leaves too little space for an
435  //     ellipsis that turns out to be needed later.  These cases are commented
436  //     more completely in Elide().
437  int remaining_width = mirroring_context_->remaining_width(x);
438  for (Runs::iterator i(runs.begin()); i != runs.end(); ++i) {
439    if (i->pixel_width > remaining_width) {
440      // This run or one before it needs to be elided.
441      for (Classifications::iterator j(i->classifications.begin());
442           j != i->classifications.end(); ++j) {
443        const int width = (*j)->GetStringSize().width();
444        if (width > remaining_width) {
445          // This classification or one before it needs to be elided.  Erase all
446          // further classifications and runs so Elide() can simply reverse-
447          // iterate over everything to find the specific classification to
448          // elide.
449          i->classifications.erase(++j, i->classifications.end());
450          runs.erase(++i, runs.end());
451          Elide(&runs, remaining_width);
452          break;
453        }
454        remaining_width -= width;
455      }
456      break;
457    }
458    remaining_width -= i->pixel_width;
459  }
460
461  // Sort back into visual order so we can display the runs correctly.
462  std::sort(runs.begin(), runs.end(), &SortRunsVisually);
463
464  // Draw the runs.
465  for (Runs::iterator i(runs.begin()); i != runs.end(); ++i) {
466    const bool reverse_visible_order = (i->is_rtl != base::i18n::IsRTL());
467    if (reverse_visible_order)
468      std::reverse(i->classifications.begin(), i->classifications.end());
469    for (Classifications::const_iterator j(i->classifications.begin());
470         j != i->classifications.end(); ++j) {
471      const gfx::Size size = (*j)->GetStringSize();
472      // Align the text runs to a common baseline.
473      const gfx::Rect rect(
474          mirroring_context_->mirrored_left_coord(x, x + size.width()),
475          y + font_list_.GetBaseline() - (*j)->GetBaseline(),
476          size.width(), size.height());
477      (*j)->SetDisplayRect(rect);
478      (*j)->Draw(canvas);
479      x += size.width();
480    }
481  }
482
483  return x;
484}
485
486void OmniboxResultView::Elide(Runs* runs, int remaining_width) const {
487  // The complexity of this function is due to edge cases like the following:
488  // We have 100 px of available space, an initial classification that takes 86
489  // px, and a font that has a 15 px wide ellipsis character.  Now if the first
490  // classification is followed by several very narrow classifications (e.g. 3
491  // px wide each), we don't know whether we need to elide or not at the time we
492  // see the first classification -- it depends on how many subsequent
493  // classifications follow, and some of those may be in the next run (or
494  // several runs!).  This is why instead we let our caller move forward until
495  // we know we definitely need to elide, and then in this function we move
496  // backward again until we find a string that we can successfully do the
497  // eliding on.
498  bool first_classification = true;
499  for (Runs::reverse_iterator i(runs->rbegin()); i != runs->rend(); ++i) {
500    for (Classifications::reverse_iterator j(i->classifications.rbegin());
501         j != i->classifications.rend(); ++j) {
502      if (!first_classification) {
503        // We also add this classification's width (sans ellipsis) back to the
504        // available width since we want to consider the available space we'll
505        // have when we draw this classification.
506        remaining_width += (*j)->GetStringSize().width();
507
508        // For all but the first classification we consider, we need to append
509        // an ellipsis, since there isn't enough room to draw it after this
510        // classification.
511        (*j)->SetText((*j)->text() + kEllipsis);
512      }
513      first_classification = false;
514
515      // Can we fit at least an ellipsis?
516      gfx::Font font((*j)->GetStyle(gfx::BOLD) ?
517          (*j)->GetPrimaryFont().DeriveFont(0, gfx::Font::BOLD) :
518          (*j)->GetPrimaryFont());
519      string16 elided_text(
520          gfx::ElideText((*j)->text(), font, remaining_width, gfx::ELIDE_AT_END));
521      Classifications::reverse_iterator prior(j + 1);
522      const bool on_first_classification = (prior == i->classifications.rend());
523      if (elided_text.empty() && (remaining_width >= ellipsis_width_) &&
524          on_first_classification) {
525        // Edge case: This classification is bold, we can't fit a bold ellipsis
526        // but we can fit a normal one, and this is the first classification in
527        // the run.  We should display a lone normal ellipsis, because appending
528        // one to the end of the previous run might put it in the wrong visual
529        // location (if the previous run is reversed from the normal visual
530        // order).
531        // NOTE: If this isn't the first classification in the run, we don't
532        // need to bother with this; see note below.
533        elided_text = kEllipsis;
534      }
535      if (!elided_text.empty()) {
536        // Success.  Elide this classification and stop.
537        (*j)->SetText(elided_text);
538
539        // If we could only fit an ellipsis, then only make it bold if there was
540        // an immediate prior classification in this run that was also bold, or
541        // it will look orphaned.
542        if ((*j)->GetStyle(gfx::BOLD) && (elided_text.length() == 1) &&
543            (on_first_classification || !(*prior)->GetStyle(gfx::BOLD)))
544          (*j)->SetStyle(gfx::BOLD, false);
545
546        // Erase any other classifications that come after the elided one.
547        i->classifications.erase(j.base(), i->classifications.end());
548        runs->erase(i.base(), runs->end());
549        return;
550      }
551
552      // We couldn't fit an ellipsis.  Move back one classification,
553      // append an ellipsis, and try again.
554      // NOTE: In the edge case that a bold ellipsis doesn't fit but a
555      // normal one would, and we reach here, then there is a previous
556      // classification in this run, and so either:
557      //   * It's normal, and will be able to draw successfully with the
558      //     ellipsis we'll append to it, or
559      //   * It is also bold, in which case we don't want to fall back
560      //     to a normal ellipsis anyway (see comment above).
561    }
562  }
563
564  // We couldn't draw anything.
565  runs->clear();
566}
567
568void OmniboxResultView::Layout() {
569  const gfx::ImageSkia icon = GetIcon();
570
571  icon_bounds_.SetRect(edge_item_padding_ +
572      ((icon.width() == default_icon_size_) ?
573          0 : LocationBarView::kIconInternalPadding),
574      (height() - icon.height()) / 2, icon.width(), icon.height());
575
576  int text_x = edge_item_padding_ + default_icon_size_ + item_padding_;
577  int text_height = GetTextHeight();
578  int text_width;
579
580  if (match_.associated_keyword.get()) {
581    const int kw_collapsed_size =
582        keyword_icon_->width() + edge_item_padding_;
583    const int max_kw_x = width() - kw_collapsed_size;
584    const int kw_x =
585        animation_->CurrentValueBetween(max_kw_x, edge_item_padding_);
586    const int kw_text_x = kw_x + keyword_icon_->width() + item_padding_;
587
588    text_width = kw_x - text_x - item_padding_;
589    keyword_text_bounds_.SetRect(kw_text_x, 0,
590        std::max(width() - kw_text_x - edge_item_padding_, 0), text_height);
591    keyword_icon_->SetPosition(gfx::Point(kw_x,
592        (height() - keyword_icon_->height()) / 2));
593  } else {
594    text_width = width() - text_x - edge_item_padding_;
595  }
596
597  text_bounds_.SetRect(text_x, std::max(0, (height() - text_height) / 2),
598      std::max(text_width, 0), text_height);
599}
600
601void OmniboxResultView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
602  animation_->SetSlideDuration(width() / 4);
603}
604
605void OmniboxResultView::OnPaint(gfx::Canvas* canvas) {
606  const ResultViewState state = GetState();
607  if (state != NORMAL)
608    canvas->DrawColor(GetColor(state, BACKGROUND));
609
610  if (!match_.associated_keyword.get() ||
611      keyword_icon_->x() > icon_bounds_.right()) {
612    // Paint the icon.
613    canvas->DrawImageInt(GetIcon(), GetMirroredXForRect(icon_bounds_),
614                         icon_bounds_.y());
615
616    // Paint the text.
617    int x = GetMirroredXForRect(text_bounds_);
618    mirroring_context_->Initialize(x, text_bounds_.width());
619    PaintMatch(canvas, match_, x);
620  }
621
622  if (match_.associated_keyword.get()) {
623    // Paint the keyword text.
624    int x = GetMirroredXForRect(keyword_text_bounds_);
625    mirroring_context_->Initialize(x, keyword_text_bounds_.width());
626    PaintMatch(canvas, *match_.associated_keyword.get(), x);
627  }
628}
629
630void OmniboxResultView::AnimationProgressed(const ui::Animation* animation) {
631  Layout();
632  SchedulePaint();
633}
634