root/chrome/browser/ui/gtk/tabs/tab_renderer_gtk.cc

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. GetWidgetBoundsRelativeToParent
  2. GetResizedGdkPixbufFromSkBitmap
  3. waiting_to_loading_frame_count_ratio
  4. animation_frame_
  5. animation_frame_
  6. ValidateLoadingAnimation
  7. Observe
  8. previous_media_state
  9. target_
  10. FaviconCrashAnimation
  11. AnimateToState
  12. AnimationCanceled
  13. unselected_title_color_
  14. Observe
  15. UpdateData
  16. UpdateFromModel
  17. SetBlocked
  18. is_blocked
  19. IsActive
  20. IsSelected
  21. IsVisible
  22. SetVisible
  23. ValidateLoadingAnimation
  24. PaintFaviconArea
  25. MaybeAdjustLeftForMiniTab
  26. GetMinimumUnselectedSize
  27. GetMinimumSelectedSize
  28. GetStandardSize
  29. GetMiniWidth
  30. GetContentHeight
  31. GetNonMirroredBounds
  32. GetRequisition
  33. StartMiniTabTitleAnimation
  34. StopMiniTabTitleAnimation
  35. SetBounds
  36. Raise
  37. GetTitle
  38. AnimationProgressed
  39. AnimationCanceled
  40. AnimationEnded
  41. StartCrashAnimation
  42. StopCrashAnimation
  43. IsPerformingCrashAnimation
  44. StartMediaIndicatorAnimation
  45. SetFaviconHidingOffset
  46. DisplayCrashedFavicon
  47. ResetCrashedFavicon
  48. Paint
  49. PaintToSurface
  50. SchedulePaint
  51. GetLocalBounds
  52. Layout
  53. MoveCloseButtonWidget
  54. PaintTab
  55. PaintTitle
  56. PaintIcon
  57. PaintMediaIndicator
  58. PaintTabBackground
  59. DrawTabBackground
  60. DrawTabShadow
  61. PaintInactiveTabBackground
  62. PaintActiveTabBackground
  63. PaintLoadingAnimation
  64. IconCapacity
  65. ShouldShowIcon
  66. ShouldShowMediaIndicator
  67. ShouldShowCloseBox
  68. MakeCloseButton
  69. GetThrobValue
  70. CloseButtonClicked
  71. OnCloseButtonClicked
  72. OnCloseButtonMouseRelease
  73. OnExposeEvent
  74. OnSizeAllocate
  75. OnEnterNotifyEvent
  76. OnLeaveNotifyEvent
  77. InitResources

// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/gtk/tabs/tab_renderer_gtk.h"

#include <algorithm>
#include <utility>

#include "base/debug/trace_event.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/defaults.h"
#include "chrome/browser/extensions/tab_helper.h"
#include "chrome/browser/favicon/favicon_tab_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/gtk/bookmarks/bookmark_utils_gtk.h"
#include "chrome/browser/ui/gtk/custom_button.h"
#include "chrome/browser/ui/gtk/gtk_theme_service.h"
#include "chrome/browser/ui/gtk/gtk_util.h"
#include "chrome/browser/ui/tab_contents/core_tab_helper.h"
#include "chrome/browser/ui/tabs/tab_utils.h"
#include "content/public/browser/notification_source.h"
#include "content/public/browser/web_contents.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "grit/ui_resources.h"
#include "skia/ext/image_operations.h"
#include "ui/base/gtk/gtk_screen_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/animation/slide_animation.h"
#include "ui/gfx/animation/throb_animation.h"
#include "ui/gfx/canvas_skia_paint.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/gtk_compat.h"
#include "ui/gfx/gtk_util.h"
#include "ui/gfx/image/cairo_cached_surface.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/pango_util.h"
#include "ui/gfx/platform_font_pango.h"
#include "ui/gfx/skbitmap_operations.h"

using content::WebContents;

namespace {

const int kFontPixelSize = 12;
const int kLeftPadding = 16;
const int kTopPadding = 6;
const int kRightPadding = 15;
const int kBottomPadding = 5;
const int kFaviconTitleSpacing = 4;
const int kTitleCloseButtonSpacing = 5;
const int kStandardTitleWidth = 175;
const int kDropShadowOffset = 2;
const int kInactiveTabBackgroundOffsetY = 15;

// When a non-mini-tab becomes a mini-tab the width of the tab animates. If
// the width of a mini-tab is >= kMiniTabRendererAsNormalTabWidth then the tab
// is rendered as a normal tab. This is done to avoid having the title
// immediately disappear when transitioning a tab from normal to mini-tab.
const int kMiniTabRendererAsNormalTabWidth =
    browser_defaults::kMiniTabWidth + 30;

// The tab images are designed to overlap the toolbar by 1 pixel. For now we
// don't actually overlap the toolbar, so this is used to know how many pixels
// at the bottom of the tab images are to be ignored.
const int kToolbarOverlap = 1;

// How long the hover state takes.
const int kHoverDurationMs = 90;

// How opaque to make the hover state (out of 1).
const double kHoverOpacity = 0.33;

// Opacity for non-active selected tabs.
const double kSelectedTabOpacity = 0.45;

// Selected (but not active) tabs have their throb value scaled down by this.
const double kSelectedTabThrobScale = 0.5;

// Max opacity for the mini-tab title change animation.
const double kMiniTitleChangeThrobOpacity = 0.75;

// Duration for when the title of an inactive mini-tab changes.
const int kMiniTitleChangeThrobDuration = 1000;

// The horizontal offset used to position the close button in the tab.
const int kCloseButtonHorzFuzz = 4;

// Gets the bounds of |widget| relative to |parent|.
gfx::Rect GetWidgetBoundsRelativeToParent(GtkWidget* parent,
                                          GtkWidget* widget) {
  gfx::Rect bounds = ui::GetWidgetScreenBounds(widget);
  bounds.Offset(-ui::GetWidgetScreenOffset(parent));
  return bounds;
}

// Returns a GdkPixbuf after resizing the SkBitmap as necessary to the
// specified desired width and height. Caller must g_object_unref the returned
// pixbuf when no longer used.
GdkPixbuf* GetResizedGdkPixbufFromSkBitmap(const SkBitmap& bitmap,
                                           int dest_w,
                                           int dest_h) {
  float float_dest_w = static_cast<float>(dest_w);
  float float_dest_h = static_cast<float>(dest_h);
  int bitmap_w = bitmap.width();
  int bitmap_h = bitmap.height();

  // Scale proportionately.
  float scale = std::min(float_dest_w / bitmap_w,
                         float_dest_h / bitmap_h);
  int final_dest_w = static_cast<int>(bitmap_w * scale);
  int final_dest_h = static_cast<int>(bitmap_h * scale);

  GdkPixbuf* pixbuf;
  if (final_dest_w == bitmap_w && final_dest_h == bitmap_h) {
    pixbuf = gfx::GdkPixbufFromSkBitmap(bitmap);
  } else {
    SkBitmap resized_icon = skia::ImageOperations::Resize(
        bitmap,
        skia::ImageOperations::RESIZE_BETTER,
        final_dest_w, final_dest_h);
    pixbuf = gfx::GdkPixbufFromSkBitmap(resized_icon);
  }
  return pixbuf;
}

}  // namespace

TabRendererGtk::LoadingAnimation::Data::Data(
    GtkThemeService* theme_service) {
  // The loading animation image is a strip of states. Each state must be
  // square, so the height must divide the width evenly.
  SkBitmap loading_animation_frames =
      theme_service->GetImageNamed(IDR_THROBBER).AsBitmap();
  DCHECK(!loading_animation_frames.isNull());
  DCHECK_EQ(loading_animation_frames.width() %
            loading_animation_frames.height(), 0);
  loading_animation_frame_count =
      loading_animation_frames.width() /
      loading_animation_frames.height();

  SkBitmap waiting_animation_frames =
      theme_service->GetImageNamed(IDR_THROBBER_WAITING).AsBitmap();
  DCHECK(!waiting_animation_frames.isNull());
  DCHECK_EQ(waiting_animation_frames.width() %
            waiting_animation_frames.height(), 0);
  waiting_animation_frame_count =
      waiting_animation_frames.width() /
      waiting_animation_frames.height();

  waiting_to_loading_frame_count_ratio =
      waiting_animation_frame_count /
      loading_animation_frame_count;
  // TODO(beng): eventually remove this when we have a proper themeing system.
  //             themes not supporting IDR_THROBBER_WAITING are causing this
  //             value to be 0 which causes DIV0 crashes. The value of 5
  //             matches the current bitmaps in our source.
  if (waiting_to_loading_frame_count_ratio == 0)
    waiting_to_loading_frame_count_ratio = 5;
}

TabRendererGtk::LoadingAnimation::Data::Data(
    int loading, int waiting, int waiting_to_loading)
    : loading_animation_frame_count(loading),
      waiting_animation_frame_count(waiting),
      waiting_to_loading_frame_count_ratio(waiting_to_loading) {
}

bool TabRendererGtk::initialized_ = false;
int TabRendererGtk::tab_active_l_width_ = 0;
int TabRendererGtk::tab_active_l_height_ = 0;
int TabRendererGtk::tab_inactive_l_height_ = 0;
gfx::Font* TabRendererGtk::title_font_ = NULL;
int TabRendererGtk::title_font_height_ = 0;
int TabRendererGtk::close_button_width_ = 0;
int TabRendererGtk::close_button_height_ = 0;

////////////////////////////////////////////////////////////////////////////////
// TabRendererGtk::LoadingAnimation, public:
//
TabRendererGtk::LoadingAnimation::LoadingAnimation(
    GtkThemeService* theme_service)
    : data_(new Data(theme_service)),
      theme_service_(theme_service),
      animation_state_(ANIMATION_NONE),
      animation_frame_(0) {
  registrar_.Add(this,
                 chrome::NOTIFICATION_BROWSER_THEME_CHANGED,
                 content::Source<ThemeService>(theme_service_));
}

TabRendererGtk::LoadingAnimation::LoadingAnimation(
    const LoadingAnimation::Data& data)
    : data_(new Data(data)),
      theme_service_(NULL),
      animation_state_(ANIMATION_NONE),
      animation_frame_(0) {
}

TabRendererGtk::LoadingAnimation::~LoadingAnimation() {}

bool TabRendererGtk::LoadingAnimation::ValidateLoadingAnimation(
    AnimationState animation_state) {
  bool has_changed = false;
  if (animation_state_ != animation_state) {
    // The waiting animation is the reverse of the loading animation, but at a
    // different rate - the following reverses and scales the animation_frame_
    // so that the frame is at an equivalent position when going from one
    // animation to the other.
    if (animation_state_ == ANIMATION_WAITING &&
        animation_state == ANIMATION_LOADING) {
      animation_frame_ = data_->loading_animation_frame_count -
          (animation_frame_ / data_->waiting_to_loading_frame_count_ratio);
    }
    animation_state_ = animation_state;
    has_changed = true;
  }

  if (animation_state_ != ANIMATION_NONE) {
    animation_frame_ = (animation_frame_ + 1) %
                       ((animation_state_ == ANIMATION_WAITING) ?
                         data_->waiting_animation_frame_count :
                         data_->loading_animation_frame_count);
    has_changed = true;
  } else {
    animation_frame_ = 0;
  }
  return has_changed;
}

void TabRendererGtk::LoadingAnimation::Observe(
    int type,
    const content::NotificationSource& source,
    const content::NotificationDetails& details) {
  DCHECK(type == chrome::NOTIFICATION_BROWSER_THEME_CHANGED);
  data_.reset(new Data(theme_service_));
}

TabRendererGtk::TabData::TabData()
    : is_default_favicon(false),
      loading(false),
      crashed(false),
      incognito(false),
      show_icon(true),
      mini(false),
      blocked(false),
      animating_mini_change(false),
      app(false),
      media_state(TAB_MEDIA_STATE_NONE),
      previous_media_state(TAB_MEDIA_STATE_NONE) {
}

TabRendererGtk::TabData::~TabData() {}

////////////////////////////////////////////////////////////////////////////////
// FaviconCrashAnimation
//
//  A custom animation subclass to manage the favicon crash animation.
class TabRendererGtk::FaviconCrashAnimation : public gfx::LinearAnimation,
                                              public gfx::AnimationDelegate {
 public:
  explicit FaviconCrashAnimation(TabRendererGtk* target)
      : gfx::LinearAnimation(1000, 25, this),
        target_(target) {
  }
  virtual ~FaviconCrashAnimation() {}

  // gfx::Animation overrides:
  virtual void AnimateToState(double state) OVERRIDE {
    const double kHidingOffset = 27;

    if (state < .5) {
      target_->SetFaviconHidingOffset(
          static_cast<int>(floor(kHidingOffset * 2.0 * state)));
    } else {
      target_->DisplayCrashedFavicon();
      target_->SetFaviconHidingOffset(
          static_cast<int>(
              floor(kHidingOffset - ((state - .5) * 2.0 * kHidingOffset))));
    }
  }

  // gfx::AnimationDelegate overrides:
  virtual void AnimationCanceled(const gfx::Animation* animation) OVERRIDE {
    target_->SetFaviconHidingOffset(0);
  }

 private:
  TabRendererGtk* target_;

  DISALLOW_COPY_AND_ASSIGN(FaviconCrashAnimation);
};

////////////////////////////////////////////////////////////////////////////////
// TabRendererGtk, public:

TabRendererGtk::TabRendererGtk(GtkThemeService* theme_service)
    : showing_icon_(false),
      showing_media_indicator_(false),
      showing_close_button_(false),
      favicon_hiding_offset_(0),
      should_display_crashed_favicon_(false),
      animating_media_state_(TAB_MEDIA_STATE_NONE),
      loading_animation_(theme_service),
      background_offset_x_(0),
      background_offset_y_(kInactiveTabBackgroundOffsetY),
      theme_service_(theme_service),
      close_button_color_(0),
      is_active_(false),
      selected_title_color_(SK_ColorBLACK),
      unselected_title_color_(SkColorSetRGB(64, 64, 64)) {
  InitResources();

  theme_service_->InitThemesFor(this);
  registrar_.Add(this, chrome::NOTIFICATION_BROWSER_THEME_CHANGED,
                 content::Source<ThemeService>(theme_service_));

  tab_.Own(gtk_fixed_new());
  gtk_widget_set_app_paintable(tab_.get(), TRUE);
  g_signal_connect(tab_.get(), "expose-event",
                   G_CALLBACK(OnExposeEventThunk), this);
  g_signal_connect(tab_.get(), "size-allocate",
                   G_CALLBACK(OnSizeAllocateThunk), this);
  close_button_.reset(MakeCloseButton());
  gtk_widget_show(tab_.get());

  hover_animation_.reset(new gfx::SlideAnimation(this));
  hover_animation_->SetSlideDuration(kHoverDurationMs);
}

TabRendererGtk::~TabRendererGtk() {
  tab_.Destroy();
}

void TabRendererGtk::Observe(int type,
                             const content::NotificationSource& source,
                             const content::NotificationDetails& details) {
  DCHECK(chrome::NOTIFICATION_BROWSER_THEME_CHANGED);
  selected_title_color_ =
      theme_service_->GetColor(ThemeProperties::COLOR_TAB_TEXT);
  unselected_title_color_ =
      theme_service_->GetColor(ThemeProperties::COLOR_BACKGROUND_TAB_TEXT);
}

void TabRendererGtk::UpdateData(WebContents* contents,
                                bool app,
                                bool loading_only) {
  DCHECK(contents);
  FaviconTabHelper* favicon_tab_helper =
      FaviconTabHelper::FromWebContents(contents);

  if (!loading_only) {
    data_.title = contents->GetTitle();
    data_.incognito = contents->GetBrowserContext()->IsOffTheRecord();

    TabMediaState next_media_state;
    if (contents->IsCrashed()) {
      data_.crashed = true;
      next_media_state = TAB_MEDIA_STATE_NONE;
    } else {
      data_.crashed = false;
      next_media_state = chrome::GetTabMediaStateForContents(contents);
    }
    if (data_.media_state != next_media_state) {
      data_.previous_media_state = data_.media_state;
      data_.media_state = next_media_state;
    }

    data_.favicon = favicon_tab_helper->GetFavicon().AsBitmap();
    data_.app = app;

    // Make a cairo cached version of the favicon.
    if (!data_.favicon.isNull()) {
      // Instead of resizing the icon during each frame, create our resized
      // icon resource now, send it to the xserver and use that each frame
      // instead.

      // For source images smaller than the favicon square, scale them as if
      // they were padded to fit the favicon square, so we don't blow up tiny
      // favicons into larger or nonproportional results.
      GdkPixbuf* pixbuf = GetResizedGdkPixbufFromSkBitmap(data_.favicon,
          gfx::kFaviconSize, gfx::kFaviconSize);
      data_.cairo_favicon.UsePixbuf(pixbuf);
      g_object_unref(pixbuf);
    } else {
      data_.cairo_favicon.Reset();
    }

    // This is kind of a hacky way to determine whether our icon is the default
    // favicon. But the plumbing that would be necessary to do it right would
    // be a good bit of work and would sully code for other platforms which
    // don't care to custom-theme the favicon. Hopefully the default favicon
    // will eventually be chromium-themable and this code will go away.
    data_.is_default_favicon =
        (data_.favicon.pixelRef() ==
        ui::ResourceBundle::GetSharedInstance().GetImageNamed(
            IDR_DEFAULT_FAVICON).AsBitmap().pixelRef());
  }

  // Loading state also involves whether we show the favicon, since that's where
  // we display the throbber.
  data_.loading = contents->IsLoading();
  data_.show_icon = favicon_tab_helper->ShouldDisplayFavicon();
}

void TabRendererGtk::UpdateFromModel() {
  // Force a layout, since the tab may have grown a favicon.
  Layout();
  SchedulePaint();

  if (data_.crashed) {
    if (!should_display_crashed_favicon_ && !IsPerformingCrashAnimation())
      StartCrashAnimation();
  } else {
    if (IsPerformingCrashAnimation())
      StopCrashAnimation();
    ResetCrashedFavicon();
  }

  if (data_.media_state != data_.previous_media_state) {
    data_.previous_media_state = data_.media_state;
    if (data_.media_state != TAB_MEDIA_STATE_NONE)
      animating_media_state_ = data_.media_state;
    StartMediaIndicatorAnimation();
  }
}

void TabRendererGtk::SetBlocked(bool blocked) {
  if (data_.blocked == blocked)
    return;
  data_.blocked = blocked;
  // TODO(zelidrag) bug 32399: Make tabs pulse on Linux as well.
}

bool TabRendererGtk::is_blocked() const {
  return data_.blocked;
}

bool TabRendererGtk::IsActive() const {
  return is_active_;
}

bool TabRendererGtk::IsSelected() const {
  return true;
}

bool TabRendererGtk::IsVisible() const {
  return gtk_widget_get_visible(tab_.get());
}

void TabRendererGtk::SetVisible(bool visible) const {
  if (visible) {
    gtk_widget_show(tab_.get());
    if (data_.mini)
      gtk_widget_show(close_button_->widget());
  } else {
    gtk_widget_hide_all(tab_.get());
  }
}

bool TabRendererGtk::ValidateLoadingAnimation(AnimationState animation_state) {
  return loading_animation_.ValidateLoadingAnimation(animation_state);
}

void TabRendererGtk::PaintFaviconArea(GtkWidget* widget, cairo_t* cr) {
  DCHECK(ShouldShowIcon());

  cairo_rectangle(cr,
                  x() + favicon_bounds_.x(),
                  y() + favicon_bounds_.y(),
                  favicon_bounds_.width(),
                  favicon_bounds_.height());
  cairo_clip(cr);

  // The tab is rendered into a windowless widget whose offset is at the
  // coordinate event->area.  Translate by these offsets so we can render at
  // (0,0) to match Windows' rendering metrics.
  cairo_matrix_t cairo_matrix;
  cairo_matrix_init_translate(&cairo_matrix, x(), y());
  cairo_set_matrix(cr, &cairo_matrix);

  // Which background should we be painting?
  int theme_id;
  int offset_y = 0;
  if (IsActive()) {
    theme_id = IDR_THEME_TOOLBAR;
  } else {
    theme_id = data_.incognito ? IDR_THEME_TAB_BACKGROUND_INCOGNITO :
               IDR_THEME_TAB_BACKGROUND;

    if (!theme_service_->HasCustomImage(theme_id))
      offset_y = background_offset_y_;
  }

  // Paint the background behind the favicon.
  const gfx::Image tab_bg = theme_service_->GetImageNamed(theme_id);
  tab_bg.ToCairo()->SetSource(cr, widget, -x(), -offset_y);
  cairo_pattern_set_extend(cairo_get_source(cr), CAIRO_EXTEND_REPEAT);
  cairo_rectangle(cr,
                  favicon_bounds_.x(), favicon_bounds_.y(),
                  favicon_bounds_.width(), favicon_bounds_.height());
  cairo_fill(cr);

  if (!IsActive()) {
    double throb_value = GetThrobValue();
    if (throb_value > 0) {
      cairo_push_group(cr);
      gfx::Image active_bg =
          theme_service_->GetImageNamed(IDR_THEME_TOOLBAR);
      active_bg.ToCairo()->SetSource(cr, widget, -x(), 0);
      cairo_pattern_set_extend(cairo_get_source(cr), CAIRO_EXTEND_REPEAT);

      cairo_rectangle(cr,
                      favicon_bounds_.x(), favicon_bounds_.y(),
                      favicon_bounds_.width(), favicon_bounds_.height());
      cairo_fill(cr);

      cairo_pop_group_to_source(cr);
      cairo_paint_with_alpha(cr, throb_value);
    }
  }

  PaintIcon(widget, cr);
}

void TabRendererGtk::MaybeAdjustLeftForMiniTab(gfx::Rect* icon_bounds) const {
  if (!(mini() || data_.animating_mini_change) ||
      bounds_.width() >= kMiniTabRendererAsNormalTabWidth)
    return;
  const int mini_delta = kMiniTabRendererAsNormalTabWidth - GetMiniWidth();
  const int ideal_delta = bounds_.width() - GetMiniWidth();
  const int ideal_x = (GetMiniWidth() - icon_bounds->width()) / 2;
  icon_bounds->set_x(icon_bounds->x() + static_cast<int>(
      (1 - static_cast<float>(ideal_delta) / static_cast<float>(mini_delta)) *
      (ideal_x - icon_bounds->x())));
}

// static
gfx::Size TabRendererGtk::GetMinimumUnselectedSize() {
  InitResources();

  gfx::Size minimum_size;
  minimum_size.set_width(kLeftPadding + kRightPadding);
  // Since we use bitmap images, the real minimum height of the image is
  // defined most accurately by the height of the end cap images.
  minimum_size.set_height(tab_active_l_height_ - kToolbarOverlap);
  return minimum_size;
}

// static
gfx::Size TabRendererGtk::GetMinimumSelectedSize() {
  gfx::Size minimum_size = GetMinimumUnselectedSize();
  minimum_size.set_width(kLeftPadding + gfx::kFaviconSize + kRightPadding);
  return minimum_size;
}

// static
gfx::Size TabRendererGtk::GetStandardSize() {
  gfx::Size standard_size = GetMinimumUnselectedSize();
  standard_size.Enlarge(kFaviconTitleSpacing + kStandardTitleWidth, 0);
  return standard_size;
}

// static
int TabRendererGtk::GetMiniWidth() {
  return browser_defaults::kMiniTabWidth;
}

// static
int TabRendererGtk::GetContentHeight() {
  // The height of the content of the Tab is the largest of the favicon,
  // the title text and the close button graphic.
  int content_height = std::max(gfx::kFaviconSize, title_font_height_);
  return std::max(content_height, close_button_height_);
}

gfx::Rect TabRendererGtk::GetNonMirroredBounds(GtkWidget* parent) const {
  // The tabstrip widget is a windowless widget so the tab widget's allocation
  // is relative to the browser titlebar.  We need the bounds relative to the
  // tabstrip.
  gfx::Rect bounds = GetWidgetBoundsRelativeToParent(parent, widget());
  bounds.set_x(gtk_util::MirroredLeftPointForRect(parent, bounds));
  return bounds;
}

gfx::Rect TabRendererGtk::GetRequisition() const {
  return gfx::Rect(requisition_.x(), requisition_.y(),
                   requisition_.width(), requisition_.height());
}

void TabRendererGtk::StartMiniTabTitleAnimation() {
  if (!mini_title_animation_.get()) {
    mini_title_animation_.reset(new gfx::ThrobAnimation(this));
    mini_title_animation_->SetThrobDuration(kMiniTitleChangeThrobDuration);
  }

  if (!mini_title_animation_->is_animating())
    mini_title_animation_->StartThrobbing(-1);
}

void TabRendererGtk::StopMiniTabTitleAnimation() {
  if (mini_title_animation_.get())
    mini_title_animation_->Stop();
}

void TabRendererGtk::SetBounds(const gfx::Rect& bounds) {
  requisition_ = bounds;
  gtk_widget_set_size_request(tab_.get(), bounds.width(), bounds.height());
}

////////////////////////////////////////////////////////////////////////////////
// TabRendererGtk, protected:

void TabRendererGtk::Raise() const {
  if (gtk_button_get_event_window(GTK_BUTTON(close_button_->widget())))
    gdk_window_raise(gtk_button_get_event_window(
        GTK_BUTTON(close_button_->widget())));
}

base::string16 TabRendererGtk::GetTitle() const {
  return data_.title;
}

///////////////////////////////////////////////////////////////////////////////
// TabRendererGtk, gfx::AnimationDelegate implementation:

void TabRendererGtk::AnimationProgressed(const gfx::Animation* animation) {
  gtk_widget_queue_draw(tab_.get());
}

void TabRendererGtk::AnimationCanceled(const gfx::Animation* animation) {
  if (media_indicator_animation_ == animation)
    animating_media_state_ = data_.media_state;
  AnimationEnded(animation);
}

void TabRendererGtk::AnimationEnded(const gfx::Animation* animation) {
  if (media_indicator_animation_ == animation)
    animating_media_state_ = data_.media_state;
  gtk_widget_queue_draw(tab_.get());
}

////////////////////////////////////////////////////////////////////////////////
// TabRendererGtk, private:

void TabRendererGtk::StartCrashAnimation() {
  if (!crash_animation_.get())
    crash_animation_.reset(new FaviconCrashAnimation(this));
  crash_animation_->Stop();
  crash_animation_->Start();
}

void TabRendererGtk::StopCrashAnimation() {
  if (!crash_animation_.get())
    return;
  crash_animation_->Stop();
}

bool TabRendererGtk::IsPerformingCrashAnimation() const {
  return crash_animation_.get() && crash_animation_->is_animating();
}

void TabRendererGtk::StartMediaIndicatorAnimation() {
  media_indicator_animation_ =
      chrome::CreateTabMediaIndicatorFadeAnimation(data_.media_state);
  media_indicator_animation_->set_delegate(this);
  media_indicator_animation_->Start();
}

void TabRendererGtk::SetFaviconHidingOffset(int offset) {
  favicon_hiding_offset_ = offset;
  SchedulePaint();
}

void TabRendererGtk::DisplayCrashedFavicon() {
  should_display_crashed_favicon_ = true;
}

void TabRendererGtk::ResetCrashedFavicon() {
  should_display_crashed_favicon_ = false;
}

void TabRendererGtk::Paint(GtkWidget* widget, cairo_t* cr) {
  // Don't paint if we're narrower than we can render correctly. (This should
  // only happen during animations).
  if (width() < GetMinimumUnselectedSize().width() && !mini())
    return;

  // See if the model changes whether the icons should be painted.
  const bool show_icon = ShouldShowIcon();
  const bool show_media_indicator = ShouldShowMediaIndicator();
  const bool show_close_button = ShouldShowCloseBox();
  if (show_icon != showing_icon_ ||
      show_media_indicator != showing_media_indicator_ ||
      show_close_button != showing_close_button_)
    Layout();

  PaintTabBackground(widget, cr);

  if (!mini() || width() > kMiniTabRendererAsNormalTabWidth)
    PaintTitle(widget, cr);

  if (show_icon)
    PaintIcon(widget, cr);

  if (show_media_indicator)
    PaintMediaIndicator(widget, cr);
}

cairo_surface_t* TabRendererGtk::PaintToSurface(GtkWidget* widget,
                                                cairo_t* cr) {
  cairo_surface_t* target = cairo_get_target(cr);
  cairo_surface_t* out_surface = cairo_surface_create_similar(
      target,
      CAIRO_CONTENT_COLOR_ALPHA,
      width(), height());

  cairo_t* out_cr = cairo_create(out_surface);
  Paint(widget, out_cr);
  cairo_destroy(out_cr);

  return out_surface;
}

void TabRendererGtk::SchedulePaint() {
  gtk_widget_queue_draw(tab_.get());
}

gfx::Rect TabRendererGtk::GetLocalBounds() {
  return gfx::Rect(0, 0, bounds_.width(), bounds_.height());
}

void TabRendererGtk::Layout() {
  gfx::Rect local_bounds = GetLocalBounds();
  if (local_bounds.IsEmpty())
    return;
  local_bounds.Inset(kLeftPadding, kTopPadding, kRightPadding, kBottomPadding);

  // Figure out who is tallest.
  int content_height = GetContentHeight();

  // Size the Favicon.
  showing_icon_ = ShouldShowIcon();
  if (showing_icon_) {
    int favicon_top = kTopPadding + (content_height - gfx::kFaviconSize) / 2;
    favicon_bounds_.SetRect(local_bounds.x(), favicon_top,
                            gfx::kFaviconSize, gfx::kFaviconSize);
    MaybeAdjustLeftForMiniTab(&favicon_bounds_);
  } else {
    favicon_bounds_.SetRect(local_bounds.x(), local_bounds.y(), 0, 0);
  }

  // Size the Close button.
  showing_close_button_ = ShouldShowCloseBox();
  if (showing_close_button_) {
    int close_button_top = kTopPadding +
        (content_height - close_button_height_) / 2;
    int close_button_left =
        local_bounds.right() - close_button_width_ + kCloseButtonHorzFuzz;
    close_button_bounds_.SetRect(close_button_left,
                                 close_button_top,
                                 close_button_width_,
                                 close_button_height_);

    // If the close button color has changed, generate a new one.
    if (theme_service_) {
      SkColor tab_text_color =
          theme_service_->GetColor(ThemeProperties::COLOR_TAB_TEXT);
      if (!close_button_color_ || tab_text_color != close_button_color_) {
        close_button_color_ = tab_text_color;
        ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
        close_button_->SetBackground(close_button_color_,
            rb.GetImageNamed(IDR_CLOSE_1).AsBitmap(),
            rb.GetImageNamed(IDR_CLOSE_1_MASK).AsBitmap());
      }
    }
  } else {
    close_button_bounds_.SetRect(0, 0, 0, 0);
  }

  showing_media_indicator_ = ShouldShowMediaIndicator();
  if (showing_media_indicator_) {
    const gfx::Image& media_indicator_image =
        chrome::GetTabMediaIndicatorImage(animating_media_state_);
    media_indicator_bounds_.set_width(media_indicator_image.Width());
    media_indicator_bounds_.set_height(media_indicator_image.Height());
    media_indicator_bounds_.set_y(
        kTopPadding + (content_height - media_indicator_bounds_.height()) / 2);
    const int right = showing_close_button_ ?
        close_button_bounds_.x() : local_bounds.right();
    media_indicator_bounds_.set_x(std::max(
        local_bounds.x(), right - media_indicator_bounds_.width()));
    MaybeAdjustLeftForMiniTab(&media_indicator_bounds_);
  } else {
    media_indicator_bounds_.SetRect(local_bounds.x(), local_bounds.y(), 0, 0);
  }

  if (!mini() || width() >= kMiniTabRendererAsNormalTabWidth) {
    // Size the Title text to fill the remaining space.
    int title_left = favicon_bounds_.right() + kFaviconTitleSpacing;
    int title_top = kTopPadding;

    // If the user has big fonts, the title will appear rendered too far down
    // on the y-axis if we use the regular top padding, so we need to adjust it
    // so that the text appears centered.
    gfx::Size minimum_size = GetMinimumUnselectedSize();
    int text_height = title_top + title_font_height_ + kBottomPadding;
    if (text_height > minimum_size.height())
      title_top -= (text_height - minimum_size.height()) / 2;

    int title_width;
    if (showing_media_indicator_) {
      title_width = media_indicator_bounds_.x() - kTitleCloseButtonSpacing -
          title_left;
    } else if (close_button_bounds_.width() && close_button_bounds_.height()) {
      title_width = close_button_bounds_.x() - kTitleCloseButtonSpacing -
          title_left;
    } else {
      title_width = local_bounds.width() - title_left;
    }
    title_width = std::max(title_width, 0);
    title_bounds_.SetRect(title_left, title_top, title_width, content_height);
  }

  favicon_bounds_.set_x(
      gtk_util::MirroredLeftPointForRect(tab_.get(), favicon_bounds_));
  media_indicator_bounds_.set_x(
      gtk_util::MirroredLeftPointForRect(tab_.get(), media_indicator_bounds_));
  close_button_bounds_.set_x(
      gtk_util::MirroredLeftPointForRect(tab_.get(), close_button_bounds_));
  title_bounds_.set_x(
      gtk_util::MirroredLeftPointForRect(tab_.get(), title_bounds_));

  MoveCloseButtonWidget();
}

void TabRendererGtk::MoveCloseButtonWidget() {
  if (!close_button_bounds_.IsEmpty()) {
    gtk_fixed_move(GTK_FIXED(tab_.get()), close_button_->widget(),
                   close_button_bounds_.x(), close_button_bounds_.y());
    gtk_widget_show(close_button_->widget());
  } else {
    gtk_widget_hide(close_button_->widget());
  }
}

void TabRendererGtk::PaintTab(GtkWidget* widget, GdkEventExpose* event) {
  cairo_t* cr = gdk_cairo_create(gtk_widget_get_window(widget));
  gdk_cairo_rectangle(cr, &event->area);
  cairo_clip(cr);

  // The tab is rendered into a windowless widget whose offset is at the
  // coordinate event->area.  Translate by these offsets so we can render at
  // (0,0) to match Windows' rendering metrics.
  cairo_matrix_t cairo_matrix;
  cairo_matrix_init_translate(&cairo_matrix, event->area.x, event->area.y);
  cairo_set_matrix(cr, &cairo_matrix);

  // Save the original x offset so we can position background images properly.
  background_offset_x_ = event->area.x;

  Paint(widget, cr);
  cairo_destroy(cr);
}

void TabRendererGtk::PaintTitle(GtkWidget* widget, cairo_t* cr) {
  if (title_bounds_.IsEmpty())
    return;

  // Paint the Title.
  base::string16 title = data_.title;
  if (title.empty()) {
    title = data_.loading ?
        l10n_util::GetStringUTF16(IDS_TAB_LOADING_TITLE) :
        CoreTabHelper::GetDefaultTitle();
  } else {
    Browser::FormatTitleForDisplay(&title);
  }

  GtkAllocation allocation;
  gtk_widget_get_allocation(widget, &allocation);
  gfx::Rect bounds(allocation);

  // Draw the text directly onto the Cairo context. This is necessary for
  // getting the draw order correct, and automatically applying transformations
  // such as scaling when a tab is detached.
  gfx::CanvasSkiaPaintCairo canvas(cr, bounds.size(), true);

  SkColor title_color = IsSelected() ? selected_title_color_
                                     : unselected_title_color_;

  // Disable subpixel rendering. This does not work because the canvas has a
  // transparent background.
  int flags = gfx::Canvas::NO_ELLIPSIS | gfx::Canvas::NO_SUBPIXEL_RENDERING;
  canvas.DrawFadeTruncatingStringRectWithFlags(
      title, gfx::Canvas::TruncateFadeTail, gfx::FontList(*title_font_),
      title_color, title_bounds_, flags);
}

void TabRendererGtk::PaintIcon(GtkWidget* widget, cairo_t* cr) {
  if (loading_animation_.animation_state() != ANIMATION_NONE) {
    PaintLoadingAnimation(widget, cr);
    return;
  }

  gfx::CairoCachedSurface* to_display = NULL;
  if (should_display_crashed_favicon_) {
    to_display = theme_service_->GetImageNamed(IDR_SAD_FAVICON).ToCairo();
  } else if (!data_.favicon.isNull()) {
    if (data_.is_default_favicon && theme_service_->UsingNativeTheme()) {
      to_display = GtkThemeService::GetDefaultFavicon(true).ToCairo();
    } else if (data_.cairo_favicon.valid()) {
      to_display = &data_.cairo_favicon;
    }
  }

  if (to_display) {
    to_display->SetSource(cr, widget, favicon_bounds_.x(),
                          favicon_bounds_.y() + favicon_hiding_offset_);
    cairo_paint(cr);
  }
}

void TabRendererGtk::PaintMediaIndicator(GtkWidget* widget, cairo_t* cr) {
  if (media_indicator_bounds_.IsEmpty() || !media_indicator_animation_)
    return;

  double opaqueness = media_indicator_animation_->GetCurrentValue();
  if (data_.media_state == TAB_MEDIA_STATE_NONE)
    opaqueness = 1.0 - opaqueness;  // Fading out, not in.

  const gfx::Image& media_indicator_image =
      chrome::GetTabMediaIndicatorImage(animating_media_state_);
  media_indicator_image.ToCairo()->SetSource(
      cr, widget, media_indicator_bounds_.x(), media_indicator_bounds_.y());
  cairo_paint_with_alpha(cr, opaqueness);
}

void TabRendererGtk::PaintTabBackground(GtkWidget* widget, cairo_t* cr) {
  if (IsActive()) {
    PaintActiveTabBackground(widget, cr);
  } else {
    PaintInactiveTabBackground(widget, cr);

    double throb_value = GetThrobValue();
    if (throb_value > 0) {
      cairo_push_group(cr);
      PaintActiveTabBackground(widget, cr);
      cairo_pop_group_to_source(cr);
      cairo_paint_with_alpha(cr, throb_value);
    }
  }
}

void TabRendererGtk::DrawTabBackground(
    cairo_t* cr,
    GtkWidget* widget,
    const gfx::Image& tab_bg,
    int offset_x,
    int offset_y) {
  tab_bg.ToCairo()->SetSource(cr, widget, -offset_x, -offset_y);
  cairo_pattern_set_extend(cairo_get_source(cr), CAIRO_EXTEND_REPEAT);

  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();

  // Draw left edge
  gfx::Image& tab_l_mask = rb.GetNativeImageNamed(IDR_TAB_ALPHA_LEFT);
  tab_l_mask.ToCairo()->MaskSource(cr, widget, 0, 0);

  // Draw center
  cairo_rectangle(cr,
                  tab_active_l_width_, kDropShadowOffset,
                  width() - (2 * tab_active_l_width_),
                  tab_inactive_l_height_);
  cairo_fill(cr);

  // Draw right edge
  gfx::Image& tab_r_mask = rb.GetNativeImageNamed(IDR_TAB_ALPHA_RIGHT);
  tab_r_mask.ToCairo()->MaskSource(cr, widget,
                                    width() - tab_active_l_width_, 0);
}

void TabRendererGtk::DrawTabShadow(cairo_t* cr,
                                   GtkWidget* widget,
                                   int left_idr,
                                   int center_idr,
                                   int right_idr) {
  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
  gfx::Image& active_image_l = rb.GetNativeImageNamed(left_idr);
  gfx::Image& active_image_c = rb.GetNativeImageNamed(center_idr);
  gfx::Image& active_image_r = rb.GetNativeImageNamed(right_idr);

  // Draw left drop shadow
  active_image_l.ToCairo()->SetSource(cr, widget, 0, 0);
  cairo_paint(cr);

  // Draw the center shadow
  active_image_c.ToCairo()->SetSource(cr, widget, 0, 0);
  cairo_pattern_set_extend(cairo_get_source(cr), CAIRO_EXTEND_REPEAT);
  cairo_rectangle(cr, tab_active_l_width_, 0,
                  width() - (2 * tab_active_l_width_),
                  height());
  cairo_fill(cr);

  // Draw right drop shadow
  active_image_r.ToCairo()->SetSource(
      cr, widget, width() - active_image_r.ToCairo()->Width(), 0);
  cairo_paint(cr);
}

void TabRendererGtk::PaintInactiveTabBackground(GtkWidget* widget,
                                                cairo_t* cr) {
  int theme_id = data_.incognito ?
      IDR_THEME_TAB_BACKGROUND_INCOGNITO : IDR_THEME_TAB_BACKGROUND;

  gfx::Image tab_bg = theme_service_->GetImageNamed(theme_id);

  // If the theme is providing a custom background image, then its top edge
  // should be at the top of the tab. Otherwise, we assume that the background
  // image is a composited foreground + frame image.
  int offset_y = theme_service_->HasCustomImage(theme_id) ?
      0 : background_offset_y_;

  DrawTabBackground(cr, widget, tab_bg, background_offset_x_, offset_y);

  DrawTabShadow(cr, widget, IDR_TAB_INACTIVE_LEFT, IDR_TAB_INACTIVE_CENTER,
                IDR_TAB_INACTIVE_RIGHT);
}

void TabRendererGtk::PaintActiveTabBackground(GtkWidget* widget,
                                              cairo_t* cr) {
  gfx::Image tab_bg = theme_service_->GetImageNamed(IDR_THEME_TOOLBAR);

  DrawTabBackground(cr, widget, tab_bg, background_offset_x_, 0);
  DrawTabShadow(cr, widget, IDR_TAB_ACTIVE_LEFT, IDR_TAB_ACTIVE_CENTER,
                IDR_TAB_ACTIVE_RIGHT);
}

void TabRendererGtk::PaintLoadingAnimation(GtkWidget* widget,
                                           cairo_t* cr) {
  int id = loading_animation_.animation_state() == ANIMATION_WAITING ?
           IDR_THROBBER_WAITING : IDR_THROBBER;
  gfx::Image throbber = theme_service_->GetImageNamed(id);

  const int image_size = throbber.ToCairo()->Height();
  const int image_offset = loading_animation_.animation_frame() * image_size;
  DCHECK(image_size == favicon_bounds_.height());
  DCHECK(image_size == favicon_bounds_.width());

  throbber.ToCairo()->SetSource(cr, widget, favicon_bounds_.x() - image_offset,
                                 favicon_bounds_.y());
  cairo_rectangle(cr, favicon_bounds_.x(), favicon_bounds_.y(),
                  image_size, image_size);
  cairo_fill(cr);
}

int TabRendererGtk::IconCapacity() const {
  if (height() < GetMinimumUnselectedSize().height())
    return 0;
  const int available_width =
      std::max(0, width() - kLeftPadding - kRightPadding);
  const int kPaddingBetweenIcons = 2;
  if (available_width >= gfx::kFaviconSize &&
      available_width < (gfx::kFaviconSize + kPaddingBetweenIcons)) {
    return 1;
  }
  return available_width / (gfx::kFaviconSize + kPaddingBetweenIcons);
}

bool TabRendererGtk::ShouldShowIcon() const {
  return chrome::ShouldTabShowFavicon(
      IconCapacity(), mini(), IsActive(), data_.show_icon,
      animating_media_state_);
}

bool TabRendererGtk::ShouldShowMediaIndicator() const {
  return chrome::ShouldTabShowMediaIndicator(
      IconCapacity(), mini(), IsActive(), data_.show_icon,
      animating_media_state_);
}

bool TabRendererGtk::ShouldShowCloseBox() const {
  return chrome::ShouldTabShowCloseButton(IconCapacity(), mini(), IsActive());
}

CustomDrawButton* TabRendererGtk::MakeCloseButton() {
  CustomDrawButton* button = CustomDrawButton::CloseButtonBar(theme_service_);
  button->ForceChromeTheme();

  g_signal_connect(button->widget(), "clicked",
                   G_CALLBACK(OnCloseButtonClickedThunk), this);
  g_signal_connect(button->widget(), "button-release-event",
                   G_CALLBACK(OnCloseButtonMouseReleaseThunk), this);
  g_signal_connect(button->widget(), "enter-notify-event",
                   G_CALLBACK(OnEnterNotifyEventThunk), this);
  g_signal_connect(button->widget(), "leave-notify-event",
                   G_CALLBACK(OnLeaveNotifyEventThunk), this);
  gtk_widget_set_can_focus(button->widget(), FALSE);
  gtk_fixed_put(GTK_FIXED(tab_.get()), button->widget(), 0, 0);

  return button;
}

double TabRendererGtk::GetThrobValue() {
  bool is_selected = IsSelected();
  double min = is_selected ? kSelectedTabOpacity : 0;
  double scale = is_selected ? kSelectedTabThrobScale : 1;

  if (mini_title_animation_.get() && mini_title_animation_->is_animating()) {
    return mini_title_animation_->GetCurrentValue() *
        kMiniTitleChangeThrobOpacity * scale + min;
  }

  if (hover_animation_.get())
    return kHoverOpacity * hover_animation_->GetCurrentValue() * scale + min;

  return is_selected ? kSelectedTabOpacity : 0;
}

void TabRendererGtk::CloseButtonClicked() {
  // Nothing to do.
}

void TabRendererGtk::OnCloseButtonClicked(GtkWidget* widget) {
  CloseButtonClicked();
}

gboolean TabRendererGtk::OnCloseButtonMouseRelease(GtkWidget* widget,
                                                   GdkEventButton* event) {
  if (event->button == 2) {
    CloseButtonClicked();
    return TRUE;
  }

  return FALSE;
}

gboolean TabRendererGtk::OnExposeEvent(GtkWidget* widget,
                                       GdkEventExpose* event) {
  TRACE_EVENT0("ui::gtk", "TabRendererGtk::OnExposeEvent");

  PaintTab(widget, event);
  gtk_container_propagate_expose(GTK_CONTAINER(tab_.get()),
                                 close_button_->widget(), event);
  return TRUE;
}

void TabRendererGtk::OnSizeAllocate(GtkWidget* widget,
                                    GtkAllocation* allocation) {
  gfx::Rect bounds = gfx::Rect(allocation->x, allocation->y,
                               allocation->width, allocation->height);

  // Nothing to do if the bounds are the same.  If we don't catch this, we'll
  // get an infinite loop of size-allocate signals.
  if (bounds_ == bounds)
    return;

  bounds_ = bounds;
  Layout();
}

gboolean TabRendererGtk::OnEnterNotifyEvent(GtkWidget* widget,
                                            GdkEventCrossing* event) {
  hover_animation_->SetTweenType(gfx::Tween::EASE_OUT);
  hover_animation_->Show();
  return FALSE;
}

gboolean TabRendererGtk::OnLeaveNotifyEvent(GtkWidget* widget,
                                            GdkEventCrossing* event) {
  hover_animation_->SetTweenType(gfx::Tween::EASE_IN);
  hover_animation_->Hide();
  return FALSE;
}

// static
void TabRendererGtk::InitResources() {
  if (initialized_)
    return;

  // Grab the pixel sizes of our masking images.
  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
  GdkPixbuf* tab_active_l = rb.GetNativeImageNamed(
      IDR_TAB_ACTIVE_LEFT).ToGdkPixbuf();
  tab_active_l_width_ = gdk_pixbuf_get_width(tab_active_l);
  tab_active_l_height_ = gdk_pixbuf_get_height(tab_active_l);

  GdkPixbuf* tab_inactive_l = rb.GetNativeImageNamed(
      IDR_TAB_INACTIVE_LEFT).ToGdkPixbuf();
  tab_inactive_l_height_ = gdk_pixbuf_get_height(tab_inactive_l);

  GdkPixbuf* tab_close = rb.GetNativeImageNamed(IDR_CLOSE_1).ToGdkPixbuf();
  close_button_width_ = gdk_pixbuf_get_width(tab_close);
  close_button_height_ = gdk_pixbuf_get_height(tab_close);

  const gfx::Font& base_font = rb.GetFont(ui::ResourceBundle::BaseFont);
  title_font_ = new gfx::Font(base_font.GetFontName(), kFontPixelSize);
  title_font_height_ = title_font_->GetHeight();

  initialized_ = true;
}

/* [<][>][^][v][top][bottom][index][help] */