Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / ui / views / tabs / tab.cc
blobead528ebfd30c1881ed02f7414f45352abefd6ab
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"
7 #include <limits>
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"
53 #if defined(USE_AURA)
54 #include "ui/aura/env.h"
55 #endif
57 using base::UserMetricsAction;
59 namespace {
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
101 // animation.
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,
139 int image_offset,
140 int dst_x,
141 int dst_y,
142 int icon_width,
143 int icon_height,
144 bool filter,
145 const SkPaint& paint) {
146 // NOTE: the clipping is a work around for 69528, it shouldn't be necessary.
147 canvas->Save();
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,
152 filter, paint);
153 canvas->Restore();
156 // Draws the icon image at the center of |bounds|.
157 void DrawIconCenter(gfx::Canvas* canvas,
158 const gfx::ImageSkia& image,
159 int image_offset,
160 int icon_width,
161 int icon_height,
162 const gfx::Rect& bounds,
163 bool filter,
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) {
182 if (animation)
183 animation->Stop();
186 } // namespace
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 {
194 public:
195 explicit FaviconCrashAnimation(Tab* target)
196 : gfx::LinearAnimation(1000, 25, this),
197 target_(target) {
199 ~FaviconCrashAnimation() override {}
201 // gfx::Animation overrides:
202 void AnimateToState(double state) override {
203 const double kHidingOffset = 27;
205 if (state < .5) {
206 target_->SetFaviconHidingOffset(
207 static_cast<int>(floor(kHidingOffset * 2.0 * state)));
208 } else {
209 target_->DisplayCrashedFavicon();
210 target_->SetFaviconHidingOffset(
211 static_cast<int>(
212 floor(kHidingOffset - ((state - .5) * 2.0 * kHidingOffset))));
216 // gfx::AnimationDelegate overrides:
217 void AnimationCanceled(const gfx::Animation* animation) override {
218 target_->SetFaviconHidingOffset(0);
221 private:
222 Tab* target_;
224 DISALLOW_COPY_AND_ASSIGN(FaviconCrashAnimation);
227 ////////////////////////////////////////////////////////////////////////////////
228 // TabCloseButton
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 {
234 public:
235 explicit TabCloseButton(Tab* tab)
236 : views::ImageButton(tab),
237 tab_(tab) {
238 SetEventTargeter(
239 scoped_ptr<views::ViewTargeter>(new views::ViewTargeter(this)));
242 ~TabCloseButton() override {}
244 // views::View:
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))
251 return NULL;
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
260 // sees them.
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);
278 event->SetHandled();
281 const char* GetClassName() const override { return kTabCloseButtonName; }
283 private:
284 // Returns the rectangular bounds of parent tab's visible region in the
285 // local coordinate space of |this|.
286 gfx::Rect GetTabBounds() const {
287 gfx::Path tab_mask;
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();
328 #endif
330 return contents_bounds.Intersects(rect) ? this : parent();
333 // views:MaskedTargeterDelegate:
334 bool GetHitTestMask(gfx::Path* mask) const override {
335 DCHECK(mask);
336 mask->reset();
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));
349 return true;
352 return false;
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))
371 return false;
373 return MaskedTargeterDelegate::DoesIntersectRect(target, rect);
376 Tab* tab_;
378 DISALLOW_COPY_AND_ASSIGN(TabCloseButton);
381 ////////////////////////////////////////////////////////////////////////////////
382 // ImageCacheEntry
384 Tab::ImageCacheEntry::ImageCacheEntry()
385 : resource_id(-1),
386 scale_factor(ui::SCALE_FACTOR_NONE) {
389 Tab::ImageCacheEntry::~ImageCacheEntry() {}
391 ////////////////////////////////////////////////////////////////////////////////
392 // Tab, statics:
394 // static
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 ////////////////////////////////////////////////////////////////////////////////
402 // Tab, public:
404 Tab::Tab(TabController* controller)
405 : controller_(controller),
406 closing_(false),
407 dragging_(false),
408 detached_(false),
409 favicon_hiding_offset_(0),
410 immersive_loading_step_(0),
411 should_display_crashed_favicon_(false),
412 close_button_(NULL),
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) {
421 DCHECK(controller);
422 InitTabResources();
424 // So we get don't get enter/exit on children and don't prematurely stop the
425 // hover.
426 set_notify_enter_exit_on_child(true);
428 set_id(VIEW_ID_TAB);
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_);
437 SetEventTargeter(
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);
459 Tab::~Tab() {
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) {
480 DCHECK(GetWidget());
482 if (data_.Equals(data))
483 return;
485 TabRendererData old(data_);
486 data_ = data;
488 base::string16 title = data_.title;
489 if (title.empty()) {
490 title = data_.loading ?
491 l10n_util::GetStringUTF16(IDS_TAB_LOADING_TITLE) :
492 CoreTabHelper::GetDefaultTitle();
493 } else {
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.
506 if (IsSelected() ||
507 (data_.crashed_status
508 != base::TERMINATION_STATUS_PROCESS_WAS_KILLED &&
509 data_.crashed_status
510 != base::TERMINATION_STATUS_PROCESS_WAS_KILLED_BY_OOM)) {
511 StartCrashAnimation();
513 #else
514 StartCrashAnimation();
515 #endif
517 } else {
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());
530 DataChanged(old);
532 Layout();
533 SchedulePaint();
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.
541 return;
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() {
562 if (!data().pinned)
563 return;
564 if (!pinned_title_change_animation_) {
565 gfx::MultiAnimation::Parts parts;
566 parts.push_back(
567 gfx::MultiAnimation::Part(kPinnedTitleChangeAnimationDuration1MS,
568 gfx::Tween::EASE_OUT));
569 parts.push_back(
570 gfx::MultiAnimation::Part(kPinnedTitleChangeAnimationDuration2MS,
571 gfx::Tween::ZERO));
572 parts.push_back(
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.
608 InitTabResources();
609 int height = tab_active_.image_l->height();
610 return gfx::Size(GetLayoutInsets(TAB).width(), height);
613 // static
614 gfx::Size Tab::GetMinimumSelectedSize() {
615 gfx::Size minimum_size = GetMinimumUnselectedSize();
616 minimum_size.Enlarge(gfx::kFaviconSize, 0);
617 return minimum_size;
620 // static
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;
629 // static
630 int Tab::GetTouchWidth() {
631 return kTouchWidth;
634 // static
635 int Tab::GetPinnedWidth() {
636 return GetMinimumUnselectedSize().width() +
637 GetLayoutConstant(TAB_PINNED_CONTENT_WIDTH);
640 // static
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())
652 return;
653 SchedulePaint();
656 void Tab::AnimationCanceled(const gfx::Animation* animation) {
657 SchedulePaint();
660 void Tab::AnimationEnded(const gfx::Animation* animation) {
661 SchedulePaint();
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"));
673 else
674 content::RecordAction(UserMetricsAction("CloseTab_RecordingIndicator"));
675 } else {
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) {
695 if (!closing())
696 controller_->ShowContextMenuForTab(this, point, source_type);
699 ////////////////////////////////////////////////////////////////////////////////
700 // Tab, views::MaskedTargeterDelegate overrides:
702 bool Tab::GetHitTestMask(gfx::Path* mask) const {
703 DCHECK(mask);
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.
715 gfx::Rect clip;
716 controller_->ShouldPaintTab(this, &clip);
717 if (clip.size().GetArea()) {
718 SkRect intersection(mask->getBounds());
719 mask->reset();
720 if (!intersection.intersect(RectToSkRect(clip)))
721 return false;
722 mask->addRect(intersection);
724 return true;
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)
734 return;
736 gfx::Rect clip;
737 if (!controller_->ShouldPaintTab(this, &clip))
738 return;
739 if (!clip.IsEmpty()) {
740 canvas->Save();
741 canvas->ClipRect(clip);
744 if (controller_->IsImmersiveStyle())
745 PaintImmersiveTab(canvas);
746 else
747 PaintTab(canvas);
749 if (!clip.IsEmpty())
750 canvas->Restore();
753 void Tab::Layout() {
754 gfx::Rect lb = GetContentsBounds();
755 if (lb.IsEmpty())
756 return;
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);
766 if (showing_icon_) {
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();
804 gfx::Rect bounds(
805 std::max(lb.x(), right - image_size.width()),
806 lb.y() + (lb.height() - image_size.height() + 1) / 2,
807 image_size.width(),
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();
818 if (show_title) {
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_) {
824 title_width =
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() {
843 LoadTabImages();
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);
859 origin->set_y(-4);
860 return true;
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);
883 if (!IsSelected()) {
884 // Don't allow dragging non-selected tabs.
885 return false;
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);
899 return true;
902 bool Tab::OnMouseDragged(const ui::MouseEvent& event) {
903 controller_->ContinueDrag(this, event);
904 return true;
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))
916 return;
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
928 // the mouse.
929 Tab* closest_tab = controller_->GetTabAt(this, event.location());
930 if (closest_tab)
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
937 // selected.
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),
972 parent());
973 ui::ListSelectionModel original_selection;
974 original_selection.Copy(controller_->GetSelectionModel());
975 tab_activated_with_last_tap_down_ = !IsActive();
976 if (!IsSelected())
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);
983 break;
986 case ui::ET_GESTURE_END:
987 controller_->EndDrag(END_DRAG_COMPLETE);
988 break;
990 case ui::ET_GESTURE_SCROLL_UPDATE:
991 controller_->ContinueDrag(this, *event);
992 break;
994 default:
995 break;
997 event->SetHandled();
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);
1006 if (IsSelected())
1007 state->AddStateFlag(ui::AX_STATE_SELECTED);
1010 ////////////////////////////////////////////////////////////////////////////////
1011 // Tab, private
1013 void Tab::MaybeAdjustLeftForPinnedTab(gfx::Rect* bounds) const {
1014 if (ShouldRenderAsNormalTab())
1015 return;
1016 const int ideal_delta = width() - GetPinnedWidth();
1017 const int ideal_x = (GetPinnedWidth() - bounds->width()) / 2;
1018 bounds->set_x(
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)
1030 return;
1032 if (data().blocked)
1033 StartPulse();
1034 else
1035 StopPulse();
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_) {
1046 Layout();
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);
1057 if (show_icon)
1058 PaintIcon(canvas);
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.
1072 int alpha = 255;
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.
1080 SkColor color =
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|.
1095 gfx::Rect eye_rect(
1096 bar_rect.x() + eye_offset, 0, eye_width, kImmersiveBarHeight);
1097 canvas->FillRect(eye_rect, kEyeColor);
1098 } else {
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) {
1115 if (IsActive()) {
1116 PaintActiveTabBackground(canvas);
1117 } else {
1118 if (pinned_title_change_animation_ &&
1119 pinned_title_change_animation_->is_animating()) {
1120 PaintInactiveTabBackgroundWithTitleChange(canvas);
1121 } else {
1122 PaintInactiveTabBackground(canvas);
1125 double throb_value = GetThrobValue();
1126 if (throb_value > 0) {
1127 canvas->SaveLayerAlpha(static_cast<int>(throb_value * 0xff),
1128 GetLocalBounds());
1129 PaintActiveTabBackground(canvas);
1130 canvas->Restore();
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;
1146 int x1 = radius;
1147 int x2 = -radius;
1148 int x;
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) {
1152 x = x1;
1153 } else {
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));
1164 SkPaint paint;
1165 paint.setShader(shader.get());
1166 hover_canvas.DrawRect(gfx::Rect(x - radius, -radius, radius * 2, radius * 2),
1167 paint);
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);
1180 } else {
1181 canvas->DrawImageInt(hover_image, 0, 0);
1185 void Tab::PaintInactiveTabBackground(gfx::Canvas* canvas) {
1186 int tab_id;
1187 int frame_id;
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();
1205 if (can_cache) {
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);
1216 } else {
1217 PaintInactiveTabBackgroundUsingResourceId(canvas, tab_id);
1221 void Tab::PaintInactiveTabBackgroundUsingResourceId(gfx::Canvas* canvas,
1222 int tab_id) {
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
1249 // tab.
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,
1257 false);
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,
1276 tab_image->l_width,
1277 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,
1296 height());
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_;
1309 // Draw left edge.
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);
1316 // Draw right edge.
1317 gfx::ImageSkia tab_r = gfx::ImageSkiaOperations::CreateTiledImage(
1318 *tab_background,
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,
1328 kDropShadowHeight,
1329 tab_image->l_width,
1330 kDropShadowHeight,
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())
1344 return;
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);
1360 } else {
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(
1367 canvas, bounds,
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;
1399 } else {
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());
1407 } else {
1408 ScheduleIconPaint();
1412 int Tab::IconCapacity() const {
1413 const gfx::Size min_size(GetMinimumUnselectedSize());
1414 if (height() < min_size.height())
1415 return 0;
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
1428 // shift.
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() :
1440 data_.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() :
1447 data_.media_state);
1450 bool Tab::ShouldShowCloseBox() const {
1451 if (!IsActive() && controller_->ShouldHideCloseButtonForInactiveTabs())
1452 return false;
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
1469 // much.
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 +
1478 min;
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())
1513 return;
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 {
1523 DCHECK(path);
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
1535 // tab.
1536 const SkScalar kTabInset = 6;
1537 const SkScalar kTabTop = 2;
1538 #endif
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);
1548 // Left end cap.
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);
1563 // Right end cap.
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);
1571 path->close();
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;
1581 return gfx::Rect(
1582 main_bar_left, 0, main_bar_right - main_bar_left, kImmersiveBarHeight);
1585 void Tab::GetTabIdAndFrameId(views::Widget* widget,
1586 int* tab_id,
1587 int* frame_id) const {
1588 if (widget &&
1589 widget->GetTopLevelWidget()->ShouldWindowContentsBeTransparent()) {
1590 *tab_id = IDR_THEME_TAB_BACKGROUND_V;
1591 *frame_id = 0;
1592 } else if (data().incognito) {
1593 *tab_id = IDR_THEME_TAB_BACKGROUND_INCOGNITO;
1594 *frame_id = IDR_THEME_FRAME_INCOGNITO;
1595 } else {
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:
1612 // static
1613 void Tab::InitTabResources() {
1614 static bool initialized = false;
1615 if (initialized)
1616 return;
1618 initialized = true;
1619 image_cache_ = new ImageCache();
1621 // Load the tab images once now, and maybe again later if the theme changes.
1622 LoadTabImages();
1625 // static
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();
1646 // static
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) {
1654 return i->image;
1657 return gfx::ImageSkia();
1660 // static
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();