1// Copyright (c) 2011 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/autocomplete/autocomplete_result_view.h"
13
14#include <algorithm>  // NOLINT
15
16#include "base/i18n/bidi_line_iterator.h"
17#include "chrome/browser/ui/views/autocomplete/autocomplete_result_view_model.h"
18#include "chrome/browser/ui/views/location_bar/location_bar_view.h"
19#include "grit/generated_resources.h"
20#include "grit/theme_resources.h"
21#include "ui/base/l10n/l10n_util.h"
22#include "ui/base/resource/resource_bundle.h"
23#include "ui/base/text/text_elider.h"
24#include "ui/gfx/canvas_skia.h"
25#include "ui/gfx/color_utils.h"
26
27#if defined(OS_LINUX)
28#include "chrome/browser/ui/gtk/gtk_util.h"
29#include "ui/gfx/skia_utils_gtk.h"
30#endif
31
32namespace {
33
34const char16 kEllipsis[] = { 0x2026 };
35
36// The minimum distance between the top and bottom of the {icon|text} and the
37// top or bottom of the row.
38const int kMinimumIconVerticalPadding = 2;
39
40#if defined(TOUCH_UI)
41const int kMinimumTextVerticalPadding = 15;
42#else
43const int kMinimumTextVerticalPadding = 3;
44#endif
45
46}  // namespace
47
48////////////////////////////////////////////////////////////////////////////////
49// AutocompleteResultView, public:
50
51// Precalculated data used to draw the portion of a match classification that
52// fits entirely within one run.
53struct AutocompleteResultView::ClassificationData {
54  string16 text;
55  const gfx::Font* font;
56  SkColor color;
57  int pixel_width;
58};
59
60// Precalculated data used to draw a complete visual run within the match.
61// This will include all or part of at leasdt one, and possibly several,
62// classifications.
63struct AutocompleteResultView::RunData {
64  size_t run_start;  // Offset within the match text where this run begins.
65  int visual_order;  // Where this run occurs in visual order.  The earliest
66  // run drawn is run 0.
67  bool is_rtl;
68  int pixel_width;
69  Classifications classifications;  // Classification pieces within this run,
70                                    // in logical order.
71};
72
73// This class is a utility class for calculations affected by whether the result
74// view is horizontally mirrored.  The drawing functions can be written as if
75// all drawing occurs left-to-right, and then use this class to get the actual
76// coordinates to begin drawing onscreen.
77class AutocompleteResultView::MirroringContext {
78 public:
79  MirroringContext() : center_(0), right_(0) {}
80
81  // Tells the mirroring context to use the provided range as the physical
82  // bounds of the drawing region.  When coordinate mirroring is needed, the
83  // mirror point will be the center of this range.
84  void Initialize(int x, int width) {
85    center_ = x + width / 2;
86    right_ = x + width;
87  }
88
89  // Given a logical range within the drawing region, returns the coordinate of
90  // the possibly-mirrored "left" side.  (This functions exactly like
91  // View::MirroredLeftPointForRect().)
92  int mirrored_left_coord(int left, int right) const {
93    return base::i18n::IsRTL() ? (center_ + (center_ - right)) : left;
94  }
95
96  // Given a logical coordinate within the drawing region, returns the remaining
97  // width available.
98  int remaining_width(int x) const {
99    return right_ - x;
100  }
101
102 private:
103  int center_;
104  int right_;
105
106  DISALLOW_COPY_AND_ASSIGN(MirroringContext);
107};
108
109AutocompleteResultView::AutocompleteResultView(
110    AutocompleteResultViewModel* model,
111    int model_index,
112    const gfx::Font& font,
113    const gfx::Font& bold_font)
114    : model_(model),
115      model_index_(model_index),
116      normal_font_(font),
117      bold_font_(bold_font),
118      ellipsis_width_(font.GetStringWidth(string16(kEllipsis))),
119      mirroring_context_(new MirroringContext()),
120      match_(NULL, 0, false, AutocompleteMatch::URL_WHAT_YOU_TYPED) {
121  CHECK_GE(model_index, 0);
122  if (default_icon_size_ == 0) {
123    default_icon_size_ = ResourceBundle::GetSharedInstance().GetBitmapNamed(
124        AutocompleteMatch::TypeToIcon(AutocompleteMatch::URL_WHAT_YOU_TYPED))->
125        width();
126  }
127}
128
129AutocompleteResultView::~AutocompleteResultView() {
130}
131
132// static
133SkColor AutocompleteResultView::GetColor(ResultViewState state,
134                                         ColorKind kind) {
135  static bool initialized = false;
136  static SkColor colors[NUM_STATES][NUM_KINDS];
137  if (!initialized) {
138#if defined(OS_WIN)
139    colors[NORMAL][BACKGROUND] = color_utils::GetSysSkColor(COLOR_WINDOW);
140    colors[SELECTED][BACKGROUND] = color_utils::GetSysSkColor(COLOR_HIGHLIGHT);
141    colors[NORMAL][TEXT] = color_utils::GetSysSkColor(COLOR_WINDOWTEXT);
142    colors[SELECTED][TEXT] = color_utils::GetSysSkColor(COLOR_HIGHLIGHTTEXT);
143#elif defined(OS_LINUX)
144    GdkColor bg_color, selected_bg_color, text_color, selected_text_color;
145    gtk_util::GetTextColors(
146        &bg_color, &selected_bg_color, &text_color, &selected_text_color);
147    colors[NORMAL][BACKGROUND] = gfx::GdkColorToSkColor(bg_color);
148    colors[SELECTED][BACKGROUND] = gfx::GdkColorToSkColor(selected_bg_color);
149    colors[NORMAL][TEXT] = gfx::GdkColorToSkColor(text_color);
150    colors[SELECTED][TEXT] = gfx::GdkColorToSkColor(selected_text_color);
151#else
152    // TODO(beng): source from theme provider.
153    colors[NORMAL][BACKGROUND] = SK_ColorWHITE;
154    colors[SELECTED][BACKGROUND] = SK_ColorBLUE;
155    colors[NORMAL][TEXT] = SK_ColorBLACK;
156    colors[SELECTED][TEXT] = SK_ColorWHITE;
157#endif
158    colors[HOVERED][BACKGROUND] =
159        color_utils::AlphaBlend(colors[SELECTED][BACKGROUND],
160                                colors[NORMAL][BACKGROUND], 64);
161    colors[HOVERED][TEXT] = colors[NORMAL][TEXT];
162    for (int i = 0; i < NUM_STATES; ++i) {
163      colors[i][DIMMED_TEXT] =
164          color_utils::AlphaBlend(colors[i][TEXT], colors[i][BACKGROUND], 128);
165      colors[i][URL] = color_utils::GetReadableColor(SkColorSetRGB(0, 128, 0),
166                                                     colors[i][BACKGROUND]);
167    }
168    initialized = true;
169  }
170
171  return colors[state][kind];
172}
173
174void AutocompleteResultView::SetMatch(const AutocompleteMatch& match) {
175  match_ = match;
176  Layout();
177}
178
179////////////////////////////////////////////////////////////////////////////////
180// AutocompleteResultView, protected:
181
182void AutocompleteResultView::PaintMatch(gfx::Canvas* canvas,
183                                        const AutocompleteMatch& match,
184                                        int x) {
185  x = DrawString(canvas, match.contents, match.contents_class, false, x,
186                 text_bounds_.y());
187
188  // Paint the description.
189  // TODO(pkasting): Because we paint in multiple separate pieces, we can wind
190  // up with no space even for an ellipsis for one or both of these pieces.
191  // Instead, we should paint the entire match as a single long string.  This
192  // would also let us use a more properly-localizable string than we get with
193  // just the IDS_AUTOCOMPLETE_MATCH_DESCRIPTION_SEPARATOR.
194  if (!match.description.empty()) {
195    string16 separator =
196        l10n_util::GetStringUTF16(IDS_AUTOCOMPLETE_MATCH_DESCRIPTION_SEPARATOR);
197    ACMatchClassifications classifications;
198    classifications.push_back(
199        ACMatchClassification(0, ACMatchClassification::NONE));
200    x = DrawString(canvas, separator, classifications, true, x,
201                   text_bounds_.y());
202
203    DrawString(canvas, match.description, match.description_class, true, x,
204               text_bounds_.y());
205  }
206}
207
208int AutocompleteResultView::GetFontHeight() const {
209  return std::max(normal_font_.GetHeight(), bold_font_.GetHeight());
210}
211
212// static
213bool AutocompleteResultView::SortRunsLogically(const RunData& lhs,
214                                               const RunData& rhs) {
215  return lhs.run_start < rhs.run_start;
216}
217
218// static
219bool AutocompleteResultView::SortRunsVisually(const RunData& lhs,
220                                              const RunData& rhs) {
221  return lhs.visual_order < rhs.visual_order;
222}
223
224// static
225int AutocompleteResultView::default_icon_size_ = 0;
226
227AutocompleteResultView::ResultViewState
228    AutocompleteResultView::GetState() const {
229  if (model_->IsSelectedIndex(model_index_))
230    return SELECTED;
231  return model_->IsHoveredIndex(model_index_) ? HOVERED : NORMAL;
232}
233
234const SkBitmap* AutocompleteResultView::GetIcon() const {
235  const SkBitmap* bitmap = model_->GetIconIfExtensionMatch(model_index_);
236  if (bitmap)
237    return bitmap;
238
239  int icon = match_.starred ?
240      IDR_OMNIBOX_STAR : AutocompleteMatch::TypeToIcon(match_.type);
241  if (model_->IsSelectedIndex(model_index_)) {
242    switch (icon) {
243      case IDR_OMNIBOX_EXTENSION_APP:
244        icon = IDR_OMNIBOX_EXTENSION_APP_SELECTED;
245        break;
246      case IDR_OMNIBOX_HTTP:
247        icon = IDR_OMNIBOX_HTTP_SELECTED;
248        break;
249      case IDR_OMNIBOX_HISTORY:
250        icon = IDR_OMNIBOX_HISTORY_SELECTED;
251        break;
252      case IDR_OMNIBOX_SEARCH:
253        icon = IDR_OMNIBOX_SEARCH_SELECTED;
254        break;
255      case IDR_OMNIBOX_STAR:
256        icon = IDR_OMNIBOX_STAR_SELECTED;
257        break;
258      default:
259        NOTREACHED();
260        break;
261    }
262  }
263  return ResourceBundle::GetSharedInstance().GetBitmapNamed(icon);
264}
265
266int AutocompleteResultView::DrawString(
267    gfx::Canvas* canvas,
268    const string16& text,
269    const ACMatchClassifications& classifications,
270    bool force_dim,
271    int x,
272    int y) {
273  if (text.empty())
274    return x;
275
276  // Check whether or not this text is a URL.  URLs are always displayed LTR
277  // regardless of locale.
278  bool is_url = true;
279  for (ACMatchClassifications::const_iterator i(classifications.begin());
280       i != classifications.end(); ++i) {
281    if (!(i->style & ACMatchClassification::URL)) {
282      is_url = false;
283      break;
284    }
285  }
286
287  // Split the text into visual runs.  We do this first so that we don't need to
288  // worry about whether our eliding might change the visual display in
289  // unintended ways, e.g. by removing directional markings or by adding an
290  // ellipsis that's not enclosed in appropriate markings.
291  base::i18n::BiDiLineIterator bidi_line;
292  if (!bidi_line.Open(text, base::i18n::IsRTL(), is_url))
293    return x;
294  const int num_runs = bidi_line.CountRuns();
295  Runs runs;
296  for (int run = 0; run < num_runs; ++run) {
297    int run_start_int = 0, run_length_int = 0;
298    // The index we pass to GetVisualRun corresponds to the position of the run
299    // in the displayed text. For example, the string "Google in HEBREW" (where
300    // HEBREW is text in the Hebrew language) has two runs: "Google in " which
301    // is an LTR run, and "HEBREW" which is an RTL run. In an LTR context, the
302    // run "Google in " has the index 0 (since it is the leftmost run
303    // displayed). In an RTL context, the same run has the index 1 because it
304    // is the rightmost run. This is why the order in which we traverse the
305    // runs is different depending on the locale direction.
306    const UBiDiDirection run_direction = bidi_line.GetVisualRun(
307        (base::i18n::IsRTL() && !is_url) ? (num_runs - run - 1) : run,
308        &run_start_int, &run_length_int);
309    DCHECK_GT(run_length_int, 0);
310    runs.push_back(RunData());
311    RunData* current_run = &runs.back();
312    current_run->run_start = run_start_int;
313    const size_t run_end = current_run->run_start + run_length_int;
314    current_run->visual_order = run;
315    current_run->is_rtl = !is_url && (run_direction == UBIDI_RTL);
316    current_run->pixel_width = 0;
317
318    // Compute classifications for this run.
319    for (size_t i = 0; i < classifications.size(); ++i) {
320      const size_t text_start =
321          std::max(classifications[i].offset, current_run->run_start);
322      if (text_start >= run_end)
323        break;  // We're past the last classification in the run.
324
325      const size_t text_end = (i < (classifications.size() - 1)) ?
326          std::min(classifications[i + 1].offset, run_end) : run_end;
327      if (text_end <= current_run->run_start)
328        continue;  // We haven't reached the first classification in the run.
329
330      current_run->classifications.push_back(ClassificationData());
331      ClassificationData* current_data =
332          &current_run->classifications.back();
333      current_data->text = text.substr(text_start, text_end - text_start);
334
335      // Calculate style-related data.
336      const int style = classifications[i].style;
337      const bool use_bold_font = !!(style & ACMatchClassification::MATCH);
338      current_data->font = &(use_bold_font ? bold_font_ : normal_font_);
339      const ResultViewState state = GetState();
340      if (style & ACMatchClassification::URL)
341        current_data->color = GetColor(state, URL);
342      else if (style & ACMatchClassification::DIM)
343        current_data->color = GetColor(state, DIMMED_TEXT);
344      else
345        current_data->color = GetColor(state, force_dim ? DIMMED_TEXT : TEXT);
346      current_data->pixel_width =
347          current_data->font->GetStringWidth(current_data->text);
348      current_run->pixel_width += current_data->pixel_width;
349    }
350    DCHECK(!current_run->classifications.empty());
351  }
352  DCHECK(!runs.empty());
353
354  // Sort into logical order so we can elide logically.
355  std::sort(runs.begin(), runs.end(), &SortRunsLogically);
356
357  // Now determine what to elide, if anything.  Several subtle points:
358  //   * Because we have the run data, we can get edge cases correct, like
359  //     whether to place an ellipsis before or after the end of a run when the
360  //     text needs to be elided at the run boundary.
361  //   * The "or one before it" comments below refer to cases where an earlier
362  //     classification fits completely, but leaves too little space for an
363  //     ellipsis that turns out to be needed later.  These cases are commented
364  //     more completely in Elide().
365  int remaining_width = mirroring_context_->remaining_width(x);
366  for (Runs::iterator i(runs.begin()); i != runs.end(); ++i) {
367    if (i->pixel_width > remaining_width) {
368      // This run or one before it needs to be elided.
369      for (Classifications::iterator j(i->classifications.begin());
370           j != i->classifications.end(); ++j) {
371        if (j->pixel_width > remaining_width) {
372          // This classification or one before it needs to be elided.  Erase all
373          // further classifications and runs so Elide() can simply reverse-
374          // iterate over everything to find the specific classification to
375          // elide.
376          i->classifications.erase(++j, i->classifications.end());
377          runs.erase(++i, runs.end());
378          Elide(&runs, remaining_width);
379          break;
380        }
381        remaining_width -= j->pixel_width;
382      }
383      break;
384    }
385    remaining_width -= i->pixel_width;
386  }
387
388  // Sort back into visual order so we can display the runs correctly.
389  std::sort(runs.begin(), runs.end(), &SortRunsVisually);
390
391  // Draw the runs.
392  for (Runs::iterator i(runs.begin()); i != runs.end(); ++i) {
393    const bool reverse_visible_order = (i->is_rtl != base::i18n::IsRTL());
394    int flags = gfx::Canvas::NO_ELLIPSIS;  // We've already elided.
395    if (reverse_visible_order) {
396      std::reverse(i->classifications.begin(), i->classifications.end());
397      if (i->is_rtl)
398        flags |= gfx::Canvas::FORCE_RTL_DIRECTIONALITY;
399    }
400    for (Classifications::const_iterator j(i->classifications.begin());
401         j != i->classifications.end(); ++j) {
402      int left = mirroring_context_->mirrored_left_coord(x, x + j->pixel_width);
403      canvas->DrawStringInt(j->text, *j->font, j->color, left,
404                            y, j->pixel_width, j->font->GetHeight(), flags);
405      x += j->pixel_width;
406    }
407  }
408
409  return x;
410}
411
412void AutocompleteResultView::Elide(Runs* runs, int remaining_width) const {
413  // The complexity of this function is due to edge cases like the following:
414  // We have 100 px of available space, an initial classification that takes 86
415  // px, and a font that has a 15 px wide ellipsis character.  Now if the first
416  // classification is followed by several very narrow classifications (e.g. 3
417  // px wide each), we don't know whether we need to elide or not at the time we
418  // see the first classification -- it depends on how many subsequent
419  // classifications follow, and some of those may be in the next run (or
420  // several runs!).  This is why instead we let our caller move forward until
421  // we know we definitely need to elide, and then in this function we move
422  // backward again until we find a string that we can successfully do the
423  // eliding on.
424  bool first_classification = true;
425  for (Runs::reverse_iterator i(runs->rbegin()); i != runs->rend(); ++i) {
426    for (Classifications::reverse_iterator j(i->classifications.rbegin());
427         j != i->classifications.rend(); ++j) {
428      if (!first_classification) {
429        // For all but the first classification we consider, we need to append
430        // an ellipsis, since there isn't enough room to draw it after this
431        // classification.
432        j->text += kEllipsis;
433
434        // We also add this classification's width (sans ellipsis) back to the
435        // available width since we want to consider the available space we'll
436        // have when we draw this classification.
437        remaining_width += j->pixel_width;
438      }
439      first_classification = false;
440
441      // Can we fit at least an ellipsis?
442      string16 elided_text =
443          ui::ElideText(j->text, *j->font, remaining_width, false);
444      Classifications::reverse_iterator prior_classification(j);
445      ++prior_classification;
446      const bool on_first_classification =
447        (prior_classification == i->classifications.rend());
448      if (elided_text.empty() && (remaining_width >= ellipsis_width_) &&
449          on_first_classification) {
450        // Edge case: This classification is bold, we can't fit a bold ellipsis
451        // but we can fit a normal one, and this is the first classification in
452        // the run.  We should display a lone normal ellipsis, because appending
453        // one to the end of the previous run might put it in the wrong visual
454        // location (if the previous run is reversed from the normal visual
455        // order).
456        // NOTE: If this isn't the first classification in the run, we don't
457        // need to bother with this; see note below.
458        elided_text = kEllipsis;
459      }
460      if (!elided_text.empty()) {
461        // Success.  Elide this classification and stop.
462        j->text = elided_text;
463
464        // If we could only fit an ellipsis, then only make it bold if there was
465        // an immediate prior classification in this run that was also bold, or
466        // it will look orphaned.
467        if ((elided_text.length() == 1) &&
468            (on_first_classification ||
469             (prior_classification->font == &normal_font_)))
470          j->font = &normal_font_;
471
472        j->pixel_width = j->font->GetStringWidth(elided_text);
473
474        // Erase any other classifications that come after the elided one.
475        i->classifications.erase(j.base(), i->classifications.end());
476        runs->erase(i.base(), runs->end());
477        return;
478      }
479
480      // We couldn't fit an ellipsis.  Move back one classification,
481      // append an ellipsis, and try again.
482      // NOTE: In the edge case that a bold ellipsis doesn't fit but a
483      // normal one would, and we reach here, then there is a previous
484      // classification in this run, and so either:
485      //   * It's normal, and will be able to draw successfully with the
486      //     ellipsis we'll append to it, or
487      //   * It is also bold, in which case we don't want to fall back
488      //     to a normal ellipsis anyway (see comment above).
489    }
490  }
491
492  // We couldn't draw anything.
493  runs->clear();
494}
495
496gfx::Size AutocompleteResultView::GetPreferredSize() {
497  return gfx::Size(0, std::max(
498      default_icon_size_ + (kMinimumIconVerticalPadding * 2),
499      GetFontHeight() + (kMinimumTextVerticalPadding * 2)));
500}
501
502void AutocompleteResultView::Layout() {
503  const SkBitmap* icon = GetIcon();
504  icon_bounds_.SetRect(LocationBarView::kEdgeItemPadding +
505      ((icon->width() == default_icon_size_) ?
506          0 : LocationBarView::kIconInternalPadding),
507      (height() - icon->height()) / 2, icon->width(), icon->height());
508
509  int text_x = LocationBarView::kEdgeItemPadding + default_icon_size_ +
510      LocationBarView::kItemPadding;
511  int font_height = GetFontHeight();
512  text_bounds_.SetRect(text_x, std::max(0, (height() - font_height) / 2),
513      std::max(bounds().width() - text_x - LocationBarView::kEdgeItemPadding,
514      0), font_height);
515}
516
517void AutocompleteResultView::OnPaint(gfx::Canvas* canvas) {
518  const ResultViewState state = GetState();
519  if (state != NORMAL)
520    canvas->AsCanvasSkia()->drawColor(GetColor(state, BACKGROUND));
521
522  // Paint the icon.
523  canvas->DrawBitmapInt(*GetIcon(), GetMirroredXForRect(icon_bounds_),
524                        icon_bounds_.y());
525
526  // Paint the text.
527  int x = GetMirroredXForRect(text_bounds_);
528  mirroring_context_->Initialize(x, text_bounds_.width());
529  PaintMatch(canvas, match_, x);
530}
531