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/views/tabs/tab.h"
9 #include "base/command_line.h"
10 #include "base/debug/alias.h"
11 #include "base/profiler/scoped_tracker.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "chrome/browser/themes/theme_properties.h"
14 #include "chrome/browser/ui/browser.h"
15 #include "chrome/browser/ui/tab_contents/core_tab_helper.h"
16 #include "chrome/browser/ui/tabs/tab_utils.h"
17 #include "chrome/browser/ui/view_ids.h"
18 #include "chrome/browser/ui/views/layout_constants.h"
19 #include "chrome/browser/ui/views/tabs/media_indicator_button.h"
20 #include "chrome/browser/ui/views/tabs/tab_controller.h"
21 #include "chrome/browser/ui/views/theme_image_mapper.h"
22 #include "chrome/browser/ui/views/touch_uma/touch_uma.h"
23 #include "chrome/common/chrome_switches.h"
24 #include "chrome/grit/generated_resources.h"
25 #include "content/public/browser/user_metrics.h"
26 #include "grit/theme_resources.h"
27 #include "third_party/skia/include/effects/SkGradientShader.h"
28 #include "ui/accessibility/ax_view_state.h"
29 #include "ui/base/l10n/l10n_util.h"
30 #include "ui/base/models/list_selection_model.h"
31 #include "ui/base/resource/resource_bundle.h"
32 #include "ui/base/theme_provider.h"
33 #include "ui/gfx/animation/animation_container.h"
34 #include "ui/gfx/animation/multi_animation.h"
35 #include "ui/gfx/animation/throb_animation.h"
36 #include "ui/gfx/canvas.h"
37 #include "ui/gfx/color_analysis.h"
38 #include "ui/gfx/favicon_size.h"
39 #include "ui/gfx/geometry/rect_conversions.h"
40 #include "ui/gfx/image/image_skia_operations.h"
41 #include "ui/gfx/path.h"
42 #include "ui/gfx/skia_util.h"
43 #include "ui/resources/grit/ui_resources.h"
44 #include "ui/views/border.h"
45 #include "ui/views/controls/button/image_button.h"
46 #include "ui/views/controls/label.h"
47 #include "ui/views/rect_based_targeting_utils.h"
48 #include "ui/views/view_targeter.h"
49 #include "ui/views/widget/tooltip_manager.h"
50 #include "ui/views/widget/widget.h"
51 #include "ui/views/window/non_client_view.h"
54 #include "ui/aura/env.h"
57 using base::UserMetricsAction
;
61 // Height of the shadow at the top of the tab image assets.
62 const int kDropShadowHeight
= 4;
64 // How long the pulse throb takes.
65 const int kPulseDurationMs
= 200;
67 // Width of touch tabs.
68 const int kTouchWidth
= 120;
70 const int kToolbarOverlap
= 1;
71 const int kExtraLeftPaddingToBalanceCloseButtonPadding
= 2;
72 const int kAfterTitleSpacing
= 3;
74 // When a non-pinned tab becomes a pinned tab the width of the tab animates. If
75 // the width of a pinned tab is at least kPinnedTabExtraWidthToRenderAsNormal
76 // larger than the desired pinned tab width then the tab is rendered as a normal
77 // tab. This is done to avoid having the title immediately disappear when
78 // transitioning a tab from normal to pinned tab.
79 const int kPinnedTabExtraWidthToRenderAsNormal
= 30;
81 // How opaque to make the hover state (out of 1).
82 const double kHoverOpacity
= 0.33;
84 // Opacity for non-active selected tabs.
85 const double kSelectedTabOpacity
= .45;
87 // Selected (but not active) tabs have their throb value scaled down by this.
88 const double kSelectedTabThrobScale
= .5;
90 // Durations for the various parts of the pinned tab title animation.
91 const int kPinnedTitleChangeAnimationDuration1MS
= 1600;
92 const int kPinnedTitleChangeAnimationStart1MS
= 0;
93 const int kPinnedTitleChangeAnimationEnd1MS
= 1900;
94 const int kPinnedTitleChangeAnimationDuration2MS
= 0;
95 const int kPinnedTitleChangeAnimationDuration3MS
= 550;
96 const int kPinnedTitleChangeAnimationStart3MS
= 150;
97 const int kPinnedTitleChangeAnimationEnd3MS
= 800;
98 const int kPinnedTitleChangeAnimationIntervalMS
= 40;
100 // Offset from the right edge for the start of the pinned title change
102 const int kPinnedTitleChangeInitialXOffset
= 6;
104 // Radius of the radial gradient used for pinned title change animation.
105 const int kPinnedTitleChangeGradientRadius
= 20;
107 // Colors of the gradient used during the pinned title change animation.
108 const SkColor kPinnedTitleChangeGradientColor1
= SK_ColorWHITE
;
109 const SkColor kPinnedTitleChangeGradientColor2
=
110 SkColorSetARGB(0, 255, 255, 255);
112 // Max number of images to cache. This has to be at least two since rounding
113 // errors may lead to tabs in the same tabstrip having different sizes.
114 const size_t kMaxImageCacheSize
= 4;
116 // Height of the miniature tab strip in immersive mode.
117 const int kImmersiveTabHeight
= 3;
119 // Height of the small tab indicator rectangles in immersive mode.
120 const int kImmersiveBarHeight
= 2;
122 // Color for active and inactive tabs in the immersive mode light strip. These
123 // should be a little brighter than the color of the normal art assets for tabs,
124 // which for active tabs is 230, 230, 230 and for inactive is 184, 184, 184.
125 const SkColor kImmersiveActiveTabColor
= SkColorSetRGB(235, 235, 235);
126 const SkColor kImmersiveInactiveTabColor
= SkColorSetRGB(190, 190, 190);
128 // The minimum opacity (out of 1) when a tab (either active or inactive) is
129 // throbbing in the immersive mode light strip.
130 const double kImmersiveTabMinThrobOpacity
= 0.66;
132 // Number of steps in the immersive mode loading animation.
133 const int kImmersiveLoadingStepCount
= 32;
135 const char kTabCloseButtonName
[] = "TabCloseButton";
137 void DrawIconAtLocation(gfx::Canvas
* canvas
,
138 const gfx::ImageSkia
& image
,
145 const SkPaint
& paint
) {
146 // NOTE: the clipping is a work around for 69528, it shouldn't be necessary.
148 canvas
->ClipRect(gfx::Rect(dst_x
, dst_y
, icon_width
, icon_height
));
149 canvas
->DrawImageInt(image
,
150 image_offset
, 0, icon_width
, icon_height
,
151 dst_x
, dst_y
, icon_width
, icon_height
,
156 // Draws the icon image at the center of |bounds|.
157 void DrawIconCenter(gfx::Canvas
* canvas
,
158 const gfx::ImageSkia
& image
,
162 const gfx::Rect
& bounds
,
164 const SkPaint
& paint
) {
165 // Center the image within bounds.
166 int dst_x
= bounds
.x() - (icon_width
- bounds
.width()) / 2;
167 int dst_y
= bounds
.y() - (icon_height
- bounds
.height()) / 2;
168 DrawIconAtLocation(canvas
, image
, image_offset
, dst_x
, dst_y
, icon_width
,
169 icon_height
, filter
, paint
);
172 chrome::HostDesktopType
GetHostDesktopType(views::View
* view
) {
173 // Widget is NULL when tabs are detached.
174 views::Widget
* widget
= view
->GetWidget();
175 return chrome::GetHostDesktopTypeForNativeView(
176 widget
? widget
->GetNativeView() : NULL
);
179 // Stop()s |animation| and then deletes it. We do this rather than just deleting
180 // so that the delegate is notified before the destruction.
181 void StopAndDeleteAnimation(scoped_ptr
<gfx::Animation
> animation
) {
188 ////////////////////////////////////////////////////////////////////////////////
189 // FaviconCrashAnimation
191 // A custom animation subclass to manage the favicon crash animation.
192 class Tab::FaviconCrashAnimation
: public gfx::LinearAnimation
,
193 public gfx::AnimationDelegate
{
195 explicit FaviconCrashAnimation(Tab
* target
)
196 : gfx::LinearAnimation(1000, 25, this),
199 ~FaviconCrashAnimation() override
{}
201 // gfx::Animation overrides:
202 void AnimateToState(double state
) override
{
203 const double kHidingOffset
= 27;
206 target_
->SetFaviconHidingOffset(
207 static_cast<int>(floor(kHidingOffset
* 2.0 * state
)));
209 target_
->DisplayCrashedFavicon();
210 target_
->SetFaviconHidingOffset(
212 floor(kHidingOffset
- ((state
- .5) * 2.0 * kHidingOffset
))));
216 // gfx::AnimationDelegate overrides:
217 void AnimationCanceled(const gfx::Animation
* animation
) override
{
218 target_
->SetFaviconHidingOffset(0);
224 DISALLOW_COPY_AND_ASSIGN(FaviconCrashAnimation
);
227 ////////////////////////////////////////////////////////////////////////////////
230 // This is a Button subclass that causes middle clicks to be forwarded to the
231 // parent View by explicitly not handling them in OnMousePressed.
232 class Tab::TabCloseButton
: public views::ImageButton
,
233 public views::MaskedTargeterDelegate
{
235 explicit TabCloseButton(Tab
* tab
)
236 : views::ImageButton(tab
),
239 scoped_ptr
<views::ViewTargeter
>(new views::ViewTargeter(this)));
242 ~TabCloseButton() override
{}
245 View
* GetTooltipHandlerForPoint(const gfx::Point
& point
) override
{
246 // Tab close button has no children, so tooltip handler should be the same
247 // as the event handler.
248 // In addition, a hit test has to be performed for the point (as
249 // GetTooltipHandlerForPoint() is responsible for it).
250 if (!HitTestPoint(point
))
252 return GetEventHandlerForPoint(point
);
255 bool OnMousePressed(const ui::MouseEvent
& event
) override
{
256 tab_
->controller_
->OnMouseEventInTab(this, event
);
258 bool handled
= ImageButton::OnMousePressed(event
);
259 // Explicitly mark midle-mouse clicks as non-handled to ensure the tab
261 return !event
.IsMiddleMouseButton() && handled
;
264 void OnMouseMoved(const ui::MouseEvent
& event
) override
{
265 tab_
->controller_
->OnMouseEventInTab(this, event
);
266 CustomButton::OnMouseMoved(event
);
269 void OnMouseReleased(const ui::MouseEvent
& event
) override
{
270 tab_
->controller_
->OnMouseEventInTab(this, event
);
271 CustomButton::OnMouseReleased(event
);
274 void OnGestureEvent(ui::GestureEvent
* event
) override
{
275 // Consume all gesture events here so that the parent (Tab) does not
276 // start consuming gestures.
277 ImageButton::OnGestureEvent(event
);
281 const char* GetClassName() const override
{ return kTabCloseButtonName
; }
284 // Returns the rectangular bounds of parent tab's visible region in the
285 // local coordinate space of |this|.
286 gfx::Rect
GetTabBounds() const {
288 tab_
->GetHitTestMask(&tab_mask
);
290 gfx::RectF
tab_bounds_f(gfx::SkRectToRectF(tab_mask
.getBounds()));
291 views::View::ConvertRectToTarget(tab_
, this, &tab_bounds_f
);
292 return gfx::ToEnclosingRect(tab_bounds_f
);
295 // Returns the rectangular bounds of the tab close button in the local
296 // coordinate space of |this|, not including clipped regions on the top
297 // or bottom of the button. |tab_bounds| is the rectangular bounds of
298 // the parent tab's visible region in the local coordinate space of |this|.
299 gfx::Rect
GetTabCloseButtonBounds(const gfx::Rect
& tab_bounds
) const {
300 gfx::Rect
button_bounds(GetContentsBounds());
301 button_bounds
.set_x(GetMirroredXForRect(button_bounds
));
303 int top_overflow
= tab_bounds
.y() - button_bounds
.y();
304 int bottom_overflow
= button_bounds
.bottom() - tab_bounds
.bottom();
305 if (top_overflow
> 0)
306 button_bounds
.set_y(tab_bounds
.y());
307 else if (bottom_overflow
> 0)
308 button_bounds
.set_height(button_bounds
.height() - bottom_overflow
);
310 return button_bounds
;
313 // views::ViewTargeterDelegate:
314 View
* TargetForRect(View
* root
, const gfx::Rect
& rect
) override
{
315 CHECK_EQ(root
, this);
317 if (!views::UsePointBasedTargeting(rect
))
318 return ViewTargeterDelegate::TargetForRect(root
, rect
);
320 // Ignore the padding set on the button.
321 gfx::Rect contents_bounds
= GetContentsBounds();
322 contents_bounds
.set_x(GetMirroredXForRect(contents_bounds
));
324 #if defined(USE_AURA)
325 // Include the padding in hit-test for touch events.
326 if (aura::Env::GetInstance()->is_touch_down())
327 contents_bounds
= GetLocalBounds();
330 return contents_bounds
.Intersects(rect
) ? this : parent();
333 // views:MaskedTargeterDelegate:
334 bool GetHitTestMask(gfx::Path
* mask
) const override
{
338 // The parent tab may be partially occluded by another tab if we are
339 // in stacked tab mode, which means that the tab close button may also
340 // be partially occluded. Define the hit test mask of the tab close
341 // button to be the intersection of the parent tab's visible bounds
342 // and the bounds of the tab close button.
343 gfx::Rect
tab_bounds(GetTabBounds());
344 gfx::Rect
button_bounds(GetTabCloseButtonBounds(tab_bounds
));
345 gfx::Rect
intersection(gfx::IntersectRects(tab_bounds
, button_bounds
));
347 if (!intersection
.IsEmpty()) {
348 mask
->addRect(RectToSkRect(intersection
));
355 bool DoesIntersectRect(const View
* target
,
356 const gfx::Rect
& rect
) const override
{
357 CHECK_EQ(target
, this);
359 // If the request is not made in response to a gesture, use the
360 // default implementation.
361 if (views::UsePointBasedTargeting(rect
))
362 return MaskedTargeterDelegate::DoesIntersectRect(target
, rect
);
364 // The hit test request is in response to a gesture. Return false if any
365 // part of the tab close button is hidden from the user.
366 // TODO(tdanderson): Consider always returning the intersection if the
367 // non-rectangular shape of the tab can be accounted for.
368 gfx::Rect
tab_bounds(GetTabBounds());
369 gfx::Rect
button_bounds(GetTabCloseButtonBounds(tab_bounds
));
370 if (!tab_bounds
.Contains(button_bounds
))
373 return MaskedTargeterDelegate::DoesIntersectRect(target
, rect
);
378 DISALLOW_COPY_AND_ASSIGN(TabCloseButton
);
381 ////////////////////////////////////////////////////////////////////////////////
384 Tab::ImageCacheEntry::ImageCacheEntry()
386 scale_factor(ui::SCALE_FACTOR_NONE
) {
389 Tab::ImageCacheEntry::~ImageCacheEntry() {}
391 ////////////////////////////////////////////////////////////////////////////////
395 const char Tab::kViewClassName
[] = "Tab";
396 Tab::TabImage
Tab::tab_active_
= {0};
397 Tab::TabImage
Tab::tab_inactive_
= {0};
398 Tab::TabImage
Tab::tab_alpha_
= {0};
399 Tab::ImageCache
* Tab::image_cache_
= NULL
;
401 ////////////////////////////////////////////////////////////////////////////////
404 Tab::Tab(TabController
* controller
)
405 : controller_(controller
),
409 favicon_hiding_offset_(0),
410 immersive_loading_step_(0),
411 should_display_crashed_favicon_(false),
413 media_indicator_button_(NULL
),
414 title_(new views::Label()),
415 tab_activated_with_last_tap_down_(false),
416 hover_controller_(this),
417 showing_icon_(false),
418 showing_media_indicator_(false),
419 showing_close_button_(false),
420 close_button_color_(0) {
424 // So we get don't get enter/exit on children and don't prematurely stop the
426 set_notify_enter_exit_on_child(true);
430 title_
->SetHorizontalAlignment(gfx::ALIGN_TO_HEAD
);
431 title_
->SetElideBehavior(gfx::FADE_TAIL
);
432 title_
->SetHandlesTooltips(false);
433 title_
->SetAutoColorReadabilityEnabled(false);
434 title_
->SetText(CoreTabHelper::GetDefaultTitle());
435 AddChildView(title_
);
438 scoped_ptr
<views::ViewTargeter
>(new views::ViewTargeter(this)));
440 // Add the Close Button.
441 close_button_
= new TabCloseButton(this);
442 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
443 close_button_
->SetImage(views::CustomButton::STATE_NORMAL
,
444 rb
.GetImageSkiaNamed(IDR_CLOSE_1
));
445 close_button_
->SetImage(views::CustomButton::STATE_HOVERED
,
446 rb
.GetImageSkiaNamed(IDR_CLOSE_1_H
));
447 close_button_
->SetImage(views::CustomButton::STATE_PRESSED
,
448 rb
.GetImageSkiaNamed(IDR_CLOSE_1_P
));
449 close_button_
->SetAccessibleName(
450 l10n_util::GetStringUTF16(IDS_ACCNAME_CLOSE
));
451 // Disable animation so that the red danger sign shows up immediately
452 // to help avoid mis-clicks.
453 close_button_
->SetAnimationDuration(0);
454 AddChildView(close_button_
);
456 set_context_menu_controller(this);
462 void Tab::set_animation_container(gfx::AnimationContainer
* container
) {
463 animation_container_
= container
;
464 hover_controller_
.SetAnimationContainer(container
);
467 bool Tab::IsActive() const {
468 return controller_
->IsActiveTab(this);
471 void Tab::ActiveStateChanged() {
472 GetMediaIndicatorButton()->UpdateEnabledForMuteToggle();
475 bool Tab::IsSelected() const {
476 return controller_
->IsTabSelected(this);
479 void Tab::SetData(const TabRendererData
& data
) {
482 if (data_
.Equals(data
))
485 TabRendererData
old(data_
);
488 base::string16 title
= data_
.title
;
490 title
= data_
.loading
?
491 l10n_util::GetStringUTF16(IDS_TAB_LOADING_TITLE
) :
492 CoreTabHelper::GetDefaultTitle();
494 Browser::FormatTitleForDisplay(&title
);
496 title_
->SetText(title
);
498 if (data_
.IsCrashed()) {
499 if (!should_display_crashed_favicon_
&& !IsPerformingCrashAnimation()) {
500 data_
.media_state
= TAB_MEDIA_STATE_NONE
;
501 #if defined(OS_CHROMEOS)
502 // On Chrome OS, we reload killed tabs automatically when the user
503 // switches to them. Don't display animations for these unless they're
504 // selected (i.e. in the foreground) -- we won't reload these
505 // automatically since we don't want to get into a crash loop.
507 (data_
.crashed_status
508 != base::TERMINATION_STATUS_PROCESS_WAS_KILLED
&&
510 != base::TERMINATION_STATUS_PROCESS_WAS_KILLED_BY_OOM
)) {
511 StartCrashAnimation();
514 StartCrashAnimation();
518 if (IsPerformingCrashAnimation())
519 StopCrashAnimation();
520 ResetCrashedFavicon();
523 if (data_
.media_state
!= old
.media_state
)
524 GetMediaIndicatorButton()->TransitionToMediaState(data_
.media_state
);
526 if (old
.pinned
!= data_
.pinned
) {
527 StopAndDeleteAnimation(pinned_title_change_animation_
.Pass());
536 void Tab::UpdateLoadingAnimation(TabRendererData::NetworkState state
) {
537 if (state
== data_
.network_state
&&
538 state
== TabRendererData::NETWORK_STATE_NONE
) {
539 // If the network state is none and hasn't changed, do nothing. Otherwise we
540 // need to advance the animation frame.
544 TabRendererData::NetworkState old_state
= data_
.network_state
;
545 data_
.network_state
= state
;
546 AdvanceLoadingAnimation(old_state
, state
);
549 void Tab::StartPulse() {
550 pulse_animation_
.reset(new gfx::ThrobAnimation(this));
551 pulse_animation_
->SetSlideDuration(kPulseDurationMs
);
552 if (animation_container_
.get())
553 pulse_animation_
->SetContainer(animation_container_
.get());
554 pulse_animation_
->StartThrobbing(std::numeric_limits
<int>::max());
557 void Tab::StopPulse() {
558 StopAndDeleteAnimation(pulse_animation_
.Pass());
561 void Tab::StartPinnedTabTitleAnimation() {
564 if (!pinned_title_change_animation_
) {
565 gfx::MultiAnimation::Parts parts
;
567 gfx::MultiAnimation::Part(kPinnedTitleChangeAnimationDuration1MS
,
568 gfx::Tween::EASE_OUT
));
570 gfx::MultiAnimation::Part(kPinnedTitleChangeAnimationDuration2MS
,
573 gfx::MultiAnimation::Part(kPinnedTitleChangeAnimationDuration3MS
,
574 gfx::Tween::EASE_IN
));
575 parts
[0].start_time_ms
= kPinnedTitleChangeAnimationStart1MS
;
576 parts
[0].end_time_ms
= kPinnedTitleChangeAnimationEnd1MS
;
577 parts
[2].start_time_ms
= kPinnedTitleChangeAnimationStart3MS
;
578 parts
[2].end_time_ms
= kPinnedTitleChangeAnimationEnd3MS
;
579 base::TimeDelta timeout
= base::TimeDelta::FromMilliseconds(
580 kPinnedTitleChangeAnimationIntervalMS
);
581 pinned_title_change_animation_
.reset(
582 new gfx::MultiAnimation(parts
, timeout
));
583 if (animation_container_
.get())
584 pinned_title_change_animation_
->SetContainer(animation_container_
.get());
585 pinned_title_change_animation_
->set_delegate(this);
587 pinned_title_change_animation_
->Start();
590 void Tab::StopPinnedTabTitleAnimation() {
591 StopAndDeleteAnimation(pinned_title_change_animation_
.Pass());
594 int Tab::GetWidthOfLargestSelectableRegion() const {
595 // Assume the entire region to the left of the media indicator and/or close
596 // buttons is available for click-to-select. If neither are visible, the
597 // entire tab region is available.
598 const int indicator_left
= showing_media_indicator_
?
599 media_indicator_button_
->x() : width();
600 const int close_button_left
= showing_close_button_
?
601 close_button_
->x() : width();
602 return std::min(indicator_left
, close_button_left
);
605 gfx::Size
Tab::GetMinimumUnselectedSize() {
606 // Since we use images, the real minimum height of the image is
607 // defined most accurately by the height of the end cap images.
609 int height
= tab_active_
.image_l
->height();
610 return gfx::Size(GetLayoutInsets(TAB
).width(), height
);
614 gfx::Size
Tab::GetMinimumSelectedSize() {
615 gfx::Size minimum_size
= GetMinimumUnselectedSize();
616 minimum_size
.Enlarge(gfx::kFaviconSize
, 0);
621 gfx::Size
Tab::GetStandardSize() {
622 gfx::Size standard_size
= GetMinimumUnselectedSize();
623 const int title_spacing
= GetLayoutConstant(TAB_FAVICON_TITLE_SPACING
);
624 const int title_width
= GetLayoutConstant(TAB_MAXIMUM_TITLE_WIDTH
);
625 standard_size
.Enlarge(title_spacing
+ title_width
, 0);
626 return standard_size
;
630 int Tab::GetTouchWidth() {
635 int Tab::GetPinnedWidth() {
636 return GetMinimumUnselectedSize().width() +
637 GetLayoutConstant(TAB_PINNED_CONTENT_WIDTH
);
641 int Tab::GetImmersiveHeight() {
642 return kImmersiveTabHeight
;
645 ////////////////////////////////////////////////////////////////////////////////
646 // Tab, AnimationDelegate overrides:
648 void Tab::AnimationProgressed(const gfx::Animation
* animation
) {
649 // Ignore if the pulse animation is being performed on active tab because
650 // it repaints the same image. See |Tab::PaintTabBackground()|.
651 if (animation
== pulse_animation_
.get() && IsActive())
656 void Tab::AnimationCanceled(const gfx::Animation
* animation
) {
660 void Tab::AnimationEnded(const gfx::Animation
* animation
) {
664 ////////////////////////////////////////////////////////////////////////////////
665 // Tab, views::ButtonListener overrides:
667 void Tab::ButtonPressed(views::Button
* sender
, const ui::Event
& event
) {
668 if (media_indicator_button_
&& media_indicator_button_
->visible()) {
669 if (media_indicator_button_
->enabled())
670 content::RecordAction(UserMetricsAction("CloseTab_MuteToggleAvailable"));
671 else if (data_
.media_state
== TAB_MEDIA_STATE_AUDIO_PLAYING
)
672 content::RecordAction(UserMetricsAction("CloseTab_AudioIndicator"));
674 content::RecordAction(UserMetricsAction("CloseTab_RecordingIndicator"));
676 content::RecordAction(UserMetricsAction("CloseTab_NoMediaIndicator"));
679 const CloseTabSource source
=
680 (event
.type() == ui::ET_MOUSE_RELEASED
&&
681 (event
.flags() & ui::EF_FROM_TOUCH
) == 0) ? CLOSE_TAB_FROM_MOUSE
:
682 CLOSE_TAB_FROM_TOUCH
;
683 DCHECK_EQ(close_button_
, sender
);
684 controller_
->CloseTab(this, source
);
685 if (event
.type() == ui::ET_GESTURE_TAP
)
686 TouchUMA::RecordGestureAction(TouchUMA::GESTURE_TABCLOSE_TAP
);
689 ////////////////////////////////////////////////////////////////////////////////
690 // Tab, views::ContextMenuController overrides:
692 void Tab::ShowContextMenuForView(views::View
* source
,
693 const gfx::Point
& point
,
694 ui::MenuSourceType source_type
) {
696 controller_
->ShowContextMenuForTab(this, point
, source_type
);
699 ////////////////////////////////////////////////////////////////////////////////
700 // Tab, views::MaskedTargeterDelegate overrides:
702 bool Tab::GetHitTestMask(gfx::Path
* mask
) const {
705 // When the window is maximized we don't want to shave off the edges or top
706 // shadow of the tab, such that the user can click anywhere along the top
707 // edge of the screen to select a tab. Ditto for immersive fullscreen.
708 const views::Widget
* widget
= GetWidget();
709 GetHitTestMaskHelper(
710 widget
&& (widget
->IsMaximized() || widget
->IsFullscreen()), mask
);
712 // It is possible for a portion of the tab to be occluded if tabs are
713 // stacked, so modify the hit test mask to only include the visible
714 // region of the tab.
716 controller_
->ShouldPaintTab(this, &clip
);
717 if (clip
.size().GetArea()) {
718 SkRect
intersection(mask
->getBounds());
720 if (!intersection
.intersect(RectToSkRect(clip
)))
722 mask
->addRect(intersection
);
727 ////////////////////////////////////////////////////////////////////////////////
728 // Tab, views::View overrides:
730 void Tab::OnPaint(gfx::Canvas
* canvas
) {
731 // Don't paint if we're narrower than we can render correctly. (This should
732 // only happen during animations).
733 if (width() < GetMinimumUnselectedSize().width() && !data().pinned
)
737 if (!controller_
->ShouldPaintTab(this, &clip
))
739 if (!clip
.IsEmpty()) {
741 canvas
->ClipRect(clip
);
744 if (controller_
->IsImmersiveStyle())
745 PaintImmersiveTab(canvas
);
754 gfx::Rect lb
= GetContentsBounds();
758 lb
.Inset(GetLayoutInsets(TAB
));
759 showing_icon_
= ShouldShowIcon();
760 // See comments in IconCapacity().
761 const int extra_padding
=
762 (controller_
->ShouldHideCloseButtonForInactiveTabs() ||
763 (IconCapacity() < 3)) ? 0 : kExtraLeftPaddingToBalanceCloseButtonPadding
;
764 const int start
= lb
.x() + extra_padding
;
765 favicon_bounds_
.SetRect(start
, lb
.y(), 0, 0);
767 favicon_bounds_
.set_size(gfx::Size(gfx::kFaviconSize
, gfx::kFaviconSize
));
768 favicon_bounds_
.set_y(lb
.y() + (lb
.height() - gfx::kFaviconSize
+ 1) / 2);
769 MaybeAdjustLeftForPinnedTab(&favicon_bounds_
);
772 showing_close_button_
= ShouldShowCloseBox();
773 if (showing_close_button_
) {
774 // If the ratio of the close button size to tab width exceeds the maximum.
775 // The close button should be as large as possible so that there is a larger
776 // hit-target for touch events. So the close button bounds extends to the
777 // edges of the tab. However, the larger hit-target should be active only
778 // for mouse events, and the close-image should show up in the right place.
779 // So a border is added to the button with necessary padding. The close
780 // button (BaseTab::TabCloseButton) makes sure the padding is a hit-target
781 // only for touch events.
782 close_button_
->SetBorder(views::Border::NullBorder());
783 const gfx::Size
close_button_size(close_button_
->GetPreferredSize());
784 const int top
= lb
.y() + (lb
.height() - close_button_size
.height() + 1) / 2;
785 const int left
= kAfterTitleSpacing
;
786 const int close_button_end
= lb
.right() +
787 GetLayoutConstant(TAB_CLOSE_BUTTON_TRAILING_PADDING_OVERLAP
);
788 close_button_
->SetPosition(
789 gfx::Point(close_button_end
- close_button_size
.width() - left
, 0));
790 const int bottom
= height() - close_button_size
.height() - top
;
791 const int right
= width() - close_button_end
;
792 close_button_
->SetBorder(
793 views::Border::CreateEmptyBorder(top
, left
, bottom
, right
));
794 close_button_
->SizeToPreferredSize();
796 close_button_
->SetVisible(showing_close_button_
);
798 showing_media_indicator_
= ShouldShowMediaIndicator();
799 if (showing_media_indicator_
) {
800 views::ImageButton
* const button
= GetMediaIndicatorButton();
801 const gfx::Size
image_size(button
->GetPreferredSize());
802 const int right
= showing_close_button_
?
803 close_button_
->x() + close_button_
->GetInsets().left() : lb
.right();
805 std::max(lb
.x(), right
- image_size
.width()),
806 lb
.y() + (lb
.height() - image_size
.height() + 1) / 2,
808 image_size
.height());
809 MaybeAdjustLeftForPinnedTab(&bounds
);
810 button
->SetBoundsRect(bounds
);
811 button
->SetVisible(true);
812 } else if (media_indicator_button_
) {
813 media_indicator_button_
->SetVisible(false);
816 // Size the title to fill the remaining width and use all available height.
817 const bool show_title
= ShouldRenderAsNormalTab();
819 const int title_spacing
= GetLayoutConstant(TAB_FAVICON_TITLE_SPACING
);
820 int title_left
= showing_icon_
?
821 (favicon_bounds_
.right() + title_spacing
) : start
;
822 int title_width
= lb
.right() - title_left
;
823 if (showing_media_indicator_
) {
825 media_indicator_button_
->x() - kAfterTitleSpacing
- title_left
;
826 } else if (close_button_
->visible()) {
827 // Allow the title to overlay the close button's empty border padding.
828 title_width
= close_button_
->x() + close_button_
->GetInsets().left() -
829 kAfterTitleSpacing
- title_left
;
831 gfx::Rect
rect(title_left
, lb
.y(), std::max(title_width
, 0), lb
.height());
832 const int title_height
= title_
->GetPreferredSize().height();
833 if (title_height
> rect
.height()) {
834 rect
.set_y(lb
.y() - (title_height
- rect
.height()) / 2);
835 rect
.set_height(title_height
);
837 title_
->SetBoundsRect(rect
);
839 title_
->SetVisible(show_title
);
842 void Tab::OnThemeChanged() {
846 const char* Tab::GetClassName() const {
847 return kViewClassName
;
850 bool Tab::GetTooltipText(const gfx::Point
& p
, base::string16
* tooltip
) const {
851 // Note: Anything that affects the tooltip text should be accounted for when
852 // calling TooltipTextChanged() from Tab::DataChanged().
853 *tooltip
= chrome::AssembleTabTooltipText(data_
.title
, data_
.media_state
);
854 return !tooltip
->empty();
857 bool Tab::GetTooltipTextOrigin(const gfx::Point
& p
, gfx::Point
* origin
) const {
858 origin
->set_x(title_
->x() + 10);
863 bool Tab::OnMousePressed(const ui::MouseEvent
& event
) {
864 controller_
->OnMouseEventInTab(this, event
);
866 // Allow a right click from touch to drag, which corresponds to a long click.
867 if (event
.IsOnlyLeftMouseButton() ||
868 (event
.IsOnlyRightMouseButton() && event
.flags() & ui::EF_FROM_TOUCH
)) {
869 ui::ListSelectionModel original_selection
;
870 original_selection
.Copy(controller_
->GetSelectionModel());
871 // Changing the selection may cause our bounds to change. If that happens
872 // the location of the event may no longer be valid. Create a copy of the
873 // event in the parents coordinate, which won't change, and recreate an
874 // event after changing so the coordinates are correct.
875 ui::MouseEvent
event_in_parent(event
, static_cast<View
*>(this), parent());
876 if (controller_
->SupportsMultipleSelection()) {
877 if (event
.IsShiftDown() && event
.IsControlDown()) {
878 controller_
->AddSelectionFromAnchorTo(this);
879 } else if (event
.IsShiftDown()) {
880 controller_
->ExtendSelectionTo(this);
881 } else if (event
.IsControlDown()) {
882 controller_
->ToggleSelected(this);
884 // Don't allow dragging non-selected tabs.
887 } else if (!IsSelected()) {
888 controller_
->SelectTab(this);
889 content::RecordAction(UserMetricsAction("SwitchTab_Click"));
891 } else if (!IsSelected()) {
892 controller_
->SelectTab(this);
893 content::RecordAction(UserMetricsAction("SwitchTab_Click"));
895 ui::MouseEvent
cloned_event(event_in_parent
, parent(),
896 static_cast<View
*>(this));
897 controller_
->MaybeStartDrag(this, cloned_event
, original_selection
);
902 bool Tab::OnMouseDragged(const ui::MouseEvent
& event
) {
903 controller_
->ContinueDrag(this, event
);
907 void Tab::OnMouseReleased(const ui::MouseEvent
& event
) {
908 controller_
->OnMouseEventInTab(this, event
);
910 // Notify the drag helper that we're done with any potential drag operations.
911 // Clean up the drag helper, which is re-created on the next mouse press.
912 // In some cases, ending the drag will schedule the tab for destruction; if
913 // so, bail immediately, since our members are already dead and we shouldn't
914 // do anything else except drop the tab where it is.
915 if (controller_
->EndDrag(END_DRAG_COMPLETE
))
918 // Close tab on middle click, but only if the button is released over the tab
919 // (normal windows behavior is to discard presses of a UI element where the
920 // releases happen off the element).
921 if (event
.IsMiddleMouseButton()) {
922 if (HitTestPoint(event
.location())) {
923 controller_
->CloseTab(this, CLOSE_TAB_FROM_MOUSE
);
924 } else if (closing_
) {
925 // We're animating closed and a middle mouse button was pushed on us but
926 // we don't contain the mouse anymore. We assume the user is clicking
927 // quicker than the animation and we should close the tab that falls under
929 Tab
* closest_tab
= controller_
->GetTabAt(this, event
.location());
931 controller_
->CloseTab(closest_tab
, CLOSE_TAB_FROM_MOUSE
);
933 } else if (event
.IsOnlyLeftMouseButton() && !event
.IsShiftDown() &&
934 !event
.IsControlDown()) {
935 // If the tab was already selected mouse pressed doesn't change the
936 // selection. Reset it now to handle the case where multiple tabs were
938 controller_
->SelectTab(this);
940 if (media_indicator_button_
&& media_indicator_button_
->visible() &&
941 media_indicator_button_
->bounds().Contains(event
.location())) {
942 content::RecordAction(UserMetricsAction("TabMediaIndicator_Clicked"));
947 void Tab::OnMouseCaptureLost() {
948 controller_
->EndDrag(END_DRAG_CAPTURE_LOST
);
951 void Tab::OnMouseEntered(const ui::MouseEvent
& event
) {
952 hover_controller_
.Show(views::GlowHoverController::SUBTLE
);
955 void Tab::OnMouseMoved(const ui::MouseEvent
& event
) {
956 hover_controller_
.SetLocation(event
.location());
957 controller_
->OnMouseEventInTab(this, event
);
960 void Tab::OnMouseExited(const ui::MouseEvent
& event
) {
961 hover_controller_
.Hide();
964 void Tab::OnGestureEvent(ui::GestureEvent
* event
) {
965 switch (event
->type()) {
966 case ui::ET_GESTURE_TAP_DOWN
: {
967 // TAP_DOWN is only dispatched for the first touch point.
968 DCHECK_EQ(1, event
->details().touch_points());
970 // See comment in OnMousePressed() as to why we copy the event.
971 ui::GestureEvent
event_in_parent(*event
, static_cast<View
*>(this),
973 ui::ListSelectionModel original_selection
;
974 original_selection
.Copy(controller_
->GetSelectionModel());
975 tab_activated_with_last_tap_down_
= !IsActive();
977 controller_
->SelectTab(this);
978 gfx::Point
loc(event
->location());
979 views::View::ConvertPointToScreen(this, &loc
);
980 ui::GestureEvent
cloned_event(event_in_parent
, parent(),
981 static_cast<View
*>(this));
982 controller_
->MaybeStartDrag(this, cloned_event
, original_selection
);
986 case ui::ET_GESTURE_END
:
987 controller_
->EndDrag(END_DRAG_COMPLETE
);
990 case ui::ET_GESTURE_SCROLL_UPDATE
:
991 controller_
->ContinueDrag(this, *event
);
1000 void Tab::GetAccessibleState(ui::AXViewState
* state
) {
1001 state
->role
= ui::AX_ROLE_TAB
;
1002 state
->name
= data_
.title
;
1003 state
->AddStateFlag(ui::AX_STATE_MULTISELECTABLE
);
1004 state
->AddStateFlag(ui::AX_STATE_SELECTABLE
);
1005 controller_
->UpdateTabAccessibilityState(this, state
);
1007 state
->AddStateFlag(ui::AX_STATE_SELECTED
);
1010 ////////////////////////////////////////////////////////////////////////////////
1013 void Tab::MaybeAdjustLeftForPinnedTab(gfx::Rect
* bounds
) const {
1014 if (ShouldRenderAsNormalTab())
1016 const int ideal_delta
= width() - GetPinnedWidth();
1017 const int ideal_x
= (GetPinnedWidth() - bounds
->width()) / 2;
1019 bounds
->x() + static_cast<int>(
1020 (1 - static_cast<float>(ideal_delta
) /
1021 static_cast<float>(kPinnedTabExtraWidthToRenderAsNormal
)) *
1022 (ideal_x
- bounds
->x())));
1025 void Tab::DataChanged(const TabRendererData
& old
) {
1026 if (data().media_state
!= old
.media_state
|| data().title
!= old
.title
)
1027 TooltipTextChanged();
1029 if (data().blocked
== old
.blocked
)
1038 void Tab::PaintTab(gfx::Canvas
* canvas
) {
1039 // See if the model changes whether the icons should be painted.
1040 const bool show_icon
= ShouldShowIcon();
1041 const bool show_media_indicator
= ShouldShowMediaIndicator();
1042 const bool show_close_button
= ShouldShowCloseBox();
1043 if (show_icon
!= showing_icon_
||
1044 show_media_indicator
!= showing_media_indicator_
||
1045 show_close_button
!= showing_close_button_
) {
1049 PaintTabBackground(canvas
);
1051 const SkColor title_color
= GetThemeProvider()->GetColor(IsSelected() ?
1052 ThemeProperties::COLOR_TAB_TEXT
:
1053 ThemeProperties::COLOR_BACKGROUND_TAB_TEXT
);
1054 title_
->SetVisible(ShouldRenderAsNormalTab());
1055 title_
->SetEnabledColor(title_color
);
1060 // If the close button color has changed, generate a new one.
1061 if (!close_button_color_
|| title_color
!= close_button_color_
) {
1062 close_button_color_
= title_color
;
1063 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
1064 close_button_
->SetBackground(close_button_color_
,
1065 rb
.GetImageSkiaNamed(IDR_CLOSE_1
),
1066 rb
.GetImageSkiaNamed(IDR_CLOSE_1_MASK
));
1070 void Tab::PaintImmersiveTab(gfx::Canvas
* canvas
) {
1071 // Use transparency for the draw-attention animation.
1073 if (pulse_animation_
&& pulse_animation_
->is_animating() && !data().pinned
) {
1074 alpha
= pulse_animation_
->CurrentValueBetween(
1075 255, static_cast<int>(255 * kImmersiveTabMinThrobOpacity
));
1078 // Draw a gray rectangle to represent the tab. This works for pinned tabs as
1079 // well as regular ones. The active tab has a brigher bar.
1081 IsActive() ? kImmersiveActiveTabColor
: kImmersiveInactiveTabColor
;
1082 gfx::Rect bar_rect
= GetImmersiveBarRect();
1083 canvas
->FillRect(bar_rect
, SkColorSetA(color
, alpha
));
1085 // Paint network activity indicator.
1086 // TODO(jamescook): Replace this placeholder animation with a real one.
1087 // For now, let's go with a Cylon eye effect, but in blue.
1088 if (data().network_state
!= TabRendererData::NETWORK_STATE_NONE
) {
1089 const SkColor kEyeColor
= SkColorSetARGB(alpha
, 71, 138, 217);
1090 int eye_width
= bar_rect
.width() / 3;
1091 int eye_offset
= bar_rect
.width() * immersive_loading_step_
/
1092 kImmersiveLoadingStepCount
;
1093 if (eye_offset
+ eye_width
< bar_rect
.width()) {
1094 // Draw a single indicator strip because it fits inside |bar_rect|.
1096 bar_rect
.x() + eye_offset
, 0, eye_width
, kImmersiveBarHeight
);
1097 canvas
->FillRect(eye_rect
, kEyeColor
);
1099 // Draw two indicators to simulate the eye "wrapping around" to the left
1100 // side. The first part fills the remainder of the bar.
1101 int right_eye_width
= bar_rect
.width() - eye_offset
;
1102 gfx::Rect
right_eye_rect(
1103 bar_rect
.x() + eye_offset
, 0, right_eye_width
, kImmersiveBarHeight
);
1104 canvas
->FillRect(right_eye_rect
, kEyeColor
);
1105 // The second part parts the remaining |eye_width| on the left.
1106 int left_eye_width
= eye_offset
+ eye_width
- bar_rect
.width();
1107 gfx::Rect
left_eye_rect(
1108 bar_rect
.x(), 0, left_eye_width
, kImmersiveBarHeight
);
1109 canvas
->FillRect(left_eye_rect
, kEyeColor
);
1114 void Tab::PaintTabBackground(gfx::Canvas
* canvas
) {
1116 PaintActiveTabBackground(canvas
);
1118 if (pinned_title_change_animation_
&&
1119 pinned_title_change_animation_
->is_animating()) {
1120 PaintInactiveTabBackgroundWithTitleChange(canvas
);
1122 PaintInactiveTabBackground(canvas
);
1125 double throb_value
= GetThrobValue();
1126 if (throb_value
> 0) {
1127 canvas
->SaveLayerAlpha(static_cast<int>(throb_value
* 0xff),
1129 PaintActiveTabBackground(canvas
);
1135 void Tab::PaintInactiveTabBackgroundWithTitleChange(gfx::Canvas
* canvas
) {
1136 // Render the inactive tab background. We'll use this for clipping.
1137 gfx::Canvas
background_canvas(size(), canvas
->image_scale(), false);
1138 PaintInactiveTabBackground(&background_canvas
);
1140 gfx::ImageSkia
background_image(background_canvas
.ExtractImageRep());
1142 // Draw a radial gradient to hover_canvas.
1143 gfx::Canvas
hover_canvas(size(), canvas
->image_scale(), false);
1144 int radius
= kPinnedTitleChangeGradientRadius
;
1145 int x0
= width() + radius
- kPinnedTitleChangeInitialXOffset
;
1149 if (pinned_title_change_animation_
->current_part_index() == 0) {
1150 x
= pinned_title_change_animation_
->CurrentValueBetween(x0
, x1
);
1151 } else if (pinned_title_change_animation_
->current_part_index() == 1) {
1154 x
= pinned_title_change_animation_
->CurrentValueBetween(x1
, x2
);
1156 SkPoint center_point
;
1157 center_point
.iset(x
, 0);
1158 SkColor colors
[2] = { kPinnedTitleChangeGradientColor1
,
1159 kPinnedTitleChangeGradientColor2
};
1160 skia::RefPtr
<SkShader
> shader
= skia::AdoptRef(
1161 SkGradientShader::CreateRadial(
1162 center_point
, SkIntToScalar(radius
), colors
, NULL
, 2,
1163 SkShader::kClamp_TileMode
));
1165 paint
.setShader(shader
.get());
1166 hover_canvas
.DrawRect(gfx::Rect(x
- radius
, -radius
, radius
* 2, radius
* 2),
1169 // Draw the radial gradient clipped to the background into hover_image.
1170 gfx::ImageSkia hover_image
= gfx::ImageSkiaOperations::CreateMaskedImage(
1171 gfx::ImageSkia(hover_canvas
.ExtractImageRep()), background_image
);
1173 // Draw the tab background to the canvas.
1174 canvas
->DrawImageInt(background_image
, 0, 0);
1176 // And then the gradient on top of that.
1177 if (pinned_title_change_animation_
->current_part_index() == 2) {
1178 uint8 alpha
= pinned_title_change_animation_
->CurrentValueBetween(255, 0);
1179 canvas
->DrawImageInt(hover_image
, 0, 0, alpha
);
1181 canvas
->DrawImageInt(hover_image
, 0, 0);
1185 void Tab::PaintInactiveTabBackground(gfx::Canvas
* canvas
) {
1188 views::Widget
* widget
= GetWidget();
1189 GetTabIdAndFrameId(widget
, &tab_id
, &frame_id
);
1191 // Explicitly map the id so we cache correctly.
1192 const chrome::HostDesktopType host_desktop_type
= GetHostDesktopType(this);
1193 tab_id
= chrome::MapThemeImage(host_desktop_type
, tab_id
);
1195 // HasCustomImage() is only true if the theme provides the image. However,
1196 // even if the theme does not provide a tab background, the theme machinery
1197 // will make one if given a frame image.
1198 ui::ThemeProvider
* theme_provider
= GetThemeProvider();
1199 const bool theme_provided_image
= theme_provider
->HasCustomImage(tab_id
) ||
1200 (frame_id
!= 0 && theme_provider
->HasCustomImage(frame_id
));
1202 const bool can_cache
= !theme_provided_image
&&
1203 !hover_controller_
.ShouldDraw();
1206 ui::ScaleFactor scale_factor
=
1207 ui::GetSupportedScaleFactor(canvas
->image_scale());
1208 gfx::ImageSkia
cached_image(GetCachedImage(tab_id
, size(), scale_factor
));
1209 if (cached_image
.width() == 0) {
1210 gfx::Canvas
tmp_canvas(size(), canvas
->image_scale(), false);
1211 PaintInactiveTabBackgroundUsingResourceId(&tmp_canvas
, tab_id
);
1212 cached_image
= gfx::ImageSkia(tmp_canvas
.ExtractImageRep());
1213 SetCachedImage(tab_id
, scale_factor
, cached_image
);
1215 canvas
->DrawImageInt(cached_image
, 0, 0);
1217 PaintInactiveTabBackgroundUsingResourceId(canvas
, tab_id
);
1221 void Tab::PaintInactiveTabBackgroundUsingResourceId(gfx::Canvas
* canvas
,
1223 // WARNING: the inactive tab background may be cached. If you change what it
1224 // is drawn from you may need to update whether it can be cached.
1226 // The tab image needs to be lined up with the background image
1227 // so that it feels partially transparent. These offsets represent the tab
1228 // position within the frame background image.
1229 int offset
= GetMirroredX() + background_offset_
.x();
1231 gfx::ImageSkia
* tab_bg
= GetThemeProvider()->GetImageSkiaNamed(tab_id
);
1233 TabImage
* tab_image
= &tab_active_
;
1234 TabImage
* tab_inactive_image
= &tab_inactive_
;
1235 TabImage
* alpha
= &tab_alpha_
;
1237 // If the theme is providing a custom background image, then its top edge
1238 // should be at the top of the tab. Otherwise, we assume that the background
1239 // image is a composited foreground + frame image.
1240 int bg_offset_y
= GetThemeProvider()->HasCustomImage(tab_id
) ?
1241 0 : background_offset_
.y();
1243 // We need a gfx::Canvas object to be able to extract the image from.
1244 // We draw everything to this canvas and then output it to the canvas
1245 // parameter in addition to using it to mask the hover glow if needed.
1246 gfx::Canvas
background_canvas(size(), canvas
->image_scale(), false);
1248 // Draw left edge. Don't draw over the toolbar, as we're not the foreground
1250 gfx::ImageSkia tab_l
= gfx::ImageSkiaOperations::CreateTiledImage(
1251 *tab_bg
, offset
, bg_offset_y
, tab_image
->l_width
, height());
1252 gfx::ImageSkia theme_l
=
1253 gfx::ImageSkiaOperations::CreateMaskedImage(tab_l
, *alpha
->image_l
);
1254 background_canvas
.DrawImageInt(theme_l
,
1255 0, 0, theme_l
.width(), theme_l
.height() - kToolbarOverlap
,
1256 0, 0, theme_l
.width(), theme_l
.height() - kToolbarOverlap
,
1259 // Draw right edge. Again, don't draw over the toolbar.
1260 gfx::ImageSkia tab_r
= gfx::ImageSkiaOperations::CreateTiledImage(*tab_bg
,
1261 offset
+ width() - tab_image
->r_width
, bg_offset_y
,
1262 tab_image
->r_width
, height());
1263 gfx::ImageSkia theme_r
=
1264 gfx::ImageSkiaOperations::CreateMaskedImage(tab_r
, *alpha
->image_r
);
1265 background_canvas
.DrawImageInt(theme_r
,
1266 0, 0, theme_r
.width(), theme_r
.height() - kToolbarOverlap
,
1267 width() - theme_r
.width(), 0, theme_r
.width(),
1268 theme_r
.height() - kToolbarOverlap
, false);
1270 // Draw center. Instead of masking out the top portion we simply skip over
1271 // it by incrementing by GetDropShadowHeight(), since it's a simple
1272 // rectangle. And again, don't draw over the toolbar.
1273 background_canvas
.TileImageInt(*tab_bg
,
1274 offset
+ tab_image
->l_width
,
1275 bg_offset_y
+ kDropShadowHeight
,
1278 width() - tab_image
->l_width
- tab_image
->r_width
,
1279 height() - kDropShadowHeight
- kToolbarOverlap
);
1281 canvas
->DrawImageInt(
1282 gfx::ImageSkia(background_canvas
.ExtractImageRep()), 0, 0);
1284 if (!GetThemeProvider()->HasCustomImage(tab_id
) &&
1285 hover_controller_
.ShouldDraw()) {
1286 hover_controller_
.Draw(canvas
, gfx::ImageSkia(
1287 background_canvas
.ExtractImageRep()));
1290 // Now draw the highlights/shadows around the tab edge.
1291 canvas
->DrawImageInt(*tab_inactive_image
->image_l
, 0, 0);
1292 canvas
->TileImageInt(*tab_inactive_image
->image_c
,
1293 tab_inactive_image
->l_width
, 0,
1294 width() - tab_inactive_image
->l_width
-
1295 tab_inactive_image
->r_width
,
1297 canvas
->DrawImageInt(*tab_inactive_image
->image_r
,
1298 width() - tab_inactive_image
->r_width
, 0);
1301 void Tab::PaintActiveTabBackground(gfx::Canvas
* canvas
) {
1302 gfx::ImageSkia
* tab_background
=
1303 GetThemeProvider()->GetImageSkiaNamed(IDR_THEME_TOOLBAR
);
1304 int offset
= GetMirroredX() + background_offset_
.x();
1306 TabImage
* tab_image
= &tab_active_
;
1307 TabImage
* alpha
= &tab_alpha_
;
1310 gfx::ImageSkia tab_l
= gfx::ImageSkiaOperations::CreateTiledImage(
1311 *tab_background
, offset
, 0, tab_image
->l_width
, height());
1312 gfx::ImageSkia theme_l
=
1313 gfx::ImageSkiaOperations::CreateMaskedImage(tab_l
, *alpha
->image_l
);
1314 canvas
->DrawImageInt(theme_l
, 0, 0);
1317 gfx::ImageSkia tab_r
= gfx::ImageSkiaOperations::CreateTiledImage(
1319 offset
+ width() - tab_image
->r_width
, 0, tab_image
->r_width
, height());
1320 gfx::ImageSkia theme_r
=
1321 gfx::ImageSkiaOperations::CreateMaskedImage(tab_r
, *alpha
->image_r
);
1322 canvas
->DrawImageInt(theme_r
, width() - tab_image
->r_width
, 0);
1324 // Draw center. Instead of masking out the top portion we simply skip over it
1325 // by incrementing by GetDropShadowHeight(), since it's a simple rectangle.
1326 canvas
->TileImageInt(*tab_background
,
1327 offset
+ tab_image
->l_width
,
1331 width() - tab_image
->l_width
- tab_image
->r_width
,
1332 height() - kDropShadowHeight
);
1334 // Now draw the highlights/shadows around the tab edge.
1335 canvas
->DrawImageInt(*tab_image
->image_l
, 0, 0);
1336 canvas
->TileImageInt(*tab_image
->image_c
, tab_image
->l_width
, 0,
1337 width() - tab_image
->l_width
- tab_image
->r_width
, height());
1338 canvas
->DrawImageInt(*tab_image
->image_r
, width() - tab_image
->r_width
, 0);
1341 void Tab::PaintIcon(gfx::Canvas
* canvas
) {
1342 gfx::Rect bounds
= favicon_bounds_
;
1343 if (bounds
.IsEmpty())
1346 bounds
.set_x(GetMirroredXForRect(bounds
));
1348 if (data().network_state
!= TabRendererData::NETWORK_STATE_NONE
) {
1349 // Paint network activity (aka throbber) animation frame.
1350 ui::ThemeProvider
* tp
= GetThemeProvider();
1351 if (data().network_state
== TabRendererData::NETWORK_STATE_WAITING
) {
1352 if (waiting_start_time_
== base::TimeTicks())
1353 waiting_start_time_
= base::TimeTicks::Now();
1355 waiting_state_
.elapsed_time
=
1356 base::TimeTicks::Now() - waiting_start_time_
;
1357 gfx::PaintThrobberWaiting(
1358 canvas
, bounds
, tp
->GetColor(ThemeProperties::COLOR_THROBBER_WAITING
),
1359 waiting_state_
.elapsed_time
);
1361 if (loading_start_time_
== base::TimeTicks())
1362 loading_start_time_
= base::TimeTicks::Now();
1364 waiting_state_
.color
=
1365 tp
->GetColor(ThemeProperties::COLOR_THROBBER_WAITING
);
1366 gfx::PaintThrobberSpinningAfterWaiting(
1368 tp
->GetColor(ThemeProperties::COLOR_THROBBER_SPINNING
),
1369 base::TimeTicks::Now() - loading_start_time_
, &waiting_state_
);
1371 } else if (should_display_crashed_favicon_
) {
1372 // Paint crash favicon.
1373 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
1374 gfx::ImageSkia
crashed_favicon(*rb
.GetImageSkiaNamed(IDR_SAD_FAVICON
));
1375 bounds
.set_y(bounds
.y() + favicon_hiding_offset_
);
1376 DrawIconCenter(canvas
, crashed_favicon
, 0,
1377 crashed_favicon
.width(),
1378 crashed_favicon
.height(),
1379 bounds
, true, SkPaint());
1380 } else if (!data().favicon
.isNull()) {
1381 // Paint the normal favicon.
1382 DrawIconCenter(canvas
, data().favicon
, 0,
1383 data().favicon
.width(),
1384 data().favicon
.height(),
1385 bounds
, true, SkPaint());
1389 void Tab::AdvanceLoadingAnimation(TabRendererData::NetworkState old_state
,
1390 TabRendererData::NetworkState state
) {
1391 if (state
== TabRendererData::NETWORK_STATE_WAITING
) {
1392 // Waiting steps backwards.
1393 immersive_loading_step_
=
1394 (immersive_loading_step_
- 1 + kImmersiveLoadingStepCount
) %
1395 kImmersiveLoadingStepCount
;
1396 } else if (state
== TabRendererData::NETWORK_STATE_LOADING
) {
1397 immersive_loading_step_
= (immersive_loading_step_
+ 1) %
1398 kImmersiveLoadingStepCount
;
1400 waiting_start_time_
= base::TimeTicks();
1401 loading_start_time_
= base::TimeTicks();
1402 waiting_state_
= gfx::ThrobberWaitingState();
1403 immersive_loading_step_
= 0;
1405 if (controller_
->IsImmersiveStyle()) {
1406 SchedulePaintInRect(GetImmersiveBarRect());
1408 ScheduleIconPaint();
1412 int Tab::IconCapacity() const {
1413 const gfx::Size
min_size(GetMinimumUnselectedSize());
1414 if (height() < min_size
.height())
1416 const int available_width
= std::max(0, width() - min_size
.width());
1417 // All icons are the same size as the favicon.
1418 const int icon_width
= gfx::kFaviconSize
;
1419 // We need enough space to display the icons flush against each other.
1420 const int visible_icons
= available_width
/ icon_width
;
1421 // When the close button will be visible on inactive tabs, we add additional
1422 // padding to the left of the favicon to balance the whitespace inside the
1423 // non-hovered close button image; otherwise, the tab contents look too close
1424 // to the left edge. If the tab close button isn't visible on inactive tabs,
1425 // we let the tab contents take the full width of the tab, to maximize visible
1426 // content on tiny tabs. We base the determination on the inactive tab close
1427 // button state so that when a tab is activated its contents don't suddenly
1429 if (visible_icons
< 3)
1430 return visible_icons
;
1431 const int padding
= controller_
->ShouldHideCloseButtonForInactiveTabs() ?
1432 0 : kExtraLeftPaddingToBalanceCloseButtonPadding
;
1433 return (available_width
- padding
) / icon_width
;
1436 bool Tab::ShouldShowIcon() const {
1437 return chrome::ShouldTabShowFavicon(
1438 IconCapacity(), data().pinned
, IsActive(), data().show_icon
,
1439 media_indicator_button_
? media_indicator_button_
->showing_media_state() :
1443 bool Tab::ShouldShowMediaIndicator() const {
1444 return chrome::ShouldTabShowMediaIndicator(
1445 IconCapacity(), data().pinned
, IsActive(), data().show_icon
,
1446 media_indicator_button_
? media_indicator_button_
->showing_media_state() :
1450 bool Tab::ShouldShowCloseBox() const {
1451 if (!IsActive() && controller_
->ShouldHideCloseButtonForInactiveTabs())
1454 return chrome::ShouldTabShowCloseButton(
1455 IconCapacity(), data().pinned
, IsActive());
1458 bool Tab::ShouldRenderAsNormalTab() const {
1459 return !data().pinned
||
1460 (width() >= (GetPinnedWidth() + kPinnedTabExtraWidthToRenderAsNormal
));
1463 double Tab::GetThrobValue() {
1464 const bool is_selected
= IsSelected();
1465 const double min
= is_selected
? kSelectedTabOpacity
: 0;
1466 const double scale
= is_selected
? kSelectedTabThrobScale
: 1;
1468 // Showing both the pulse and title change animation at the same time is too
1470 if (pulse_animation_
&& pulse_animation_
->is_animating() &&
1471 (!pinned_title_change_animation_
||
1472 !pinned_title_change_animation_
->is_animating())) {
1473 return pulse_animation_
->GetCurrentValue() * kHoverOpacity
* scale
+ min
;
1476 if (hover_controller_
.ShouldDraw()) {
1477 return kHoverOpacity
* hover_controller_
.GetAnimationValue() * scale
+
1481 return is_selected
? kSelectedTabOpacity
: 0;
1484 void Tab::SetFaviconHidingOffset(int offset
) {
1485 favicon_hiding_offset_
= offset
;
1486 ScheduleIconPaint();
1489 void Tab::DisplayCrashedFavicon() {
1490 should_display_crashed_favicon_
= true;
1493 void Tab::ResetCrashedFavicon() {
1494 should_display_crashed_favicon_
= false;
1497 void Tab::StopCrashAnimation() {
1498 crash_icon_animation_
.reset();
1501 void Tab::StartCrashAnimation() {
1502 crash_icon_animation_
.reset(new FaviconCrashAnimation(this));
1503 crash_icon_animation_
->Start();
1506 bool Tab::IsPerformingCrashAnimation() const {
1507 return crash_icon_animation_
.get() && data_
.IsCrashed();
1510 void Tab::ScheduleIconPaint() {
1511 gfx::Rect bounds
= favicon_bounds_
;
1512 if (bounds
.IsEmpty())
1515 // Extends the area to the bottom when sad_favicon is animating.
1516 if (IsPerformingCrashAnimation())
1517 bounds
.set_height(height() - bounds
.y());
1518 bounds
.set_x(GetMirroredXForRect(bounds
));
1519 SchedulePaintInRect(bounds
);
1522 void Tab::GetHitTestMaskHelper(bool include_top_shadow
, gfx::Path
* path
) const {
1525 // Hit mask constants.
1526 const SkScalar kTabCapWidth
= 15;
1527 const SkScalar kTabTopCurveWidth
= 4;
1528 const SkScalar kTabBottomCurveWidth
= 3;
1529 #if defined(OS_MACOSX)
1530 // Mac's Cocoa UI doesn't have shadows.
1531 const SkScalar kTabInset
= 0;
1532 const SkScalar kTabTop
= 0;
1533 #elif defined(TOOLKIT_VIEWS)
1534 // The views browser UI has shadows in the left, right and top parts of the
1536 const SkScalar kTabInset
= 6;
1537 const SkScalar kTabTop
= 2;
1540 SkScalar left
= kTabInset
;
1541 SkScalar top
= kTabTop
;
1542 SkScalar right
= SkIntToScalar(width()) - kTabInset
;
1543 SkScalar bottom
= SkIntToScalar(height());
1545 // Start in the lower-left corner.
1546 path
->moveTo(left
, bottom
);
1549 path
->lineTo(left
+ kTabBottomCurveWidth
, bottom
- kTabBottomCurveWidth
);
1550 path
->lineTo(left
+ kTabCapWidth
- kTabTopCurveWidth
,
1551 top
+ kTabTopCurveWidth
);
1552 path
->lineTo(left
+ kTabCapWidth
, top
);
1554 // Extend over the top shadow area if we have one and the caller wants it.
1555 if (kTabTop
> 0 && include_top_shadow
) {
1556 path
->lineTo(left
+ kTabCapWidth
, 0);
1557 path
->lineTo(right
- kTabCapWidth
, 0);
1560 // Connect to the right cap.
1561 path
->lineTo(right
- kTabCapWidth
, top
);
1564 path
->lineTo(right
- kTabCapWidth
+ kTabTopCurveWidth
,
1565 top
+ kTabTopCurveWidth
);
1566 path
->lineTo(right
- kTabBottomCurveWidth
, bottom
- kTabBottomCurveWidth
);
1567 path
->lineTo(right
, bottom
);
1569 // Close out the path.
1570 path
->lineTo(left
, bottom
);
1574 gfx::Rect
Tab::GetImmersiveBarRect() const {
1575 // The main bar is as wide as the normal tab's horizontal top line.
1576 // This top line of the tab extends a few pixels left and right of the
1577 // center image due to pixels in the rounded corner images.
1578 const int kBarPadding
= 1;
1579 int main_bar_left
= tab_active_
.l_width
- kBarPadding
;
1580 int main_bar_right
= width() - tab_active_
.r_width
+ kBarPadding
;
1582 main_bar_left
, 0, main_bar_right
- main_bar_left
, kImmersiveBarHeight
);
1585 void Tab::GetTabIdAndFrameId(views::Widget
* widget
,
1587 int* frame_id
) const {
1589 widget
->GetTopLevelWidget()->ShouldWindowContentsBeTransparent()) {
1590 *tab_id
= IDR_THEME_TAB_BACKGROUND_V
;
1592 } else if (data().incognito
) {
1593 *tab_id
= IDR_THEME_TAB_BACKGROUND_INCOGNITO
;
1594 *frame_id
= IDR_THEME_FRAME_INCOGNITO
;
1596 *tab_id
= IDR_THEME_TAB_BACKGROUND
;
1597 *frame_id
= IDR_THEME_FRAME
;
1601 MediaIndicatorButton
* Tab::GetMediaIndicatorButton() {
1602 if (!media_indicator_button_
) {
1603 media_indicator_button_
= new MediaIndicatorButton(this);
1604 AddChildView(media_indicator_button_
); // Takes ownership.
1606 return media_indicator_button_
;
1609 ////////////////////////////////////////////////////////////////////////////////
1610 // Tab, private static:
1613 void Tab::InitTabResources() {
1614 static bool initialized
= false;
1619 image_cache_
= new ImageCache();
1621 // Load the tab images once now, and maybe again later if the theme changes.
1626 void Tab::LoadTabImages() {
1627 // We're not letting people override tab images just yet.
1628 ui::ResourceBundle
& rb
= ui::ResourceBundle::GetSharedInstance();
1630 tab_alpha_
.image_l
= rb
.GetImageSkiaNamed(IDR_TAB_ALPHA_LEFT
);
1631 tab_alpha_
.image_r
= rb
.GetImageSkiaNamed(IDR_TAB_ALPHA_RIGHT
);
1633 tab_active_
.image_l
= rb
.GetImageSkiaNamed(IDR_TAB_ACTIVE_LEFT
);
1634 tab_active_
.image_c
= rb
.GetImageSkiaNamed(IDR_TAB_ACTIVE_CENTER
);
1635 tab_active_
.image_r
= rb
.GetImageSkiaNamed(IDR_TAB_ACTIVE_RIGHT
);
1636 tab_active_
.l_width
= tab_active_
.image_l
->width();
1637 tab_active_
.r_width
= tab_active_
.image_r
->width();
1639 tab_inactive_
.image_l
= rb
.GetImageSkiaNamed(IDR_TAB_INACTIVE_LEFT
);
1640 tab_inactive_
.image_c
= rb
.GetImageSkiaNamed(IDR_TAB_INACTIVE_CENTER
);
1641 tab_inactive_
.image_r
= rb
.GetImageSkiaNamed(IDR_TAB_INACTIVE_RIGHT
);
1642 tab_inactive_
.l_width
= tab_inactive_
.image_l
->width();
1643 tab_inactive_
.r_width
= tab_inactive_
.image_r
->width();
1647 gfx::ImageSkia
Tab::GetCachedImage(int resource_id
,
1648 const gfx::Size
& size
,
1649 ui::ScaleFactor scale_factor
) {
1650 for (ImageCache::const_iterator i
= image_cache_
->begin();
1651 i
!= image_cache_
->end(); ++i
) {
1652 if (i
->resource_id
== resource_id
&& i
->scale_factor
== scale_factor
&&
1653 i
->image
.size() == size
) {
1657 return gfx::ImageSkia();
1661 void Tab::SetCachedImage(int resource_id
,
1662 ui::ScaleFactor scale_factor
,
1663 const gfx::ImageSkia
& image
) {
1664 DCHECK_NE(scale_factor
, ui::SCALE_FACTOR_NONE
);
1665 ImageCacheEntry entry
;
1666 entry
.resource_id
= resource_id
;
1667 entry
.scale_factor
= scale_factor
;
1668 entry
.image
= image
;
1669 image_cache_
->push_front(entry
);
1670 if (image_cache_
->size() > kMaxImageCacheSize
)
1671 image_cache_
->pop_back();