blob: e106328f38bd5cf728ae538c4ebcb0f6ce7293d0 [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/bind.h"
#include "base/debug/alias.h"
#include "base/macros.h"
#include "base/metrics/user_metrics.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.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/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/harmony/chrome_layout_provider.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_close_button.h"
#include "chrome/browser/ui/views/tabs/tab_controller.h"
#include "chrome/browser/ui/views/tabs/tab_icon.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/browser/ui/views/touch_uma/touch_uma.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h"
#include "components/grit/components_scaled_resources.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/material_design/material_design_controller.h"
#include "ui/base/models/list_selection_model.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/theme_provider.h"
#include "ui/compositor/clip_recorder.h"
#include "ui/gfx/animation/animation_container.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_analysis.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/geometry/rect_conversions.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;
using MD = ui::MaterialDesignController;
namespace {
const int kExtraLeftPaddingToBalanceCloseButtonPadding = 2;
// 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;
////////////////////////////////////////////////////////////////////////////////
// Drawing and utility functions
// Returns the width of the tab endcap in DIP. More precisely, this is the
// width of the curve making up either the outer or inner edge of the stroke.
//
// For non-material-refresh mode, 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.
//
// The value returned here must be at least Tab::kMinimumEndcapWidth.
float GetTabEndcapWidth() {
// TODO(pkasting): This should become a member function and vary with
// GetCornerRadius().
return GetLayoutInsets(TAB).left() -
(MD::GetMode() == MD::MATERIAL_REFRESH ? 0.0f : 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::PaintShader::MakeRadialGradient(
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 a path corresponding to the tab's content region inside the outer
// stroke. The sides of the path will be inset by |horizontal_inset|; this is
// useful when trying to clip favicons to match the overall tab shape but be
// inset from the edge.
gfx::Path GetInteriorPath(float scale,
const gfx::Size& size,
float endcap_width,
float horizontal_inset = 0) {
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 scaled_horizontal_inset = horizontal_inset * scale;
// Construct the interior path by intersecting paths representing the left
// and right halves of the tab. Compared to computing the full path at once,
// this makes it easier to avoid overdraw in the top center near minimum
// width, and to implement cases where |horizontal_inset| != 0.
gfx::Path right_path;
gfx::Path left_path;
if (MD::GetMode() == MD::MATERIAL_REFRESH) {
const float radius = (endcap_width / 2) * scale;
const float stroke_thickness = TabStrip::ShouldDrawStrokes() ? 1 : 0;
// Bottom right.
right_path.moveTo(right, bottom);
right_path.arcTo(radius, radius, 0, SkPath::kSmall_ArcSize,
SkPath::kCW_Direction, right - radius, bottom - radius);
// Right vertical.
right_path.lineTo(right - radius, radius + stroke_thickness);
// Top right.
right_path.arcTo(radius, radius, 0, SkPath::kSmall_ArcSize,
SkPath::kCCW_Direction, right - radius * 2,
stroke_thickness);
// Top edge.
right_path.lineTo(0, stroke_thickness);
right_path.lineTo(0, bottom);
right_path.close();
// Top left.
left_path.moveTo(radius * 2, stroke_thickness);
left_path.arcTo(radius, radius, 0, SkPath::kSmall_ArcSize,
SkPath::kCCW_Direction, radius, radius + stroke_thickness);
// Left vertical.
left_path.lineTo(radius, bottom - radius);
// Bottom left.
left_path.arcTo(radius, radius, 0, SkPath::kSmall_ArcSize,
SkPath::kCW_Direction, 0, bottom);
// Bottom edge.
left_path.lineTo(right, bottom);
left_path.lineTo(right, stroke_thickness);
left_path.close();
} else {
right_path.moveTo(right - 1 - scaled_horizontal_inset, bottom);
right_path.rCubicTo(-0.75 * scale, 0, -1.625 * scale, -0.5 * scale,
-2 * scale, -1.5 * scale);
right_path.lineTo(
right - 1 - scaled_horizontal_inset - (endcap_width - 2) * scale,
2.5 * scale);
right_path.rCubicTo(-0.375 * scale, -1 * scale, -1.25 * scale, -1.5 * scale,
-2 * scale, -1.5 * scale);
right_path.lineTo(0, scale);
right_path.lineTo(0, bottom);
right_path.close();
const float scaled_endcap_width = 1 + endcap_width * scale;
left_path.moveTo(scaled_endcap_width + scaled_horizontal_inset, scale);
left_path.rCubicTo(-0.75 * scale, 0, -1.625 * scale, 0.5 * scale,
-2 * scale, 1.5 * scale);
left_path.lineTo(1 + scaled_horizontal_inset + 2 * scale,
bottom - 1.5 * scale);
left_path.rCubicTo(-0.375 * scale, scale, -1.25 * scale, 1.5 * scale,
-2 * scale, 1.5 * scale);
left_path.lineTo(right, bottom);
left_path.lineTo(right, scale);
left_path.close();
}
gfx::Path complete_path;
Op(left_path, right_path, SkPathOp::kIntersect_SkPathOp, &complete_path);
return complete_path;
}
// Returns a path corresponding to the tab's outer border for a given tab
// |size|, |scale|, and |endcap_width|. 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,
float endcap_width,
const gfx::Size& size) {
const float stroke_thickness = TabStrip::ShouldDrawStrokes() ? 1 : 0;
const float top = scale - stroke_thickness;
const float right = size.width() * scale;
const float bottom = size.height() * scale;
gfx::Path path;
path.moveTo(0, bottom);
path.rLineTo(0, -1);
if (MD::GetMode() == MD::MATERIAL_REFRESH) {
const float radius = (endcap_width / 2) * scale;
const float bottom_radius = radius - stroke_thickness;
const float top_radius = radius + stroke_thickness;
// bottom left
path.arcTo(bottom_radius, bottom_radius, 0, SkPath::kSmall_ArcSize,
SkPath::kCCW_Direction, bottom_radius, bottom - radius);
// left vertical
path.lineTo(bottom_radius, top_radius);
// top left
path.arcTo(top_radius, top_radius, 0, SkPath::kSmall_ArcSize,
SkPath::kCW_Direction, radius * 2, 0);
// top line
path.lineTo(right - radius * 2, 0);
// top right
path.arcTo(top_radius, top_radius, 0, SkPath::kSmall_ArcSize,
SkPath::kCW_Direction, right - bottom_radius, radius);
// right vertical
path.lineTo(right - bottom_radius, bottom - radius);
// bottom right
path.arcTo(bottom_radius, bottom_radius, 0, SkPath::kSmall_ArcSize,
SkPath::kCCW_Direction, right, bottom - stroke_thickness);
} else {
path.rCubicTo(0.75 * scale, 0, 1.625 * scale, -0.5 * scale, 2 * scale,
-1.5 * scale);
path.lineTo((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 - (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 - 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
////////////////////////////////////////////////////////////////////////////////
// Tab, public:
// static
const char Tab::kViewClassName[] = "Tab";
Tab::Tab(TabController* controller, gfx::AnimationContainer* container)
: controller_(controller),
pulse_animation_(this),
animation_container_(container),
title_(new views::Label()),
title_animation_(this),
hover_controller_(this) {
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);
// This will cause calls to GetContentsBounds to return only the rectangle
// inside the tab shape, rather than to its extents.
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::make_unique<views::ViewTargeter>(this));
icon_ = new TabIcon;
AddChildView(icon_);
alert_indicator_button_ = new AlertIndicatorButton(this);
AddChildView(alert_indicator_button_);
// Unretained is safe here because this class outlives its close button, and
// the controller outlives this Tab.
close_button_ = new TabCloseButton(
this, base::BindRepeating(&TabController::OnMouseEventInTab,
base::Unretained(controller_)));
AddChildView(close_button_);
set_context_menu_controller(this);
const int kPulseDurationMs = 200;
pulse_animation_.SetSlideDuration(kPulseDurationMs);
pulse_animation_.SetContainer(animation_container_.get());
title_animation_.SetDuration(base::TimeDelta::FromMilliseconds(100));
title_animation_.SetContainer(animation_container_.get());
hover_controller_.SetAnimationContainer(animation_container_.get());
}
Tab::~Tab() {
}
int Tab::GetCornerRadius() const {
// TODO(pkasting): This should vary as the tab width decreases.
return ChromeLayoutProvider::Get()->GetCornerRadiusMetric(
views::EMPHASIS_MEDIUM);
}
SkColor Tab::GetAlertIndicatorColor(TabAlertState state) const {
const bool is_touch_optimized = MD::IsTouchOptimizedUiEnabled();
// If theme provider is not yet available, return the default button
// color.
const ui::ThemeProvider* theme_provider = GetThemeProvider();
if (!theme_provider)
return button_color_;
switch (state) {
case TabAlertState::AUDIO_PLAYING:
case TabAlertState::AUDIO_MUTING:
return is_touch_optimized ? theme_provider->GetColor(
ThemeProperties::COLOR_TAB_ALERT_AUDIO)
: button_color_;
case TabAlertState::MEDIA_RECORDING:
return theme_provider->GetColor(
ThemeProperties::COLOR_TAB_ALERT_RECORDING);
case TabAlertState::TAB_CAPTURING:
return is_touch_optimized
? theme_provider->GetColor(
ThemeProperties::COLOR_TAB_ALERT_CAPTURING)
: button_color_;
case TabAlertState::BLUETOOTH_CONNECTED:
case TabAlertState::USB_CONNECTED:
case TabAlertState::NONE:
return button_color_;
default:
NOTREACHED();
return button_color_;
}
}
SkColor Tab::GetCloseTabButtonColor(
views::Button::ButtonState button_state) const {
// 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 SK_ColorTRANSPARENT;
int color_id;
switch (button_state) {
case views::Button::STATE_HOVERED:
color_id = ThemeProperties::COLOR_TAB_CLOSE_BUTTON_BACKGROUND_HOVER;
break;
case views::Button::STATE_PRESSED:
color_id = ThemeProperties::COLOR_TAB_CLOSE_BUTTON_BACKGROUND_PRESSED;
break;
default:
color_id = IsActive() ? ThemeProperties::COLOR_TAB_CLOSE_BUTTON_ACTIVE
: ThemeProperties::COLOR_TAB_CLOSE_BUTTON_INACTIVE;
}
return theme_provider->GetColor(color_id);
}
bool Tab::IsActive() const {
return controller_->IsActiveTab(this);
}
void Tab::ActiveStateChanged() {
if (IsActive()) {
// Clear the blocked WebContents for active tabs because it's distracting.
icon_->SetAttention(TabIcon::AttentionType::kBlockedWebContents, false);
}
OnButtonColorMaybeChanged();
alert_indicator_button_->UpdateEnabledForMuteToggle();
if (MD::GetMode() == MD::MATERIAL_REFRESH)
RepaintSubsequentTab();
Layout();
}
void Tab::AlertStateChanged() {
Layout();
}
bool Tab::IsSelected() const {
return controller_->IsTabSelected(this);
}
void Tab::SetData(TabRendererData data) {
DCHECK(GetWidget());
if (data_ == data)
return;
TabRendererData old(std::move(data_));
data_ = std::move(data);
// Icon updating must be done first because the title depends on whether the
// loading animation is showing.
icon_->SetIcon(data_.url, data_.favicon);
icon_->SetNetworkState(data_.network_state, data_.should_hide_throbber);
icon_->SetCanPaintToLayer(controller_->CanPaintThrobberToLayer());
icon_->SetIsCrashed(data_.IsCrashed());
if (IsActive()) {
icon_->SetAttention(TabIcon::AttentionType::kBlockedWebContents, false);
} else {
// Only non-active WebContents get the blocked attention type because it's
// confusing on the active tab.
icon_->SetAttention(TabIcon::AttentionType::kBlockedWebContents,
data_.blocked);
}
base::string16 title = data_.title;
if (title.empty()) {
title = icon_->ShowingLoadingAnimation()
? l10n_util::GetStringUTF16(IDS_TAB_LOADING_TITLE)
: CoreTabHelper::GetDefaultTitle();
} else {
Browser::FormatTitleForDisplay(&title);
}
title_->SetText(title);
if (data_.alert_state != old.alert_state)
alert_indicator_button_->TransitionToAlertState(data_.alert_state);
if (old.pinned != data_.pinned)
showing_alert_indicator_ = false;
if (data_.alert_state != old.alert_state || data_.title != old.title)
TooltipTextChanged();
Layout();
SchedulePaint();
}
void Tab::StepLoadingAnimation() {
icon_->StepLoadingAnimation();
// Update the layering if necessary.
//
// TODO(brettw) this design should be changed to be a push state when the tab
// can't be painted to a layer, rather than continually polling the
// controller about the state and reevaulating that state in the icon. This
// is both overly aggressive and wasteful in the common case, and not
// frequent enough in other cases since the state can be updated and the tab
// painted before the animation is stepped.
icon_->SetCanPaintToLayer(controller_->CanPaintThrobberToLayer());
}
void Tab::StartPulse() {
pulse_animation_.StartThrobbing(std::numeric_limits<int>::max());
}
void Tab::StopPulse() {
pulse_animation_.Stop();
}
void Tab::SetTabNeedsAttention(bool attention) {
icon_->SetAttention(TabIcon::AttentionType::kTabWantsAttentionStatus,
attention);
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 = GetLayoutConstant(TAB_STANDARD_WIDTH);
const int overlap = GetOverlap();
return gfx::Size(kNetTabWidth + overlap, GetMinimumInactiveSize().height());
}
// 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 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 together are kMinimumEndcapWidth DIP wide, so
// the diagonal is (endcap_width - kMinimumEndcapWidth) DIP 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 this gives (height - 4) * scale px, or (height - 4)
// DIP.
return (GetTabEndcapWidth() - kMinimumEndcapWidth) /
(Tab::GetMinimumInactiveSize().height() - 4);
}
// static
int Tab::GetOverlap() {
// We want to overlap the endcap portions entirely.
return gfx::ToCeiledInt(GetTabEndcapWidth());
}
////////////////////////////////////////////////////////////////////////////////
// Tab, AnimationDelegate overrides:
void Tab::AnimationProgressed(const gfx::Animation* animation) {
if (animation == &title_animation_) {
title_->SetBoundsRect(gfx::Tween::RectValueBetween(
gfx::Tween::CalculateValue(gfx::Tween::FAST_OUT_SLOW_IN,
animation->GetCurrentValue()),
start_title_bounds_, target_title_bounds_));
return;
}
// Ignore if the pulse animation is being performed on active tab because
// it repaints the same image. See PaintTab().
if (animation == &pulse_animation_ && IsActive())
return;
SchedulePaint();
}
void Tab::AnimationCanceled(const gfx::Animation* animation) {
SchedulePaint();
}
void Tab::AnimationEnded(const gfx::Animation* animation) {
if (animation == &title_animation_)
title_->SetBoundsRect(target_title_bounds_);
else
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()),
GetTabEndcapWidth(), 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,
GetTabEndcapWidth()),
&clip))
return;
PaintTab(canvas, clip);
}
void Tab::PaintChildren(const views::PaintInfo& info) {
// Clip children to 1 dp inside the tab's fill path. This has no effect
// except when the tab is too narrow to completely show even one icon, at
// which point this serves to clip the favicon.
ui::ClipRecorder clip_recorder(info.context());
// The paint recording scale for tabs is consistent along the x and y axis.
const float paint_recording_scale = info.paint_recording_scale_x();
clip_recorder.ClipPathWithAntiAliasing(GetInteriorPath(
paint_recording_scale, size(), GetTabEndcapWidth(), 1 /* padding */));
View::PaintChildren(info);
}
void Tab::Layout() {
const gfx::Rect lb = GetContentsBounds();
const bool was_showing_icon = showing_icon_;
UpdateIconVisibility();
const int extra_padding = extra_padding_before_content_
? kExtraLeftPaddingToBalanceCloseButtonPadding
: 0;
const int start = lb.x() + extra_padding;
// The bounds for the favicon will include extra width for the attention
// indicator, but visually it will be smaller at kFaviconSize wide.
gfx::Rect favicon_bounds(start, lb.y(), 0, 0);
if (showing_icon_) {
// Height should go to the bottom of the tab for the crashed tab animation
// to pop out of the bottom.
favicon_bounds.set_y(lb.y() + (lb.height() - gfx::kFaviconSize + 1) / 2);
favicon_bounds.set_size(gfx::Size(icon_->GetPreferredSize().width(),
lb.height() - favicon_bounds.y()));
if (center_favicon_) {
favicon_bounds.set_x(GetContentsBounds().CenterPoint().x() -
gfx::kFaviconSize / 2);
} else {
MaybeAdjustLeftForPinnedTab(&favicon_bounds, gfx::kFaviconSize);
}
}
icon_->SetBoundsRect(favicon_bounds);
icon_->SetVisible(showing_icon_);
const int after_title_padding = GetLayoutConstant(TAB_AFTER_TITLE_PADDING);
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 = after_title_padding;
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_);
if (showing_alert_indicator_) {
const bool is_touch_optimized = MD::IsTouchOptimizedUiEnabled();
const gfx::Size image_size(alert_indicator_button_->GetPreferredSize());
const int alert_to_close_spacing =
is_touch_optimized ? after_title_padding : 0;
const int right = showing_close_button_
? close_button_->x() +
close_button_->GetInsets().left() -
alert_to_close_spacing
: 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, bounds.width());
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.
bool show_title = ShouldRenderAsNormalTab();
if (show_title) {
// When computing the spacing from the favicon, don't count the actual
// icon view width (which will include extra room for the alert indicator),
// but rather the normal favicon width which is what it will look like.
const int title_left = showing_icon_
? (favicon_bounds.x() + gfx::kFaviconSize +
GetLayoutConstant(TAB_PRE_TITLE_PADDING))
: start;
int title_right = lb.right();
if (showing_alert_indicator_) {
title_right = alert_indicator_button_->x() - after_title_padding;
} else if (showing_close_button_) {
// Allow the title to overlay the close button's empty border padding.
title_right = close_button_->x() + close_button_->GetInsets().left() -
after_title_padding;
}
const int title_width = std::max(title_right - title_left, 0);
// The Label will automatically center the font's cap height within the
// provided vertical space.
const gfx::Rect title_bounds(title_left, lb.y(), title_width, lb.height());
show_title = title_width > 0;
if (title_bounds != target_title_bounds_) {
target_title_bounds_ = title_bounds;
if (was_showing_icon == showing_icon_ || title_->bounds().IsEmpty() ||
title_bounds.IsEmpty()) {
title_animation_.Stop();
title_->SetBoundsRect(title_bounds);
} else if (!title_animation_.is_animating()) {
start_title_bounds_ = title_->bounds();
title_animation_.Start();
}
}
}
title_->SetVisible(show_title);
}
void Tab::OnThemeChanged() {
OnButtonColorMaybeChanged();
}
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::SetData().
*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 = 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);
if (MD::GetMode() == MD::MATERIAL_REFRESH)
RepaintSubsequentTab();
Layout();
}
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();
if (MD::GetMode() == MD::MATERIAL_REFRESH)
RepaintSubsequentTab();
Layout();
}
void Tab::GetAccessibleNodeData(ui::AXNodeData* node_data) {
node_data->role = ax::mojom::Role::kTab;
node_data->SetName(controller_->GetAccessibleTabName(this));
node_data->AddState(ax::mojom::State::kMultiselectable);
node_data->AddBoolAttribute(ax::mojom::BoolAttribute::kSelected,
IsSelected());
}
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 = 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();
}
////////////////////////////////////////////////////////////////////////////////
// Tab, private
void Tab::RepaintSubsequentTab() {
Tab* adjacent_tab = controller_->GetAdjacentTab(this, TabController::FORWARD);
if (adjacent_tab)
adjacent_tab->SchedulePaint();
}
void Tab::MaybeAdjustLeftForPinnedTab(gfx::Rect* bounds,
int visual_width) const {
if (ShouldRenderAsNormalTab())
return;
const int ideal_delta = width() - GetPinnedWidth();
const int ideal_x = (GetPinnedWidth() - visual_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::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();
}
}
}
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 float endcap_width = GetTabEndcapWidth();
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 =
GetInteriorPath(canvas->image_scale(), size(), endcap_width);
PaintTabBackgroundFill(canvas, fill_path, active, paint_hover_effect,
active_color, inactive_color, fill_id, y_offset);
if (TabStrip::ShouldDrawStrokes()) {
gfx::Path stroke_path = GetBorderPath(canvas->image_scale(), false, false,
endcap_width, size());
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);
}
} else {
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 =
GetInteriorPath(canvas->image_scale(), size(), endcap_width);
gfx::Path stroke_path = GetBorderPath(canvas->image_scale(), false, false,
endcap_width, 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();
}
if (TabStrip::ShouldDrawStrokes()) {
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()->drawPicture(cache.fill_record);
if (TabStrip::ShouldDrawStrokes()) {
gfx::ScopedCanvas scoped_canvas(clip ? canvas : nullptr);
if (clip)
canvas->sk_canvas()->clipPath(*clip, SkClipOp::kDifference, true);
canvas->sk_canvas()->drawPicture(cache.stroke_record);
}
}
if (!active)
PaintSeparator(canvas, inactive_color);
}
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::PaintSeparator(gfx::Canvas* canvas, SkColor inactive_color) {
if (MD::GetMode() != MD::MATERIAL_REFRESH)
return;
// If the tab to the left is active, the separator on this tab should not be
// painted.
Tab* previous_tab =
controller_->GetAdjacentTab(this, TabController::BACKWARD);
if (previous_tab && previous_tab->IsActive())
return;
const int tab_height = GetContentsBounds().height();
gfx::RectF separator_bounds;
separator_bounds.set_size(gfx::SizeF(1, 16));
separator_bounds.set_origin(gfx::PointF(
GetTabEndcapWidth() / 2, (tab_height - separator_bounds.height()) / 2));
// The following will paint the separator using an opacity that should
// cross-fade with the maximum hover animation value of this tab or the
// tab to the left. This will have the effect of fading out the separator
// while this tab's or the tab to the left's hover animation is progressing.
const double max_hover_value = std::max(
hover_controller_.GetAnimationValue(),
previous_tab ? previous_tab->hover_controller()->GetAnimationValue() : 0);
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(
SkColorSetA(color_utils::BlendTowardOppositeLuma(inactive_color, 0x5a),
gfx::Tween::IntValueBetween(max_hover_value, 255, 0)));
canvas->DrawRect(separator_bounds, flags);
}
void Tab::UpdateIconVisibility() {
center_favicon_ = false;
showing_icon_ = showing_alert_indicator_ = showing_close_button_ = false;
extra_padding_before_content_ = false;
const gfx::Size min_size(GetMinimumInactiveSize());
if (height() < min_size.height())
return;
int available_width = std::max(0, width() - min_size.width());
const bool is_touch_optimized = MD::IsTouchOptimizedUiEnabled();
const int favicon_width = gfx::kFaviconSize;
const int alert_icon_width =
alert_indicator_button_->GetPreferredSize().width();
// In case of touch optimized UI, the close button has an extra padding on the
// left that needs to be considered.
const int close_button_width =
close_button_->GetPreferredSize().width() -
(is_touch_optimized ? close_button_->GetInsets().right()
: close_button_->GetInsets().width());
int extra_padding = kExtraLeftPaddingToBalanceCloseButtonPadding;
const bool is_pinned = data().pinned;
const bool is_active = IsActive();
const bool has_favicon = data().show_icon;
const bool has_alert_icon =
(alert_indicator_button_ ? alert_indicator_button_->showing_alert_state()
: data().alert_state) != TabAlertState::NONE;
const bool hide_inactive_close_button =
controller_->ShouldHideCloseButtonForInactiveTabs();
const bool show_close_button_on_hover =
controller_->ShouldShowCloseButtonOnHover();
if (is_pinned) {
// When the tab is pinned, we can show one of the two icons. Alert icon
// is given priority over the favicon. We never show the close buton if the
// tab is pinned.
showing_alert_indicator_ = has_alert_icon;
showing_icon_ = has_favicon && !has_alert_icon;
} else {
if (is_active) {
// The close button is always visible for an active tab.
showing_close_button_ = true;
available_width -= close_button_width;
showing_alert_indicator_ =
has_alert_icon && alert_icon_width <= available_width;
available_width -= showing_alert_indicator_ ? alert_icon_width : 0;
// If all 3 icons are visible, we add an extra left padding for favicon.
// See comment for |extra_padding_before_content_|.
if (!showing_alert_indicator_)
extra_padding = 0;
showing_icon_ =
has_favicon && favicon_width + extra_padding <= available_width;
} else {
showing_alert_indicator_ =
has_alert_icon && alert_icon_width <= available_width;
available_width -= showing_alert_indicator_ ? alert_icon_width : 0;
showing_icon_ = has_favicon && favicon_width <= available_width;
available_width -= showing_icon_ ? favicon_width : 0;
// If all 3 icons are visible, we add an extra padding to the left of
// favicon. See comment for |extra_padding_before_content_|.
if (!showing_icon_ || !showing_alert_indicator_)
extra_padding = 0;
// For an inactive tab, the close button will be visible only when
// it is not forced to hide and the total width can accomodate all 3
// icons. When favicon or alert button is not visible, its space
// will be occupied by the title of this tab.
int title_width =
(!showing_icon_ + !showing_alert_indicator_) * favicon_width;
if ((!hide_inactive_close_button ||
(show_close_button_on_hover && hover_controller_.ShouldDraw())) &&
(title_width + close_button_width + extra_padding <=
available_width)) {
showing_close_button_ = true;
}
// If no other controls are visible, show favicon even though we
// don't have enough space. We'll clip the favicon in PaintChildren().
if (!showing_close_button_ && !showing_alert_indicator_ &&
!showing_icon_ && has_favicon) {
showing_icon_ = true;
center_favicon_ = true;
}
}
extra_padding_before_content_ =
showing_close_button_ && showing_icon_ && showing_alert_indicator_;
}
}
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::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);
SkColor new_button_color = title_color;
if (IsActive()) {
// This alpha value (0x2f) blends GoogleGrey800 close to GoogleGrey700.
new_button_color = color_utils::BlendTowardOppositeLuma(title_color, 0x2f);
}
if (button_color_ != new_button_color) {
button_color_ = new_button_color;
title_->SetEnabledColor(title_color);
alert_indicator_button_->OnParentTabButtonColorChanged();
if (!MD::IsTouchOptimizedUiEnabled()) {
close_button_->SetTabColor(button_color_,
color_utils::IsDark(theme_provider->GetColor(
ThemeProperties::COLOR_TOOLBAR)));
}
}
if (MD::IsTouchOptimizedUiEnabled())
close_button_->ActiveStateChanged(this);
}
Tab::BackgroundCache::BackgroundCache() = default;
Tab::BackgroundCache::~BackgroundCache() = default;