// Copyright (c) 2010 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/gtk/tabs/tab_renderer_gtk.h" #include #include #include "app/l10n_util.h" #include "app/resource_bundle.h" #include "app/throb_animation.h" #include "chrome/browser/browser.h" #include "chrome/browser/defaults.h" #include "chrome/browser/gtk/bookmark_utils_gtk.h" #include "chrome/browser/gtk/custom_button.h" #include "chrome/browser/gtk/gtk_theme_provider.h" #include "chrome/browser/gtk/gtk_util.h" #include "chrome/browser/profile.h" #include "chrome/browser/tab_contents/tab_contents.h" #include "chrome/common/notification_service.h" #include "gfx/canvas_skia_paint.h" #include "gfx/favicon_size.h" #include "gfx/skbitmap_operations.h" #include "grit/app_resources.h" #include "grit/generated_resources.h" #include "grit/theme_resources.h" namespace { const int kLeftPadding = 16; const int kTopPadding = 6; const int kRightPadding = 15; const int kBottomPadding = 5; const int kDropShadowHeight = 2; 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; // 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; const SkScalar kTabCapWidth = 15; const SkScalar kTabTopCurveWidth = 4; const SkScalar kTabBottomCurveWidth = 3; // The vertical and horizontal offset used to position the close button // in the tab. TODO(jhawkins): Ask pkasting what the Fuzz is about. const int kCloseButtonVertFuzz = 0; const int kCloseButtonHorzFuzz = 5; SkBitmap* crashed_fav_icon = NULL; // Gets the bounds of |widget| relative to |parent|. gfx::Rect GetWidgetBoundsRelativeToParent(GtkWidget* parent, GtkWidget* widget) { gfx::Point parent_pos = gtk_util::GetWidgetScreenPosition(parent); gfx::Point widget_pos = gtk_util::GetWidgetScreenPosition(widget); return gfx::Rect(widget_pos.x() - parent_pos.x(), widget_pos.y() - parent_pos.y(), widget->allocation.width, widget->allocation.height); } } // namespace TabRendererGtk::LoadingAnimation::Data::Data(ThemeProvider* theme_provider) { // The loading animation image is a strip of states. Each state must be // square, so the height must divide the width evenly. loading_animation_frames = theme_provider->GetBitmapNamed(IDR_THROBBER); DCHECK(loading_animation_frames); DCHECK_EQ(loading_animation_frames->width() % loading_animation_frames->height(), 0); loading_animation_frame_count = loading_animation_frames->width() / loading_animation_frames->height(); waiting_animation_frames = theme_provider->GetBitmapNamed(IDR_THROBBER_WAITING); DCHECK(waiting_animation_frames); 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) : waiting_animation_frames(NULL), loading_animation_frames(NULL), loading_animation_frame_count(loading), waiting_animation_frame_count(waiting), waiting_to_loading_frame_count_ratio(waiting_to_loading) { } bool TabRendererGtk::initialized_ = false; TabRendererGtk::TabImage TabRendererGtk::tab_active_ = {0}; TabRendererGtk::TabImage TabRendererGtk::tab_inactive_ = {0}; TabRendererGtk::TabImage TabRendererGtk::tab_alpha_ = {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; SkColor TabRendererGtk::selected_title_color_ = SK_ColorBLACK; SkColor TabRendererGtk::unselected_title_color_ = SkColorSetRGB(64, 64, 64); //////////////////////////////////////////////////////////////////////////////// // TabRendererGtk::LoadingAnimation, public: // TabRendererGtk::LoadingAnimation::LoadingAnimation( ThemeProvider* theme_provider) : data_(new Data(theme_provider)), theme_provider_(theme_provider), animation_state_(ANIMATION_NONE), animation_frame_(0) { registrar_.Add(this, NotificationType::BROWSER_THEME_CHANGED, NotificationService::AllSources()); } TabRendererGtk::LoadingAnimation::LoadingAnimation( const LoadingAnimation::Data& data) : data_(new Data(data)), theme_provider_(NULL), animation_state_(ANIMATION_NONE), animation_frame_(0) { } 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( NotificationType type, const NotificationSource& source, const NotificationDetails& details) { DCHECK(type == NotificationType::BROWSER_THEME_CHANGED); data_.reset(new Data(theme_provider_)); } //////////////////////////////////////////////////////////////////////////////// // FaviconCrashAnimation // // A custom animation subclass to manage the favicon crash animation. class TabRendererGtk::FavIconCrashAnimation : public LinearAnimation, public AnimationDelegate { public: explicit FavIconCrashAnimation(TabRendererGtk* target) : ALLOW_THIS_IN_INITIALIZER_LIST(LinearAnimation(1000, 25, this)), target_(target) { } virtual ~FavIconCrashAnimation() {} // Animation overrides: virtual void AnimateToState(double state) { 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)))); } } // AnimationDelegate overrides: virtual void AnimationCanceled(const Animation* animation) { target_->SetFavIconHidingOffset(0); } private: TabRendererGtk* target_; DISALLOW_COPY_AND_ASSIGN(FavIconCrashAnimation); }; //////////////////////////////////////////////////////////////////////////////// // TabRendererGtk, public: TabRendererGtk::TabRendererGtk(ThemeProvider* theme_provider) : showing_icon_(false), showing_close_button_(false), fav_icon_hiding_offset_(0), should_display_crashed_favicon_(false), loading_animation_(theme_provider), background_offset_x_(0), background_offset_y_(kInactiveTabBackgroundOffsetY), close_button_color_(0) { InitResources(); 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 SlideAnimation(this)); hover_animation_->SetSlideDuration(kHoverDurationMs); registrar_.Add(this, NotificationType::BROWSER_THEME_CHANGED, NotificationService::AllSources()); } TabRendererGtk::~TabRendererGtk() { tab_.Destroy(); for (BitmapCache::iterator it = cached_bitmaps_.begin(); it != cached_bitmaps_.end(); ++it) { delete it->second.bitmap; } } void TabRendererGtk::UpdateData(TabContents* contents, bool phantom, bool loading_only) { DCHECK(contents); theme_provider_ = GtkThemeProvider::GetFrom(contents->profile()); if (!loading_only) { data_.title = contents->GetTitle(); data_.off_the_record = contents->profile()->IsOffTheRecord(); data_.crashed = contents->is_crashed(); data_.favicon = contents->GetFavIcon(); data_.phantom = phantom; // 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() == ResourceBundle::GetSharedInstance().GetBitmapNamed( IDR_DEFAULT_FAVICON)->pixelRef()); } // Loading state also involves whether we show the favicon, since that's where // we display the throbber. data_.loading = contents->is_loading(); data_.show_icon = contents->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(); } } 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::IsSelected() const { return true; } bool TabRendererGtk::IsVisible() const { return GTK_WIDGET_FLAGS(tab_.get()) & GTK_VISIBLE; } 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(GdkEventExpose* event) { DCHECK(ShouldShowIcon()); // The paint area is the favicon bounds, but we're painting into the gdk // window belonging to the tabstrip. So the coordinates are relative to the // top left of the tab strip. event->area.x = x() + favicon_bounds_.x(); event->area.y = y() + favicon_bounds_.y(); event->area.width = favicon_bounds_.width(); event->area.height = favicon_bounds_.height(); gfx::CanvasSkiaPaint canvas(event, false); // The actual paint methods expect 0, 0 to be the tab top left (see // PaintTab). canvas.TranslateInt(x(), y()); // Paint the background behind the favicon. int theme_id; int offset_y = 0; if (IsSelected()) { theme_id = IDR_THEME_TOOLBAR; } else { if (!data_.off_the_record) { theme_id = IDR_THEME_TAB_BACKGROUND; } else { theme_id = IDR_THEME_TAB_BACKGROUND_INCOGNITO; } if (!theme_provider_->HasCustomImage(theme_id)) offset_y = background_offset_y_; } SkBitmap* tab_bg = theme_provider_->GetBitmapNamed(theme_id); canvas.TileImageInt(*tab_bg, x() + favicon_bounds_.x(), offset_y + favicon_bounds_.y(), favicon_bounds_.x(), favicon_bounds_.y(), favicon_bounds_.width(), favicon_bounds_.height()); if (!IsSelected()) { double throb_value = GetThrobValue(); if (throb_value > 0) { SkRect bounds; bounds.set(favicon_bounds_.x(), favicon_bounds_.y(), favicon_bounds_.right(), favicon_bounds_.bottom()); canvas.saveLayerAlpha(&bounds, static_cast(throb_value * 0xff), SkCanvas::kARGB_ClipLayer_SaveFlag); canvas.drawARGB(0, 255, 255, 255, SkXfermode::kClear_Mode); SkBitmap* active_bg = theme_provider_->GetBitmapNamed(IDR_THEME_TOOLBAR); canvas.TileImageInt(*active_bg, x() + favicon_bounds_.x(), favicon_bounds_.y(), favicon_bounds_.x(), favicon_bounds_.y(), favicon_bounds_.width(), favicon_bounds_.height()); canvas.restore(); } } // Now paint the icon. PaintIcon(&canvas); } // 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_.image_l->height() - kToolbarOverlap); return minimum_size; } // static gfx::Size TabRendererGtk::GetMinimumSelectedSize() { gfx::Size minimum_size = GetMinimumUnselectedSize(); minimum_size.set_width(kLeftPadding + 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(kFavIconSize, title_font_height_); return std::max(content_height, close_button_height_); } // static void TabRendererGtk::LoadTabImages() { ResourceBundle& rb = ResourceBundle::GetSharedInstance(); tab_alpha_.image_l = rb.GetBitmapNamed(IDR_TAB_ALPHA_LEFT); tab_alpha_.image_r = rb.GetBitmapNamed(IDR_TAB_ALPHA_RIGHT); tab_active_.image_l = rb.GetBitmapNamed(IDR_TAB_ACTIVE_LEFT); tab_active_.image_c = rb.GetBitmapNamed(IDR_TAB_ACTIVE_CENTER); tab_active_.image_r = rb.GetBitmapNamed(IDR_TAB_ACTIVE_RIGHT); tab_active_.l_width = tab_active_.image_l->width(); tab_active_.r_width = tab_active_.image_r->width(); tab_inactive_.image_l = rb.GetBitmapNamed(IDR_TAB_INACTIVE_LEFT); tab_inactive_.image_c = rb.GetBitmapNamed(IDR_TAB_INACTIVE_CENTER); tab_inactive_.image_r = rb.GetBitmapNamed(IDR_TAB_INACTIVE_RIGHT); tab_inactive_.l_width = tab_inactive_.image_l->width(); tab_inactive_.r_width = tab_inactive_.image_r->width(); close_button_width_ = rb.GetBitmapNamed(IDR_TAB_CLOSE)->width(); close_button_height_ = rb.GetBitmapNamed(IDR_TAB_CLOSE)->height(); } // static void TabRendererGtk::SetSelectedTitleColor(SkColor color) { selected_title_color_ = color; } // static void TabRendererGtk::SetUnselectedTitleColor(SkColor color) { unselected_title_color_ = color; } 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 ThrobAnimation(this)); mini_title_animation_->SetThrobDuration(kMiniTitleChangeThrobDuration); } if (!mini_title_animation_->is_animating()) { mini_title_animation_->StartThrobbing(2); } else if (mini_title_animation_->cycles_remaining() <= 2) { // The title changed while we're already animating. Add at most one more // cycle. This is done in an attempt to smooth out pages that continuously // change the title. mini_title_animation_->set_cycles_remaining( mini_title_animation_->cycles_remaining() + 2); } } 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()); } void TabRendererGtk::Observe(NotificationType type, const NotificationSource& source, const NotificationDetails& details) { DCHECK(type == NotificationType::BROWSER_THEME_CHANGED); // Clear our cache when we receive a theme change notification because it // contains cached bitmaps based off the previous theme. for (BitmapCache::iterator it = cached_bitmaps_.begin(); it != cached_bitmaps_.end(); ++it) { delete it->second.bitmap; } cached_bitmaps_.clear(); } //////////////////////////////////////////////////////////////////////////////// // TabRendererGtk, protected: std::wstring TabRendererGtk::GetTitle() const { return UTF16ToWideHack(data_.title); } /////////////////////////////////////////////////////////////////////////////// // TabRendererGtk, AnimationDelegate implementation: void TabRendererGtk::AnimationProgressed(const Animation* animation) { gtk_widget_queue_draw(tab_.get()); } void TabRendererGtk::AnimationCanceled(const Animation* animation) { AnimationEnded(animation); } void TabRendererGtk::AnimationEnded(const Animation* animation) { 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::SetFavIconHidingOffset(int offset) { fav_icon_hiding_offset_ = offset; SchedulePaint(); } void TabRendererGtk::DisplayCrashedFavIcon() { should_display_crashed_favicon_ = true; } void TabRendererGtk::ResetCrashedFavIcon() { should_display_crashed_favicon_ = false; } void TabRendererGtk::Paint(gfx::Canvas* canvas) { // 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_close_button = ShouldShowCloseBox(); if (show_icon != showing_icon_ || show_close_button != showing_close_button_) Layout(); if (!phantom()) { // TODO: this isn't quite right. To match the Windows side we need to render // phantom tabs to a separate layer than alpha composite that. This will do // for now though. PaintTabBackground(canvas); } if (!mini() || width() > kMiniTabRendererAsNormalTabWidth) PaintTitle(canvas); if (show_icon) PaintIcon(canvas); } SkBitmap TabRendererGtk::PaintBitmap() { gfx::CanvasSkia canvas(width(), height(), false); Paint(&canvas); return canvas.ExtractBitmap(); } cairo_surface_t* TabRendererGtk::PaintToSurface() { gfx::CanvasSkia canvas(width(), height(), false); Paint(&canvas); return cairo_surface_reference(cairo_get_target(canvas.beginPlatformPaint())); } 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 - kFavIconSize) / 2; favicon_bounds_.SetRect(local_bounds.x(), favicon_top, kFavIconSize, kFavIconSize); if ((mini() || data_.animating_mini_change) && bounds_.width() < kMiniTabRendererAsNormalTabWidth) { int mini_delta = kMiniTabRendererAsNormalTabWidth - GetMiniWidth(); int ideal_delta = bounds_.width() - GetMiniWidth(); if (ideal_delta < mini_delta) { int ideal_x = (GetMiniWidth() - kFavIconSize) / 2; int x = favicon_bounds_.x() + static_cast( (1 - static_cast(ideal_delta) / static_cast(mini_delta)) * (ideal_x - favicon_bounds_.x())); favicon_bounds_.set_x(x); } } } 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 + kCloseButtonVertFuzz + (content_height - close_button_height_) / 2; close_button_bounds_.SetRect(local_bounds.width() + kCloseButtonHorzFuzz, close_button_top, close_button_width_, close_button_height_); // If the close button color has changed, generate a new one. if (theme_provider_) { SkColor tab_text_color = theme_provider_->GetColor(BrowserThemeProvider::COLOR_TAB_TEXT); if (!close_button_color_ || tab_text_color != close_button_color_) { close_button_color_ = tab_text_color; ResourceBundle& rb = ResourceBundle::GetSharedInstance(); close_button_->SetBackground(close_button_color_, rb.GetBitmapNamed(IDR_TAB_CLOSE), rb.GetBitmapNamed(IDR_TAB_CLOSE_MASK)); } } } else { close_button_bounds_.SetRect(0, 0, 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 (close_button_bounds_.width() && close_button_bounds_.height()) { title_width = std::max(close_button_bounds_.x() - kTitleCloseButtonSpacing - title_left, 0); } else { title_width = std::max(local_bounds.width() - title_left, 0); } title_bounds_.SetRect(title_left, title_top, title_width, content_height); } favicon_bounds_.set_x( gtk_util::MirroredLeftPointForRect(tab_.get(), favicon_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()); } } SkBitmap* TabRendererGtk::GetMaskedBitmap(const SkBitmap* mask, const SkBitmap* background, int bg_offset_x, int bg_offset_y) { // We store a bitmap for each mask + background pair (4 total bitmaps). We // replace the cached image if the tab has moved relative to the background. BitmapCache::iterator it = cached_bitmaps_.find(std::make_pair(mask, background)); if (it != cached_bitmaps_.end()) { if (it->second.bg_offset_x == bg_offset_x && it->second.bg_offset_y == bg_offset_y) { return it->second.bitmap; } // The background offset changed so we should re-render with the new // offsets. delete it->second.bitmap; } SkBitmap image = SkBitmapOperations::CreateTiledBitmap( *background, bg_offset_x, bg_offset_y, mask->width(), height() + kToolbarOverlap); CachedBitmap bitmap = { bg_offset_x, bg_offset_y, new SkBitmap(SkBitmapOperations::CreateMaskedBitmap(image, *mask)) }; cached_bitmaps_[std::make_pair(mask, background)] = bitmap; return bitmap.bitmap; } void TabRendererGtk::PaintTab(GdkEventExpose* event) { gfx::CanvasSkiaPaint canvas(event, false); if (canvas.is_empty()) return; // 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. canvas.TranslateInt(event->area.x, event->area.y); // Save the original x offset so we can position background images properly. background_offset_x_ = event->area.x; Paint(&canvas); } void TabRendererGtk::PaintTitle(gfx::Canvas* canvas) { // Paint the Title. string16 title = data_.title; if (title.empty()) { title = data_.loading ? l10n_util::GetStringUTF16(IDS_TAB_LOADING_TITLE) : TabContents::GetDefaultTitle(); } else { Browser::FormatTitleForDisplay(&title); } SkColor title_color = IsSelected() ? selected_title_color_ : unselected_title_color_; canvas->DrawStringInt(UTF16ToWideHack(title), *title_font_, title_color, title_bounds_.x(), title_bounds_.y(), title_bounds_.width(), title_bounds_.height()); } void TabRendererGtk::PaintIcon(gfx::Canvas* canvas) { if (loading_animation_.animation_state() != ANIMATION_NONE) { PaintLoadingAnimation(canvas); } else { canvas->AsCanvasSkia()->save(); canvas->ClipRectInt(0, 0, width(), height() - kFavIconTitleSpacing); if (should_display_crashed_favicon_) { canvas->DrawBitmapInt(*crashed_fav_icon, 0, 0, crashed_fav_icon->width(), crashed_fav_icon->height(), favicon_bounds_.x(), favicon_bounds_.y() + fav_icon_hiding_offset_, kFavIconSize, kFavIconSize, true); } else { if (!data_.favicon.isNull()) { if (data_.is_default_favicon && theme_provider_->UseGtkTheme()) { GdkPixbuf* favicon = GtkThemeProvider::GetDefaultFavicon(true); canvas->AsCanvasSkia()->DrawGdkPixbuf( favicon, favicon_bounds_.x(), favicon_bounds_.y() + fav_icon_hiding_offset_); } else { // TODO(pkasting): Use code in tab_icon_view.cc:PaintIcon() (or switch // to using that class to render the favicon). canvas->DrawBitmapInt(data_.favicon, 0, 0, data_.favicon.width(), data_.favicon.height(), favicon_bounds_.x(), favicon_bounds_.y() + fav_icon_hiding_offset_, kFavIconSize, kFavIconSize, true); } } } canvas->AsCanvasSkia()->restore(); } } void TabRendererGtk::PaintTabBackground(gfx::Canvas* canvas) { if (IsSelected()) { PaintActiveTabBackground(canvas); } else { PaintInactiveTabBackground(canvas); double throb_value = GetThrobValue(); if (throb_value > 0) { SkRect bounds; bounds.set(0, 0, SkIntToScalar(width()), SkIntToScalar(height())); canvas->AsCanvasSkia()->saveLayerAlpha( &bounds, static_cast(throb_value * 0xff), SkCanvas::kARGB_ClipLayer_SaveFlag); canvas->AsCanvasSkia()->drawARGB(0, 255, 255, 255, SkXfermode::kClear_Mode); PaintActiveTabBackground(canvas); canvas->AsCanvasSkia()->restore(); } } } void TabRendererGtk::PaintInactiveTabBackground(gfx::Canvas* canvas) { bool is_otr = data_.off_the_record; // The tab image needs to be lined up with the background image // so that it feels partially transparent. int offset_x = background_offset_x_; int tab_id = is_otr ? IDR_THEME_TAB_BACKGROUND_INCOGNITO : IDR_THEME_TAB_BACKGROUND; SkBitmap* tab_bg = theme_provider_->GetBitmapNamed(tab_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_provider_->HasCustomImage(tab_id) ? 0 : background_offset_y_; // Draw left edge. SkBitmap* theme_l = GetMaskedBitmap(tab_alpha_.image_l, tab_bg, offset_x, offset_y); canvas->DrawBitmapInt(*theme_l, 0, 0); // Draw right edge. SkBitmap* theme_r = GetMaskedBitmap(tab_alpha_.image_r, tab_bg, offset_x + width() - tab_active_.r_width, offset_y); canvas->DrawBitmapInt(*theme_r, width() - theme_r->width(), 0); // Draw center. canvas->TileImageInt(*tab_bg, offset_x + tab_active_.l_width, kDropShadowOffset + offset_y, tab_active_.l_width, 2, width() - tab_active_.l_width - tab_active_.r_width, height() - 2); canvas->DrawBitmapInt(*tab_inactive_.image_l, 0, 0); canvas->TileImageInt(*tab_inactive_.image_c, tab_inactive_.l_width, 0, width() - tab_inactive_.l_width - tab_inactive_.r_width, height()); canvas->DrawBitmapInt(*tab_inactive_.image_r, width() - tab_inactive_.r_width, 0); } void TabRendererGtk::PaintActiveTabBackground(gfx::Canvas* canvas) { int offset_x = background_offset_x_; SkBitmap* tab_bg = theme_provider_->GetBitmapNamed(IDR_THEME_TOOLBAR); // Draw left edge. SkBitmap* theme_l = GetMaskedBitmap(tab_alpha_.image_l, tab_bg, offset_x, 0); canvas->DrawBitmapInt(*theme_l, 0, 0); // Draw right edge. SkBitmap* theme_r = GetMaskedBitmap(tab_alpha_.image_r, tab_bg, offset_x + width() - tab_active_.r_width, 0); canvas->DrawBitmapInt(*theme_r, width() - tab_active_.r_width, 0); // Draw center. canvas->TileImageInt(*tab_bg, offset_x + tab_active_.l_width, kDropShadowHeight, tab_active_.l_width, kDropShadowHeight, width() - tab_active_.l_width - tab_active_.r_width, height() - kDropShadowHeight); canvas->DrawBitmapInt(*tab_active_.image_l, 0, 0); canvas->TileImageInt(*tab_active_.image_c, tab_active_.l_width, 0, width() - tab_active_.l_width - tab_active_.r_width, height()); canvas->DrawBitmapInt(*tab_active_.image_r, width() - tab_active_.r_width, 0); } void TabRendererGtk::PaintLoadingAnimation(gfx::Canvas* canvas) { const SkBitmap* frames = (loading_animation_.animation_state() == ANIMATION_WAITING) ? loading_animation_.waiting_animation_frames() : loading_animation_.loading_animation_frames(); const int image_size = frames->height(); const int image_offset = loading_animation_.animation_frame() * image_size; DCHECK(image_size == favicon_bounds_.height()); DCHECK(image_size == favicon_bounds_.width()); canvas->DrawBitmapInt(*frames, image_offset, 0, image_size, image_size, favicon_bounds_.x(), favicon_bounds_.y(), image_size, image_size, false); } int TabRendererGtk::IconCapacity() const { if (height() < GetMinimumUnselectedSize().height()) return 0; return (width() - kLeftPadding - kRightPadding) / kFavIconSize; } bool TabRendererGtk::ShouldShowIcon() const { if (mini() && height() >= GetMinimumUnselectedSize().height()) { return true; } else if (!data_.show_icon) { return false; } else if (IsSelected()) { // The selected tab clips favicon before close button. return IconCapacity() >= 2; } // Non-selected tabs clip close button before favicon. return IconCapacity() >= 1; } bool TabRendererGtk::ShouldShowCloseBox() const { // The selected tab never clips close button. return !mini() && (IsSelected() || IconCapacity() >= 3); } CustomDrawButton* TabRendererGtk::MakeCloseButton() { CustomDrawButton* button = new CustomDrawButton(IDR_TAB_CLOSE, IDR_TAB_CLOSE_P, IDR_TAB_CLOSE_H, IDR_TAB_CLOSE); gtk_widget_set_tooltip_text(button->widget(), l10n_util::GetStringUTF8(IDS_TOOLTIP_CLOSE_TAB).c_str()); 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_UNSET_FLAGS(button->widget(), GTK_CAN_FOCUS); gtk_fixed_put(GTK_FIXED(tab_.get()), button->widget(), 0, 0); return button; } double TabRendererGtk::GetThrobValue() { if (mini_title_animation_.get() && mini_title_animation_->is_animating()) { return mini_title_animation_->GetCurrentValue() * kMiniTitleChangeThrobOpacity; } return hover_animation_.get() ? kHoverOpacity * hover_animation_->GetCurrentValue() : 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) { PaintTab(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(Tween::EASE_OUT); hover_animation_->Show(); return FALSE; } gboolean TabRendererGtk::OnLeaveNotifyEvent(GtkWidget* widget, GdkEventCrossing* event) { hover_animation_->SetTweenType(Tween::EASE_IN); hover_animation_->Hide(); return FALSE; } // static void TabRendererGtk::InitResources() { if (initialized_) return; LoadTabImages(); ResourceBundle& rb = ResourceBundle::GetSharedInstance(); // Force the font size to 9pt, which matches Windows' default font size // (taken from the system). const gfx::Font& base_font = rb.GetFont(ResourceBundle::BaseFont); title_font_ = new gfx::Font(gfx::Font::CreateFont(base_font.FontName(), 9)); title_font_height_ = title_font_->height(); crashed_fav_icon = rb.GetBitmapNamed(IDR_SAD_FAVICON); initialized_ = true; }