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 "base/strings/string_number_conversions.h" 19 #include "base/strings/string_util.h" 20 #include "chrome/browser/ui/omnibox/omnibox_popup_model.h" 21 #include "chrome/browser/ui/views/location_bar/location_bar_view.h" 22 #include "chrome/browser/ui/views/omnibox/omnibox_popup_contents_view.h" 23 #include "chrome/grit/generated_resources.h" 24 #include "grit/components_scaled_resources.h" 25 #include "grit/theme_resources.h" 26 #include "ui/base/l10n/l10n_util.h" 27 #include "ui/base/theme_provider.h" 28 #include "ui/gfx/canvas.h" 29 #include "ui/gfx/color_utils.h" 30 #include "ui/gfx/image/image.h" 31 #include "ui/gfx/range/range.h" 32 #include "ui/gfx/render_text.h" 33 #include "ui/gfx/text_utils.h" 34 #include "ui/native_theme/native_theme.h" 35 36 using ui::NativeTheme; 37 38 namespace { 39 40 // The minimum distance between the top and bottom of the {icon|text} and the 41 // top or bottom of the row. 42 const int kMinimumIconVerticalPadding = 2; 43 const int kMinimumTextVerticalPadding = 3; 44 45 // A mapping from OmniboxResultView's ResultViewState/ColorKind types to 46 // NativeTheme colors. 47 struct TranslationTable { 48 ui::NativeTheme::ColorId id; 49 OmniboxResultView::ResultViewState state; 50 OmniboxResultView::ColorKind kind; 51 } static const kTranslationTable[] = { 52 { NativeTheme::kColorId_ResultsTableNormalBackground, 53 OmniboxResultView::NORMAL, OmniboxResultView::BACKGROUND }, 54 { NativeTheme::kColorId_ResultsTableHoveredBackground, 55 OmniboxResultView::HOVERED, OmniboxResultView::BACKGROUND }, 56 { NativeTheme::kColorId_ResultsTableSelectedBackground, 57 OmniboxResultView::SELECTED, OmniboxResultView::BACKGROUND }, 58 { NativeTheme::kColorId_ResultsTableNormalText, 59 OmniboxResultView::NORMAL, OmniboxResultView::TEXT }, 60 { NativeTheme::kColorId_ResultsTableHoveredText, 61 OmniboxResultView::HOVERED, OmniboxResultView::TEXT }, 62 { NativeTheme::kColorId_ResultsTableSelectedText, 63 OmniboxResultView::SELECTED, OmniboxResultView::TEXT }, 64 { NativeTheme::kColorId_ResultsTableNormalDimmedText, 65 OmniboxResultView::NORMAL, OmniboxResultView::DIMMED_TEXT }, 66 { NativeTheme::kColorId_ResultsTableHoveredDimmedText, 67 OmniboxResultView::HOVERED, OmniboxResultView::DIMMED_TEXT }, 68 { NativeTheme::kColorId_ResultsTableSelectedDimmedText, 69 OmniboxResultView::SELECTED, OmniboxResultView::DIMMED_TEXT }, 70 { NativeTheme::kColorId_ResultsTableNormalUrl, 71 OmniboxResultView::NORMAL, OmniboxResultView::URL }, 72 { NativeTheme::kColorId_ResultsTableHoveredUrl, 73 OmniboxResultView::HOVERED, OmniboxResultView::URL }, 74 { NativeTheme::kColorId_ResultsTableSelectedUrl, 75 OmniboxResultView::SELECTED, OmniboxResultView::URL }, 76 { NativeTheme::kColorId_ResultsTableNormalDivider, 77 OmniboxResultView::NORMAL, OmniboxResultView::DIVIDER }, 78 { NativeTheme::kColorId_ResultsTableHoveredDivider, 79 OmniboxResultView::HOVERED, OmniboxResultView::DIVIDER }, 80 { NativeTheme::kColorId_ResultsTableSelectedDivider, 81 OmniboxResultView::SELECTED, OmniboxResultView::DIVIDER }, 82 }; 83 84 } // namespace 85 86 //////////////////////////////////////////////////////////////////////////////// 87 // OmniboxResultView, public: 88 89 // This class is a utility class for calculations affected by whether the result 90 // view is horizontally mirrored. The drawing functions can be written as if 91 // all drawing occurs left-to-right, and then use this class to get the actual 92 // coordinates to begin drawing onscreen. 93 class OmniboxResultView::MirroringContext { 94 public: 95 MirroringContext() : center_(0), right_(0) {} 96 97 // Tells the mirroring context to use the provided range as the physical 98 // bounds of the drawing region. When coordinate mirroring is needed, the 99 // mirror point will be the center of this range. 100 void Initialize(int x, int width) { 101 center_ = x + width / 2; 102 right_ = x + width; 103 } 104 105 // Given a logical range within the drawing region, returns the coordinate of 106 // the possibly-mirrored "left" side. (This functions exactly like 107 // View::MirroredLeftPointForRect().) 108 int mirrored_left_coord(int left, int right) const { 109 return base::i18n::IsRTL() ? (center_ + (center_ - right)) : left; 110 } 111 112 // Given a logical coordinate within the drawing region, returns the remaining 113 // width available. 114 int remaining_width(int x) const { 115 return right_ - x; 116 } 117 118 private: 119 int center_; 120 int right_; 121 122 DISALLOW_COPY_AND_ASSIGN(MirroringContext); 123 }; 124 125 OmniboxResultView::OmniboxResultView(OmniboxPopupContentsView* model, 126 int model_index, 127 LocationBarView* location_bar_view, 128 const gfx::FontList& font_list) 129 : edge_item_padding_(LocationBarView::kItemPadding), 130 item_padding_(LocationBarView::kItemPadding), 131 minimum_text_vertical_padding_(kMinimumTextVerticalPadding), 132 model_(model), 133 model_index_(model_index), 134 location_bar_view_(location_bar_view), 135 font_list_(font_list), 136 font_height_( 137 std::max(font_list.GetHeight(), 138 font_list.DeriveWithStyle(gfx::Font::BOLD).GetHeight())), 139 mirroring_context_(new MirroringContext()), 140 keyword_icon_(new views::ImageView()), 141 animation_(new gfx::SlideAnimation(this)) { 142 CHECK_GE(model_index, 0); 143 if (default_icon_size_ == 0) { 144 default_icon_size_ = 145 location_bar_view_->GetThemeProvider()->GetImageSkiaNamed( 146 AutocompleteMatch::TypeToIcon( 147 AutocompleteMatchType::URL_WHAT_YOU_TYPED))->width(); 148 } 149 keyword_icon_->set_owned_by_client(); 150 keyword_icon_->EnableCanvasFlippingForRTLUI(true); 151 keyword_icon_->SetImage(GetKeywordIcon()); 152 keyword_icon_->SizeToPreferredSize(); 153 } 154 155 OmniboxResultView::~OmniboxResultView() { 156 } 157 158 SkColor OmniboxResultView::GetColor( 159 ResultViewState state, 160 ColorKind kind) const { 161 for (size_t i = 0; i < arraysize(kTranslationTable); ++i) { 162 if (kTranslationTable[i].state == state && 163 kTranslationTable[i].kind == kind) { 164 return GetNativeTheme()->GetSystemColor(kTranslationTable[i].id); 165 } 166 } 167 168 NOTREACHED(); 169 return SK_ColorRED; 170 } 171 172 void OmniboxResultView::SetMatch(const AutocompleteMatch& match) { 173 match_ = match; 174 ResetRenderTexts(); 175 animation_->Reset(); 176 177 AutocompleteMatch* associated_keyword_match = match_.associated_keyword.get(); 178 if (associated_keyword_match) { 179 keyword_icon_->SetImage(GetKeywordIcon()); 180 if (!keyword_icon_->parent()) 181 AddChildView(keyword_icon_.get()); 182 } else if (keyword_icon_->parent()) { 183 RemoveChildView(keyword_icon_.get()); 184 } 185 186 Layout(); 187 } 188 189 void OmniboxResultView::ShowKeyword(bool show_keyword) { 190 if (show_keyword) 191 animation_->Show(); 192 else 193 animation_->Hide(); 194 } 195 196 void OmniboxResultView::Invalidate() { 197 keyword_icon_->SetImage(GetKeywordIcon()); 198 // While the text in the RenderTexts may not have changed, the styling 199 // (color/bold) may need to change. So we reset them to cause them to be 200 // recomputed in OnPaint(). 201 ResetRenderTexts(); 202 SchedulePaint(); 203 } 204 205 gfx::Size OmniboxResultView::GetPreferredSize() const { 206 return gfx::Size(0, std::max( 207 default_icon_size_ + (kMinimumIconVerticalPadding * 2), 208 GetTextHeight() + (minimum_text_vertical_padding_ * 2))); 209 } 210 211 //////////////////////////////////////////////////////////////////////////////// 212 // OmniboxResultView, protected: 213 214 OmniboxResultView::ResultViewState OmniboxResultView::GetState() const { 215 if (model_->IsSelectedIndex(model_index_)) 216 return SELECTED; 217 return model_->IsHoveredIndex(model_index_) ? HOVERED : NORMAL; 218 } 219 220 int OmniboxResultView::GetTextHeight() const { 221 return font_height_; 222 } 223 224 void OmniboxResultView::PaintMatch( 225 const AutocompleteMatch& match, 226 gfx::RenderText* contents, 227 gfx::RenderText* description, 228 gfx::Canvas* canvas, 229 int x) const { 230 int y = text_bounds_.y(); 231 232 if (!separator_rendertext_) { 233 const base::string16& separator = 234 l10n_util::GetStringUTF16(IDS_AUTOCOMPLETE_MATCH_DESCRIPTION_SEPARATOR); 235 separator_rendertext_.reset(CreateRenderText(separator).release()); 236 separator_rendertext_->SetColor(GetColor(GetState(), DIMMED_TEXT)); 237 separator_width_ = separator_rendertext_->GetContentWidth(); 238 } 239 240 int contents_max_width, description_max_width; 241 OmniboxPopupModel::ComputeMatchMaxWidths( 242 contents->GetContentWidth(), 243 separator_width_, 244 description ? description->GetContentWidth() : 0, 245 mirroring_context_->remaining_width(x), 246 !AutocompleteMatch::IsSearchType(match.type), 247 &contents_max_width, 248 &description_max_width); 249 250 x = DrawRenderText(match, contents, true, canvas, x, y, contents_max_width); 251 252 if (description_max_width != 0) { 253 x = DrawRenderText(match, separator_rendertext_.get(), false, canvas, x, y, 254 separator_width_); 255 DrawRenderText(match, description, false, canvas, x, y, 256 description_max_width); 257 } 258 } 259 260 int OmniboxResultView::DrawRenderText( 261 const AutocompleteMatch& match, 262 gfx::RenderText* render_text, 263 bool contents, 264 gfx::Canvas* canvas, 265 int x, 266 int y, 267 int max_width) const { 268 DCHECK(!render_text->text().empty()); 269 270 const int remaining_width = mirroring_context_->remaining_width(x); 271 int right_x = x + max_width; 272 273 // Infinite suggestions should appear with the leading ellipses vertically 274 // stacked. 275 if (contents && 276 (match.type == AutocompleteMatchType::SEARCH_SUGGEST_INFINITE)) { 277 // When the directionality of suggestion doesn't match the UI, we try to 278 // vertically stack the ellipsis by restricting the end edge (right_x). 279 const bool is_ui_rtl = base::i18n::IsRTL(); 280 const bool is_match_contents_rtl = 281 (render_text->GetTextDirection() == base::i18n::RIGHT_TO_LEFT); 282 const int offset = 283 GetDisplayOffset(match, is_ui_rtl, is_match_contents_rtl); 284 285 scoped_ptr<gfx::RenderText> prefix_render_text( 286 CreateRenderText(base::UTF8ToUTF16( 287 match.GetAdditionalInfo(kACMatchPropertyContentsPrefix)))); 288 const int prefix_width = prefix_render_text->GetContentWidth(); 289 int prefix_x = x; 290 291 const int max_match_contents_width = model_->max_match_contents_width(); 292 293 if (is_ui_rtl != is_match_contents_rtl) { 294 // RTL infinite suggestions appear near the left edge in LTR UI, while LTR 295 // infinite suggestions appear near the right edge in RTL UI. This is 296 // against the natural horizontal alignment of the text. We reduce the 297 // width of the box for suggestion display, so that the suggestions appear 298 // in correct confines. This reduced width allows us to modify the text 299 // alignment (see below). 300 right_x = x + std::min(remaining_width - prefix_width, 301 std::max(offset, max_match_contents_width)); 302 prefix_x = right_x; 303 // We explicitly set the horizontal alignment so that when LTR suggestions 304 // show in RTL UI (or vice versa), their ellipses appear stacked in a 305 // single column. 306 render_text->SetHorizontalAlignment( 307 is_match_contents_rtl ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT); 308 } else { 309 // If the dropdown is wide enough, place the ellipsis at the position 310 // where the omitted text would have ended. Otherwise reduce the offset of 311 // the ellipsis such that the widest suggestion reaches the end of the 312 // dropdown. 313 const int start_offset = std::max(prefix_width, 314 std::min(remaining_width - max_match_contents_width, offset)); 315 right_x = x + std::min(remaining_width, start_offset + max_width); 316 x += start_offset; 317 prefix_x = x - prefix_width; 318 } 319 prefix_render_text->SetDirectionalityMode(is_match_contents_rtl ? 320 gfx::DIRECTIONALITY_FORCE_RTL : gfx::DIRECTIONALITY_FORCE_LTR); 321 prefix_render_text->SetHorizontalAlignment( 322 is_match_contents_rtl ? gfx::ALIGN_RIGHT : gfx::ALIGN_LEFT); 323 prefix_render_text->SetDisplayRect(gfx::Rect( 324 mirroring_context_->mirrored_left_coord( 325 prefix_x, prefix_x + prefix_width), y, 326 prefix_width, height())); 327 prefix_render_text->Draw(canvas); 328 } 329 330 // Set the display rect to trigger eliding. 331 render_text->SetDisplayRect(gfx::Rect( 332 mirroring_context_->mirrored_left_coord(x, right_x), y, 333 right_x - x, height())); 334 render_text->Draw(canvas); 335 return right_x; 336 } 337 338 scoped_ptr<gfx::RenderText> OmniboxResultView::CreateRenderText( 339 const base::string16& text) const { 340 scoped_ptr<gfx::RenderText> render_text(gfx::RenderText::CreateInstance()); 341 render_text->SetDisplayRect(gfx::Rect(gfx::Size(INT_MAX, 0))); 342 render_text->SetCursorEnabled(false); 343 render_text->SetElideBehavior(gfx::ELIDE_TAIL); 344 render_text->SetFontList(font_list_); 345 render_text->SetText(text); 346 return render_text.Pass(); 347 } 348 349 scoped_ptr<gfx::RenderText> OmniboxResultView::CreateClassifiedRenderText( 350 const base::string16& text, 351 const ACMatchClassifications& classifications, 352 bool force_dim) const { 353 scoped_ptr<gfx::RenderText> render_text(CreateRenderText(text)); 354 const size_t text_length = render_text->text().length(); 355 for (size_t i = 0; i < classifications.size(); ++i) { 356 const size_t text_start = classifications[i].offset; 357 if (text_start >= text_length) 358 break; 359 360 const size_t text_end = (i < (classifications.size() - 1)) ? 361 std::min(classifications[i + 1].offset, text_length) : 362 text_length; 363 const gfx::Range current_range(text_start, text_end); 364 365 // Calculate style-related data. 366 if (classifications[i].style & ACMatchClassification::MATCH) 367 render_text->ApplyStyle(gfx::BOLD, true, current_range); 368 369 ColorKind color_kind = TEXT; 370 if (classifications[i].style & ACMatchClassification::URL) { 371 color_kind = URL; 372 // Consider logical string for domain "ABC.com/hello" where ABC are 373 // Hebrew (RTL) characters. This string should ideally show as 374 // "CBA.com/hello". If we do not force LTR on URL, it will appear as 375 // "com/hello.CBA". 376 // With IDN and RTL TLDs, it might be okay to allow RTL rendering of URLs, 377 // but it still has some pitfalls like : 378 // ABC.COM/abc-pqr/xyz/FGH will appear as HGF/abc-pqr/xyz/MOC.CBA which 379 // really confuses the path hierarchy of the URL. 380 // Also, if the URL supports https, the appearance will change into LTR 381 // directionality. 382 // In conclusion, LTR rendering of URL is probably the safest bet. 383 render_text->SetDirectionalityMode(gfx::DIRECTIONALITY_FORCE_LTR); 384 } else if (force_dim || 385 (classifications[i].style & ACMatchClassification::DIM)) { 386 color_kind = DIMMED_TEXT; 387 } 388 render_text->ApplyColor(GetColor(GetState(), color_kind), current_range); 389 } 390 return render_text.Pass(); 391 } 392 393 int OmniboxResultView::GetMatchContentsWidth() const { 394 InitContentsRenderTextIfNecessary(); 395 return contents_rendertext_ ? contents_rendertext_->GetContentWidth() : 0; 396 } 397 398 // TODO(skanuj): This is probably identical across all OmniboxResultView rows in 399 // the omnibox dropdown. Consider sharing the result. 400 int OmniboxResultView::GetDisplayOffset( 401 const AutocompleteMatch& match, 402 bool is_ui_rtl, 403 bool is_match_contents_rtl) const { 404 if (match.type != AutocompleteMatchType::SEARCH_SUGGEST_INFINITE) 405 return 0; 406 407 const base::string16& input_text = 408 base::UTF8ToUTF16(match.GetAdditionalInfo(kACMatchPropertyInputText)); 409 int contents_start_index = 0; 410 base::StringToInt(match.GetAdditionalInfo(kACMatchPropertyContentsStartIndex), 411 &contents_start_index); 412 413 scoped_ptr<gfx::RenderText> input_render_text(CreateRenderText(input_text)); 414 const gfx::Range& glyph_bounds = 415 input_render_text->GetGlyphBounds(contents_start_index); 416 const int start_padding = is_match_contents_rtl ? 417 std::max(glyph_bounds.start(), glyph_bounds.end()) : 418 std::min(glyph_bounds.start(), glyph_bounds.end()); 419 420 return is_ui_rtl ? 421 (input_render_text->GetContentWidth() - start_padding) : start_padding; 422 } 423 424 // static 425 int OmniboxResultView::default_icon_size_ = 0; 426 427 gfx::ImageSkia OmniboxResultView::GetIcon() const { 428 const gfx::Image image = model_->GetIconIfExtensionMatch(model_index_); 429 if (!image.IsEmpty()) 430 return image.AsImageSkia(); 431 432 int icon = model_->IsStarredMatch(match_) ? 433 IDR_OMNIBOX_STAR : AutocompleteMatch::TypeToIcon(match_.type); 434 if (GetState() == SELECTED) { 435 switch (icon) { 436 case IDR_OMNIBOX_EXTENSION_APP: 437 icon = IDR_OMNIBOX_EXTENSION_APP_SELECTED; 438 break; 439 case IDR_OMNIBOX_HTTP: 440 icon = IDR_OMNIBOX_HTTP_SELECTED; 441 break; 442 case IDR_OMNIBOX_SEARCH: 443 icon = IDR_OMNIBOX_SEARCH_SELECTED; 444 break; 445 case IDR_OMNIBOX_STAR: 446 icon = IDR_OMNIBOX_STAR_SELECTED; 447 break; 448 default: 449 NOTREACHED(); 450 break; 451 } 452 } 453 return *(location_bar_view_->GetThemeProvider()->GetImageSkiaNamed(icon)); 454 } 455 456 const gfx::ImageSkia* OmniboxResultView::GetKeywordIcon() const { 457 // NOTE: If we ever begin returning icons of varying size, then callers need 458 // to ensure that |keyword_icon_| is resized each time its image is reset. 459 return location_bar_view_->GetThemeProvider()->GetImageSkiaNamed( 460 (GetState() == SELECTED) ? IDR_OMNIBOX_TTS_SELECTED : IDR_OMNIBOX_TTS); 461 } 462 463 bool OmniboxResultView::ShowOnlyKeywordMatch() const { 464 return match_.associated_keyword && 465 (keyword_icon_->x() <= icon_bounds_.right()); 466 } 467 468 void OmniboxResultView::ResetRenderTexts() const { 469 contents_rendertext_.reset(); 470 description_rendertext_.reset(); 471 separator_rendertext_.reset(); 472 keyword_contents_rendertext_.reset(); 473 keyword_description_rendertext_.reset(); 474 } 475 476 void OmniboxResultView::InitContentsRenderTextIfNecessary() const { 477 if (!contents_rendertext_) { 478 contents_rendertext_.reset( 479 CreateClassifiedRenderText( 480 match_.contents, match_.contents_class, false).release()); 481 } 482 } 483 484 void OmniboxResultView::Layout() { 485 const gfx::ImageSkia icon = GetIcon(); 486 487 icon_bounds_.SetRect(edge_item_padding_ + 488 ((icon.width() == default_icon_size_) ? 489 0 : LocationBarView::kIconInternalPadding), 490 (height() - icon.height()) / 2, icon.width(), icon.height()); 491 492 int text_x = edge_item_padding_ + default_icon_size_ + item_padding_; 493 int text_width = width() - text_x - edge_item_padding_; 494 495 if (match_.associated_keyword.get()) { 496 const int kw_collapsed_size = 497 keyword_icon_->width() + edge_item_padding_; 498 const int max_kw_x = width() - kw_collapsed_size; 499 const int kw_x = 500 animation_->CurrentValueBetween(max_kw_x, edge_item_padding_); 501 const int kw_text_x = kw_x + keyword_icon_->width() + item_padding_; 502 503 text_width = kw_x - text_x - item_padding_; 504 keyword_text_bounds_.SetRect( 505 kw_text_x, 0, 506 std::max(width() - kw_text_x - edge_item_padding_, 0), height()); 507 keyword_icon_->SetPosition( 508 gfx::Point(kw_x, (height() - keyword_icon_->height()) / 2)); 509 } 510 511 text_bounds_.SetRect(text_x, 0, std::max(text_width, 0), height()); 512 } 513 514 void OmniboxResultView::OnBoundsChanged(const gfx::Rect& previous_bounds) { 515 animation_->SetSlideDuration(width() / 4); 516 } 517 518 void OmniboxResultView::OnPaint(gfx::Canvas* canvas) { 519 const ResultViewState state = GetState(); 520 if (state != NORMAL) 521 canvas->DrawColor(GetColor(state, BACKGROUND)); 522 523 // NOTE: While animating the keyword match, both matches may be visible. 524 525 if (!ShowOnlyKeywordMatch()) { 526 canvas->DrawImageInt(GetIcon(), GetMirroredXForRect(icon_bounds_), 527 icon_bounds_.y()); 528 int x = GetMirroredXForRect(text_bounds_); 529 mirroring_context_->Initialize(x, text_bounds_.width()); 530 InitContentsRenderTextIfNecessary(); 531 if (!description_rendertext_ && !match_.description.empty()) { 532 description_rendertext_.reset( 533 CreateClassifiedRenderText( 534 match_.description, match_.description_class, true).release()); 535 } 536 PaintMatch(match_, contents_rendertext_.get(), 537 description_rendertext_.get(), canvas, x); 538 } 539 540 AutocompleteMatch* keyword_match = match_.associated_keyword.get(); 541 if (keyword_match) { 542 int x = GetMirroredXForRect(keyword_text_bounds_); 543 mirroring_context_->Initialize(x, keyword_text_bounds_.width()); 544 if (!keyword_contents_rendertext_) { 545 keyword_contents_rendertext_.reset( 546 CreateClassifiedRenderText(keyword_match->contents, 547 keyword_match->contents_class, 548 false).release()); 549 } 550 if (!keyword_description_rendertext_ && 551 !keyword_match->description.empty()) { 552 keyword_description_rendertext_.reset( 553 CreateClassifiedRenderText(keyword_match->description, 554 keyword_match->description_class, 555 true).release()); 556 } 557 PaintMatch(*keyword_match, keyword_contents_rendertext_.get(), 558 keyword_description_rendertext_.get(), canvas, x); 559 } 560 } 561 562 void OmniboxResultView::AnimationProgressed(const gfx::Animation* animation) { 563 Layout(); 564 SchedulePaint(); 565 } 566