1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/ui/gtk/tabs/tab_renderer_gtk.h"
10 #include "base/debug/trace_event.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "chrome/browser/chrome_notification_types.h"
13 #include "chrome/browser/defaults.h"
14 #include "chrome/browser/extensions/tab_helper.h"
15 #include "chrome/browser/favicon/favicon_tab_helper.h"
16 #include "chrome/browser/profiles/profile.h"
17 #include "chrome/browser/themes/theme_properties.h"
18 #include "chrome/browser/ui/browser.h"
19 #include "chrome/browser/ui/gtk/bookmarks/bookmark_utils_gtk.h"
20 #include "chrome/browser/ui/gtk/custom_button.h"
21 #include "chrome/browser/ui/gtk/gtk_theme_service.h"
22 #include "chrome/browser/ui/gtk/gtk_util.h"
23 #include "chrome/browser/ui/tab_contents/core_tab_helper.h"
24 #include "chrome/browser/ui/tabs/tab_utils.h"
25 #include "content/public/browser/notification_source.h"
26 #include "content/public/browser/web_contents.h"
27 #include "grit/generated_resources.h"
28 #include "grit/theme_resources.h"
29 #include "grit/ui_resources.h"
30 #include "skia/ext/image_operations.h"
31 #include "ui/base/gtk/gtk_screen_util.h"
32 #include "ui/base/l10n/l10n_util.h"
33 #include "ui/base/resource/resource_bundle.h"
34 #include "ui/gfx/animation/slide_animation.h"
35 #include "ui/gfx/animation/throb_animation.h"
36 #include "ui/gfx/canvas_skia_paint.h"
37 #include "ui/gfx/favicon_size.h"
38 #include "ui/gfx/gtk_compat.h"
39 #include "ui/gfx/gtk_util.h"
40 #include "ui/gfx/image/cairo_cached_surface.h"
41 #include "ui/gfx/image/image.h"
42 #include "ui/gfx/pango_util.h"
43 #include "ui/gfx/platform_font_pango.h"
44 #include "ui/gfx/skbitmap_operations.h"
46 using content::WebContents
;
50 const int kFontPixelSize
= 12;
51 const int kLeftPadding
= 16;
52 const int kTopPadding
= 6;
53 const int kRightPadding
= 15;
54 const int kBottomPadding
= 5;
55 const int kFaviconTitleSpacing
= 4;
56 const int kTitleCloseButtonSpacing
= 5;
57 const int kStandardTitleWidth
= 175;
58 const int kDropShadowOffset
= 2;
59 const int kInactiveTabBackgroundOffsetY
= 15;
61 // When a non-mini-tab becomes a mini-tab the width of the tab animates. If
62 // the width of a mini-tab is >= kMiniTabRendererAsNormalTabWidth then the tab
63 // is rendered as a normal tab. This is done to avoid having the title
64 // immediately disappear when transitioning a tab from normal to mini-tab.
65 const int kMiniTabRendererAsNormalTabWidth
=
66 browser_defaults::kMiniTabWidth
+ 30;
68 // The tab images are designed to overlap the toolbar by 1 pixel. For now we
69 // don't actually overlap the toolbar, so this is used to know how many pixels
70 // at the bottom of the tab images are to be ignored.
71 const int kToolbarOverlap
= 1;
73 // How long the hover state takes.
74 const int kHoverDurationMs
= 90;
76 // How opaque to make the hover state (out of 1).
77 const double kHoverOpacity
= 0.33;
79 // Opacity for non-active selected tabs.
80 const double kSelectedTabOpacity
= 0.45;
82 // Selected (but not active) tabs have their throb value scaled down by this.
83 const double kSelectedTabThrobScale
= 0.5;
85 // Max opacity for the mini-tab title change animation.
86 const double kMiniTitleChangeThrobOpacity
= 0.75;
88 // Duration for when the title of an inactive mini-tab changes.
89 const int kMiniTitleChangeThrobDuration
= 1000;
91 // The horizontal offset used to position the close button in the tab.
92 const int kCloseButtonHorzFuzz
= 4;
94 // Gets the bounds of |widget| relative to |parent|.
95 gfx::Rect
GetWidgetBoundsRelativeToParent(GtkWidget
* parent
,
97 gfx::Rect bounds
= ui::GetWidgetScreenBounds(widget
);
98 bounds
.Offset(-ui::GetWidgetScreenOffset(parent
));
102 // Returns a GdkPixbuf after resizing the SkBitmap as necessary to the
103 // specified desired width and height. Caller must g_object_unref the returned
104 // pixbuf when no longer used.
105 GdkPixbuf
* GetResizedGdkPixbufFromSkBitmap(const SkBitmap
& bitmap
,
108 float float_dest_w
= static_cast<float>(dest_w
);
109 float float_dest_h
= static_cast<float>(dest_h
);
110 int bitmap_w
= bitmap
.width();
111 int bitmap_h
= bitmap
.height();
113 // Scale proportionately.
114 float scale
= std::min(float_dest_w
/ bitmap_w
,
115 float_dest_h
/ bitmap_h
);
116 int final_dest_w
= static_cast<int>(bitmap_w
* scale
);
117 int final_dest_h
= static_cast<int>(bitmap_h
* scale
);
120 if (final_dest_w
== bitmap_w
&& final_dest_h
== bitmap_h
) {
121 pixbuf
= gfx::GdkPixbufFromSkBitmap(bitmap
);
123 SkBitmap resized_icon
= skia::ImageOperations::Resize(
125 skia::ImageOperations::RESIZE_BETTER
,
126 final_dest_w
, final_dest_h
);
127 pixbuf
= gfx::GdkPixbufFromSkBitmap(resized_icon
);
134 TabRendererGtk::LoadingAnimation::Data::Data(
135 GtkThemeService
* theme_service
) {
136 // The loading animation image is a strip of states. Each state must be
137 // square, so the height must divide the width evenly.
138 SkBitmap loading_animation_frames
=
139 theme_service
->GetImageNamed(IDR_THROBBER
).AsBitmap();
140 DCHECK(!loading_animation_frames
.isNull());
141 DCHECK_EQ(loading_animation_frames
.width() %
142 loading_animation_frames
.height(), 0);
143 loading_animation_frame_count
=
144 loading_animation_frames
.width() /
145 loading_animation_frames
.height();
147 SkBitmap waiting_animation_frames
=
148 theme_service
->GetImageNamed(IDR_THROBBER_WAITING
).AsBitmap();
149 DCHECK(!waiting_animation_frames
.isNull());
150 DCHECK_EQ(waiting_animation_frames
.width() %
151 waiting_animation_frames
.height(), 0);
152 waiting_animation_frame_count
=
153 waiting_animation_frames
.width() /
154 waiting_animation_frames
.height();
156 waiting_to_loading_frame_count_ratio
=
157 waiting_animation_frame_count
/
158 loading_animation_frame_count
;
159 // TODO(beng): eventually remove this when we have a proper themeing system.
160 // themes not supporting IDR_THROBBER_WAITING are causing this
161 // value to be 0 which causes DIV0 crashes. The value of 5
162 // matches the current bitmaps in our source.
163 if (waiting_to_loading_frame_count_ratio
== 0)
164 waiting_to_loading_frame_count_ratio
= 5;
167 TabRendererGtk::LoadingAnimation::Data::Data(
168 int loading
, int waiting
, int waiting_to_loading
)
169 : loading_animation_frame_count(loading
),
170 waiting_animation_frame_count(waiting
),
171 waiting_to_loading_frame_count_ratio(waiting_to_loading
) {
174 bool TabRendererGtk::initialized_
= false;
175 int TabRendererGtk::tab_active_l_width_
= 0;
176 int TabRendererGtk::tab_active_l_height_
= 0;
177 int TabRendererGtk::tab_inactive_l_height_
= 0;
178 gfx::Font
* TabRendererGtk::title_font_
= NULL
;
179 int TabRendererGtk::title_font_height_
= 0;
180 int TabRendererGtk::close_button_width_
= 0;
181 int TabRendererGtk::close_button_height_
= 0;
183 ////////////////////////////////////////////////////////////////////////////////
184 // TabRendererGtk::LoadingAnimation, public:
186 TabRendererGtk::LoadingAnimation::LoadingAnimation(
187 GtkThemeService
* theme_service
)
188 : data_(new Data(theme_service
)),
189 theme_service_(theme_service
),
190 animation_state_(ANIMATION_NONE
),
191 animation_frame_(0) {
193 chrome::NOTIFICATION_BROWSER_THEME_CHANGED
,
194 content::Source
<ThemeService
>(theme_service_
));
197 TabRendererGtk::LoadingAnimation::LoadingAnimation(
198 const LoadingAnimation::Data
& data
)
199 : data_(new Data(data
)),
200 theme_service_(NULL
),
201 animation_state_(ANIMATION_NONE
),
202 animation_frame_(0) {
205 TabRendererGtk::LoadingAnimation::~LoadingAnimation() {}
207 bool TabRendererGtk::LoadingAnimation::ValidateLoadingAnimation(
208 AnimationState animation_state
) {
209 bool has_changed
= false;
210 if (animation_state_
!= animation_state
) {
211 // The waiting animation is the reverse of the loading animation, but at a
212 // different rate - the following reverses and scales the animation_frame_
213 // so that the frame is at an equivalent position when going from one
214 // animation to the other.
215 if (animation_state_
== ANIMATION_WAITING
&&
216 animation_state
== ANIMATION_LOADING
) {
217 animation_frame_
= data_
->loading_animation_frame_count
-
218 (animation_frame_
/ data_
->waiting_to_loading_frame_count_ratio
);
220 animation_state_
= animation_state
;
224 if (animation_state_
!= ANIMATION_NONE
) {
225 animation_frame_
= (animation_frame_
+ 1) %
226 ((animation_state_
== ANIMATION_WAITING
) ?
227 data_
->waiting_animation_frame_count
:
228 data_
->loading_animation_frame_count
);
231 animation_frame_
= 0;
236 void TabRendererGtk::LoadingAnimation::Observe(
238 const content::NotificationSource
& source
,
239 const content::NotificationDetails
& details
) {
240 DCHECK(type
== chrome::NOTIFICATION_BROWSER_THEME_CHANGED
);
241 data_
.reset(new Data(theme_service_
));
244 TabRendererGtk::TabData::TabData()
245 : is_default_favicon(false),
252 animating_mini_change(false),
254 media_state(TAB_MEDIA_STATE_NONE
),
255 previous_media_state(TAB_MEDIA_STATE_NONE
) {
258 TabRendererGtk::TabData::~TabData() {}
260 ////////////////////////////////////////////////////////////////////////////////
261 // FaviconCrashAnimation
263 // A custom animation subclass to manage the favicon crash animation.
264 class TabRendererGtk::FaviconCrashAnimation
: public gfx::LinearAnimation
,
265 public gfx::AnimationDelegate
{
267 explicit FaviconCrashAnimation(TabRendererGtk
* target
)
268 : gfx::LinearAnimation(1000, 25, this),
271 virtual ~FaviconCrashAnimation() {}
273 // gfx::Animation overrides:
274 virtual void AnimateToState(double state
) OVERRIDE
{
275 const double kHidingOffset
= 27;
278 target_
->SetFaviconHidingOffset(
279 static_cast<int>(floor(kHidingOffset
* 2.0 * state
)));
281 target_
->DisplayCrashedFavicon();
282 target_
->SetFaviconHidingOffset(
284 floor(kHidingOffset
- ((state
- .5) * 2.0 * kHidingOffset
))));
288 // gfx::AnimationDelegate overrides:
289 virtual void AnimationCanceled(const gfx::Animation
* animation
) OVERRIDE
{
290 target_
->SetFaviconHidingOffset(0);
294 TabRendererGtk
* target_
;
296 DISALLOW_COPY_AND_ASSIGN(FaviconCrashAnimation
);
299 ////////////////////////////////////////////////////////////////////////////////
300 // TabRendererGtk, public:
302 TabRendererGtk::TabRendererGtk(GtkThemeService
* theme_service
)
303 : showing_icon_(false),
304 showing_media_indicator_(false),
305 showing_close_button_(false),
306 favicon_hiding_offset_(0),
307 should_display_crashed_favicon_(false),
308 animating_media_state_(TAB_MEDIA_STATE_NONE
),
309 loading_animation_(theme_service
),
310 background_offset_x_(0),
311 background_offset_y_(kInactiveTabBackgroundOffsetY
),
312 theme_service_(theme_service
),
313 close_button_color_(0),
315 selected_title_color_(SK_ColorBLACK
),
316 unselected_title_color_(SkColorSetRGB(64, 64, 64)) {
319 theme_service_
->InitThemesFor(this);
320 registrar_
.Add(this, chrome::NOTIFICATION_BROWSER_THEME_CHANGED
,
321 content::Source
<ThemeService
>(theme_service_
));
323 tab_
.Own(gtk_fixed_new());
324 gtk_widget_set_app_paintable(tab_
.get(), TRUE
);
325 g_signal_connect(tab_
.get(), "expose-event",
326 G_CALLBACK(OnExposeEventThunk
), this);
327 g_signal_connect(tab_
.get(), "size-allocate",
328 G_CALLBACK(OnSizeAllocateThunk
), this);
329 close_button_
.reset(MakeCloseButton());
330 gtk_widget_show(tab_
.get());
332 hover_animation_
.reset(new gfx::SlideAnimation(this));
333 hover_animation_
->SetSlideDuration(kHoverDurationMs
);
336 TabRendererGtk::~TabRendererGtk() {
340 void TabRendererGtk::Observe(int type
,
341 const content::NotificationSource
& source
,
342 const content::NotificationDetails
& details
) {
343 DCHECK(chrome::NOTIFICATION_BROWSER_THEME_CHANGED
);
344 selected_title_color_
=
345 theme_service_
->GetColor(ThemeProperties::COLOR_TAB_TEXT
);
346 unselected_title_color_
=
347 theme_service_
->GetColor(ThemeProperties::COLOR_BACKGROUND_TAB_TEXT
);
350 void TabRendererGtk::UpdateData(WebContents
* contents
,
354 FaviconTabHelper
* favicon_tab_helper
=
355 FaviconTabHelper::FromWebContents(contents
);
358 data_
.title
= contents
->GetTitle();
359 data_
.incognito
= contents
->GetBrowserContext()->IsOffTheRecord();
361 TabMediaState next_media_state
;
362 if (contents
->IsCrashed()) {
363 data_
.crashed
= true;
364 next_media_state
= TAB_MEDIA_STATE_NONE
;
366 data_
.crashed
= false;
367 next_media_state
= chrome::GetTabMediaStateForContents(contents
);
369 if (data_
.media_state
!= next_media_state
) {
370 data_
.previous_media_state
= data_
.media_state
;
371 data_
.media_state
= next_media_state
;
375 extensions::TabHelper::FromWebContents(contents
)->GetExtensionAppIcon();
377 data_
.favicon
= *app_icon
;
379 data_
.favicon
= favicon_tab_helper
->GetFavicon().AsBitmap();
384 // Make a cairo cached version of the favicon.
385 if (!data_
.favicon
.isNull()) {
386 // Instead of resizing the icon during each frame, create our resized
387 // icon resource now, send it to the xserver and use that each frame
390 // For source images smaller than the favicon square, scale them as if
391 // they were padded to fit the favicon square, so we don't blow up tiny
392 // favicons into larger or nonproportional results.
393 GdkPixbuf
* pixbuf
= GetResizedGdkPixbufFromSkBitmap(data_
.favicon
,
394 gfx::kFaviconSize
, gfx::kFaviconSize
);
395 data_
.cairo_favicon
.UsePixbuf(pixbuf
);
396 g_object_unref(pixbuf
);
398 data_
.cairo_favicon
.Reset();
401 // This is kind of a hacky way to determine whether our icon is the default
402 // favicon. But the plumbing that would be necessary to do it right would
403 // be a good bit of work and would sully code for other platforms which
404 // don't care to custom-theme the favicon. Hopefully the default favicon
405 // will eventually be chromium-themable and this code will go away.
406 data_
.is_default_favicon
=
407 (data_
.favicon
.pixelRef() ==
408 ui::ResourceBundle::GetSharedInstance().GetImageNamed(
409 IDR_DEFAULT_FAVICON
).AsBitmap().pixelRef());
412 // Loading state also involves whether we show the favicon, since that's where
413 // we display the throbber.
414 data_
.loading
= contents
->IsLoading();
415 data_
.show_icon
= favicon_tab_helper
->ShouldDisplayFavicon();
418 void TabRendererGtk::UpdateFromModel() {
419 // Force a layout, since the tab may have grown a favicon.
424 if (!should_display_crashed_favicon_
&& !IsPerformingCrashAnimation())
425 StartCrashAnimation();
427 if (IsPerformingCrashAnimation())
428 StopCrashAnimation();
429 ResetCrashedFavicon();
432 if (data_
.media_state
!= data_
.previous_media_state
) {
433 data_
.previous_media_state
= data_
.media_state
;
434 if (data_
.media_state
!= TAB_MEDIA_STATE_NONE
)
435 animating_media_state_
= data_
.media_state
;
436 StartMediaIndicatorAnimation();
440 void TabRendererGtk::SetBlocked(bool blocked
) {
441 if (data_
.blocked
== blocked
)
443 data_
.blocked
= blocked
;
444 // TODO(zelidrag) bug 32399: Make tabs pulse on Linux as well.
447 bool TabRendererGtk::is_blocked() const {
448 return data_
.blocked
;
451 bool TabRendererGtk::IsActive() const {
455 bool TabRendererGtk::IsSelected() const {
459 bool TabRendererGtk::IsVisible() const {
460 return gtk_widget_get_visible(tab_
.get());
463 void TabRendererGtk::SetVisible(bool visible
) const {
465 gtk_widget_show(tab_
.get());
467 gtk_widget_show(close_button_
->widget());
469 gtk_widget_hide_all(tab_
.get());
473 bool TabRendererGtk::ValidateLoadingAnimation(AnimationState animation_state
) {
474 return loading_animation_
.ValidateLoadingAnimation(animation_state
);
477 void TabRendererGtk::PaintFaviconArea(GtkWidget
* widget
, cairo_t
* cr
) {
478 DCHECK(ShouldShowIcon());
481 x() + favicon_bounds_
.x(),
482 y() + favicon_bounds_
.y(),
483 favicon_bounds_
.width(),
484 favicon_bounds_
.height());
487 // The tab is rendered into a windowless widget whose offset is at the
488 // coordinate event->area. Translate by these offsets so we can render at
489 // (0,0) to match Windows' rendering metrics.
490 cairo_matrix_t cairo_matrix
;
491 cairo_matrix_init_translate(&cairo_matrix
, x(), y());
492 cairo_set_matrix(cr
, &cairo_matrix
);
494 // Which background should we be painting?
498 theme_id
= IDR_THEME_TOOLBAR
;
500 theme_id
= data_
.incognito
? IDR_THEME_TAB_BACKGROUND_INCOGNITO
:
501 IDR_THEME_TAB_BACKGROUND
;
503 if (!theme_service_
->HasCustomImage(theme_id
))
504 offset_y
= background_offset_y_
;
507 // Paint the background behind the favicon.
508 const gfx::Image tab_bg
= theme_service_
->GetImageNamed(theme_id
);
509 tab_bg
.ToCairo()->SetSource(cr
, widget
, -x(), -offset_y
);
510 cairo_pattern_set_extend(cairo_get_source(cr
), CAIRO_EXTEND_REPEAT
);
512 favicon_bounds_
.x(), favicon_bounds_
.y(),
513 favicon_bounds_
.width(), favicon_bounds_
.height());
517 double throb_value
= GetThrobValue();
518 if (throb_value
> 0) {
519 cairo_push_group(cr
);
520 gfx::Image active_bg
=
521 theme_service_
->GetImageNamed(IDR_THEME_TOOLBAR
);
522 active_bg
.ToCairo()->SetSource(cr
, widget
, -x(), 0);
523 cairo_pattern_set_extend(cairo_get_source(cr
), CAIRO_EXTEND_REPEAT
);
526 favicon_bounds_
.x(), favicon_bounds_
.y(),
527 favicon_bounds_
.width(), favicon_bounds_
.height());
530 cairo_pop_group_to_source(cr
);
531 cairo_paint_with_alpha(cr
, throb_value
);
535 PaintIcon(widget
, cr
);
538 void TabRendererGtk::MaybeAdjustLeftForMiniTab(gfx::Rect
* icon_bounds
) const {
539 if (!(mini() || data_
.animating_mini_change
) ||
540 bounds_
.width() >= kMiniTabRendererAsNormalTabWidth
)
542 const int mini_delta
= kMiniTabRendererAsNormalTabWidth
- GetMiniWidth();
543 const int ideal_delta
= bounds_
.width() - GetMiniWidth();
544 const int ideal_x
= (GetMiniWidth() - icon_bounds
->width()) / 2;
545 icon_bounds
->set_x(icon_bounds
->x() + static_cast<int>(
546 (1 - static_cast<float>(ideal_delta
) / static_cast<float>(mini_delta
)) *
547 (ideal_x
- icon_bounds
->x())));
551 gfx::Size
TabRendererGtk::GetMinimumUnselectedSize() {
554 gfx::Size minimum_size
;
555 minimum_size
.set_width(kLeftPadding
+ kRightPadding
);
556 // Since we use bitmap images, the real minimum height of the image is
557 // defined most accurately by the height of the end cap images.
558 minimum_size
.set_height(tab_active_l_height_
- kToolbarOverlap
);
563 gfx::Size
TabRendererGtk::GetMinimumSelectedSize() {
564 gfx::Size minimum_size
= GetMinimumUnselectedSize();
565 minimum_size
.set_width(kLeftPadding
+ gfx::kFaviconSize
+ kRightPadding
);
570 gfx::Size
TabRendererGtk::GetStandardSize() {
571 gfx::Size standard_size
= GetMinimumUnselectedSize();
572 standard_size
.Enlarge(kFaviconTitleSpacing
+ kStandardTitleWidth
, 0);
573 return standard_size
;
577 int TabRendererGtk::GetMiniWidth() {
578 return browser_defaults::kMiniTabWidth
;
582 int TabRendererGtk::GetContentHeight() {
583 // The height of the content of the Tab is the largest of the favicon,
584 // the title text and the close button graphic.
585 int content_height
= std::max(gfx::kFaviconSize
, title_font_height_
);
586 return std::max(content_height
, close_button_height_
);
589 gfx::Rect
TabRendererGtk::GetNonMirroredBounds(GtkWidget
* parent
) const {
590 // The tabstrip widget is a windowless widget so the tab widget's allocation
591 // is relative to the browser titlebar. We need the bounds relative to the
593 gfx::Rect bounds
= GetWidgetBoundsRelativeToParent(parent
, widget());
594 bounds
.set_x(gtk_util::MirroredLeftPointForRect(parent
, bounds
));
598 gfx::Rect
TabRendererGtk::GetRequisition() const {
599 return gfx::Rect(requisition_
.x(), requisition_
.y(),
600 requisition_
.width(), requisition_
.height());
603 void TabRendererGtk::StartMiniTabTitleAnimation() {
604 if (!mini_title_animation_
.get()) {
605 mini_title_animation_
.reset(new gfx::ThrobAnimation(this));
606 mini_title_animation_
->SetThrobDuration(kMiniTitleChangeThrobDuration
);
609 if (!mini_title_animation_
->is_animating())
610 mini_title_animation_
->StartThrobbing(-1);
613 void TabRendererGtk::StopMiniTabTitleAnimation() {
614 if (mini_title_animation_
.get())
615 mini_title_animation_
->Stop();
618 void TabRendererGtk::SetBounds(const gfx::Rect
& bounds
) {
619 requisition_
= bounds
;
620 gtk_widget_set_size_request(tab_
.get(), bounds
.width(), bounds
.height());
623 ////////////////////////////////////////////////////////////////////////////////
624 // TabRendererGtk, protected:
626 void TabRendererGtk::Raise() const {
627 if (gtk_button_get_event_window(GTK_BUTTON(close_button_
->widget())))
628 gdk_window_raise(gtk_button_get_event_window(
629 GTK_BUTTON(close_button_
->widget())));
632 base::string16
TabRendererGtk::GetTitle() const {
636 ///////////////////////////////////////////////////////////////////////////////
637 // TabRendererGtk, gfx::AnimationDelegate implementation:
639 void TabRendererGtk::AnimationProgressed(const gfx::Animation
* animation
) {
640 gtk_widget_queue_draw(tab_
.get());
643 void TabRendererGtk::AnimationCanceled(const gfx::Animation
* animation
) {
644 if (media_indicator_animation_
== animation
)
645 animating_media_state_
= data_
.media_state
;
646 AnimationEnded(animation
);
649 void TabRendererGtk::AnimationEnded(const gfx::Animation
* animation
) {
650 if (media_indicator_animation_
== animation
)
651 animating_media_state_
= data_
.media_state
;
652 gtk_widget_queue_draw(tab_
.get());
655 ////////////////////////////////////////////////////////////////////////////////
656 // TabRendererGtk, private:
658 void TabRendererGtk::StartCrashAnimation() {
659 if (!crash_animation_
.get())
660 crash_animation_
.reset(new FaviconCrashAnimation(this));
661 crash_animation_
->Stop();
662 crash_animation_
->Start();
665 void TabRendererGtk::StopCrashAnimation() {
666 if (!crash_animation_
.get())
668 crash_animation_
->Stop();
671 bool TabRendererGtk::IsPerformingCrashAnimation() const {
672 return crash_animation_
.get() && crash_animation_
->is_animating();
675 void TabRendererGtk::StartMediaIndicatorAnimation() {
676 media_indicator_animation_
=
677 chrome::CreateTabMediaIndicatorFadeAnimation(data_
.media_state
);
678 media_indicator_animation_
->set_delegate(this);
679 media_indicator_animation_
->Start();
682 void TabRendererGtk::SetFaviconHidingOffset(int offset
) {
683 favicon_hiding_offset_
= offset
;
687 void TabRendererGtk::DisplayCrashedFavicon() {
688 should_display_crashed_favicon_
= true;
691 void TabRendererGtk::ResetCrashedFavicon() {
692 should_display_crashed_favicon_
= false;
695 void TabRendererGtk::Paint(GtkWidget
* widget
, cairo_t
* cr
) {
696 // Don't paint if we're narrower than we can render correctly. (This should
697 // only happen during animations).
698 if (width() < GetMinimumUnselectedSize().width() && !mini())
701 // See if the model changes whether the icons should be painted.
702 const bool show_icon
= ShouldShowIcon();
703 const bool show_media_indicator
= ShouldShowMediaIndicator();
704 const bool show_close_button
= ShouldShowCloseBox();
705 if (show_icon
!= showing_icon_
||
706 show_media_indicator
!= showing_media_indicator_
||
707 show_close_button
!= showing_close_button_
)
710 PaintTabBackground(widget
, cr
);
712 if (!mini() || width() > kMiniTabRendererAsNormalTabWidth
)
713 PaintTitle(widget
, cr
);
716 PaintIcon(widget
, cr
);
718 if (show_media_indicator
)
719 PaintMediaIndicator(widget
, cr
);
722 cairo_surface_t
* TabRendererGtk::PaintToSurface(GtkWidget
* widget
,
724 cairo_surface_t
* target
= cairo_get_target(cr
);
725 cairo_surface_t
* out_surface
= cairo_surface_create_similar(
727 CAIRO_CONTENT_COLOR_ALPHA
,
730 cairo_t
* out_cr
= cairo_create(out_surface
);
731 Paint(widget
, out_cr
);
732 cairo_destroy(out_cr
);
737 void TabRendererGtk::SchedulePaint() {
738 gtk_widget_queue_draw(tab_
.get());
741 gfx::Rect
TabRendererGtk::GetLocalBounds() {
742 return gfx::Rect(0, 0, bounds_
.width(), bounds_
.height());
745 void TabRendererGtk::Layout() {
746 gfx::Rect local_bounds
= GetLocalBounds();
747 if (local_bounds
.IsEmpty())
749 local_bounds
.Inset(kLeftPadding
, kTopPadding
, kRightPadding
, kBottomPadding
);
751 // Figure out who is tallest.
752 int content_height
= GetContentHeight();
755 showing_icon_
= ShouldShowIcon();
757 int favicon_top
= kTopPadding
+ (content_height
- gfx::kFaviconSize
) / 2;
758 favicon_bounds_
.SetRect(local_bounds
.x(), favicon_top
,
759 gfx::kFaviconSize
, gfx::kFaviconSize
);
760 MaybeAdjustLeftForMiniTab(&favicon_bounds_
);
762 favicon_bounds_
.SetRect(local_bounds
.x(), local_bounds
.y(), 0, 0);
765 // Size the Close button.
766 showing_close_button_
= ShouldShowCloseBox();
767 if (showing_close_button_
) {
768 int close_button_top
= kTopPadding
+
769 (content_height
- close_button_height_
) / 2;
770 int close_button_left
=
771 local_bounds
.right() - close_button_width_
+ kCloseButtonHorzFuzz
;
772 close_button_bounds_
.SetRect(close_button_left
,
775 close_button_height_
);
777 // If the close button color has changed, generate a new one.
778 if (theme_service_
) {
779 SkColor tab_text_color
=
780 theme_service_
->GetColor(ThemeProperties::COLOR_TAB_TEXT
);
781 if (!close_button_color_
|| tab_text_color
!= close_button_color_
) {
782 close_button_color_
= tab_text_color
;
783 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
784 close_button_
->SetBackground(close_button_color_
,
785 rb
.GetImageNamed(IDR_CLOSE_1
).AsBitmap(),
786 rb
.GetImageNamed(IDR_CLOSE_1_MASK
).AsBitmap());
790 close_button_bounds_
.SetRect(0, 0, 0, 0);
793 showing_media_indicator_
= ShouldShowMediaIndicator();
794 if (showing_media_indicator_
) {
795 const gfx::Image
& media_indicator_image
=
796 chrome::GetTabMediaIndicatorImage(animating_media_state_
);
797 media_indicator_bounds_
.set_width(media_indicator_image
.Width());
798 media_indicator_bounds_
.set_height(media_indicator_image
.Height());
799 media_indicator_bounds_
.set_y(
800 kTopPadding
+ (content_height
- media_indicator_bounds_
.height()) / 2);
801 const int right
= showing_close_button_
?
802 close_button_bounds_
.x() : local_bounds
.right();
803 media_indicator_bounds_
.set_x(std::max(
804 local_bounds
.x(), right
- media_indicator_bounds_
.width()));
805 MaybeAdjustLeftForMiniTab(&media_indicator_bounds_
);
807 media_indicator_bounds_
.SetRect(local_bounds
.x(), local_bounds
.y(), 0, 0);
810 if (!mini() || width() >= kMiniTabRendererAsNormalTabWidth
) {
811 // Size the Title text to fill the remaining space.
812 int title_left
= favicon_bounds_
.right() + kFaviconTitleSpacing
;
813 int title_top
= kTopPadding
;
815 // If the user has big fonts, the title will appear rendered too far down
816 // on the y-axis if we use the regular top padding, so we need to adjust it
817 // so that the text appears centered.
818 gfx::Size minimum_size
= GetMinimumUnselectedSize();
819 int text_height
= title_top
+ title_font_height_
+ kBottomPadding
;
820 if (text_height
> minimum_size
.height())
821 title_top
-= (text_height
- minimum_size
.height()) / 2;
824 if (showing_media_indicator_
) {
825 title_width
= media_indicator_bounds_
.x() - kTitleCloseButtonSpacing
-
827 } else if (close_button_bounds_
.width() && close_button_bounds_
.height()) {
828 title_width
= close_button_bounds_
.x() - kTitleCloseButtonSpacing
-
831 title_width
= local_bounds
.width() - title_left
;
833 title_width
= std::max(title_width
, 0);
834 title_bounds_
.SetRect(title_left
, title_top
, title_width
, content_height
);
837 favicon_bounds_
.set_x(
838 gtk_util::MirroredLeftPointForRect(tab_
.get(), favicon_bounds_
));
839 media_indicator_bounds_
.set_x(
840 gtk_util::MirroredLeftPointForRect(tab_
.get(), media_indicator_bounds_
));
841 close_button_bounds_
.set_x(
842 gtk_util::MirroredLeftPointForRect(tab_
.get(), close_button_bounds_
));
844 gtk_util::MirroredLeftPointForRect(tab_
.get(), title_bounds_
));
846 MoveCloseButtonWidget();
849 void TabRendererGtk::MoveCloseButtonWidget() {
850 if (!close_button_bounds_
.IsEmpty()) {
851 gtk_fixed_move(GTK_FIXED(tab_
.get()), close_button_
->widget(),
852 close_button_bounds_
.x(), close_button_bounds_
.y());
853 gtk_widget_show(close_button_
->widget());
855 gtk_widget_hide(close_button_
->widget());
859 void TabRendererGtk::PaintTab(GtkWidget
* widget
, GdkEventExpose
* event
) {
860 cairo_t
* cr
= gdk_cairo_create(gtk_widget_get_window(widget
));
861 gdk_cairo_rectangle(cr
, &event
->area
);
864 // The tab is rendered into a windowless widget whose offset is at the
865 // coordinate event->area. Translate by these offsets so we can render at
866 // (0,0) to match Windows' rendering metrics.
867 cairo_matrix_t cairo_matrix
;
868 cairo_matrix_init_translate(&cairo_matrix
, event
->area
.x
, event
->area
.y
);
869 cairo_set_matrix(cr
, &cairo_matrix
);
871 // Save the original x offset so we can position background images properly.
872 background_offset_x_
= event
->area
.x
;
878 void TabRendererGtk::PaintTitle(GtkWidget
* widget
, cairo_t
* cr
) {
879 if (title_bounds_
.IsEmpty())
883 base::string16 title
= data_
.title
;
885 title
= data_
.loading
?
886 l10n_util::GetStringUTF16(IDS_TAB_LOADING_TITLE
) :
887 CoreTabHelper::GetDefaultTitle();
889 Browser::FormatTitleForDisplay(&title
);
892 GtkAllocation allocation
;
893 gtk_widget_get_allocation(widget
, &allocation
);
894 gfx::Rect
bounds(allocation
);
896 // Draw the text directly onto the Cairo context. This is necessary for
897 // getting the draw order correct, and automatically applying transformations
898 // such as scaling when a tab is detached.
899 gfx::CanvasSkiaPaintCairo
canvas(cr
, bounds
.size(), true);
901 SkColor title_color
= IsSelected() ? selected_title_color_
902 : unselected_title_color_
;
904 // Disable subpixel rendering. This does not work because the canvas has a
905 // transparent background.
906 int flags
= gfx::Canvas::NO_ELLIPSIS
| gfx::Canvas::NO_SUBPIXEL_RENDERING
;
907 canvas
.DrawFadeTruncatingStringRectWithFlags(
908 title
, gfx::Canvas::TruncateFadeTail
, gfx::FontList(*title_font_
),
909 title_color
, title_bounds_
, flags
);
912 void TabRendererGtk::PaintIcon(GtkWidget
* widget
, cairo_t
* cr
) {
913 if (loading_animation_
.animation_state() != ANIMATION_NONE
) {
914 PaintLoadingAnimation(widget
, cr
);
918 gfx::CairoCachedSurface
* to_display
= NULL
;
919 if (should_display_crashed_favicon_
) {
920 to_display
= theme_service_
->GetImageNamed(IDR_SAD_FAVICON
).ToCairo();
921 } else if (!data_
.favicon
.isNull()) {
922 if (data_
.is_default_favicon
&& theme_service_
->UsingNativeTheme()) {
923 to_display
= GtkThemeService::GetDefaultFavicon(true).ToCairo();
924 } else if (data_
.cairo_favicon
.valid()) {
925 to_display
= &data_
.cairo_favicon
;
930 to_display
->SetSource(cr
, widget
, favicon_bounds_
.x(),
931 favicon_bounds_
.y() + favicon_hiding_offset_
);
936 void TabRendererGtk::PaintMediaIndicator(GtkWidget
* widget
, cairo_t
* cr
) {
937 if (media_indicator_bounds_
.IsEmpty() || !media_indicator_animation_
)
940 double opaqueness
= media_indicator_animation_
->GetCurrentValue();
941 if (data_
.media_state
== TAB_MEDIA_STATE_NONE
)
942 opaqueness
= 1.0 - opaqueness
; // Fading out, not in.
944 const gfx::Image
& media_indicator_image
=
945 chrome::GetTabMediaIndicatorImage(animating_media_state_
);
946 media_indicator_image
.ToCairo()->SetSource(
947 cr
, widget
, media_indicator_bounds_
.x(), media_indicator_bounds_
.y());
948 cairo_paint_with_alpha(cr
, opaqueness
);
951 void TabRendererGtk::PaintTabBackground(GtkWidget
* widget
, cairo_t
* cr
) {
953 PaintActiveTabBackground(widget
, cr
);
955 PaintInactiveTabBackground(widget
, cr
);
957 double throb_value
= GetThrobValue();
958 if (throb_value
> 0) {
959 cairo_push_group(cr
);
960 PaintActiveTabBackground(widget
, cr
);
961 cairo_pop_group_to_source(cr
);
962 cairo_paint_with_alpha(cr
, throb_value
);
967 void TabRendererGtk::DrawTabBackground(
970 const gfx::Image
& tab_bg
,
973 tab_bg
.ToCairo()->SetSource(cr
, widget
, -offset_x
, -offset_y
);
974 cairo_pattern_set_extend(cairo_get_source(cr
), CAIRO_EXTEND_REPEAT
);
976 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
979 gfx::Image
& tab_l_mask
= rb
.GetNativeImageNamed(IDR_TAB_ALPHA_LEFT
);
980 tab_l_mask
.ToCairo()->MaskSource(cr
, widget
, 0, 0);
984 tab_active_l_width_
, kDropShadowOffset
,
985 width() - (2 * tab_active_l_width_
),
986 tab_inactive_l_height_
);
990 gfx::Image
& tab_r_mask
= rb
.GetNativeImageNamed(IDR_TAB_ALPHA_RIGHT
);
991 tab_r_mask
.ToCairo()->MaskSource(cr
, widget
,
992 width() - tab_active_l_width_
, 0);
995 void TabRendererGtk::DrawTabShadow(cairo_t
* cr
,
1000 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
1001 gfx::Image
& active_image_l
= rb
.GetNativeImageNamed(left_idr
);
1002 gfx::Image
& active_image_c
= rb
.GetNativeImageNamed(center_idr
);
1003 gfx::Image
& active_image_r
= rb
.GetNativeImageNamed(right_idr
);
1005 // Draw left drop shadow
1006 active_image_l
.ToCairo()->SetSource(cr
, widget
, 0, 0);
1009 // Draw the center shadow
1010 active_image_c
.ToCairo()->SetSource(cr
, widget
, 0, 0);
1011 cairo_pattern_set_extend(cairo_get_source(cr
), CAIRO_EXTEND_REPEAT
);
1012 cairo_rectangle(cr
, tab_active_l_width_
, 0,
1013 width() - (2 * tab_active_l_width_
),
1017 // Draw right drop shadow
1018 active_image_r
.ToCairo()->SetSource(
1019 cr
, widget
, width() - active_image_r
.ToCairo()->Width(), 0);
1023 void TabRendererGtk::PaintInactiveTabBackground(GtkWidget
* widget
,
1025 int theme_id
= data_
.incognito
?
1026 IDR_THEME_TAB_BACKGROUND_INCOGNITO
: IDR_THEME_TAB_BACKGROUND
;
1028 gfx::Image tab_bg
= theme_service_
->GetImageNamed(theme_id
);
1030 // If the theme is providing a custom background image, then its top edge
1031 // should be at the top of the tab. Otherwise, we assume that the background
1032 // image is a composited foreground + frame image.
1033 int offset_y
= theme_service_
->HasCustomImage(theme_id
) ?
1034 0 : background_offset_y_
;
1036 DrawTabBackground(cr
, widget
, tab_bg
, background_offset_x_
, offset_y
);
1038 DrawTabShadow(cr
, widget
, IDR_TAB_INACTIVE_LEFT
, IDR_TAB_INACTIVE_CENTER
,
1039 IDR_TAB_INACTIVE_RIGHT
);
1042 void TabRendererGtk::PaintActiveTabBackground(GtkWidget
* widget
,
1044 gfx::Image tab_bg
= theme_service_
->GetImageNamed(IDR_THEME_TOOLBAR
);
1046 DrawTabBackground(cr
, widget
, tab_bg
, background_offset_x_
, 0);
1047 DrawTabShadow(cr
, widget
, IDR_TAB_ACTIVE_LEFT
, IDR_TAB_ACTIVE_CENTER
,
1048 IDR_TAB_ACTIVE_RIGHT
);
1051 void TabRendererGtk::PaintLoadingAnimation(GtkWidget
* widget
,
1053 int id
= loading_animation_
.animation_state() == ANIMATION_WAITING
?
1054 IDR_THROBBER_WAITING
: IDR_THROBBER
;
1055 gfx::Image throbber
= theme_service_
->GetImageNamed(id
);
1057 const int image_size
= throbber
.ToCairo()->Height();
1058 const int image_offset
= loading_animation_
.animation_frame() * image_size
;
1059 DCHECK(image_size
== favicon_bounds_
.height());
1060 DCHECK(image_size
== favicon_bounds_
.width());
1062 throbber
.ToCairo()->SetSource(cr
, widget
, favicon_bounds_
.x() - image_offset
,
1063 favicon_bounds_
.y());
1064 cairo_rectangle(cr
, favicon_bounds_
.x(), favicon_bounds_
.y(),
1065 image_size
, image_size
);
1069 int TabRendererGtk::IconCapacity() const {
1070 if (height() < GetMinimumUnselectedSize().height())
1072 const int available_width
=
1073 std::max(0, width() - kLeftPadding
- kRightPadding
);
1074 const int kPaddingBetweenIcons
= 2;
1075 if (available_width
>= gfx::kFaviconSize
&&
1076 available_width
< (gfx::kFaviconSize
+ kPaddingBetweenIcons
)) {
1079 return available_width
/ (gfx::kFaviconSize
+ kPaddingBetweenIcons
);
1082 bool TabRendererGtk::ShouldShowIcon() const {
1083 return chrome::ShouldTabShowFavicon(
1084 IconCapacity(), mini(), IsActive(), data_
.show_icon
,
1085 animating_media_state_
);
1088 bool TabRendererGtk::ShouldShowMediaIndicator() const {
1089 return chrome::ShouldTabShowMediaIndicator(
1090 IconCapacity(), mini(), IsActive(), data_
.show_icon
,
1091 animating_media_state_
);
1094 bool TabRendererGtk::ShouldShowCloseBox() const {
1095 return chrome::ShouldTabShowCloseButton(IconCapacity(), mini(), IsActive());
1098 CustomDrawButton
* TabRendererGtk::MakeCloseButton() {
1099 CustomDrawButton
* button
= CustomDrawButton::CloseButtonBar(theme_service_
);
1100 button
->ForceChromeTheme();
1102 g_signal_connect(button
->widget(), "clicked",
1103 G_CALLBACK(OnCloseButtonClickedThunk
), this);
1104 g_signal_connect(button
->widget(), "button-release-event",
1105 G_CALLBACK(OnCloseButtonMouseReleaseThunk
), this);
1106 g_signal_connect(button
->widget(), "enter-notify-event",
1107 G_CALLBACK(OnEnterNotifyEventThunk
), this);
1108 g_signal_connect(button
->widget(), "leave-notify-event",
1109 G_CALLBACK(OnLeaveNotifyEventThunk
), this);
1110 gtk_widget_set_can_focus(button
->widget(), FALSE
);
1111 gtk_fixed_put(GTK_FIXED(tab_
.get()), button
->widget(), 0, 0);
1116 double TabRendererGtk::GetThrobValue() {
1117 bool is_selected
= IsSelected();
1118 double min
= is_selected
? kSelectedTabOpacity
: 0;
1119 double scale
= is_selected
? kSelectedTabThrobScale
: 1;
1121 if (mini_title_animation_
.get() && mini_title_animation_
->is_animating()) {
1122 return mini_title_animation_
->GetCurrentValue() *
1123 kMiniTitleChangeThrobOpacity
* scale
+ min
;
1126 if (hover_animation_
.get())
1127 return kHoverOpacity
* hover_animation_
->GetCurrentValue() * scale
+ min
;
1129 return is_selected
? kSelectedTabOpacity
: 0;
1132 void TabRendererGtk::CloseButtonClicked() {
1136 void TabRendererGtk::OnCloseButtonClicked(GtkWidget
* widget
) {
1137 CloseButtonClicked();
1140 gboolean
TabRendererGtk::OnCloseButtonMouseRelease(GtkWidget
* widget
,
1141 GdkEventButton
* event
) {
1142 if (event
->button
== 2) {
1143 CloseButtonClicked();
1150 gboolean
TabRendererGtk::OnExposeEvent(GtkWidget
* widget
,
1151 GdkEventExpose
* event
) {
1152 TRACE_EVENT0("ui::gtk", "TabRendererGtk::OnExposeEvent");
1154 PaintTab(widget
, event
);
1155 gtk_container_propagate_expose(GTK_CONTAINER(tab_
.get()),
1156 close_button_
->widget(), event
);
1160 void TabRendererGtk::OnSizeAllocate(GtkWidget
* widget
,
1161 GtkAllocation
* allocation
) {
1162 gfx::Rect bounds
= gfx::Rect(allocation
->x
, allocation
->y
,
1163 allocation
->width
, allocation
->height
);
1165 // Nothing to do if the bounds are the same. If we don't catch this, we'll
1166 // get an infinite loop of size-allocate signals.
1167 if (bounds_
== bounds
)
1174 gboolean
TabRendererGtk::OnEnterNotifyEvent(GtkWidget
* widget
,
1175 GdkEventCrossing
* event
) {
1176 hover_animation_
->SetTweenType(gfx::Tween::EASE_OUT
);
1177 hover_animation_
->Show();
1181 gboolean
TabRendererGtk::OnLeaveNotifyEvent(GtkWidget
* widget
,
1182 GdkEventCrossing
* event
) {
1183 hover_animation_
->SetTweenType(gfx::Tween::EASE_IN
);
1184 hover_animation_
->Hide();
1189 void TabRendererGtk::InitResources() {
1193 // Grab the pixel sizes of our masking images.
1194 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
1195 GdkPixbuf
* tab_active_l
= rb
.GetNativeImageNamed(
1196 IDR_TAB_ACTIVE_LEFT
).ToGdkPixbuf();
1197 tab_active_l_width_
= gdk_pixbuf_get_width(tab_active_l
);
1198 tab_active_l_height_
= gdk_pixbuf_get_height(tab_active_l
);
1200 GdkPixbuf
* tab_inactive_l
= rb
.GetNativeImageNamed(
1201 IDR_TAB_INACTIVE_LEFT
).ToGdkPixbuf();
1202 tab_inactive_l_height_
= gdk_pixbuf_get_height(tab_inactive_l
);
1204 GdkPixbuf
* tab_close
= rb
.GetNativeImageNamed(IDR_CLOSE_1
).ToGdkPixbuf();
1205 close_button_width_
= gdk_pixbuf_get_width(tab_close
);
1206 close_button_height_
= gdk_pixbuf_get_height(tab_close
);
1208 const gfx::Font
& base_font
= rb
.GetFont(ui::ResourceBundle::BaseFont
);
1209 title_font_
= new gfx::Font(base_font
.GetFontName(), kFontPixelSize
);
1210 title_font_height_
= title_font_
->GetHeight();
1212 initialized_
= true;