blob: 5dea01f6a0b45c6719d804ec0a58e8ee6b669f62 [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 "cc/paint/paint_flags.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/layout_constants.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/canvas.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/native_theme/native_theme.h"
#include "ui/resources/grit/ui_resources.h"
#include "url/gurl.h"
namespace {
constexpr int kAttentionIndicatorRadius = 3;
// 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;
}
} // 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() {
set_can_process_events_within_subtree(false);
// The minimum size to avoid clipping the attention indicator.
SetPreferredSize(gfx::Size(gfx::kFaviconSize + kAttentionIndicatorRadius,
gfx::kFaviconSize + kAttentionIndicatorRadius));
}
TabIcon::~TabIcon() = default;
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;
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
if (icon.BackedBySameObjectAs(*rb.GetImageSkiaNamed(IDR_DEFAULT_FAVICON)) ||
ShouldThemifyFaviconForUrl(url)) {
themed_favicon_ = ThemeImage(icon);
} else {
themed_favicon_ = gfx::ImageSkia();
}
SchedulePaint();
}
void TabIcon::SetNetworkState(TabNetworkState network_state,
bool inhibit_loading_animation) {
if (network_state_ == network_state &&
inhibit_loading_animation_ == inhibit_loading_animation)
return;
bool old_showing_load = ShowingLoadingAnimation();
network_state_ = network_state;
inhibit_loading_animation_ = inhibit_loading_animation;
bool new_showing_load = ShowingLoadingAnimation();
RefreshLayer();
if (old_showing_load && !new_showing_load) {
// Loading animation transitioning from on to off.
waiting_start_time_ = base::TimeTicks();
loading_start_time_ = base::TimeTicks();
waiting_state_ = gfx::ThrobberWaitingState();
SchedulePaint();
} else if (!old_showing_load && new_showing_load) {
// Loading animation transitioning from off to on. The animation painting
// function will lazily initialize the data.
SchedulePaint();
}
}
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 (!crash_animation_)
crash_animation_ = std::make_unique<CrashAnimation>(this);
if (!crash_animation_->is_animating())
crash_animation_->Start();
}
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 network_state_ != TabNetworkState::kNone &&
network_state_ != TabNetworkState::kError;
}
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() {
if (ShowingLoadingAnimation())
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(0, gfx::kFaviconSize),
static_cast<int>(contents_bounds.height() * hiding_fraction_),
std::min(gfx::kFaviconSize, contents_bounds.width()),
std::min(gfx::kFaviconSize, contents_bounds.height()));
// Loading animation.
if (ShowingLoadingAnimation()) {
PaintLoadingAnimation(canvas, icon_bounds);
return;
}
// Figure out which icon to paint.
gfx::ImageSkia* icon_to_paint = nullptr;
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));
}
icon_to_paint = &crashed_icon_;
} else {
if (themed_favicon_.isNull())
icon_to_paint = &favicon_;
else
icon_to_paint = &themed_favicon_;
}
if (attention_types_ != 0 && !should_display_crashed_favicon_) {
PaintAttentionIndicatorAndIcon(canvas, *icon_to_paint, icon_bounds);
} else if (!icon_to_paint->isNull()) {
canvas->DrawImageInt(*icon_to_paint, 0, 0, icon_bounds.width(),
icon_bounds.height(), icon_bounds.x(), icon_bounds.y(),
icon_bounds.width(), icon_bounds.height(), false);
}
}
void TabIcon::OnThemeChanged() {
crashed_icon_ = gfx::ImageSkia(); // Force recomputation if crashed.
if (!themed_favicon_.isNull())
themed_favicon_ = ThemeImage(favicon_);
}
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,
const gfx::Rect& bounds) {
const ui::ThemeProvider* tp = GetThemeProvider();
if (network_state_ == TabNetworkState::kWaiting) {
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_);
}
}
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();
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) {
return gfx::ImageSkiaOperations::CreateHSLShiftedImage(
source, GetThemeProvider()->GetTint(ThemeProperties::TINT_BUTTONS));
}