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 #include "chrome/browser/ui/gtk/tabs/tab_renderer_gtk.h" 6 7 #include <algorithm> 8 #include <utility> 9 10 #include "base/utf_string_conversions.h" 11 #include "chrome/browser/defaults.h" 12 #include "chrome/browser/extensions/extension_tab_helper.h" 13 #include "chrome/browser/profiles/profile.h" 14 #include "chrome/browser/ui/browser.h" 15 #include "chrome/browser/ui/gtk/bookmarks/bookmark_utils_gtk.h" 16 #include "chrome/browser/ui/gtk/custom_button.h" 17 #include "chrome/browser/ui/gtk/gtk_theme_service.h" 18 #include "chrome/browser/ui/gtk/gtk_util.h" 19 #include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h" 20 #include "content/browser/tab_contents/tab_contents.h" 21 #include "content/common/notification_service.h" 22 #include "grit/app_resources.h" 23 #include "grit/generated_resources.h" 24 #include "grit/theme_resources.h" 25 #include "ui/base/animation/slide_animation.h" 26 #include "ui/base/animation/throb_animation.h" 27 #include "ui/base/l10n/l10n_util.h" 28 #include "ui/base/resource/resource_bundle.h" 29 #include "ui/gfx/canvas_skia_paint.h" 30 #include "ui/gfx/favicon_size.h" 31 #include "ui/gfx/platform_font_gtk.h" 32 #include "ui/gfx/skbitmap_operations.h" 33 34 namespace { 35 36 const int kFontPixelSize = 12; 37 const int kLeftPadding = 16; 38 const int kTopPadding = 6; 39 const int kRightPadding = 15; 40 const int kBottomPadding = 5; 41 const int kDropShadowHeight = 2; 42 const int kFaviconTitleSpacing = 4; 43 const int kTitleCloseButtonSpacing = 5; 44 const int kStandardTitleWidth = 175; 45 const int kDropShadowOffset = 2; 46 const int kInactiveTabBackgroundOffsetY = 15; 47 48 // When a non-mini-tab becomes a mini-tab the width of the tab animates. If 49 // the width of a mini-tab is >= kMiniTabRendererAsNormalTabWidth then the tab 50 // is rendered as a normal tab. This is done to avoid having the title 51 // immediately disappear when transitioning a tab from normal to mini-tab. 52 const int kMiniTabRendererAsNormalTabWidth = 53 browser_defaults::kMiniTabWidth + 30; 54 55 // The tab images are designed to overlap the toolbar by 1 pixel. For now we 56 // don't actually overlap the toolbar, so this is used to know how many pixels 57 // at the bottom of the tab images are to be ignored. 58 const int kToolbarOverlap = 1; 59 60 // How long the hover state takes. 61 const int kHoverDurationMs = 90; 62 63 // How opaque to make the hover state (out of 1). 64 const double kHoverOpacity = 0.33; 65 66 // Max opacity for the mini-tab title change animation. 67 const double kMiniTitleChangeThrobOpacity = 0.75; 68 69 // Duration for when the title of an inactive mini-tab changes. 70 const int kMiniTitleChangeThrobDuration = 1000; 71 72 const SkScalar kTabCapWidth = 15; 73 const SkScalar kTabTopCurveWidth = 4; 74 const SkScalar kTabBottomCurveWidth = 3; 75 76 // The vertical and horizontal offset used to position the close button 77 // in the tab. TODO(jhawkins): Ask pkasting what the Fuzz is about. 78 const int kCloseButtonVertFuzz = 0; 79 const int kCloseButtonHorzFuzz = 5; 80 81 SkBitmap* crashed_favicon = NULL; 82 83 // Gets the bounds of |widget| relative to |parent|. 84 gfx::Rect GetWidgetBoundsRelativeToParent(GtkWidget* parent, 85 GtkWidget* widget) { 86 gfx::Point parent_pos = gtk_util::GetWidgetScreenPosition(parent); 87 gfx::Point widget_pos = gtk_util::GetWidgetScreenPosition(widget); 88 return gfx::Rect(widget_pos.x() - parent_pos.x(), 89 widget_pos.y() - parent_pos.y(), 90 widget->allocation.width, widget->allocation.height); 91 } 92 93 } // namespace 94 95 TabRendererGtk::LoadingAnimation::Data::Data( 96 ui::ThemeProvider* theme_provider) { 97 // The loading animation image is a strip of states. Each state must be 98 // square, so the height must divide the width evenly. 99 loading_animation_frames = theme_provider->GetBitmapNamed(IDR_THROBBER); 100 DCHECK(loading_animation_frames); 101 DCHECK_EQ(loading_animation_frames->width() % 102 loading_animation_frames->height(), 0); 103 loading_animation_frame_count = 104 loading_animation_frames->width() / 105 loading_animation_frames->height(); 106 107 waiting_animation_frames = 108 theme_provider->GetBitmapNamed(IDR_THROBBER_WAITING); 109 DCHECK(waiting_animation_frames); 110 DCHECK_EQ(waiting_animation_frames->width() % 111 waiting_animation_frames->height(), 0); 112 waiting_animation_frame_count = 113 waiting_animation_frames->width() / 114 waiting_animation_frames->height(); 115 116 waiting_to_loading_frame_count_ratio = 117 waiting_animation_frame_count / 118 loading_animation_frame_count; 119 // TODO(beng): eventually remove this when we have a proper themeing system. 120 // themes not supporting IDR_THROBBER_WAITING are causing this 121 // value to be 0 which causes DIV0 crashes. The value of 5 122 // matches the current bitmaps in our source. 123 if (waiting_to_loading_frame_count_ratio == 0) 124 waiting_to_loading_frame_count_ratio = 5; 125 } 126 127 TabRendererGtk::LoadingAnimation::Data::Data( 128 int loading, int waiting, int waiting_to_loading) 129 : waiting_animation_frames(NULL), 130 loading_animation_frames(NULL), 131 loading_animation_frame_count(loading), 132 waiting_animation_frame_count(waiting), 133 waiting_to_loading_frame_count_ratio(waiting_to_loading) { 134 } 135 136 bool TabRendererGtk::initialized_ = false; 137 TabRendererGtk::TabImage TabRendererGtk::tab_active_ = {0}; 138 TabRendererGtk::TabImage TabRendererGtk::tab_inactive_ = {0}; 139 TabRendererGtk::TabImage TabRendererGtk::tab_alpha_ = {0}; 140 gfx::Font* TabRendererGtk::title_font_ = NULL; 141 int TabRendererGtk::title_font_height_ = 0; 142 int TabRendererGtk::close_button_width_ = 0; 143 int TabRendererGtk::close_button_height_ = 0; 144 SkColor TabRendererGtk::selected_title_color_ = SK_ColorBLACK; 145 SkColor TabRendererGtk::unselected_title_color_ = SkColorSetRGB(64, 64, 64); 146 147 //////////////////////////////////////////////////////////////////////////////// 148 // TabRendererGtk::LoadingAnimation, public: 149 // 150 TabRendererGtk::LoadingAnimation::LoadingAnimation( 151 ui::ThemeProvider* theme_provider) 152 : data_(new Data(theme_provider)), 153 theme_service_(theme_provider), 154 animation_state_(ANIMATION_NONE), 155 animation_frame_(0) { 156 registrar_.Add(this, 157 NotificationType::BROWSER_THEME_CHANGED, 158 NotificationService::AllSources()); 159 } 160 161 TabRendererGtk::LoadingAnimation::LoadingAnimation( 162 const LoadingAnimation::Data& data) 163 : data_(new Data(data)), 164 theme_service_(NULL), 165 animation_state_(ANIMATION_NONE), 166 animation_frame_(0) { 167 } 168 169 TabRendererGtk::LoadingAnimation::~LoadingAnimation() {} 170 171 bool TabRendererGtk::LoadingAnimation::ValidateLoadingAnimation( 172 AnimationState animation_state) { 173 bool has_changed = false; 174 if (animation_state_ != animation_state) { 175 // The waiting animation is the reverse of the loading animation, but at a 176 // different rate - the following reverses and scales the animation_frame_ 177 // so that the frame is at an equivalent position when going from one 178 // animation to the other. 179 if (animation_state_ == ANIMATION_WAITING && 180 animation_state == ANIMATION_LOADING) { 181 animation_frame_ = data_->loading_animation_frame_count - 182 (animation_frame_ / data_->waiting_to_loading_frame_count_ratio); 183 } 184 animation_state_ = animation_state; 185 has_changed = true; 186 } 187 188 if (animation_state_ != ANIMATION_NONE) { 189 animation_frame_ = (animation_frame_ + 1) % 190 ((animation_state_ == ANIMATION_WAITING) ? 191 data_->waiting_animation_frame_count : 192 data_->loading_animation_frame_count); 193 has_changed = true; 194 } else { 195 animation_frame_ = 0; 196 } 197 return has_changed; 198 } 199 200 void TabRendererGtk::LoadingAnimation::Observe( 201 NotificationType type, 202 const NotificationSource& source, 203 const NotificationDetails& details) { 204 DCHECK(type == NotificationType::BROWSER_THEME_CHANGED); 205 data_.reset(new Data(theme_service_)); 206 } 207 208 //////////////////////////////////////////////////////////////////////////////// 209 // FaviconCrashAnimation 210 // 211 // A custom animation subclass to manage the favicon crash animation. 212 class TabRendererGtk::FaviconCrashAnimation : public ui::LinearAnimation, 213 public ui::AnimationDelegate { 214 public: 215 explicit FaviconCrashAnimation(TabRendererGtk* target) 216 : ALLOW_THIS_IN_INITIALIZER_LIST(ui::LinearAnimation(1000, 25, this)), 217 target_(target) { 218 } 219 virtual ~FaviconCrashAnimation() {} 220 221 // ui::Animation overrides: 222 virtual void AnimateToState(double state) { 223 const double kHidingOffset = 27; 224 225 if (state < .5) { 226 target_->SetFaviconHidingOffset( 227 static_cast<int>(floor(kHidingOffset * 2.0 * state))); 228 } else { 229 target_->DisplayCrashedFavicon(); 230 target_->SetFaviconHidingOffset( 231 static_cast<int>( 232 floor(kHidingOffset - ((state - .5) * 2.0 * kHidingOffset)))); 233 } 234 } 235 236 // ui::AnimationDelegate overrides: 237 virtual void AnimationCanceled(const ui::Animation* animation) { 238 target_->SetFaviconHidingOffset(0); 239 } 240 241 private: 242 TabRendererGtk* target_; 243 244 DISALLOW_COPY_AND_ASSIGN(FaviconCrashAnimation); 245 }; 246 247 //////////////////////////////////////////////////////////////////////////////// 248 // TabRendererGtk, public: 249 250 TabRendererGtk::TabRendererGtk(ui::ThemeProvider* theme_provider) 251 : showing_icon_(false), 252 showing_close_button_(false), 253 favicon_hiding_offset_(0), 254 should_display_crashed_favicon_(false), 255 loading_animation_(theme_provider), 256 background_offset_x_(0), 257 background_offset_y_(kInactiveTabBackgroundOffsetY), 258 close_button_color_(0) { 259 InitResources(); 260 261 tab_.Own(gtk_fixed_new()); 262 gtk_widget_set_app_paintable(tab_.get(), TRUE); 263 g_signal_connect(tab_.get(), "expose-event", 264 G_CALLBACK(OnExposeEventThunk), this); 265 g_signal_connect(tab_.get(), "size-allocate", 266 G_CALLBACK(OnSizeAllocateThunk), this); 267 close_button_.reset(MakeCloseButton()); 268 gtk_widget_show(tab_.get()); 269 270 hover_animation_.reset(new ui::SlideAnimation(this)); 271 hover_animation_->SetSlideDuration(kHoverDurationMs); 272 273 registrar_.Add(this, NotificationType::BROWSER_THEME_CHANGED, 274 NotificationService::AllSources()); 275 } 276 277 TabRendererGtk::~TabRendererGtk() { 278 tab_.Destroy(); 279 for (BitmapCache::iterator it = cached_bitmaps_.begin(); 280 it != cached_bitmaps_.end(); ++it) { 281 delete it->second.bitmap; 282 } 283 } 284 285 void TabRendererGtk::UpdateData(TabContents* contents, 286 bool app, 287 bool loading_only) { 288 DCHECK(contents); 289 theme_service_ = GtkThemeService::GetFrom(contents->profile()); 290 291 if (!loading_only) { 292 data_.title = contents->GetTitle(); 293 data_.incognito = contents->profile()->IsOffTheRecord(); 294 data_.crashed = contents->is_crashed(); 295 296 SkBitmap* app_icon = 297 TabContentsWrapper::GetCurrentWrapperForContents(contents)-> 298 extension_tab_helper()->GetExtensionAppIcon(); 299 if (app_icon) 300 data_.favicon = *app_icon; 301 else 302 data_.favicon = contents->GetFavicon(); 303 304 data_.app = app; 305 // This is kind of a hacky way to determine whether our icon is the default 306 // favicon. But the plumbing that would be necessary to do it right would 307 // be a good bit of work and would sully code for other platforms which 308 // don't care to custom-theme the favicon. Hopefully the default favicon 309 // will eventually be chromium-themable and this code will go away. 310 data_.is_default_favicon = 311 (data_.favicon.pixelRef() == 312 ResourceBundle::GetSharedInstance().GetBitmapNamed( 313 IDR_DEFAULT_FAVICON)->pixelRef()); 314 } 315 316 // Loading state also involves whether we show the favicon, since that's where 317 // we display the throbber. 318 data_.loading = contents->is_loading(); 319 data_.show_icon = contents->ShouldDisplayFavicon(); 320 } 321 322 void TabRendererGtk::UpdateFromModel() { 323 // Force a layout, since the tab may have grown a favicon. 324 Layout(); 325 SchedulePaint(); 326 327 if (data_.crashed) { 328 if (!should_display_crashed_favicon_ && !IsPerformingCrashAnimation()) 329 StartCrashAnimation(); 330 } else { 331 if (IsPerformingCrashAnimation()) 332 StopCrashAnimation(); 333 ResetCrashedFavicon(); 334 } 335 } 336 337 void TabRendererGtk::SetBlocked(bool blocked) { 338 if (data_.blocked == blocked) 339 return; 340 data_.blocked = blocked; 341 // TODO(zelidrag) bug 32399: Make tabs pulse on Linux as well. 342 } 343 344 bool TabRendererGtk::is_blocked() const { 345 return data_.blocked; 346 } 347 348 bool TabRendererGtk::IsSelected() const { 349 return true; 350 } 351 352 bool TabRendererGtk::IsVisible() const { 353 return GTK_WIDGET_FLAGS(tab_.get()) & GTK_VISIBLE; 354 } 355 356 void TabRendererGtk::SetVisible(bool visible) const { 357 if (visible) { 358 gtk_widget_show(tab_.get()); 359 if (data_.mini) 360 gtk_widget_show(close_button_->widget()); 361 } else { 362 gtk_widget_hide_all(tab_.get()); 363 } 364 } 365 366 bool TabRendererGtk::ValidateLoadingAnimation(AnimationState animation_state) { 367 return loading_animation_.ValidateLoadingAnimation(animation_state); 368 } 369 370 void TabRendererGtk::PaintFaviconArea(GdkEventExpose* event) { 371 DCHECK(ShouldShowIcon()); 372 373 // The paint area is the favicon bounds, but we're painting into the gdk 374 // window belonging to the tabstrip. So the coordinates are relative to the 375 // top left of the tab strip. 376 event->area.x = x() + favicon_bounds_.x(); 377 event->area.y = y() + favicon_bounds_.y(); 378 event->area.width = favicon_bounds_.width(); 379 event->area.height = favicon_bounds_.height(); 380 gfx::CanvasSkiaPaint canvas(event, false); 381 382 // The actual paint methods expect 0, 0 to be the tab top left (see 383 // PaintTab). 384 canvas.TranslateInt(x(), y()); 385 386 // Paint the background behind the favicon. 387 int theme_id; 388 int offset_y = 0; 389 if (IsSelected()) { 390 theme_id = IDR_THEME_TOOLBAR; 391 } else { 392 if (!data_.incognito) { 393 theme_id = IDR_THEME_TAB_BACKGROUND; 394 } else { 395 theme_id = IDR_THEME_TAB_BACKGROUND_INCOGNITO; 396 } 397 if (!theme_service_->HasCustomImage(theme_id)) 398 offset_y = background_offset_y_; 399 } 400 SkBitmap* tab_bg = theme_service_->GetBitmapNamed(theme_id); 401 canvas.TileImageInt(*tab_bg, 402 x() + favicon_bounds_.x(), offset_y + favicon_bounds_.y(), 403 favicon_bounds_.x(), favicon_bounds_.y(), 404 favicon_bounds_.width(), favicon_bounds_.height()); 405 406 if (!IsSelected()) { 407 double throb_value = GetThrobValue(); 408 if (throb_value > 0) { 409 SkRect bounds; 410 bounds.set(favicon_bounds_.x(), favicon_bounds_.y(), 411 favicon_bounds_.right(), favicon_bounds_.bottom()); 412 canvas.saveLayerAlpha(&bounds, static_cast<int>(throb_value * 0xff), 413 SkCanvas::kARGB_ClipLayer_SaveFlag); 414 canvas.drawARGB(0, 255, 255, 255, SkXfermode::kClear_Mode); 415 SkBitmap* active_bg = theme_service_->GetBitmapNamed(IDR_THEME_TOOLBAR); 416 canvas.TileImageInt(*active_bg, 417 x() + favicon_bounds_.x(), favicon_bounds_.y(), 418 favicon_bounds_.x(), favicon_bounds_.y(), 419 favicon_bounds_.width(), favicon_bounds_.height()); 420 canvas.restore(); 421 } 422 } 423 424 // Now paint the icon. 425 PaintIcon(&canvas); 426 } 427 428 bool TabRendererGtk::ShouldShowIcon() const { 429 if (mini() && height() >= GetMinimumUnselectedSize().height()) { 430 return true; 431 } else if (!data_.show_icon) { 432 return false; 433 } else if (IsSelected()) { 434 // The selected tab clips favicon before close button. 435 return IconCapacity() >= 2; 436 } 437 // Non-selected tabs clip close button before favicon. 438 return IconCapacity() >= 1; 439 } 440 441 // static 442 gfx::Size TabRendererGtk::GetMinimumUnselectedSize() { 443 InitResources(); 444 445 gfx::Size minimum_size; 446 minimum_size.set_width(kLeftPadding + kRightPadding); 447 // Since we use bitmap images, the real minimum height of the image is 448 // defined most accurately by the height of the end cap images. 449 minimum_size.set_height(tab_active_.image_l->height() - kToolbarOverlap); 450 return minimum_size; 451 } 452 453 // static 454 gfx::Size TabRendererGtk::GetMinimumSelectedSize() { 455 gfx::Size minimum_size = GetMinimumUnselectedSize(); 456 minimum_size.set_width(kLeftPadding + kFaviconSize + kRightPadding); 457 return minimum_size; 458 } 459 460 // static 461 gfx::Size TabRendererGtk::GetStandardSize() { 462 gfx::Size standard_size = GetMinimumUnselectedSize(); 463 standard_size.Enlarge(kFaviconTitleSpacing + kStandardTitleWidth, 0); 464 return standard_size; 465 } 466 467 // static 468 int TabRendererGtk::GetMiniWidth() { 469 return browser_defaults::kMiniTabWidth; 470 } 471 472 // static 473 int TabRendererGtk::GetContentHeight() { 474 // The height of the content of the Tab is the largest of the favicon, 475 // the title text and the close button graphic. 476 int content_height = std::max(kFaviconSize, title_font_height_); 477 return std::max(content_height, close_button_height_); 478 } 479 480 // static 481 void TabRendererGtk::LoadTabImages() { 482 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 483 484 tab_alpha_.image_l = rb.GetBitmapNamed(IDR_TAB_ALPHA_LEFT); 485 tab_alpha_.image_r = rb.GetBitmapNamed(IDR_TAB_ALPHA_RIGHT); 486 487 tab_active_.image_l = rb.GetBitmapNamed(IDR_TAB_ACTIVE_LEFT); 488 tab_active_.image_c = rb.GetBitmapNamed(IDR_TAB_ACTIVE_CENTER); 489 tab_active_.image_r = rb.GetBitmapNamed(IDR_TAB_ACTIVE_RIGHT); 490 tab_active_.l_width = tab_active_.image_l->width(); 491 tab_active_.r_width = tab_active_.image_r->width(); 492 493 tab_inactive_.image_l = rb.GetBitmapNamed(IDR_TAB_INACTIVE_LEFT); 494 tab_inactive_.image_c = rb.GetBitmapNamed(IDR_TAB_INACTIVE_CENTER); 495 tab_inactive_.image_r = rb.GetBitmapNamed(IDR_TAB_INACTIVE_RIGHT); 496 tab_inactive_.l_width = tab_inactive_.image_l->width(); 497 tab_inactive_.r_width = tab_inactive_.image_r->width(); 498 499 close_button_width_ = rb.GetBitmapNamed(IDR_TAB_CLOSE)->width(); 500 close_button_height_ = rb.GetBitmapNamed(IDR_TAB_CLOSE)->height(); 501 } 502 503 // static 504 void TabRendererGtk::SetSelectedTitleColor(SkColor color) { 505 selected_title_color_ = color; 506 } 507 508 // static 509 void TabRendererGtk::SetUnselectedTitleColor(SkColor color) { 510 unselected_title_color_ = color; 511 } 512 513 gfx::Rect TabRendererGtk::GetNonMirroredBounds(GtkWidget* parent) const { 514 // The tabstrip widget is a windowless widget so the tab widget's allocation 515 // is relative to the browser titlebar. We need the bounds relative to the 516 // tabstrip. 517 gfx::Rect bounds = GetWidgetBoundsRelativeToParent(parent, widget()); 518 bounds.set_x(gtk_util::MirroredLeftPointForRect(parent, bounds)); 519 return bounds; 520 } 521 522 gfx::Rect TabRendererGtk::GetRequisition() const { 523 return gfx::Rect(requisition_.x(), requisition_.y(), 524 requisition_.width(), requisition_.height()); 525 } 526 527 void TabRendererGtk::StartMiniTabTitleAnimation() { 528 if (!mini_title_animation_.get()) { 529 mini_title_animation_.reset(new ui::ThrobAnimation(this)); 530 mini_title_animation_->SetThrobDuration(kMiniTitleChangeThrobDuration); 531 } 532 533 if (!mini_title_animation_->is_animating()) { 534 mini_title_animation_->StartThrobbing(2); 535 } else if (mini_title_animation_->cycles_remaining() <= 2) { 536 // The title changed while we're already animating. Add at most one more 537 // cycle. This is done in an attempt to smooth out pages that continuously 538 // change the title. 539 mini_title_animation_->set_cycles_remaining( 540 mini_title_animation_->cycles_remaining() + 2); 541 } 542 } 543 544 void TabRendererGtk::StopMiniTabTitleAnimation() { 545 if (mini_title_animation_.get()) 546 mini_title_animation_->Stop(); 547 } 548 549 void TabRendererGtk::SetBounds(const gfx::Rect& bounds) { 550 requisition_ = bounds; 551 gtk_widget_set_size_request(tab_.get(), bounds.width(), bounds.height()); 552 } 553 554 void TabRendererGtk::Observe(NotificationType type, 555 const NotificationSource& source, 556 const NotificationDetails& details) { 557 DCHECK(type == NotificationType::BROWSER_THEME_CHANGED); 558 559 // Clear our cache when we receive a theme change notification because it 560 // contains cached bitmaps based off the previous theme. 561 for (BitmapCache::iterator it = cached_bitmaps_.begin(); 562 it != cached_bitmaps_.end(); ++it) { 563 delete it->second.bitmap; 564 } 565 cached_bitmaps_.clear(); 566 } 567 568 //////////////////////////////////////////////////////////////////////////////// 569 // TabRendererGtk, protected: 570 571 string16 TabRendererGtk::GetTitle() const { 572 return data_.title; 573 } 574 575 /////////////////////////////////////////////////////////////////////////////// 576 // TabRendererGtk, ui::AnimationDelegate implementation: 577 578 void TabRendererGtk::AnimationProgressed(const ui::Animation* animation) { 579 gtk_widget_queue_draw(tab_.get()); 580 } 581 582 void TabRendererGtk::AnimationCanceled(const ui::Animation* animation) { 583 AnimationEnded(animation); 584 } 585 586 void TabRendererGtk::AnimationEnded(const ui::Animation* animation) { 587 gtk_widget_queue_draw(tab_.get()); 588 } 589 590 //////////////////////////////////////////////////////////////////////////////// 591 // TabRendererGtk, private: 592 593 void TabRendererGtk::StartCrashAnimation() { 594 if (!crash_animation_.get()) 595 crash_animation_.reset(new FaviconCrashAnimation(this)); 596 crash_animation_->Stop(); 597 crash_animation_->Start(); 598 } 599 600 void TabRendererGtk::StopCrashAnimation() { 601 if (!crash_animation_.get()) 602 return; 603 crash_animation_->Stop(); 604 } 605 606 bool TabRendererGtk::IsPerformingCrashAnimation() const { 607 return crash_animation_.get() && crash_animation_->is_animating(); 608 } 609 610 void TabRendererGtk::SetFaviconHidingOffset(int offset) { 611 favicon_hiding_offset_ = offset; 612 SchedulePaint(); 613 } 614 615 void TabRendererGtk::DisplayCrashedFavicon() { 616 should_display_crashed_favicon_ = true; 617 } 618 619 void TabRendererGtk::ResetCrashedFavicon() { 620 should_display_crashed_favicon_ = false; 621 } 622 623 void TabRendererGtk::Paint(gfx::Canvas* canvas) { 624 // Don't paint if we're narrower than we can render correctly. (This should 625 // only happen during animations). 626 if (width() < GetMinimumUnselectedSize().width() && !mini()) 627 return; 628 629 // See if the model changes whether the icons should be painted. 630 const bool show_icon = ShouldShowIcon(); 631 const bool show_close_button = ShouldShowCloseBox(); 632 if (show_icon != showing_icon_ || 633 show_close_button != showing_close_button_) 634 Layout(); 635 636 PaintTabBackground(canvas); 637 638 if (!mini() || width() > kMiniTabRendererAsNormalTabWidth) 639 PaintTitle(canvas); 640 641 if (show_icon) 642 PaintIcon(canvas); 643 } 644 645 SkBitmap TabRendererGtk::PaintBitmap() { 646 gfx::CanvasSkia canvas(width(), height(), false); 647 Paint(&canvas); 648 return canvas.ExtractBitmap(); 649 } 650 651 cairo_surface_t* TabRendererGtk::PaintToSurface() { 652 gfx::CanvasSkia canvas(width(), height(), false); 653 Paint(&canvas); 654 return cairo_surface_reference(cairo_get_target(canvas.beginPlatformPaint())); 655 } 656 657 void TabRendererGtk::SchedulePaint() { 658 gtk_widget_queue_draw(tab_.get()); 659 } 660 661 gfx::Rect TabRendererGtk::GetLocalBounds() { 662 return gfx::Rect(0, 0, bounds_.width(), bounds_.height()); 663 } 664 665 void TabRendererGtk::Layout() { 666 gfx::Rect local_bounds = GetLocalBounds(); 667 if (local_bounds.IsEmpty()) 668 return; 669 local_bounds.Inset(kLeftPadding, kTopPadding, kRightPadding, kBottomPadding); 670 671 // Figure out who is tallest. 672 int content_height = GetContentHeight(); 673 674 // Size the Favicon. 675 showing_icon_ = ShouldShowIcon(); 676 if (showing_icon_) { 677 int favicon_top = kTopPadding + (content_height - kFaviconSize) / 2; 678 favicon_bounds_.SetRect(local_bounds.x(), favicon_top, 679 kFaviconSize, kFaviconSize); 680 if ((mini() || data_.animating_mini_change) && 681 bounds_.width() < kMiniTabRendererAsNormalTabWidth) { 682 int mini_delta = kMiniTabRendererAsNormalTabWidth - GetMiniWidth(); 683 int ideal_delta = bounds_.width() - GetMiniWidth(); 684 if (ideal_delta < mini_delta) { 685 int ideal_x = (GetMiniWidth() - kFaviconSize) / 2; 686 int x = favicon_bounds_.x() + static_cast<int>( 687 (1 - static_cast<float>(ideal_delta) / 688 static_cast<float>(mini_delta)) * 689 (ideal_x - favicon_bounds_.x())); 690 favicon_bounds_.set_x(x); 691 } 692 } 693 } else { 694 favicon_bounds_.SetRect(local_bounds.x(), local_bounds.y(), 0, 0); 695 } 696 697 // Size the Close button. 698 showing_close_button_ = ShouldShowCloseBox(); 699 if (showing_close_button_) { 700 int close_button_top = 701 kTopPadding + kCloseButtonVertFuzz + 702 (content_height - close_button_height_) / 2; 703 close_button_bounds_.SetRect(local_bounds.width() + kCloseButtonHorzFuzz, 704 close_button_top, close_button_width_, 705 close_button_height_); 706 707 // If the close button color has changed, generate a new one. 708 if (theme_service_) { 709 SkColor tab_text_color = 710 theme_service_->GetColor(ThemeService::COLOR_TAB_TEXT); 711 if (!close_button_color_ || tab_text_color != close_button_color_) { 712 close_button_color_ = tab_text_color; 713 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 714 close_button_->SetBackground(close_button_color_, 715 rb.GetBitmapNamed(IDR_TAB_CLOSE), 716 rb.GetBitmapNamed(IDR_TAB_CLOSE_MASK)); 717 } 718 } 719 } else { 720 close_button_bounds_.SetRect(0, 0, 0, 0); 721 } 722 723 if (!mini() || width() >= kMiniTabRendererAsNormalTabWidth) { 724 // Size the Title text to fill the remaining space. 725 int title_left = favicon_bounds_.right() + kFaviconTitleSpacing; 726 int title_top = kTopPadding; 727 728 // If the user has big fonts, the title will appear rendered too far down 729 // on the y-axis if we use the regular top padding, so we need to adjust it 730 // so that the text appears centered. 731 gfx::Size minimum_size = GetMinimumUnselectedSize(); 732 int text_height = title_top + title_font_height_ + kBottomPadding; 733 if (text_height > minimum_size.height()) 734 title_top -= (text_height - minimum_size.height()) / 2; 735 736 int title_width; 737 if (close_button_bounds_.width() && close_button_bounds_.height()) { 738 title_width = std::max(close_button_bounds_.x() - 739 kTitleCloseButtonSpacing - title_left, 0); 740 } else { 741 title_width = std::max(local_bounds.width() - title_left, 0); 742 } 743 title_bounds_.SetRect(title_left, title_top, title_width, content_height); 744 } 745 746 favicon_bounds_.set_x( 747 gtk_util::MirroredLeftPointForRect(tab_.get(), favicon_bounds_)); 748 close_button_bounds_.set_x( 749 gtk_util::MirroredLeftPointForRect(tab_.get(), close_button_bounds_)); 750 title_bounds_.set_x( 751 gtk_util::MirroredLeftPointForRect(tab_.get(), title_bounds_)); 752 753 MoveCloseButtonWidget(); 754 } 755 756 void TabRendererGtk::MoveCloseButtonWidget() { 757 if (!close_button_bounds_.IsEmpty()) { 758 gtk_fixed_move(GTK_FIXED(tab_.get()), close_button_->widget(), 759 close_button_bounds_.x(), close_button_bounds_.y()); 760 gtk_widget_show(close_button_->widget()); 761 } else { 762 gtk_widget_hide(close_button_->widget()); 763 } 764 } 765 766 SkBitmap* TabRendererGtk::GetMaskedBitmap(const SkBitmap* mask, 767 const SkBitmap* background, int bg_offset_x, int bg_offset_y) { 768 // We store a bitmap for each mask + background pair (4 total bitmaps). We 769 // replace the cached image if the tab has moved relative to the background. 770 BitmapCache::iterator it = cached_bitmaps_.find(std::make_pair(mask, 771 background)); 772 if (it != cached_bitmaps_.end()) { 773 if (it->second.bg_offset_x == bg_offset_x && 774 it->second.bg_offset_y == bg_offset_y) { 775 return it->second.bitmap; 776 } 777 // The background offset changed so we should re-render with the new 778 // offsets. 779 delete it->second.bitmap; 780 } 781 SkBitmap image = SkBitmapOperations::CreateTiledBitmap( 782 *background, bg_offset_x, bg_offset_y, mask->width(), 783 height() + kToolbarOverlap); 784 CachedBitmap bitmap = { 785 bg_offset_x, 786 bg_offset_y, 787 new SkBitmap(SkBitmapOperations::CreateMaskedBitmap(image, *mask)) 788 }; 789 cached_bitmaps_[std::make_pair(mask, background)] = bitmap; 790 return bitmap.bitmap; 791 } 792 793 void TabRendererGtk::PaintTab(GdkEventExpose* event) { 794 gfx::CanvasSkiaPaint canvas(event, false); 795 if (canvas.is_empty()) 796 return; 797 798 // The tab is rendered into a windowless widget whose offset is at the 799 // coordinate event->area. Translate by these offsets so we can render at 800 // (0,0) to match Windows' rendering metrics. 801 canvas.TranslateInt(event->area.x, event->area.y); 802 803 // Save the original x offset so we can position background images properly. 804 background_offset_x_ = event->area.x; 805 806 Paint(&canvas); 807 } 808 809 void TabRendererGtk::PaintTitle(gfx::Canvas* canvas) { 810 // Paint the Title. 811 string16 title = data_.title; 812 if (title.empty()) { 813 title = data_.loading ? 814 l10n_util::GetStringUTF16(IDS_TAB_LOADING_TITLE) : 815 TabContentsWrapper::GetDefaultTitle(); 816 } else { 817 Browser::FormatTitleForDisplay(&title); 818 } 819 820 SkColor title_color = IsSelected() ? selected_title_color_ 821 : unselected_title_color_; 822 canvas->DrawStringInt(title, *title_font_, title_color, 823 title_bounds_.x(), title_bounds_.y(), 824 title_bounds_.width(), title_bounds_.height()); 825 } 826 827 void TabRendererGtk::PaintIcon(gfx::Canvas* canvas) { 828 if (loading_animation_.animation_state() != ANIMATION_NONE) { 829 PaintLoadingAnimation(canvas); 830 } else { 831 canvas->Save(); 832 canvas->ClipRectInt(0, 0, width(), height() - kFaviconTitleSpacing); 833 if (should_display_crashed_favicon_) { 834 canvas->DrawBitmapInt(*crashed_favicon, 0, 0, 835 crashed_favicon->width(), 836 crashed_favicon->height(), 837 favicon_bounds_.x(), 838 favicon_bounds_.y() + favicon_hiding_offset_, 839 kFaviconSize, kFaviconSize, 840 true); 841 } else { 842 if (!data_.favicon.isNull()) { 843 if (data_.is_default_favicon && theme_service_->UseGtkTheme()) { 844 GdkPixbuf* favicon = GtkThemeService::GetDefaultFavicon(true); 845 canvas->AsCanvasSkia()->DrawGdkPixbuf( 846 favicon, favicon_bounds_.x(), 847 favicon_bounds_.y() + favicon_hiding_offset_); 848 } else { 849 // If the favicon is an app icon, it is allowed to be drawn slightly 850 // larger than the standard favicon. 851 int faviconHeightOffset = data_.app ? -2 : 0; 852 int faviconWidthDelta = data_.app ? 853 data_.favicon.width() - kFaviconSize : 0; 854 int faviconHeightDelta = data_.app ? 855 data_.favicon.height() - kFaviconSize : 0; 856 857 // TODO(pkasting): Use code in tab_icon_view.cc:PaintIcon() (or switch 858 // to using that class to render the favicon). 859 canvas->DrawBitmapInt(data_.favicon, 0, 0, 860 data_.favicon.width(), 861 data_.favicon.height(), 862 favicon_bounds_.x() - faviconWidthDelta/2, 863 favicon_bounds_.y() + faviconHeightOffset 864 - faviconHeightDelta/2 865 + favicon_hiding_offset_, 866 kFaviconSize + faviconWidthDelta, 867 kFaviconSize + faviconHeightDelta, 868 true); 869 } 870 } 871 } 872 canvas->Restore(); 873 } 874 } 875 876 void TabRendererGtk::PaintTabBackground(gfx::Canvas* canvas) { 877 if (IsSelected()) { 878 PaintActiveTabBackground(canvas); 879 } else { 880 PaintInactiveTabBackground(canvas); 881 882 double throb_value = GetThrobValue(); 883 if (throb_value > 0) { 884 canvas->SaveLayerAlpha(static_cast<int>(throb_value * 0xff), 885 gfx::Rect(width(), height())); 886 canvas->AsCanvasSkia()->drawARGB(0, 255, 255, 255, 887 SkXfermode::kClear_Mode); 888 PaintActiveTabBackground(canvas); 889 canvas->Restore(); 890 } 891 } 892 } 893 894 void TabRendererGtk::PaintInactiveTabBackground(gfx::Canvas* canvas) { 895 896 // The tab image needs to be lined up with the background image 897 // so that it feels partially transparent. 898 int offset_x = background_offset_x_; 899 900 int tab_id = data_.incognito ? 901 IDR_THEME_TAB_BACKGROUND_INCOGNITO : IDR_THEME_TAB_BACKGROUND; 902 903 SkBitmap* tab_bg = theme_service_->GetBitmapNamed(tab_id); 904 905 // If the theme is providing a custom background image, then its top edge 906 // should be at the top of the tab. Otherwise, we assume that the background 907 // image is a composited foreground + frame image. 908 int offset_y = theme_service_->HasCustomImage(tab_id) ? 909 0 : background_offset_y_; 910 911 // Draw left edge. 912 SkBitmap* theme_l = GetMaskedBitmap(tab_alpha_.image_l, tab_bg, offset_x, 913 offset_y); 914 canvas->DrawBitmapInt(*theme_l, 0, 0); 915 916 // Draw right edge. 917 SkBitmap* theme_r = GetMaskedBitmap(tab_alpha_.image_r, tab_bg, 918 offset_x + width() - tab_active_.r_width, offset_y); 919 920 canvas->DrawBitmapInt(*theme_r, width() - theme_r->width(), 0); 921 922 // Draw center. 923 canvas->TileImageInt(*tab_bg, 924 offset_x + tab_active_.l_width, kDropShadowOffset + offset_y, 925 tab_active_.l_width, 2, 926 width() - tab_active_.l_width - tab_active_.r_width, height() - 2); 927 928 canvas->DrawBitmapInt(*tab_inactive_.image_l, 0, 0); 929 canvas->TileImageInt(*tab_inactive_.image_c, tab_inactive_.l_width, 0, 930 width() - tab_inactive_.l_width - tab_inactive_.r_width, height()); 931 canvas->DrawBitmapInt(*tab_inactive_.image_r, 932 width() - tab_inactive_.r_width, 0); 933 } 934 935 void TabRendererGtk::PaintActiveTabBackground(gfx::Canvas* canvas) { 936 int offset_x = background_offset_x_; 937 938 SkBitmap* tab_bg = theme_service_->GetBitmapNamed(IDR_THEME_TOOLBAR); 939 940 // Draw left edge. 941 SkBitmap* theme_l = GetMaskedBitmap(tab_alpha_.image_l, tab_bg, offset_x, 0); 942 canvas->DrawBitmapInt(*theme_l, 0, 0); 943 944 // Draw right edge. 945 SkBitmap* theme_r = GetMaskedBitmap(tab_alpha_.image_r, tab_bg, 946 offset_x + width() - tab_active_.r_width, 0); 947 canvas->DrawBitmapInt(*theme_r, width() - tab_active_.r_width, 0); 948 949 // Draw center. 950 canvas->TileImageInt(*tab_bg, 951 offset_x + tab_active_.l_width, kDropShadowHeight, 952 tab_active_.l_width, kDropShadowHeight, 953 width() - tab_active_.l_width - tab_active_.r_width, 954 height() - kDropShadowHeight); 955 956 canvas->DrawBitmapInt(*tab_active_.image_l, 0, 0); 957 canvas->TileImageInt(*tab_active_.image_c, tab_active_.l_width, 0, 958 width() - tab_active_.l_width - tab_active_.r_width, height()); 959 canvas->DrawBitmapInt(*tab_active_.image_r, width() - tab_active_.r_width, 0); 960 } 961 962 void TabRendererGtk::PaintLoadingAnimation(gfx::Canvas* canvas) { 963 const SkBitmap* frames = 964 (loading_animation_.animation_state() == ANIMATION_WAITING) ? 965 loading_animation_.waiting_animation_frames() : 966 loading_animation_.loading_animation_frames(); 967 const int image_size = frames->height(); 968 const int image_offset = loading_animation_.animation_frame() * image_size; 969 DCHECK(image_size == favicon_bounds_.height()); 970 DCHECK(image_size == favicon_bounds_.width()); 971 972 // NOTE: the clipping is a work around for 69528, it shouldn't be necessary. 973 canvas->Save(); 974 canvas->ClipRectInt( 975 favicon_bounds_.x(), favicon_bounds_.y(), image_size, image_size); 976 canvas->DrawBitmapInt(*frames, image_offset, 0, image_size, image_size, 977 favicon_bounds_.x(), favicon_bounds_.y(), image_size, image_size, 978 false); 979 canvas->Restore(); 980 } 981 982 int TabRendererGtk::IconCapacity() const { 983 if (height() < GetMinimumUnselectedSize().height()) 984 return 0; 985 return (width() - kLeftPadding - kRightPadding) / kFaviconSize; 986 } 987 988 bool TabRendererGtk::ShouldShowCloseBox() const { 989 // The selected tab never clips close button. 990 return !mini() && (IsSelected() || IconCapacity() >= 3); 991 } 992 993 CustomDrawButton* TabRendererGtk::MakeCloseButton() { 994 CustomDrawButton* button = new CustomDrawButton(IDR_TAB_CLOSE, 995 IDR_TAB_CLOSE_P, IDR_TAB_CLOSE_H, IDR_TAB_CLOSE); 996 997 gtk_widget_set_tooltip_text(button->widget(), 998 l10n_util::GetStringUTF8(IDS_TOOLTIP_CLOSE_TAB).c_str()); 999 1000 g_signal_connect(button->widget(), "clicked", 1001 G_CALLBACK(OnCloseButtonClickedThunk), this); 1002 g_signal_connect(button->widget(), "button-release-event", 1003 G_CALLBACK(OnCloseButtonMouseReleaseThunk), this); 1004 g_signal_connect(button->widget(), "enter-notify-event", 1005 G_CALLBACK(OnEnterNotifyEventThunk), this); 1006 g_signal_connect(button->widget(), "leave-notify-event", 1007 G_CALLBACK(OnLeaveNotifyEventThunk), this); 1008 GTK_WIDGET_UNSET_FLAGS(button->widget(), GTK_CAN_FOCUS); 1009 gtk_fixed_put(GTK_FIXED(tab_.get()), button->widget(), 0, 0); 1010 1011 return button; 1012 } 1013 1014 double TabRendererGtk::GetThrobValue() { 1015 if (mini_title_animation_.get() && mini_title_animation_->is_animating()) { 1016 return mini_title_animation_->GetCurrentValue() * 1017 kMiniTitleChangeThrobOpacity; 1018 } 1019 return hover_animation_.get() ? 1020 kHoverOpacity * hover_animation_->GetCurrentValue() : 0; 1021 } 1022 1023 void TabRendererGtk::CloseButtonClicked() { 1024 // Nothing to do. 1025 } 1026 1027 void TabRendererGtk::OnCloseButtonClicked(GtkWidget* widget) { 1028 CloseButtonClicked(); 1029 } 1030 1031 gboolean TabRendererGtk::OnCloseButtonMouseRelease(GtkWidget* widget, 1032 GdkEventButton* event) { 1033 if (event->button == 2) { 1034 CloseButtonClicked(); 1035 return TRUE; 1036 } 1037 1038 return FALSE; 1039 } 1040 1041 gboolean TabRendererGtk::OnExposeEvent(GtkWidget* widget, 1042 GdkEventExpose* event) { 1043 PaintTab(event); 1044 gtk_container_propagate_expose(GTK_CONTAINER(tab_.get()), 1045 close_button_->widget(), event); 1046 return TRUE; 1047 } 1048 1049 void TabRendererGtk::OnSizeAllocate(GtkWidget* widget, 1050 GtkAllocation* allocation) { 1051 gfx::Rect bounds = gfx::Rect(allocation->x, allocation->y, 1052 allocation->width, allocation->height); 1053 1054 // Nothing to do if the bounds are the same. If we don't catch this, we'll 1055 // get an infinite loop of size-allocate signals. 1056 if (bounds_ == bounds) 1057 return; 1058 1059 bounds_ = bounds; 1060 Layout(); 1061 } 1062 1063 gboolean TabRendererGtk::OnEnterNotifyEvent(GtkWidget* widget, 1064 GdkEventCrossing* event) { 1065 hover_animation_->SetTweenType(ui::Tween::EASE_OUT); 1066 hover_animation_->Show(); 1067 return FALSE; 1068 } 1069 1070 gboolean TabRendererGtk::OnLeaveNotifyEvent(GtkWidget* widget, 1071 GdkEventCrossing* event) { 1072 hover_animation_->SetTweenType(ui::Tween::EASE_IN); 1073 hover_animation_->Hide(); 1074 return FALSE; 1075 } 1076 1077 // static 1078 void TabRendererGtk::InitResources() { 1079 if (initialized_) 1080 return; 1081 1082 LoadTabImages(); 1083 1084 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 1085 const gfx::Font& base_font = rb.GetFont(ResourceBundle::BaseFont); 1086 title_font_ = new gfx::Font(base_font.GetFontName(), kFontPixelSize); 1087 title_font_height_ = title_font_->GetHeight(); 1088 1089 crashed_favicon = rb.GetBitmapNamed(IDR_SAD_FAVICON); 1090 1091 initialized_ = true; 1092 } 1093