// 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 #include #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(dest_w); float float_dest_h = static_cast(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(bitmap_w * scale); int final_dest_h = static_cast(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(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(floor(kHidingOffset * 2.0 * state))); } else { target_->DisplayCrashedFavicon(); target_->SetFaviconHidingOffset( static_cast( 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(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; } SkBitmap* app_icon = extensions::TabHelper::FromWebContents(contents)->GetExtensionAppIcon(); if (app_icon) { data_.favicon = *app_icon; } else { 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( (1 - static_cast(ideal_delta) / static_cast(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; }