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