blob: 660f6da098040143a4de57e35184fa44fca88c7b [file] [log] [blame]
// Copyright 2017 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_icon.h"
#include "base/time/default_tick_clock.h"
#include "cc/paint/paint_flags.h"
#include "chrome/browser/favicon/favicon_utils.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/views/tabs/tab_renderer_data.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/webui_url_constants.h"
#include "components/grit/components_scaled_resources.h"
#include "content/public/common/url_constants.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/theme_provider.h"
#include "ui/gfx/animation/animation_delegate.h"
#include "ui/gfx/animation/linear_animation.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/native_theme/native_theme.h"
#include "ui/resources/grit/ui_resources.h"
#include "ui/views/border.h"
#include "url/gurl.h"
namespace {
constexpr int kAttentionIndicatorRadius = 3;
constexpr int kNewLoadingAnimationStrokeWidthDp = 2;
// 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_piece() != chrome::kChromeUIHelpHost &&
url.host_piece() != chrome::kChromeUIUberHost &&
url.host_piece() != chrome::kChromeUIAppLauncherPageHost;
}
bool NetworkStateIsAnimated(TabNetworkState network_state) {
return network_state != TabNetworkState::kNone &&
network_state != TabNetworkState::kError;
}
} // namespace
// Helper class that manages the favicon crash animation.
class TabIcon::CrashAnimation : public gfx::LinearAnimation,
public gfx::AnimationDelegate {
public:
explicit CrashAnimation(TabIcon* target)
: gfx::LinearAnimation(base::TimeDelta::FromSeconds(1), 25, this),
target_(target) {}
~CrashAnimation() override = default;
// gfx::Animation overrides:
void AnimateToState(double state) override {
if (state < .5) {
// Animate the normal icon down.
target_->hiding_fraction_ = state * 2.0;
} else {
// Animate the crashed icon up.
target_->should_display_crashed_favicon_ = true;
target_->hiding_fraction_ = 1.0 - (state - 0.5) * 2.0;
}
target_->SchedulePaint();
}
private:
TabIcon* target_;
DISALLOW_COPY_AND_ASSIGN(CrashAnimation);
};
TabIcon::TabIcon()
: clock_(base::DefaultTickClock::GetInstance()),
use_new_loading_animation_(
base::FeatureList::IsEnabled(features::kNewTabLoadingAnimation)),
favicon_fade_in_animation_(base::TimeDelta::FromMilliseconds(250),
gfx::LinearAnimation::kDefaultFrameRate,
this) {
set_can_process_events_within_subtree(false);
// The minimum size to avoid clipping the attention indicator.
const int preferred_width =
gfx::kFaviconSize + kAttentionIndicatorRadius + GetInsets().width();
SetPreferredSize(gfx::Size(preferred_width, preferred_width));
// Initial state (before any data) should not be animating.
DCHECK(!ShowingLoadingAnimation());
}
TabIcon::~TabIcon() = default;
void TabIcon::SetData(const TabRendererData& data) {
const bool was_showing_load = ShowingLoadingAnimation();
inhibit_loading_animation_ = data.should_hide_throbber;
SetIcon(data.url, data.favicon);
SetNetworkState(data.network_state);
SetIsCrashed(data.IsCrashed());
has_tab_renderer_data_ = true;
const bool showing_load = ShowingLoadingAnimation();
RefreshLayer();
if (was_showing_load && !showing_load) {
// Loading animation transitioning from on to off.
loading_animation_start_time_ = base::TimeTicks();
waiting_state_ = gfx::ThrobberWaitingState();
SchedulePaint();
} else if (!was_showing_load && showing_load) {
// Loading animation transitioning from off to on. The animation painting
// function will lazily initialize the data.
SchedulePaint();
}
}
void TabIcon::SetAttention(AttentionType type, bool enabled) {
int previous_attention_type = attention_types_;
if (enabled)
attention_types_ |= static_cast<int>(type);
else
attention_types_ &= ~static_cast<int>(type);
if (attention_types_ != previous_attention_type)
SchedulePaint();
}
bool TabIcon::ShowingLoadingAnimation() const {
if (inhibit_loading_animation_)
return false;
return NetworkStateIsAnimated(network_state_);
}
bool TabIcon::ShowingAttentionIndicator() const {
return attention_types_ > 0;
}
void TabIcon::SetCanPaintToLayer(bool can_paint_to_layer) {
if (can_paint_to_layer == can_paint_to_layer_)
return;
can_paint_to_layer_ = can_paint_to_layer;
RefreshLayer();
}
void TabIcon::StepLoadingAnimation(const base::TimeDelta& elapsed_time) {
// Only update elapsed time in the kWaiting state. This is later used as a
// starting point for PaintThrobberSpinningAfterWaiting().
if (network_state_ == TabNetworkState::kWaiting)
waiting_state_.elapsed_time = elapsed_time;
if (ShowingLoadingAnimation())
SchedulePaint();
}
void TabIcon::SetBackgroundColor(SkColor bg_color) {
bg_color_ = bg_color;
SchedulePaint();
}
void TabIcon::OnPaint(gfx::Canvas* canvas) {
// Compute the bounds adjusted for the hiding fraction.
gfx::Rect contents_bounds = GetContentsBounds();
if (contents_bounds.IsEmpty())
return;
gfx::Rect icon_bounds(
GetMirroredXWithWidthInView(contents_bounds.x(), gfx::kFaviconSize),
contents_bounds.y() +
static_cast<int>(contents_bounds.height() * hiding_fraction_),
std::min(gfx::kFaviconSize, contents_bounds.width()),
std::min(gfx::kFaviconSize, contents_bounds.height()));
// The old animation replaces the favicon and should early-abort.
if (!use_new_loading_animation_ && ShowingLoadingAnimation()) {
PaintLoadingAnimation(canvas, icon_bounds);
return;
}
// Don't paint the attention indicator during the loading animation.
if (!ShowingLoadingAnimation() && ShowingAttentionIndicator() &&
!should_display_crashed_favicon_) {
PaintAttentionIndicatorAndIcon(canvas, GetIconToPaint(), icon_bounds);
} else {
MaybePaintFavicon(canvas, GetIconToPaint(), icon_bounds);
}
if (ShowingLoadingAnimation())
PaintLoadingAnimation(canvas, icon_bounds);
}
void TabIcon::OnThemeChanged() {
crashed_icon_ = gfx::ImageSkia(); // Force recomputation if crashed.
if (!themed_favicon_.isNull())
themed_favicon_ = ThemeImage(favicon_);
}
void TabIcon::AnimationProgressed(const gfx::Animation* animation) {
SchedulePaint();
}
void TabIcon::AnimationEnded(const gfx::Animation* animation) {
RefreshLayer();
SchedulePaint();
}
void TabIcon::PaintAttentionIndicatorAndIcon(gfx::Canvas* canvas,
const gfx::ImageSkia& icon,
const gfx::Rect& bounds) {
gfx::Point circle_center(
bounds.x() + (base::i18n::IsRTL() ? 0 : gfx::kFaviconSize),
bounds.y() + gfx::kFaviconSize);
// The attention 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 (!icon.isNull()) {
canvas->SaveLayerAlpha(0xff);
canvas->DrawImageInt(icon, 0, 0, bounds.width(), bounds.height(),
bounds.x(), bounds.y(), bounds.width(),
bounds.height(), false);
cc::PaintFlags clear_flags;
clear_flags.setAntiAlias(true);
clear_flags.setBlendMode(SkBlendMode::kClear);
const float kIndicatorCropRadius = 4.5f;
canvas->DrawCircle(circle_center, kIndicatorCropRadius, clear_flags);
canvas->Restore();
}
// Draws the actual attention indicator.
cc::PaintFlags indicator_flags;
indicator_flags.setColor(GetNativeTheme()->GetSystemColor(
ui::NativeTheme::kColorId_ProminentButtonColor));
indicator_flags.setAntiAlias(true);
canvas->DrawCircle(circle_center, kAttentionIndicatorRadius, indicator_flags);
}
void TabIcon::PaintLoadingAnimation(gfx::Canvas* canvas, gfx::Rect bounds) {
const ui::ThemeProvider* tp = GetThemeProvider();
base::Optional<SkScalar> stroke_width;
if (use_new_loading_animation_)
stroke_width = kNewLoadingAnimationStrokeWidthDp;
if (network_state_ == TabNetworkState::kWaiting) {
gfx::PaintThrobberWaiting(
canvas, bounds,
tp->GetColor(ThemeProperties::COLOR_TAB_THROBBER_WAITING),
waiting_state_.elapsed_time, stroke_width);
} else {
const base::TimeTicks current_time = clock_->NowTicks();
if (loading_animation_start_time_.is_null())
loading_animation_start_time_ = current_time;
waiting_state_.color =
tp->GetColor(ThemeProperties::COLOR_TAB_THROBBER_WAITING);
gfx::PaintThrobberSpinningAfterWaiting(
canvas, bounds,
tp->GetColor(ThemeProperties::COLOR_TAB_THROBBER_SPINNING),
current_time - loading_animation_start_time_, &waiting_state_,
stroke_width);
}
}
const gfx::ImageSkia& TabIcon::GetIconToPaint() {
if (should_display_crashed_favicon_) {
if (crashed_icon_.isNull()) {
// Lazily create a themed sad tab icon.
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
crashed_icon_ = ThemeImage(*rb.GetImageSkiaNamed(IDR_CRASH_SAD_FAVICON));
}
return crashed_icon_;
}
return themed_favicon_.isNull() ? favicon_ : themed_favicon_;
}
void TabIcon::MaybePaintFavicon(gfx::Canvas* canvas,
const gfx::ImageSkia& icon,
const gfx::Rect& bounds) {
if (icon.isNull())
return;
if (ShowingLoadingAnimation()) {
// Never paint the favicon during the waiting animation.
if (network_state_ == TabNetworkState::kWaiting)
return;
// Don't paint the default favicon while we're still loading.
if (!HasNonDefaultFavicon())
return;
}
std::unique_ptr<gfx::ScopedCanvas> scoped_canvas;
bool use_scale_filter = false;
if (ShowingLoadingAnimation() || favicon_fade_in_animation_.is_animating()) {
scoped_canvas = std::make_unique<gfx::ScopedCanvas>(canvas);
use_scale_filter = true;
// The favicon is initially inset with the width of the loading-animation
// stroke + an additional dp to create some visual separation.
const float kInitialFaviconInsetDp = 1 + kNewLoadingAnimationStrokeWidthDp;
const float kInitialFaviconDiameterDp =
gfx::kFaviconSize - 2 * kInitialFaviconInsetDp;
// This a full outset circle of the favicon square. The animation ends with
// the entire favicon shown.
const float kFinalFaviconDiameterDp = sqrt(2) * gfx::kFaviconSize;
SkScalar diameter = kInitialFaviconDiameterDp;
if (favicon_fade_in_animation_.is_animating()) {
diameter += gfx::Tween::CalculateValue(
gfx::Tween::EASE_OUT,
favicon_fade_in_animation_.GetCurrentValue()) *
(kFinalFaviconDiameterDp - kInitialFaviconDiameterDp);
}
SkPath path;
gfx::PointF center = gfx::RectF(bounds).CenterPoint();
path.addCircle(center.x(), center.y(), diameter / 2);
canvas->ClipPath(path, true);
// This scales and offsets painting so that the drawn favicon is downscaled
// to fit in the cropping area.
const float offset =
(gfx::kFaviconSize -
std::min(diameter, SkFloatToScalar(gfx::kFaviconSize))) /
2;
const float scale = std::min(diameter, SkFloatToScalar(gfx::kFaviconSize)) /
gfx::kFaviconSize;
canvas->Translate(gfx::Vector2d(offset, offset));
canvas->Scale(scale, scale);
}
canvas->DrawImageInt(icon, 0, 0, bounds.width(), bounds.height(), bounds.x(),
bounds.y(), bounds.width(), bounds.height(),
use_scale_filter);
}
bool TabIcon::HasNonDefaultFavicon() const {
return !favicon_.isNull() && !favicon_.BackedBySameObjectAs(
favicon::GetDefaultFavicon().AsImageSkia());
}
void TabIcon::SetIcon(const GURL& url, const gfx::ImageSkia& icon) {
// Detect when updating to the same icon. This avoids re-theming and
// re-painting.
if (favicon_.BackedBySameObjectAs(icon))
return;
favicon_ = icon;
if (!HasNonDefaultFavicon() || ShouldThemifyFaviconForUrl(url)) {
themed_favicon_ = ThemeImage(icon);
} else {
themed_favicon_ = gfx::ImageSkia();
}
SchedulePaint();
}
void TabIcon::SetNetworkState(TabNetworkState network_state) {
const bool was_animated = NetworkStateIsAnimated(network_state_);
network_state_ = network_state;
const bool is_animated = NetworkStateIsAnimated(network_state_);
if (use_new_loading_animation_ && was_animated != is_animated) {
if (was_animated && HasNonDefaultFavicon()) {
favicon_fade_in_animation_.Start();
} else {
favicon_fade_in_animation_.Stop();
favicon_fade_in_animation_.SetCurrentValue(0.0);
}
}
}
void TabIcon::SetIsCrashed(bool is_crashed) {
if (is_crashed == is_crashed_)
return;
is_crashed_ = is_crashed;
if (!is_crashed_) {
// Transitioned from crashed to non-crashed.
if (crash_animation_)
crash_animation_->Stop();
should_display_crashed_favicon_ = false;
hiding_fraction_ = 0.0;
} else {
// Transitioned from non-crashed to crashed.
if (!has_tab_renderer_data_) {
// This is the initial SetData(), so show the crashed icon directly
// without animating.
should_display_crashed_favicon_ = true;
} else {
if (!crash_animation_)
crash_animation_ = std::make_unique<CrashAnimation>(this);
if (!crash_animation_->is_animating())
crash_animation_->Start();
}
}
SchedulePaint();
}
void TabIcon::RefreshLayer() {
// Since the loading animation can run for a long time, paint animation to a
// separate layer when possible to reduce repaint overhead.
bool should_paint_to_layer =
can_paint_to_layer_ &&
(ShowingLoadingAnimation() || favicon_fade_in_animation_.is_animating());
if (should_paint_to_layer == !!layer())
return;
// Change layer mode.
if (should_paint_to_layer) {
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
} else {
DestroyLayer();
}
}
gfx::ImageSkia TabIcon::ThemeImage(const gfx::ImageSkia& source) {
if (!GetThemeProvider()->HasCustomColor(
ThemeProperties::COLOR_TOOLBAR_BUTTON_ICON))
return source;
return gfx::ImageSkiaOperations::CreateColorMask(
source,
GetThemeProvider()->GetColor(ThemeProperties::COLOR_TOOLBAR_BUTTON_ICON));
}