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"
8 #include <atlbase.h> // NOLINT
9 #include <atlwin.h> // NOLINT
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/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/gfx/text_utils.h"
31 #include "ui/native_theme/native_theme.h"
34 #include "ui/native_theme/native_theme_win.h"
38 #include "ui/native_theme/native_theme_aura.h"
43 const base::char16 kEllipsis
[] = { 0x2026, 0x0 };
45 // The minimum distance between the top and bottom of the {icon|text} and the
46 // top or bottom of the row.
47 const int kMinimumIconVerticalPadding
= 2;
48 const int kMinimumTextVerticalPadding
= 3;
52 ////////////////////////////////////////////////////////////////////////////////
53 // OmniboxResultView, public:
55 // Precalculated data used to draw a complete visual run within the match.
56 // This will include all or part of at least one, and possibly several,
58 struct OmniboxResultView::RunData
{
59 RunData() : run_start(0), visual_order(0), is_rtl(false), pixel_width(0) {}
61 size_t run_start
; // Offset within the match text where this run begins.
62 int visual_order
; // Where this run occurs in visual order. The earliest
63 // run drawn is run 0.
67 // Styled text classification pieces within this run, in logical order.
68 Classifications classifications
;
71 // This class is a utility class for calculations affected by whether the result
72 // view is horizontally mirrored. The drawing functions can be written as if
73 // all drawing occurs left-to-right, and then use this class to get the actual
74 // coordinates to begin drawing onscreen.
75 class OmniboxResultView::MirroringContext
{
77 MirroringContext() : center_(0), right_(0) {}
79 // Tells the mirroring context to use the provided range as the physical
80 // bounds of the drawing region. When coordinate mirroring is needed, the
81 // mirror point will be the center of this range.
82 void Initialize(int x
, int width
) {
83 center_
= x
+ width
/ 2;
87 // Given a logical range within the drawing region, returns the coordinate of
88 // the possibly-mirrored "left" side. (This functions exactly like
89 // View::MirroredLeftPointForRect().)
90 int mirrored_left_coord(int left
, int right
) const {
91 return base::i18n::IsRTL() ? (center_
+ (center_
- right
)) : left
;
94 // Given a logical coordinate within the drawing region, returns the remaining
96 int remaining_width(int x
) const {
104 DISALLOW_COPY_AND_ASSIGN(MirroringContext
);
107 OmniboxResultView::OmniboxResultView(OmniboxResultViewModel
* model
,
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
),
115 model_index_(model_index
),
116 location_bar_view_(location_bar_view
),
117 font_list_(font_list
),
119 std::max(font_list
.GetHeight(),
120 font_list
.DeriveWithStyle(gfx::Font::BOLD
).GetHeight())),
121 ellipsis_width_(gfx::GetStringWidth(base::string16(kEllipsis
),
123 mirroring_context_(new MirroringContext()),
124 keyword_icon_(new views::ImageView()),
125 animation_(new gfx::SlideAnimation(this)) {
126 CHECK_GE(model_index
, 0);
127 if (default_icon_size_
== 0) {
129 location_bar_view_
->GetThemeProvider()->GetImageSkiaNamed(
130 AutocompleteMatch::TypeToIcon(
131 AutocompleteMatchType::URL_WHAT_YOU_TYPED
))->width();
133 keyword_icon_
->set_owned_by_client();
134 keyword_icon_
->EnableCanvasFlippingForRTLUI(true);
135 keyword_icon_
->SetImage(GetKeywordIcon());
136 keyword_icon_
->SizeToPreferredSize();
139 OmniboxResultView::~OmniboxResultView() {
142 SkColor
OmniboxResultView::GetColor(
143 ResultViewState state
,
144 ColorKind kind
) const {
145 const ui::NativeTheme
* theme
= GetNativeTheme();
147 if (theme
== ui::NativeThemeWin::instance()) {
148 static bool win_initialized
= false;
149 static SkColor win_colors
[NUM_STATES
][NUM_KINDS
];
150 if (!win_initialized
) {
151 win_colors
[NORMAL
][BACKGROUND
] = color_utils::GetSysSkColor(COLOR_WINDOW
);
152 win_colors
[SELECTED
][BACKGROUND
] =
153 color_utils::GetSysSkColor(COLOR_HIGHLIGHT
);
154 win_colors
[NORMAL
][TEXT
] = color_utils::GetSysSkColor(COLOR_WINDOWTEXT
);
155 win_colors
[SELECTED
][TEXT
] =
156 color_utils::GetSysSkColor(COLOR_HIGHLIGHTTEXT
);
157 CommonInitColors(theme
, win_colors
);
158 win_initialized
= true;
160 return win_colors
[state
][kind
];
163 static bool initialized
= false;
164 static SkColor colors
[NUM_STATES
][NUM_KINDS
];
166 colors
[NORMAL
][BACKGROUND
] = theme
->GetSystemColor(
167 ui::NativeTheme::kColorId_TextfieldDefaultBackground
);
168 colors
[NORMAL
][TEXT
] = theme
->GetSystemColor(
169 ui::NativeTheme::kColorId_TextfieldDefaultColor
);
170 colors
[NORMAL
][URL
] = SkColorSetARGB(0xff, 0x00, 0x99, 0x33);
171 colors
[SELECTED
][BACKGROUND
] = theme
->GetSystemColor(
172 ui::NativeTheme::kColorId_TextfieldSelectionBackgroundFocused
);
173 colors
[SELECTED
][TEXT
] = theme
->GetSystemColor(
174 ui::NativeTheme::kColorId_TextfieldSelectionColor
);
175 colors
[SELECTED
][URL
] = SkColorSetARGB(0xff, 0x00, 0x66, 0x22);
176 colors
[HOVERED
][URL
] = SkColorSetARGB(0xff, 0x00, 0x66, 0x22);
177 CommonInitColors(theme
, colors
);
180 return colors
[state
][kind
];
183 void OmniboxResultView::SetMatch(const AutocompleteMatch
& match
) {
187 if (match
.associated_keyword
.get()) {
188 keyword_icon_
->SetImage(GetKeywordIcon());
190 if (!keyword_icon_
->parent())
191 AddChildView(keyword_icon_
.get());
192 } else if (keyword_icon_
->parent()) {
193 RemoveChildView(keyword_icon_
.get());
199 void OmniboxResultView::ShowKeyword(bool show_keyword
) {
206 void OmniboxResultView::Invalidate() {
207 keyword_icon_
->SetImage(GetKeywordIcon());
211 gfx::Size
OmniboxResultView::GetPreferredSize() {
212 return gfx::Size(0, std::max(
213 default_icon_size_
+ (kMinimumIconVerticalPadding
* 2),
214 GetTextHeight() + (minimum_text_vertical_padding_
* 2)));
217 ////////////////////////////////////////////////////////////////////////////////
218 // OmniboxResultView, protected:
220 OmniboxResultView::ResultViewState
OmniboxResultView::GetState() const {
221 if (model_
->IsSelectedIndex(model_index_
))
223 return model_
->IsHoveredIndex(model_index_
) ? HOVERED
: NORMAL
;
226 int OmniboxResultView::GetTextHeight() const {
230 void OmniboxResultView::PaintMatch(gfx::Canvas
* canvas
,
231 const AutocompleteMatch
& match
,
233 x
= DrawString(canvas
, match
.contents
, match
.contents_class
, false, x
,
236 // Paint the description.
237 // TODO(pkasting): Because we paint in multiple separate pieces, we can wind
238 // up with no space even for an ellipsis for one or both of these pieces.
239 // Instead, we should paint the entire match as a single long string. This
240 // would also let us use a more properly-localizable string than we get with
241 // just the IDS_AUTOCOMPLETE_MATCH_DESCRIPTION_SEPARATOR.
242 if (!match
.description
.empty()) {
243 base::string16 separator
=
244 l10n_util::GetStringUTF16(IDS_AUTOCOMPLETE_MATCH_DESCRIPTION_SEPARATOR
);
245 ACMatchClassifications classifications
;
246 classifications
.push_back(
247 ACMatchClassification(0, ACMatchClassification::NONE
));
248 x
= DrawString(canvas
, separator
, classifications
, true, x
,
251 DrawString(canvas
, match
.description
, match
.description_class
, true, x
,
257 void OmniboxResultView::CommonInitColors(const ui::NativeTheme
* theme
,
258 SkColor colors
[][NUM_KINDS
]) {
259 colors
[HOVERED
][BACKGROUND
] =
260 color_utils::AlphaBlend(colors
[SELECTED
][BACKGROUND
],
261 colors
[NORMAL
][BACKGROUND
], 64);
262 colors
[HOVERED
][TEXT
] = colors
[NORMAL
][TEXT
];
263 #if defined(USE_AURA)
264 const bool is_aura
= theme
== ui::NativeThemeAura::instance();
266 const bool is_aura
= false;
268 for (int i
= 0; i
< NUM_STATES
; ++i
) {
271 color_utils::AlphaBlend(SK_ColorBLACK
, colors
[i
][BACKGROUND
], 0xdd);
272 colors
[i
][DIMMED_TEXT
] =
273 color_utils::AlphaBlend(SK_ColorBLACK
, colors
[i
][BACKGROUND
], 0xbb);
275 colors
[i
][DIMMED_TEXT
] =
276 color_utils::AlphaBlend(colors
[i
][TEXT
], colors
[i
][BACKGROUND
], 128);
277 colors
[i
][URL
] = color_utils::GetReadableColor(SkColorSetRGB(0, 128, 0),
278 colors
[i
][BACKGROUND
]);
281 // TODO(joi): Programmatically draw the dropdown border using
282 // this color as well. (Right now it's drawn as black with 25%
285 color_utils::AlphaBlend(colors
[i
][TEXT
], colors
[i
][BACKGROUND
], 0x34);
290 bool OmniboxResultView::SortRunsLogically(const RunData
& lhs
,
291 const RunData
& rhs
) {
292 return lhs
.run_start
< rhs
.run_start
;
296 bool OmniboxResultView::SortRunsVisually(const RunData
& lhs
,
297 const RunData
& rhs
) {
298 return lhs
.visual_order
< rhs
.visual_order
;
302 int OmniboxResultView::default_icon_size_
= 0;
304 gfx::ImageSkia
OmniboxResultView::GetIcon() const {
305 const gfx::Image image
= model_
->GetIconIfExtensionMatch(model_index_
);
306 if (!image
.IsEmpty())
307 return image
.AsImageSkia();
309 int icon
= match_
.starred
?
310 IDR_OMNIBOX_STAR
: AutocompleteMatch::TypeToIcon(match_
.type
);
311 if (GetState() == SELECTED
) {
313 case IDR_OMNIBOX_EXTENSION_APP
:
314 icon
= IDR_OMNIBOX_EXTENSION_APP_SELECTED
;
316 case IDR_OMNIBOX_HTTP
:
317 icon
= IDR_OMNIBOX_HTTP_SELECTED
;
319 case IDR_OMNIBOX_SEARCH
:
320 icon
= IDR_OMNIBOX_SEARCH_SELECTED
;
322 case IDR_OMNIBOX_STAR
:
323 icon
= IDR_OMNIBOX_STAR_SELECTED
;
330 return *(location_bar_view_
->GetThemeProvider()->GetImageSkiaNamed(icon
));
333 const gfx::ImageSkia
* OmniboxResultView::GetKeywordIcon() const {
334 // NOTE: If we ever begin returning icons of varying size, then callers need
335 // to ensure that |keyword_icon_| is resized each time its image is reset.
336 return location_bar_view_
->GetThemeProvider()->GetImageSkiaNamed(
337 (GetState() == SELECTED
) ? IDR_OMNIBOX_TTS_SELECTED
: IDR_OMNIBOX_TTS
);
340 int OmniboxResultView::DrawString(
342 const base::string16
& text
,
343 const ACMatchClassifications
& classifications
,
350 // Check whether or not this text is a URL. URLs are always displayed LTR
351 // regardless of locale.
353 for (ACMatchClassifications::const_iterator
i(classifications
.begin());
354 i
!= classifications
.end(); ++i
) {
355 if (!(i
->style
& ACMatchClassification::URL
)) {
361 scoped_ptr
<gfx::RenderText
> render_text(gfx::RenderText::CreateInstance());
362 const size_t text_length
= text
.length();
363 render_text
->SetText(text
);
364 render_text
->SetFontList(font_list_
);
365 render_text
->SetMultiline(false);
366 render_text
->SetCursorEnabled(false);
368 render_text
->SetDirectionalityMode(gfx::DIRECTIONALITY_FORCE_LTR
);
370 // Apply classifications.
371 for (size_t i
= 0; i
< classifications
.size(); ++i
) {
372 const size_t text_start
= classifications
[i
].offset
;
373 if (text_start
>= text_length
)
376 const size_t text_end
= (i
< (classifications
.size() - 1)) ?
377 std::min(classifications
[i
+ 1].offset
, text_length
) : text_length
;
378 const gfx::Range
current_range(text_start
, text_end
);
380 // Calculate style-related data.
381 if (classifications
[i
].style
& ACMatchClassification::MATCH
)
382 render_text
->ApplyStyle(gfx::BOLD
, true, current_range
);
384 ColorKind color_kind
= TEXT
;
385 if (classifications
[i
].style
& ACMatchClassification::URL
) {
387 } else if (force_dim
||
388 (classifications
[i
].style
& ACMatchClassification::DIM
)) {
389 color_kind
= DIMMED_TEXT
;
391 render_text
->ApplyColor(GetColor(GetState(), color_kind
), current_range
);
394 int remaining_width
= mirroring_context_
->remaining_width(x
);
396 // No need to try anything if we can't even show a solitary character.
397 if ((text_length
== 1) &&
398 (remaining_width
< render_text
->GetContentWidth())) {
402 if (render_text
->GetContentWidth() > remaining_width
)
403 render_text
->SetElideBehavior(gfx::ELIDE_AT_END
);
405 // Set the display rect to trigger eliding.
406 render_text
->SetDisplayRect(gfx::Rect(
407 mirroring_context_
->mirrored_left_coord(x
, x
+ remaining_width
), y
,
408 remaining_width
, height()));
409 render_text
->set_clip_to_display_rect(true);
410 render_text
->Draw(canvas
);
412 // Need to call GetContentWidth again as the SetDisplayRect may modify it.
413 return x
+ render_text
->GetContentWidth();
416 void OmniboxResultView::Layout() {
417 const gfx::ImageSkia icon
= GetIcon();
419 icon_bounds_
.SetRect(edge_item_padding_
+
420 ((icon
.width() == default_icon_size_
) ?
421 0 : LocationBarView::kIconInternalPadding
),
422 (height() - icon
.height()) / 2, icon
.width(), icon
.height());
424 int text_x
= edge_item_padding_
+ default_icon_size_
+ item_padding_
;
425 int text_width
= width() - text_x
- edge_item_padding_
;
427 if (match_
.associated_keyword
.get()) {
428 const int kw_collapsed_size
=
429 keyword_icon_
->width() + edge_item_padding_
;
430 const int max_kw_x
= width() - kw_collapsed_size
;
432 animation_
->CurrentValueBetween(max_kw_x
, edge_item_padding_
);
433 const int kw_text_x
= kw_x
+ keyword_icon_
->width() + item_padding_
;
435 text_width
= kw_x
- text_x
- item_padding_
;
436 keyword_text_bounds_
.SetRect(
438 std::max(width() - kw_text_x
- edge_item_padding_
, 0), height());
439 keyword_icon_
->SetPosition(
440 gfx::Point(kw_x
, (height() - keyword_icon_
->height()) / 2));
443 text_bounds_
.SetRect(text_x
, 0, std::max(text_width
, 0), height());
446 void OmniboxResultView::OnBoundsChanged(const gfx::Rect
& previous_bounds
) {
447 animation_
->SetSlideDuration(width() / 4);
450 void OmniboxResultView::OnPaint(gfx::Canvas
* canvas
) {
451 const ResultViewState state
= GetState();
453 canvas
->DrawColor(GetColor(state
, BACKGROUND
));
455 if (!match_
.associated_keyword
.get() ||
456 keyword_icon_
->x() > icon_bounds_
.right()) {
458 canvas
->DrawImageInt(GetIcon(), GetMirroredXForRect(icon_bounds_
),
462 int x
= GetMirroredXForRect(text_bounds_
);
463 mirroring_context_
->Initialize(x
, text_bounds_
.width());
464 PaintMatch(canvas
, match_
, x
);
467 if (match_
.associated_keyword
.get()) {
468 // Paint the keyword text.
469 int x
= GetMirroredXForRect(keyword_text_bounds_
);
470 mirroring_context_
->Initialize(x
, keyword_text_bounds_
.width());
471 PaintMatch(canvas
, *match_
.associated_keyword
.get(), x
);
475 void OmniboxResultView::AnimationProgressed(const gfx::Animation
* animation
) {