[go: nahoru, domu]

blob: 901375ad3a848231e62036ad307a1aa263ca4210 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/views/tabs/tab.h"
#include <stddef.h>
#include <limits>
#include <utility>
#include "base/command_line.h"
#include "base/debug/alias.h"
#include "base/macros.h"
#include "base/metrics/user_metrics.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "cc/paint/paint_flags.h"
#include "cc/paint/paint_recorder.h"
#include "cc/paint/paint_shader.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/tab_contents/core_tab_helper.h"
#include "chrome/browser/ui/tabs/tab_utils.h"
#include "chrome/browser/ui/view_ids.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/tabs/alert_indicator_button.h"
#include "chrome/browser/ui/views/tabs/browser_tab_strip_controller.h"
#include "chrome/browser/ui/views/tabs/tab_controller.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/browser/ui/views/touch_uma/touch_uma.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h"
#include "components/grit/components_scaled_resources.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/common/url_constants.h"
#include "third_party/skia/include/effects/SkGradientShader.h"
#include "third_party/skia/include/pathops/SkPathOps.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/list_selection_model.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/theme_provider.h"
#include "ui/gfx/animation/animation_container.h"
#include "ui/gfx/animation/throb_animation.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_analysis.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/path.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/gfx/skia_util.h"
#include "ui/resources/grit/ui_resources.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/rect_based_targeting_utils.h"
#include "ui/views/view_targeter.h"
#include "ui/views/widget/tooltip_manager.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/non_client_view.h"
#if defined(USE_AURA)
#include "ui/aura/env.h"
#endif
using base::UserMetricsAction;
namespace {
// Width of touch tabs.
const int kTouchWidth = 120;
const int kExtraLeftPaddingToBalanceCloseButtonPadding = 2;
const int kAfterTitleSpacing = 4;
// When a non-pinned tab becomes a pinned tab the width of the tab animates. If
// the width of a pinned tab is at least kPinnedTabExtraWidthToRenderAsNormal
// larger than the desired pinned tab width then the tab is rendered as a normal
// tab. This is done to avoid having the title immediately disappear when
// transitioning a tab from normal to pinned tab.
const int kPinnedTabExtraWidthToRenderAsNormal = 30;
// How opaque to make the hover state (out of 1).
const double kHoverOpacity = 0.33;
// Opacity of the active tab background painted over inactive selected tabs.
const double kSelectedTabOpacity = 0.3;
// Inactive selected tabs have their throb value scaled by this.
const double kSelectedTabThrobScale = 0.95 - kSelectedTabOpacity;
const char kTabCloseButtonName[] = "TabCloseButton";
////////////////////////////////////////////////////////////////////////////////
// Drawing and utility functions
// Returns the width of the tab endcap at scale 1. More precisely, this is the
// width of the curve making up either the outer or inner edge of the stroke;
// since these two curves are horizontally offset by 1 px (regardless of scale),
// the total width of the endcap from tab outer edge to the inside end of the
// stroke inner edge is (GetUnscaledEndcapWidth() * scale) + 1.
float GetUnscaledEndcapWidth() {
return GetLayoutInsets(TAB).left() - 0.5f;
}
void DrawHighlight(gfx::Canvas* canvas,
const SkPoint& p,
SkScalar radius,
SkColor color) {
const SkColor colors[2] = { color, SkColorSetA(color, 0) };
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setShader(cc::WrapSkShader(SkGradientShader::MakeRadial(
p, radius, colors, nullptr, 2, SkShader::kClamp_TileMode)));
canvas->sk_canvas()->drawRect(
SkRect::MakeXYWH(p.x() - radius, p.y() - radius, radius * 2, radius * 2),
flags);
}
// Returns whether the favicon for the given URL should be colored according to
// the browser theme.
bool ShouldThemifyFaviconForUrl(const GURL& url) {
return url.SchemeIs(content::kChromeUIScheme) &&
url.host() != chrome::kChromeUIHelpHost &&
url.host() != chrome::kChromeUIUberHost &&
url.host() != chrome::kChromeUIAppLauncherPageHost;
}
// Returns a path corresponding to the tab's content region inside the outer
// stroke.
gfx::Path GetFillPath(float scale, const gfx::Size& size) {
const float right = size.width() * scale;
// The bottom of the tab needs to be pixel-aligned or else when we call
// ClipPath with anti-aliasing enabled it can cause artifacts.
const float bottom = std::ceil(size.height() * scale);
const float unscaled_endcap_width = GetUnscaledEndcapWidth();
gfx::Path fill;
fill.moveTo(right - 1, bottom);
fill.rCubicTo(-0.75 * scale, 0, -1.625 * scale, -0.5 * scale, -2 * scale,
-1.5 * scale);
fill.lineTo(right - 1 - (unscaled_endcap_width - 2) * scale, 2.5 * scale);
// Prevent overdraw in the center near minimum width (only happens if
// scale < 2). We could instead avoid this by increasing the tab inset
// values, but that would shift all the content inward as well, unless we
// then overlapped the content on the endcaps, by which point we'd have a
// huge mess.
const float scaled_endcap_width = 1 + unscaled_endcap_width * scale;
const float overlap = scaled_endcap_width * 2 - right;
const float offset = (overlap > 0) ? (overlap / 2) : 0;
fill.rCubicTo(-0.375 * scale, -1 * scale, -1.25 * scale + offset,
-1.5 * scale, -2 * scale + offset, -1.5 * scale);
if (overlap < 0)
fill.lineTo(scaled_endcap_width, scale);
fill.rCubicTo(-0.75 * scale, 0, -1.625 * scale - offset, 0.5 * scale,
-2 * scale - offset, 1.5 * scale);
fill.lineTo(1 + 2 * scale, bottom - 1.5 * scale);
fill.rCubicTo(-0.375 * scale, scale, -1.25 * scale, 1.5 * scale, -2 * scale,
1.5 * scale);
fill.close();
return fill;
}
// Returns a path corresponding to the tab's outer border for a given tab |size|
// and |scale|. If |unscale_at_end| is true, this path will be normalized to a
// 1x scale by scaling by 1/scale before returning. If |extend_to_top| is true,
// the path is extended vertically to the top of the tab bounds. The caller
// uses this for Fitts' Law purposes in maximized/fullscreen mode.
gfx::Path GetBorderPath(float scale,
bool unscale_at_end,
bool extend_to_top,
const gfx::Size& size) {
const float top = scale - 1;
const float right = size.width() * scale;
const float bottom = size.height() * scale;
const float unscaled_endcap_width = GetUnscaledEndcapWidth();
gfx::Path path;
path.moveTo(0, bottom);
path.rLineTo(0, -1);
path.rCubicTo(0.75 * scale, 0, 1.625 * scale, -0.5 * scale, 2 * scale,
-1.5 * scale);
path.lineTo((unscaled_endcap_width - 2) * scale, top + 1.5 * scale);
if (extend_to_top) {
// Create the vertical extension by extending the side diagonals until
// they reach the top of the bounds.
const float dy = 2.5 * scale - 1;
const float dx = Tab::GetInverseDiagonalSlope() * dy;
path.rLineTo(dx, -dy);
path.lineTo(right - (unscaled_endcap_width - 2) * scale - dx, 0);
path.rLineTo(dx, dy);
} else {
path.rCubicTo(0.375 * scale, -scale, 1.25 * scale, -1.5 * scale, 2 * scale,
-1.5 * scale);
path.lineTo(right - unscaled_endcap_width * scale, top);
path.rCubicTo(0.75 * scale, 0, 1.625 * scale, 0.5 * scale, 2 * scale,
1.5 * scale);
}
path.lineTo(right - 2 * scale, bottom - 1 - 1.5 * scale);
path.rCubicTo(0.375 * scale, scale, 1.25 * scale, 1.5 * scale, 2 * scale,
1.5 * scale);
path.rLineTo(0, 1);
path.close();
if (unscale_at_end && (scale != 1))
path.transform(SkMatrix::MakeScale(1.f / scale));
return path;
}
} // namespace
////////////////////////////////////////////////////////////////////////////////
// FaviconCrashAnimation
//
// A custom animation subclass to manage the favicon crash animation.
class Tab::FaviconCrashAnimation : public gfx::LinearAnimation,
public gfx::AnimationDelegate {
public:
explicit FaviconCrashAnimation(Tab* target)
: gfx::LinearAnimation(1000, 25, this),
target_(target) {
}
~FaviconCrashAnimation() override {}
// gfx::Animation overrides:
void AnimateToState(double state) override {
const double kHidingOffset =
Tab::GetMinimumInactiveSize().height() - GetLayoutInsets(TAB).height();
if (state < .5) {
// Animate the normal icon down.
target_->SetFaviconHidingOffset(
static_cast<int>(floor(kHidingOffset * 2.0 * state)));
} else {
// Animate the crashed icon up.
target_->SetShouldDisplayCrashedFavicon(true);
target_->SetFaviconHidingOffset(
static_cast<int>(
floor(kHidingOffset - ((state - .5) * 2.0 * kHidingOffset))));
}
}
private:
Tab* target_;
DISALLOW_COPY_AND_ASSIGN(FaviconCrashAnimation);
};
////////////////////////////////////////////////////////////////////////////////
// TabCloseButton
//
// This is a Button subclass that causes middle clicks to be forwarded to the
// parent View by explicitly not handling them in OnMousePressed.
class Tab::TabCloseButton : public views::ImageButton,
public views::MaskedTargeterDelegate {
public:
explicit TabCloseButton(Tab* tab)
: views::ImageButton(tab),
tab_(tab) {
SetEventTargeter(
std::unique_ptr<views::ViewTargeter>(new views::ViewTargeter(this)));
}
~TabCloseButton() override {}
// views::View:
View* GetTooltipHandlerForPoint(const gfx::Point& point) override {
// Tab close button has no children, so tooltip handler should be the same
// as the event handler.
// In addition, a hit test has to be performed for the point (as
// GetTooltipHandlerForPoint() is responsible for it).
if (!HitTestPoint(point))
return NULL;
return GetEventHandlerForPoint(point);
}
bool OnMousePressed(const ui::MouseEvent& event) override {
tab_->controller_->OnMouseEventInTab(this, event);
bool handled = ImageButton::OnMousePressed(event);
// Explicitly mark midle-mouse clicks as non-handled to ensure the tab
// sees them.
return !event.IsMiddleMouseButton() && handled;
}
void OnMouseMoved(const ui::MouseEvent& event) override {
tab_->controller_->OnMouseEventInTab(this, event);
CustomButton::OnMouseMoved(event);
}
void OnMouseReleased(const ui::MouseEvent& event) override {
tab_->controller_->OnMouseEventInTab(this, event);
CustomButton::OnMouseReleased(event);
}
void OnGestureEvent(ui::GestureEvent* event) override {
// Consume all gesture events here so that the parent (Tab) does not
// start consuming gestures.
ImageButton::OnGestureEvent(event);
event->SetHandled();
}
const char* GetClassName() const override { return kTabCloseButtonName; }
private:
// views::MaskedTargeterDelegate:
View* TargetForRect(View* root, const gfx::Rect& rect) override {
CHECK_EQ(root, this);
if (!views::UsePointBasedTargeting(rect))
return ViewTargeterDelegate::TargetForRect(root, rect);
// Ignore the padding set on the button.
gfx::Rect contents_bounds = GetContentsBounds();
contents_bounds.set_x(GetMirroredXForRect(contents_bounds));
#if defined(USE_AURA)
// Include the padding in hit-test for touch events.
// TODO(pkasting): It seems like touch events would generate rects rather
// than points and thus use the TargetForRect() call above. If this is
// reached, it may be from someone calling GetEventHandlerForPoint() while a
// touch happens to be occurring. In such a case, maybe we don't want this
// code to run? It's possible this block should be removed, or maybe this
// whole function deleted. Note that in these cases, we should probably
// also remove the padding on the close button bounds (see Tab::Layout()),
// as it will be pointless.
if (aura::Env::GetInstance()->is_touch_down())
contents_bounds = GetLocalBounds();
#endif
return contents_bounds.Intersects(rect) ? this : parent();
}
// We need to define this so hit-testing won't include the border region.
bool GetHitTestMask(gfx::Path* mask) const override {
gfx::Rect button_bounds(GetContentsBounds());
button_bounds.set_x(GetMirroredXForRect(button_bounds));
mask->addRect(gfx::RectToSkRect(button_bounds));
return true;
}
Tab* tab_;
DISALLOW_COPY_AND_ASSIGN(TabCloseButton);
};
////////////////////////////////////////////////////////////////////////////////
// ThrobberView
//
// A Layer-backed view for updating a waiting or loading tab throbber.
class Tab::ThrobberView : public views::View {
public:
explicit ThrobberView(Tab* owner);
// Resets the times tracking when the throbber changes state.
void ResetStartTimes();
private:
// views::View:
bool CanProcessEventsWithinSubtree() const override;
void OnPaint(gfx::Canvas* canvas) override;
Tab* owner_; // Weak. Owns |this|.
// The point in time when the tab icon was first painted in the waiting state.
base::TimeTicks waiting_start_time_;
// The point in time when the tab icon was first painted in the loading state.
base::TimeTicks loading_start_time_;
// Paint state for the throbber after the most recent waiting paint.
gfx::ThrobberWaitingState waiting_state_;
DISALLOW_COPY_AND_ASSIGN(ThrobberView);
};
Tab::ThrobberView::ThrobberView(Tab* owner) : owner_(owner) {}
void Tab::ThrobberView::ResetStartTimes() {
waiting_start_time_ = base::TimeTicks();
loading_start_time_ = base::TimeTicks();
waiting_state_ = gfx::ThrobberWaitingState();
}
bool Tab::ThrobberView::CanProcessEventsWithinSubtree() const {
return false;
}
void Tab::ThrobberView::OnPaint(gfx::Canvas* canvas) {
const TabRendererData::NetworkState state = owner_->data().network_state;
if (state == TabRendererData::NETWORK_STATE_NONE ||
state == TabRendererData::NETWORK_STATE_ERROR)
return;
const ui::ThemeProvider* tp = GetThemeProvider();
const gfx::Rect bounds = GetLocalBounds();
if (state == TabRendererData::NETWORK_STATE_WAITING) {
if (waiting_start_time_ == base::TimeTicks())
waiting_start_time_ = base::TimeTicks::Now();
waiting_state_.elapsed_time = base::TimeTicks::Now() - waiting_start_time_;
gfx::PaintThrobberWaiting(
canvas, bounds,
tp->GetColor(ThemeProperties::COLOR_TAB_THROBBER_WAITING),
waiting_state_.elapsed_time);
} else {
if (loading_start_time_ == base::TimeTicks())
loading_start_time_ = base::TimeTicks::Now();
waiting_state_.color =
tp->GetColor(ThemeProperties::COLOR_TAB_THROBBER_WAITING);
gfx::PaintThrobberSpinningAfterWaiting(
canvas, bounds,
tp->GetColor(ThemeProperties::COLOR_TAB_THROBBER_SPINNING),
base::TimeTicks::Now() - loading_start_time_, &waiting_state_);
}
}
////////////////////////////////////////////////////////////////////////////////
// Tab, public:
// static
const char Tab::kViewClassName[] = "Tab";
Tab::Tab(TabController* controller, gfx::AnimationContainer* container)
: controller_(controller),
closing_(false),
dragging_(false),
detached_(false),
favicon_hiding_offset_(0),
should_display_crashed_favicon_(false),
pulse_animation_(new gfx::ThrobAnimation(this)),
crash_icon_animation_(new FaviconCrashAnimation(this)),
animation_container_(container),
throbber_(nullptr),
alert_indicator_button_(nullptr),
close_button_(nullptr),
title_(new views::Label()),
tab_activated_with_last_tap_down_(false),
hover_controller_(this),
showing_icon_(false),
showing_alert_indicator_(false),
showing_close_button_(false),
button_color_(SK_ColorTRANSPARENT) {
DCHECK(controller);
// So we get don't get enter/exit on children and don't prematurely stop the
// hover.
set_notify_enter_exit_on_child(true);
set_id(VIEW_ID_TAB);
SetBorder(views::CreateEmptyBorder(GetLayoutInsets(TAB)));
title_->SetHorizontalAlignment(gfx::ALIGN_TO_HEAD);
title_->SetElideBehavior(gfx::FADE_TAIL);
title_->SetHandlesTooltips(false);
title_->SetAutoColorReadabilityEnabled(false);
title_->SetText(CoreTabHelper::GetDefaultTitle());
AddChildView(title_);
SetEventTargeter(
std::unique_ptr<views::ViewTargeter>(new views::ViewTargeter(this)));
throbber_ = new ThrobberView(this);
throbber_->SetVisible(false);
AddChildView(throbber_);
alert_indicator_button_ = new AlertIndicatorButton(this);
AddChildView(alert_indicator_button_);
close_button_ = new TabCloseButton(this);
close_button_->SetAccessibleName(
l10n_util::GetStringUTF16(IDS_ACCNAME_CLOSE));
// The normal image is set by OnButtonColorMaybeChanged() because it depends
// on the current theme and active state. The hovered and pressed images
// don't depend on the these, so we can set them here.
const gfx::ImageSkia& hovered = gfx::CreateVectorIcon(
kTabCloseHoveredPressedIcon, SkColorSetRGB(0xDB, 0x44, 0x37));
const gfx::ImageSkia& pressed = gfx::CreateVectorIcon(
kTabCloseHoveredPressedIcon, SkColorSetRGB(0xA8, 0x35, 0x2A));
close_button_->SetImage(views::CustomButton::STATE_HOVERED, &hovered);
close_button_->SetImage(views::CustomButton::STATE_PRESSED, &pressed);
// Disable animation so that the red danger sign shows up immediately
// to help avoid mis-clicks.
close_button_->SetAnimationDuration(0);
AddChildView(close_button_);
set_context_menu_controller(this);
const int kPulseDurationMs = 200;
pulse_animation_->SetSlideDuration(kPulseDurationMs);
pulse_animation_->SetContainer(animation_container_.get());
hover_controller_.SetAnimationContainer(animation_container_.get());
}
Tab::~Tab() {
}
bool Tab::IsActive() const {
return controller_->IsActiveTab(this);
}
void Tab::ActiveStateChanged() {
// The pinned tab title changed indicator is only shown for inactive tabs.
// When transitioning between active and inactive always reset the state
// to enforce that.
SetPinnedTabTitleChangedIndicatorVisible(false);
OnButtonColorMaybeChanged();
alert_indicator_button_->UpdateEnabledForMuteToggle();
Layout();
}
void Tab::AlertStateChanged() {
Layout();
}
bool Tab::IsSelected() const {
return controller_->IsTabSelected(this);
}
void Tab::SetData(const TabRendererData& data) {
DCHECK(GetWidget());
if (data_.Equals(data))
return;
TabRendererData old(data_);
UpdateLoadingAnimation(data.network_state);
data_ = data;
base::string16 title = data_.title;
if (title.empty()) {
title = data_.loading ?
l10n_util::GetStringUTF16(IDS_TAB_LOADING_TITLE) :
CoreTabHelper::GetDefaultTitle();
} else {
Browser::FormatTitleForDisplay(&title);
}
title_->SetText(title);
if (!data_.IsCrashed()) {
crash_icon_animation_->Stop();
SetShouldDisplayCrashedFavicon(false);
favicon_hiding_offset_ = 0;
} else if (!should_display_crashed_favicon_ &&
!crash_icon_animation_->is_animating()) {
data_.alert_state = TabAlertState::NONE;
crash_icon_animation_->Start();
}
if (data_.alert_state != old.alert_state)
alert_indicator_button_->TransitionToAlertState(data_.alert_state);
if (old.pinned != data_.pinned)
showing_pinned_tab_title_changed_indicator_ = false;
DataChanged(old);
Layout();
SchedulePaint();
}
void Tab::UpdateLoadingAnimation(TabRendererData::NetworkState state) {
if (state == data_.network_state &&
(state == TabRendererData::NETWORK_STATE_NONE ||
state == TabRendererData::NETWORK_STATE_ERROR)) {
// If the network state is none or is a network error and hasn't changed,
// do nothing. Otherwise we need to advance the animation frame.
return;
}
data_.network_state = state;
AdvanceLoadingAnimation();
}
void Tab::StartPulse() {
pulse_animation_->StartThrobbing(std::numeric_limits<int>::max());
}
void Tab::StopPulse() {
pulse_animation_->Stop();
}
void Tab::SetPinnedTabTitleChangedIndicatorVisible(bool value) {
if (value == showing_pinned_tab_title_changed_indicator_)
return;
DCHECK(!value || data().pinned);
showing_pinned_tab_title_changed_indicator_ = value;
SchedulePaint();
}
int Tab::GetWidthOfLargestSelectableRegion() const {
// Assume the entire region to the left of the alert indicator and/or close
// buttons is available for click-to-select. If neither are visible, the
// entire tab region is available.
const int indicator_left =
showing_alert_indicator_ ? alert_indicator_button_->x() : width();
const int close_button_left = showing_close_button_ ?
close_button_->x() : width();
return std::min(indicator_left, close_button_left);
}
// static
gfx::Size Tab::GetMinimumInactiveSize() {
return gfx::Size(GetLayoutInsets(TAB).width(), GetLayoutConstant(TAB_HEIGHT));
}
// static
gfx::Size Tab::GetMinimumActiveSize() {
gfx::Size minimum_size = GetMinimumInactiveSize();
minimum_size.Enlarge(gfx::kFaviconSize, 0);
return minimum_size;
}
// static
gfx::Size Tab::GetStandardSize() {
const int kNetTabWidth = 193;
return gfx::Size(kNetTabWidth + kOverlap, GetMinimumInactiveSize().height());
}
// static
int Tab::GetTouchWidth() {
return kTouchWidth;
}
// static
int Tab::GetPinnedWidth() {
constexpr int kTabPinnedContentWidth = 23;
return GetMinimumInactiveSize().width() + kTabPinnedContentWidth;
}
// static
float Tab::GetInverseDiagonalSlope() {
// This is computed from the border path as follows:
// * The unscaled endcap width is enough for the whole stroke outer curve,
// i.e. the side diagonal plus the curves on both its ends.
// * The bottom and top curve are each (2 * scale) px wide, so the diagonal is
// (unscaled endcap width - 2 - 2) * scale px wide.
// * The bottom and top curve are each 1.5 px high. Additionally, there is an
// extra 1 px below the bottom curve and (scale - 1) px above the top curve,
// so the diagonal is ((height - 1.5 - 1.5) * scale - 1 - (scale - 1)) px
// high.
// Simplifying these gives the expression below.
return (GetUnscaledEndcapWidth() - 4) /
(GetMinimumInactiveSize().height() - 4);
}
////////////////////////////////////////////////////////////////////////////////
// Tab, AnimationDelegate overrides:
void Tab::AnimationProgressed(const gfx::Animation* animation) {
// Ignore if the pulse animation is being performed on active tab because
// it repaints the same image. See PaintTab().
if ((animation != pulse_animation_.get()) || !IsActive())
SchedulePaint();
}
void Tab::AnimationCanceled(const gfx::Animation* animation) {
SchedulePaint();
}
void Tab::AnimationEnded(const gfx::Animation* animation) {
SchedulePaint();
}
////////////////////////////////////////////////////////////////////////////////
// Tab, views::ButtonListener overrides:
void Tab::ButtonPressed(views::Button* sender, const ui::Event& event) {
if (!alert_indicator_button_ || !alert_indicator_button_->visible())
base::RecordAction(UserMetricsAction("CloseTab_NoAlertIndicator"));
else if (alert_indicator_button_->enabled())
base::RecordAction(UserMetricsAction("CloseTab_MuteToggleAvailable"));
else if (data_.alert_state == TabAlertState::AUDIO_PLAYING)
base::RecordAction(UserMetricsAction("CloseTab_AudioIndicator"));
else
base::RecordAction(UserMetricsAction("CloseTab_RecordingIndicator"));
const CloseTabSource source =
(event.type() == ui::ET_MOUSE_RELEASED &&
!(event.flags() & ui::EF_FROM_TOUCH)) ? CLOSE_TAB_FROM_MOUSE
: CLOSE_TAB_FROM_TOUCH;
DCHECK_EQ(close_button_, sender);
controller_->CloseTab(this, source);
if (event.type() == ui::ET_GESTURE_TAP)
TouchUMA::RecordGestureAction(TouchUMA::GESTURE_TABCLOSE_TAP);
}
////////////////////////////////////////////////////////////////////////////////
// Tab, views::ContextMenuController overrides:
void Tab::ShowContextMenuForView(views::View* source,
const gfx::Point& point,
ui::MenuSourceType source_type) {
if (!closing())
controller_->ShowContextMenuForTab(this, point, source_type);
}
////////////////////////////////////////////////////////////////////////////////
// Tab, views::MaskedTargeterDelegate overrides:
bool Tab::GetHitTestMask(gfx::Path* mask) const {
// When the window is maximized we don't want to shave off the edges or top
// shadow of the tab, such that the user can click anywhere along the top
// edge of the screen to select a tab. Ditto for immersive fullscreen.
const views::Widget* widget = GetWidget();
*mask = GetBorderPath(
GetWidget()->GetCompositor()->device_scale_factor(), true,
widget && (widget->IsMaximized() || widget->IsFullscreen()), size());
return true;
}
////////////////////////////////////////////////////////////////////////////////
// Tab, views::View overrides:
void Tab::ViewHierarchyChanged(const ViewHierarchyChangedDetails& details) {
// If this hierarchy changed has resulted in us being part of a widget
// hierarchy for the first time, we can now get at the theme provider, and
// should recalculate the button color.
if (details.is_add)
OnButtonColorMaybeChanged();
}
void Tab::OnPaint(gfx::Canvas* canvas) {
// Don't paint if we're narrower than we can render correctly. (This should
// only happen during animations).
if (width() < GetMinimumInactiveSize().width() && !data().pinned)
return;
gfx::Path clip;
if (!controller_->ShouldPaintTab(
this, base::Bind(&GetBorderPath, canvas->image_scale(), true, false),
&clip))
return;
PaintTab(canvas, clip);
}
void Tab::Layout() {
const gfx::Rect lb = GetContentsBounds();
showing_icon_ = ShouldShowIcon();
// See comments in IconCapacity().
const int extra_padding =
(controller_->ShouldHideCloseButtonForInactiveTabs() ||
(IconCapacity() < 3)) ? 0 : kExtraLeftPaddingToBalanceCloseButtonPadding;
const int start = lb.x() + extra_padding;
favicon_bounds_.SetRect(start, lb.y(), 0, 0);
if (showing_icon_) {
favicon_bounds_.set_size(gfx::Size(gfx::kFaviconSize, gfx::kFaviconSize));
favicon_bounds_.set_y(lb.y() + (lb.height() - gfx::kFaviconSize + 1) / 2);
MaybeAdjustLeftForPinnedTab(&favicon_bounds_);
}
throbber_->SetBoundsRect(favicon_bounds_);
showing_close_button_ = ShouldShowCloseBox();
if (showing_close_button_) {
// If the ratio of the close button size to tab width exceeds the maximum.
// The close button should be as large as possible so that there is a larger
// hit-target for touch events. So the close button bounds extends to the
// edges of the tab. However, the larger hit-target should be active only
// for touch events, and the close-image should show up in the right place.
// So a border is added to the button with necessary padding. The close
// button (Tab::TabCloseButton) makes sure the padding is a hit-target only
// for touch events.
// TODO(pkasting): The padding should maybe be removed, see comments in
// TabCloseButton::TargetForRect().
close_button_->SetBorder(views::NullBorder());
const gfx::Size close_button_size(close_button_->GetPreferredSize());
const int top = lb.y() + (lb.height() - close_button_size.height() + 1) / 2;
const int left = kAfterTitleSpacing;
const int close_button_end = lb.right();
close_button_->SetPosition(
gfx::Point(close_button_end - close_button_size.width() - left, 0));
const int bottom = height() - close_button_size.height() - top;
const int right = width() - close_button_end;
close_button_->SetBorder(
views::CreateEmptyBorder(top, left, bottom, right));
close_button_->SizeToPreferredSize();
}
close_button_->SetVisible(showing_close_button_);
showing_alert_indicator_ = ShouldShowAlertIndicator();
if (showing_alert_indicator_) {
const gfx::Size image_size(alert_indicator_button_->GetPreferredSize());
const int right = showing_close_button_ ?
close_button_->x() + close_button_->GetInsets().left() : lb.right();
gfx::Rect bounds(
std::max(lb.x(), right - image_size.width()),
lb.y() + (lb.height() - image_size.height() + 1) / 2,
image_size.width(),
image_size.height());
MaybeAdjustLeftForPinnedTab(&bounds);
alert_indicator_button_->SetBoundsRect(bounds);
}
alert_indicator_button_->SetVisible(showing_alert_indicator_);
// Size the title to fill the remaining width and use all available height.
const bool show_title = ShouldRenderAsNormalTab();
if (show_title) {
constexpr int kTitleSpacing = 6;
int title_left =
showing_icon_ ? (favicon_bounds_.right() + kTitleSpacing) : start;
int title_width = lb.right() - title_left;
if (showing_alert_indicator_) {
title_width =
alert_indicator_button_->x() - kAfterTitleSpacing - title_left;
} else if (showing_close_button_) {
// Allow the title to overlay the close button's empty border padding.
title_width = close_button_->x() + close_button_->GetInsets().left() -
kAfterTitleSpacing - title_left;
}
// The Label will automatically center the font's cap height within the
// provided vertical space.
title_->SetBoundsRect(
gfx::Rect(title_left, lb.y(), std::max(title_width, 0), lb.height()));
}
title_->SetVisible(show_title);
}
void Tab::OnThemeChanged() {
OnButtonColorMaybeChanged();
favicon_ = gfx::ImageSkia();
}
const char* Tab::GetClassName() const {
return kViewClassName;
}
bool Tab::GetTooltipText(const gfx::Point& p, base::string16* tooltip) const {
// Note: Anything that affects the tooltip text should be accounted for when
// calling TooltipTextChanged() from Tab::DataChanged().
*tooltip = chrome::AssembleTabTooltipText(data_.title, data_.alert_state);
return !tooltip->empty();
}
bool Tab::GetTooltipTextOrigin(const gfx::Point& p, gfx::Point* origin) const {
origin->set_x(title_->x() + 10);
origin->set_y(-4);
return true;
}
bool Tab::OnMousePressed(const ui::MouseEvent& event) {
controller_->OnMouseEventInTab(this, event);
// Allow a right click from touch to drag, which corresponds to a long click.
if (event.IsOnlyLeftMouseButton() ||
(event.IsOnlyRightMouseButton() && event.flags() & ui::EF_FROM_TOUCH)) {
ui::ListSelectionModel original_selection;
original_selection.Copy(controller_->GetSelectionModel());
// Changing the selection may cause our bounds to change. If that happens
// the location of the event may no longer be valid. Create a copy of the
// event in the parents coordinate, which won't change, and recreate an
// event after changing so the coordinates are correct.
ui::MouseEvent event_in_parent(event, static_cast<View*>(this), parent());
if (controller_->SupportsMultipleSelection()) {
if (event.IsShiftDown() && event.IsControlDown()) {
controller_->AddSelectionFromAnchorTo(this);
} else if (event.IsShiftDown()) {
controller_->ExtendSelectionTo(this);
} else if (event.IsControlDown()) {
controller_->ToggleSelected(this);
if (!IsSelected()) {
// Don't allow dragging non-selected tabs.
return false;
}
} else if (!IsSelected()) {
controller_->SelectTab(this);
base::RecordAction(UserMetricsAction("SwitchTab_Click"));
}
} else if (!IsSelected()) {
controller_->SelectTab(this);
base::RecordAction(UserMetricsAction("SwitchTab_Click"));
}
ui::MouseEvent cloned_event(event_in_parent, parent(),
static_cast<View*>(this));
controller_->MaybeStartDrag(this, cloned_event, original_selection);
}
return true;
}
bool Tab::OnMouseDragged(const ui::MouseEvent& event) {
controller_->ContinueDrag(this, event);
return true;
}
void Tab::OnMouseReleased(const ui::MouseEvent& event) {
controller_->OnMouseEventInTab(this, event);
// Notify the drag helper that we're done with any potential drag operations.
// Clean up the drag helper, which is re-created on the next mouse press.
// In some cases, ending the drag will schedule the tab for destruction; if
// so, bail immediately, since our members are already dead and we shouldn't
// do anything else except drop the tab where it is.
if (controller_->EndDrag(END_DRAG_COMPLETE))
return;
// Close tab on middle click, but only if the button is released over the tab
// (normal windows behavior is to discard presses of a UI element where the
// releases happen off the element).
if (event.IsMiddleMouseButton()) {
if (HitTestPoint(event.location())) {
controller_->CloseTab(this, CLOSE_TAB_FROM_MOUSE);
} else if (closing_) {
// We're animating closed and a middle mouse button was pushed on us but
// we don't contain the mouse anymore. We assume the user is clicking
// quicker than the animation and we should close the tab that falls under
// the mouse.
Tab* closest_tab = controller_->GetTabAt(this, event.location());
if (closest_tab)
controller_->CloseTab(closest_tab, CLOSE_TAB_FROM_MOUSE);
}
} else if (event.IsOnlyLeftMouseButton() && !event.IsShiftDown() &&
!event.IsControlDown()) {
// If the tab was already selected mouse pressed doesn't change the
// selection. Reset it now to handle the case where multiple tabs were
// selected.
controller_->SelectTab(this);
if (alert_indicator_button_ && alert_indicator_button_->visible() &&
alert_indicator_button_->bounds().Contains(event.location())) {
base::RecordAction(UserMetricsAction("TabAlertIndicator_Clicked"));
}
}
}
void Tab::OnMouseCaptureLost() {
controller_->EndDrag(END_DRAG_CAPTURE_LOST);
}
void Tab::OnMouseEntered(const ui::MouseEvent& event) {
hover_controller_.Show(views::GlowHoverController::SUBTLE);
}
void Tab::OnMouseMoved(const ui::MouseEvent& event) {
hover_controller_.SetLocation(event.location());
controller_->OnMouseEventInTab(this, event);
}
void Tab::OnMouseExited(const ui::MouseEvent& event) {
hover_controller_.Hide();
}
void Tab::OnGestureEvent(ui::GestureEvent* event) {
switch (event->type()) {
case ui::ET_GESTURE_TAP_DOWN: {
// TAP_DOWN is only dispatched for the first touch point.
DCHECK_EQ(1, event->details().touch_points());
// See comment in OnMousePressed() as to why we copy the event.
ui::GestureEvent event_in_parent(*event, static_cast<View*>(this),
parent());
ui::ListSelectionModel original_selection;
original_selection.Copy(controller_->GetSelectionModel());
tab_activated_with_last_tap_down_ = !IsActive();
if (!IsSelected())
controller_->SelectTab(this);
gfx::Point loc(event->location());
views::View::ConvertPointToScreen(this, &loc);
ui::GestureEvent cloned_event(event_in_parent, parent(),
static_cast<View*>(this));
controller_->MaybeStartDrag(this, cloned_event, original_selection);
break;
}
case ui::ET_GESTURE_END:
controller_->EndDrag(END_DRAG_COMPLETE);
break;
case ui::ET_GESTURE_SCROLL_UPDATE:
controller_->ContinueDrag(this, *event);
break;
default:
break;
}
event->SetHandled();
}
void Tab::GetAccessibleNodeData(ui::AXNodeData* node_data) {
node_data->role = ui::AX_ROLE_TAB;
node_data->SetName(controller_->GetAccessibleTabName(this));
node_data->AddStateFlag(ui::AX_STATE_MULTISELECTABLE);
node_data->AddStateFlag(ui::AX_STATE_SELECTABLE);
controller_->UpdateTabAccessibilityState(this, node_data);
if (IsSelected())
node_data->AddStateFlag(ui::AX_STATE_SELECTED);
}
////////////////////////////////////////////////////////////////////////////////
// Tab, private
void Tab::MaybeAdjustLeftForPinnedTab(gfx::Rect* bounds) const {
if (ShouldRenderAsNormalTab())
return;
const int ideal_delta = width() - GetPinnedWidth();
const int ideal_x = (GetPinnedWidth() - bounds->width()) / 2;
bounds->set_x(
bounds->x() + static_cast<int>(
(1 - static_cast<float>(ideal_delta) /
static_cast<float>(kPinnedTabExtraWidthToRenderAsNormal)) *
(ideal_x - bounds->x())));
}
void Tab::DataChanged(const TabRendererData& old) {
// We may overzealously reset the favicon cache here but this check eliminates
// at least some unnecessary re-computations and fixes the behavior of
// about:crash.
if (!old.favicon.BackedBySameObjectAs(data().favicon))
favicon_ = gfx::ImageSkia();
if (data().alert_state != old.alert_state || data().title != old.title)
TooltipTextChanged();
if (data().blocked == old.blocked)
return;
if (data().blocked)
StartPulse();
else
StopPulse();
}
void Tab::PaintTab(gfx::Canvas* canvas, const gfx::Path& clip) {
int active_tab_fill_id = 0;
int active_tab_y_offset = 0;
if (GetThemeProvider()->HasCustomImage(IDR_THEME_TOOLBAR)) {
active_tab_fill_id = IDR_THEME_TOOLBAR;
active_tab_y_offset = -GetLayoutInsets(TAB).top();
}
if (IsActive()) {
PaintTabBackground(canvas, true /* active */, active_tab_fill_id,
active_tab_y_offset, nullptr /* clip */);
} else {
PaintInactiveTabBackground(canvas, clip);
const double throb_value = GetThrobValue();
if (throb_value > 0) {
canvas->SaveLayerAlpha(gfx::ToRoundedInt(throb_value * 0xff),
GetLocalBounds());
PaintTabBackground(canvas, true /* active */, active_tab_fill_id,
active_tab_y_offset, nullptr /* clip */);
canvas->Restore();
}
}
if (showing_icon_)
PaintIcon(canvas);
}
void Tab::PaintInactiveTabBackground(gfx::Canvas* canvas,
const gfx::Path& clip) {
bool has_custom_image;
int fill_id = controller_->GetBackgroundResourceId(&has_custom_image);
// The offset used to read from the image specified by |fill_id|.
int y_offset = 0;
if (!has_custom_image) {
fill_id = 0;
} else if (!GetThemeProvider()->HasCustomImage(fill_id)) {
// If there's a custom frame image but no custom image for the tab itself,
// then the tab's background will be the frame's image, so we need to
// provide an offset into the image to read from.
y_offset = background_offset_.y();
}
PaintTabBackground(canvas, false /* active */, fill_id, y_offset,
controller_->MaySetClip() ? &clip : nullptr);
}
void Tab::PaintTabBackground(gfx::Canvas* canvas,
bool active,
int fill_id,
int y_offset,
const gfx::Path* clip) {
// |y_offset| is only set when |fill_id| is being used.
DCHECK(!y_offset || fill_id);
const ui::ThemeProvider* tp = GetThemeProvider();
const SkColor active_color = tp->GetColor(ThemeProperties::COLOR_TOOLBAR);
const SkColor inactive_color =
tp->GetColor(ThemeProperties::COLOR_BACKGROUND_TAB);
const SkColor stroke_color = controller_->GetToolbarTopSeparatorColor();
const bool paint_hover_effect = !active && hover_controller_.ShouldDraw();
// If there is a |fill_id| we don't try to cache. This could be improved
// but would require knowing then the image from the ThemeProvider had been
// changed, and invalidating when the tab's x-coordinate or background_offset_
// changed.
// Similarly, if |paint_hover_effect|, we don't try to cache since hover
// effects change on every invalidation and we would need to invalidate the
// cache based on the hover states.
if (fill_id || paint_hover_effect) {
gfx::Path fill_path = GetFillPath(canvas->image_scale(), size());
gfx::Path stroke_path =
GetBorderPath(canvas->image_scale(), false, false, size());
PaintTabBackgroundFill(canvas, fill_path, active, paint_hover_effect,
active_color, inactive_color, fill_id, y_offset);
gfx::ScopedCanvas scoped_canvas(clip ? canvas : nullptr);
if (clip)
canvas->sk_canvas()->clipPath(*clip, SkClipOp::kDifference, true);
PaintTabBackgroundStroke(canvas, fill_path, stroke_path, active,
stroke_color);
return;
}
BackgroundCache& cache =
active ? background_active_cache_ : background_inactive_cache_;
if (!cache.CacheKeyMatches(canvas->image_scale(), size(), active_color,
inactive_color, stroke_color)) {
gfx::Path fill_path = GetFillPath(canvas->image_scale(), size());
gfx::Path stroke_path =
GetBorderPath(canvas->image_scale(), false, false, size());
cc::PaintRecorder recorder;
{
gfx::Canvas cache_canvas(
recorder.beginRecording(size().width(), size().height()),
canvas->image_scale());
PaintTabBackgroundFill(&cache_canvas, fill_path, active,
paint_hover_effect, active_color, inactive_color,
fill_id, y_offset);
cache.fill_record = recorder.finishRecordingAsPicture();
}
{
gfx::Canvas cache_canvas(
recorder.beginRecording(size().width(), size().height()),
canvas->image_scale());
PaintTabBackgroundStroke(&cache_canvas, fill_path, stroke_path, active,
stroke_color);
cache.stroke_record = recorder.finishRecordingAsPicture();
}
cache.SetCacheKey(canvas->image_scale(), size(), active_color,
inactive_color, stroke_color);
}
canvas->sk_canvas()->PlaybackPaintRecord(cache.fill_record);
gfx::ScopedCanvas scoped_canvas(clip ? canvas : nullptr);
if (clip)
canvas->sk_canvas()->clipPath(*clip, SkClipOp::kDifference, true);
canvas->sk_canvas()->PlaybackPaintRecord(cache.stroke_record);
}
void Tab::PaintTabBackgroundFill(gfx::Canvas* canvas,
const gfx::Path& fill_path,
bool active,
bool paint_hover_effect,
SkColor active_color,
SkColor inactive_color,
int fill_id,
int y_offset) {
gfx::ScopedCanvas scoped_canvas(canvas);
const float scale = canvas->UndoDeviceScaleFactor();
canvas->ClipPath(fill_path, true);
if (fill_id) {
gfx::ScopedCanvas scale_scoper(canvas);
canvas->sk_canvas()->scale(scale, scale);
canvas->TileImageInt(*GetThemeProvider()->GetImageSkiaNamed(fill_id),
GetMirroredX() + background_offset_.x(), y_offset, 0,
0, width(), height());
} else {
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(active ? active_color : inactive_color);
canvas->DrawRect(gfx::ScaleToEnclosingRect(GetLocalBounds(), scale), flags);
}
if (paint_hover_effect) {
SkPoint hover_location(gfx::PointToSkPoint(hover_controller_.location()));
hover_location.scale(SkFloatToScalar(scale));
const SkScalar kMinHoverRadius = 16;
const SkScalar radius =
std::max(SkFloatToScalar(width() / 4.f), kMinHoverRadius);
DrawHighlight(canvas, hover_location, radius * scale,
SkColorSetA(active_color, hover_controller_.GetAlpha()));
}
}
void Tab::PaintTabBackgroundStroke(gfx::Canvas* canvas,
const gfx::Path& fill_path,
const gfx::Path& stroke_path,
bool active,
SkColor color) {
gfx::ScopedCanvas scoped_canvas(canvas);
const float scale = canvas->UndoDeviceScaleFactor();
if (!active) {
// Clip out the bottom line; this will be drawn for us by
// TabStrip::PaintChildren().
canvas->ClipRect(gfx::RectF(width() * scale, height() * scale - 1));
}
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(color);
SkPath path;
Op(stroke_path, fill_path, kDifference_SkPathOp, &path);
canvas->DrawPath(path, flags);
}
void Tab::PaintPinnedTabTitleChangedIndicatorAndIcon(
gfx::Canvas* canvas,
const gfx::Rect& favicon_draw_bounds) {
// The pinned tab title changed indicator consists of two parts:
// . a clear (totally transparent) part over the bottom right (or left in rtl)
// of the favicon. This is done by drawing the favicon to a layer, then
// drawing the clear part on top of the favicon.
// . a circle in the bottom right (or left in rtl) of the favicon.
if (!favicon_.isNull()) {
canvas->SaveLayerAlpha(0xff);
canvas->DrawImageInt(favicon_, 0, 0, favicon_draw_bounds.width(),
favicon_draw_bounds.height(), favicon_draw_bounds.x(),
favicon_draw_bounds.y(), favicon_draw_bounds.width(),
favicon_draw_bounds.height(), false);
cc::PaintFlags clear_flags;
clear_flags.setAntiAlias(true);
clear_flags.setBlendMode(SkBlendMode::kClear);
const float kIndicatorCropRadius = 4.5f;
int circle_x =
favicon_draw_bounds.x() + (base::i18n::IsRTL() ? 0 : gfx::kFaviconSize);
int circle_y = favicon_draw_bounds.y() + gfx::kFaviconSize;
canvas->DrawCircle(gfx::Point(circle_x, circle_y), kIndicatorCropRadius,
clear_flags);
canvas->Restore();
}
// Draws the actual pinned tab title changed indicator.
cc::PaintFlags indicator_flags;
indicator_flags.setColor(GetNativeTheme()->GetSystemColor(
ui::NativeTheme::kColorId_ProminentButtonColor));
indicator_flags.setAntiAlias(true);
const int kIndicatorRadius = 3;
const int indicator_x = GetMirroredXWithWidthInView(
favicon_bounds_.right() - kIndicatorRadius, kIndicatorRadius * 2);
const int indicator_y = favicon_bounds_.bottom() - kIndicatorRadius;
canvas->DrawCircle(gfx::Point(indicator_x + kIndicatorRadius,
indicator_y + kIndicatorRadius),
kIndicatorRadius, indicator_flags);
}
void Tab::PaintIcon(gfx::Canvas* canvas) {
gfx::Rect bounds = favicon_bounds_;
bounds.set_x(GetMirroredXForRect(bounds));
bounds.Offset(0, favicon_hiding_offset_);
bounds.Intersect(GetContentsBounds());
if (bounds.IsEmpty())
return;
// Throbber will do its own painting.
if (data().network_state != TabRendererData::NETWORK_STATE_NONE &&
data().network_state != TabRendererData::NETWORK_STATE_ERROR) {
return;
}
// Ensure that |favicon_| is created.
if (favicon_.isNull()) {
ui::ResourceBundle* rb = &ui::ResourceBundle::GetSharedInstance();
favicon_ = should_display_crashed_favicon_
? *rb->GetImageSkiaNamed(IDR_CRASH_SAD_FAVICON)
: data().favicon;
// Themify the icon if it's a chrome:// page or if it's the sadtab favicon.
// This ensures chrome:// pages are visible over the tab background. This is
// similar to code in the bookmarks bar.
if (!favicon_.isNull() &&
(should_display_crashed_favicon_ ||
favicon_.BackedBySameObjectAs(
*rb->GetImageSkiaNamed(IDR_DEFAULT_FAVICON)) ||
ShouldThemifyFaviconForUrl(data().url))) {
favicon_ = gfx::ImageSkiaOperations::CreateHSLShiftedImage(
favicon_, GetThemeProvider()->GetTint(ThemeProperties::TINT_BUTTONS));
}
}
if (showing_pinned_tab_title_changed_indicator_ &&
!should_display_crashed_favicon_) {
PaintPinnedTabTitleChangedIndicatorAndIcon(canvas, bounds);
} else if (!favicon_.isNull()) {
canvas->DrawImageInt(favicon_, 0, 0, bounds.width(), bounds.height(),
bounds.x(), bounds.y(), bounds.width(),
bounds.height(), false);
}
}
void Tab::AdvanceLoadingAnimation() {
const TabRendererData::NetworkState state = data().network_state;
if (state == TabRendererData::NETWORK_STATE_NONE ||
state == TabRendererData::NETWORK_STATE_ERROR) {
throbber_->ResetStartTimes();
throbber_->SetVisible(false);
ScheduleIconPaint();
return;
}
// Since the throbber can animate for a long time, paint to a separate layer
// when possible to reduce repaint overhead.
const bool paint_to_layer = controller_->CanPaintThrobberToLayer();
if (paint_to_layer != !!throbber_->layer()) {
if (paint_to_layer) {
throbber_->SetPaintToLayer();
throbber_->layer()->SetFillsBoundsOpaquely(false);
ScheduleIconPaint(); // Ensure the non-layered throbber goes away.
} else {
throbber_->DestroyLayer();
}
}
if (!throbber_->visible()) {
ScheduleIconPaint(); // Repaint the icon area to hide the favicon.
throbber_->SetVisible(true);
}
throbber_->SchedulePaint();
}
int Tab::IconCapacity() const {
const gfx::Size min_size(GetMinimumInactiveSize());
if (height() < min_size.height())
return 0;
const int available_width = std::max(0, width() - min_size.width());
// All icons are the same size as the favicon.
const int icon_width = gfx::kFaviconSize;
// We need enough space to display the icons flush against each other.
const int visible_icons = available_width / icon_width;
// When the close button will be visible on inactive tabs, we add additional
// padding to the left of the favicon to balance the whitespace inside the
// non-hovered close button image; otherwise, the tab contents look too close
// to the left edge. If the tab close button isn't visible on inactive tabs,
// we let the tab contents take the full width of the tab, to maximize visible
// content on tiny tabs. We base the determination on the inactive tab close
// button state so that when a tab is activated its contents don't suddenly
// shift.
if (visible_icons < 3)
return visible_icons;
const int padding = controller_->ShouldHideCloseButtonForInactiveTabs() ?
0 : kExtraLeftPaddingToBalanceCloseButtonPadding;
return (available_width - padding) / icon_width;
}
bool Tab::ShouldShowIcon() const {
return chrome::ShouldTabShowFavicon(
IconCapacity(), data().pinned, IsActive(), data().show_icon,
alert_indicator_button_ ? alert_indicator_button_->showing_alert_state()
: data_.alert_state);
}
bool Tab::ShouldShowAlertIndicator() const {
return chrome::ShouldTabShowAlertIndicator(
IconCapacity(), data().pinned, IsActive(), data().show_icon,
alert_indicator_button_ ? alert_indicator_button_->showing_alert_state()
: data_.alert_state);
}
bool Tab::ShouldShowCloseBox() const {
if (!IsActive() && controller_->ShouldHideCloseButtonForInactiveTabs())
return false;
return chrome::ShouldTabShowCloseButton(
IconCapacity(), data().pinned, IsActive());
}
bool Tab::ShouldRenderAsNormalTab() const {
return !data().pinned ||
(width() >= (GetPinnedWidth() + kPinnedTabExtraWidthToRenderAsNormal));
}
double Tab::GetThrobValue() {
const bool is_selected = IsSelected();
double val = is_selected ? kSelectedTabOpacity : 0;
const double offset =
is_selected ? (kSelectedTabThrobScale * kHoverOpacity) : kHoverOpacity;
if (pulse_animation_->is_animating())
val += pulse_animation_->GetCurrentValue() * offset;
else if (hover_controller_.ShouldDraw())
val += hover_controller_.GetAnimationValue() * offset;
return val;
}
void Tab::SetShouldDisplayCrashedFavicon(bool value) {
if (value == should_display_crashed_favicon_)
return;
should_display_crashed_favicon_ = value;
favicon_ = gfx::ImageSkia();
}
void Tab::SetFaviconHidingOffset(int offset) {
favicon_hiding_offset_ = offset;
ScheduleIconPaint();
}
void Tab::OnButtonColorMaybeChanged() {
// The theme provider may be null if we're not currently in a widget
// hierarchy.
const ui::ThemeProvider* theme_provider = GetThemeProvider();
if (!theme_provider)
return;
const SkColor title_color = theme_provider->GetColor(IsActive() ?
ThemeProperties::COLOR_TAB_TEXT :
ThemeProperties::COLOR_BACKGROUND_TAB_TEXT);
const SkColor new_button_color = SkColorSetA(title_color, 0xA0);
if (button_color_ != new_button_color) {
button_color_ = new_button_color;
title_->SetEnabledColor(title_color);
alert_indicator_button_->OnParentTabButtonColorChanged();
const gfx::ImageSkia& close_button_normal_image =
gfx::CreateVectorIcon(kTabCloseNormalIcon, button_color_);
close_button_->SetImage(views::CustomButton::STATE_NORMAL,
&close_button_normal_image);
}
}
void Tab::ScheduleIconPaint() {
gfx::Rect bounds = favicon_bounds_;
if (bounds.IsEmpty())
return;
// Extends the area to the bottom when the crash animation is in progress.
if (crash_icon_animation_->is_animating())
bounds.set_height(height() - bounds.y());
bounds.set_x(GetMirroredXForRect(bounds));
SchedulePaintInRect(bounds);
}
Tab::BackgroundCache::BackgroundCache() = default;
Tab::BackgroundCache::~BackgroundCache() = default;