blob: 35f6b1f5ff8fd10c8ca840419181ab50c3dccb4d [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/glic/browser_ui/glic_animated_effect_view.h"
#include "base/debug/crash_logging.h"
#include "base/debug/dump_without_crashing.h"
#include "chrome/browser/actor/ui/actor_border_view_controller.h"
#include "chrome/browser/glic/host/context/glic_tab_data.h"
#include "chrome/browser/glic/public/context/glic_sharing_manager.h"
#include "chrome/browser/glic/public/glic_keyed_service_factory.h"
#include "chrome/browser/glic/resources/grit/glic_browser_resources.h"
#include "chrome/browser/glic/widget/glic_window_controller.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/themes/theme_service_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/contents_web_view.h"
#include "chrome/browser/ui/views/frame/multi_contents_view.h"
#include "chrome/common/chrome_features.h"
#include "content/public/browser/context_factory.h"
#include "content/public/browser/gpu_data_manager.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/color_parser.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/compositor.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/canvas.h"
#include "ui/native_theme/native_theme.h"
#include "ui/views/view_class_properties.h"
namespace glic {
namespace {
// The amount of time for the opacity to go from 0 to 1.
constexpr static base::TimeDelta kOpacityRampUpDuration =
base::Milliseconds(500);
// The amount of time for the opacity to go from 1 to 0 in a fast ramp up.
constexpr static base::TimeDelta kFastOpacityRampUpDuration =
base::Milliseconds(200);
// The amount of time for the opacity to go from 1 to 0.
constexpr static base::TimeDelta kOpacityRampDownDuration =
base::Milliseconds(200);
// Time since creation will roll over after this time to prevent growing
// indefinitely.
constexpr static base::TimeDelta kMaxTime = base::Hours(1);
int64_t TimeTicksToMicroseconds(base::TimeTicks tick) {
return (tick - base::TimeTicks()).InMicroseconds();
}
std::vector<SkColor> GetParameterizedColors() {
std::vector<SkColor> colors;
if (base::FeatureList::IsEnabled(features::kGlicParameterizedShader)) {
std::vector<std::string> unparsed_colors =
base::SplitString(::features::kGlicParameterizedShaderColors.Get(), "#",
base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
for (const auto& unparsed : unparsed_colors) {
SkColor result;
if (!content::ParseHexColorString("#" + unparsed, &result)) {
return std::vector<SkColor>();
}
colors.push_back(result);
}
}
return colors;
}
std::vector<float> GetParameterizedFloats() {
std::vector<float> floats;
if (base::FeatureList::IsEnabled(features::kGlicParameterizedShader)) {
std::vector<std::string> unparsed_floats =
base::SplitString(::features::kGlicParameterizedShaderFloats.Get(), "#",
base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
for (const auto& unparsed : unparsed_floats) {
double result;
if (!base::StringToDouble(unparsed, &result)) {
return std::vector<float>();
}
floats.push_back(static_cast<float>(result));
}
}
return floats;
}
} // namespace
GlicAnimatedEffectView::GlicAnimatedEffectView(Browser* browser,
std::unique_ptr<Tester> tester)
: browser_(browser),
creation_time_(base::TimeTicks::Now()),
tester_(std::move(tester)),
colors_(GetParameterizedColors()),
floats_(GetParameterizedFloats()),
theme_service_(
ThemeServiceFactory::GetForProfile(browser->GetProfile())) {
auto* gpu_data_manager = content::GpuDataManager::GetInstance();
has_hardware_acceleration_ =
gpu_data_manager->IsGpuRasterizationForUIEnabled();
// Upon GPU crashing, the hardware acceleration status might change. This
// will observe GPU changes to keep hardware acceleration status updated.
gpu_data_manager_observer_.Observe(gpu_data_manager);
UpdateShader();
CHECK(!shader_.empty()) << "Shader not initialized.";
}
GlicAnimatedEffectView::~GlicAnimatedEffectView() = default;
void GlicAnimatedEffectView::OnPaint(gfx::Canvas* canvas) {
if (!compositor_) {
return;
}
std::vector<cc::PaintShader::FloatUniform> float_uniforms;
std::vector<cc::PaintShader::Float2Uniform> float2_uniforms;
std::vector<cc::PaintShader::Float4Uniform> float4_uniforms;
std::vector<cc::PaintShader::IntUniform> int_uniforms;
PopulateShaderUniforms(float_uniforms, float2_uniforms, float4_uniforms,
int_uniforms);
if (base::FeatureList::IsEnabled(features::kGlicParameterizedShader)) {
for (int i = 0; i < static_cast<int>(colors_.size()); ++i) {
float4_uniforms.push_back(
{.name = SkString(absl::StrFormat("u_color%d", i + 1)),
.value =
SkV4{static_cast<float>(SkColorGetR(colors_[i]) / 255.0),
static_cast<float>(SkColorGetG(colors_[i]) / 255.0),
static_cast<float>(SkColorGetB(colors_[i]) / 255.0), 1.f}});
}
for (int i = 0; i < static_cast<int>(floats_.size()); ++i) {
float_uniforms.push_back(
{.name = SkString(absl::StrFormat("u_float%d", i + 1)),
.value = floats_[i]});
}
}
views::View::OnPaint(canvas);
cc::PaintFlags flags;
auto shader = cc::PaintShader::MakeSkSLCommand(
shader_, std::move(float_uniforms), std::move(float2_uniforms),
std::move(float4_uniforms), std::move(int_uniforms),
cached_paint_shader_);
flags.setShader(shader);
if (base::FeatureList::IsEnabled(features::kGlicUseShaderCache)) {
cached_paint_shader_ = shader;
}
DrawEffect(canvas, flags);
}
void GlicAnimatedEffectView::OnAnimationStep(base::TimeTicks timestamp) {
if (tester_) [[unlikely]] {
timestamp = tester_->GetTestTimestamp();
}
last_animation_step_time_ = timestamp;
if (first_frame_time_.is_null()) {
first_frame_time_ = timestamp;
}
if (first_cycle_frame_.is_null()) {
first_cycle_frame_ = timestamp;
// The time gaps when the effect is in steady state cause discontinuous
// effect states when switching tabs. By keeping track of the total
// steady time, we can have a continuous effect time. Each steady time
// interval is added to the total at the very beginning of an upcoming
// animation.
// Note: the opacity ramp up / down is not part of the shader animation.
if (!last_cycle_frame_.is_null()) {
total_steady_time_ += timestamp - last_cycle_frame_;
last_cycle_frame_ = base::TimeTicks{};
}
}
if (record_first_ramp_down_frame_) {
record_first_ramp_down_frame_ = false;
first_ramp_down_frame_ = timestamp;
}
base::TimeDelta opacity_since_first_frame = timestamp - first_frame_time_;
opacity_ = GetOpacity(timestamp);
progress_ = GetEffectProgress(timestamp);
// TODO(liuwilliam): Ideally this should be done in paint-related methods.
// Consider moving it to LayerDelegate::OnPaintLayer().
CHECK(layer());
layer()->SetOpacity(opacity_);
// Don't animate if the animations have exhausted and we haven't started
// ramping down. We shouldn't be an observer for more than 60 seconds
// (CompositorAnimationObserver::NotifyFailure()).
bool opacity_ramp_up_done =
opacity_ == 1.f && !opacity_since_first_frame.is_zero();
bool show_steady_state = IsCycleDone(timestamp) && opacity_ramp_up_done &&
first_ramp_down_frame_.is_null();
if (show_steady_state) {
// If skipping the animation the class does not need to be an animation
// observer.
compositor_->RemoveAnimationObserver(this);
if (last_cycle_frame_.is_null()) {
last_cycle_frame_ = timestamp;
}
return;
}
bool opacity_ramp_down_done =
opacity_ == 0.f && !first_ramp_down_frame_.is_null();
if (opacity_ramp_down_done) {
StopShowing();
return;
}
SchedulePaint();
}
void GlicAnimatedEffectView::OnCompositingShuttingDown(
ui::Compositor* compositor) {
StopShowing();
}
void GlicAnimatedEffectView::OnGpuInfoUpdate() {
auto* gpu_data_manager = content::GpuDataManager::GetInstance();
bool has_hardware_acceleration =
gpu_data_manager->IsGpuRasterizationForUIEnabled();
if (has_hardware_acceleration_ != has_hardware_acceleration) {
has_hardware_acceleration_ = has_hardware_acceleration;
UpdateShader();
if (IsShowing()) {
SchedulePaint();
}
}
}
bool GlicAnimatedEffectView::IsShowing() const {
// `compositor_` is set when the effect starts to show and is unset when the
// effect stops showing.
return !!compositor_;
}
float GlicAnimatedEffectView::GetEffectTimeForTesting() const {
return GetEffectTime();
}
void GlicAnimatedEffectView::Show() {
if (compositor_) {
// The user can click on the glic icon after the window is shown. The
// animation is already playing at that time.
return;
}
if (!parent()) {
base::debug::DumpWithoutCrashing();
return;
}
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
layer()->SetRoundedCornerRadius(corner_radius_);
layer()->SetIsFastRoundedCorner(true);
SetVisible(true);
skip_animation_cycle_ =
gfx::Animation::PrefersReducedMotion() || ForceSimplifiedShader();
ui::Compositor* compositor = layer()->GetCompositor();
if (!compositor) {
base::debug::DumpWithoutCrashing();
return;
}
compositor_ = compositor;
compositor_animation_observation_.Observe(compositor_.get());
compositor_observation_.Observe(compositor_.get());
if (tester_) [[unlikely]] {
tester_->AnimationStarted();
}
}
void GlicAnimatedEffectView::StopShowing() {
if (!compositor_) {
return;
}
compositor_observation_.Reset();
compositor_animation_observation_.Reset();
compositor_ = nullptr;
first_frame_time_ = base::TimeTicks{};
first_cycle_frame_ = base::TimeTicks{};
last_cycle_frame_ = base::TimeTicks{};
first_ramp_down_frame_ = base::TimeTicks{};
record_first_ramp_down_frame_ = false;
total_steady_time_ = base::Milliseconds(0);
opacity_ = 0.f;
emphasis_ = 0.f;
// `DestroyLayer()` schedules another paint to repaint the affected area by
// the destroyed layer.
DestroyLayer();
SetVisible(false);
}
void GlicAnimatedEffectView::ResetAnimationCycle() {
// TOOD(crbug.com/398319435): Remove once we know why this is called before
// `Show()`.
if (!compositor_) {
SCOPED_CRASH_KEY_NUMBER("crbug-398319435", "opacity", opacity_);
SCOPED_CRASH_KEY_NUMBER("crbug-398319435", "emphasis", emphasis_);
SCOPED_CRASH_KEY_NUMBER("crbug-398319435", "creation",
TimeTicksToMicroseconds(creation_time_));
SCOPED_CRASH_KEY_NUMBER("crbug-398319435", "first_frame",
TimeTicksToMicroseconds(first_frame_time_));
SCOPED_CRASH_KEY_NUMBER("crbug-398319435", "first_emphasis",
TimeTicksToMicroseconds(first_cycle_frame_));
SCOPED_CRASH_KEY_NUMBER("crbug-398319435", "last_step",
TimeTicksToMicroseconds(last_animation_step_time_));
SCOPED_CRASH_KEY_NUMBER("crbug-398319435", "first_rampdown",
TimeTicksToMicroseconds(first_ramp_down_frame_));
base::debug::DumpWithoutCrashing();
// Gracefully handling the crash case in crbug.com/398319435 by
// closing(minimizing) the glic window.
// TODO(crbug.com/413442838): Add tests to reproduce the dump without crash
// and validate the solution.
GetGlicService()->window_controller().Close();
return;
}
CHECK(compositor_->HasObserver(this));
if (!compositor_->HasAnimationObserver(this)) {
compositor_->AddAnimationObserver(this);
}
first_cycle_frame_ = base::TimeTicks{};
SchedulePaint();
if (tester_) [[unlikely]] {
tester_->AnimationReset();
}
}
float GlicAnimatedEffectView::GetOpacity(base::TimeTicks timestamp) {
auto ramp_up_duration = skip_animation_cycle_ ? kFastOpacityRampUpDuration
: kOpacityRampUpDuration;
if (!first_ramp_down_frame_.is_null()) {
// The ramp up opacity could be any value between 0-1 during the ramp up
// time. Thus, the ramping down opacity must be deducted from the value of
// ramp up opacity at the time of `first_ramp_down_frame_`.
base::TimeDelta delta = first_ramp_down_frame_ - first_frame_time_;
float ramp_up_opacity =
std::clamp(static_cast<float>(delta.InMillisecondsF() /
ramp_up_duration.InMillisecondsF()),
0.0f, 1.0f);
base::TimeDelta time_since_first_ramp_down_frame =
timestamp - first_ramp_down_frame_;
float ramp_down_opacity =
static_cast<float>(time_since_first_ramp_down_frame.InMillisecondsF() /
kOpacityRampDownDuration.InMillisecondsF());
ramp_down_opacity_ =
std::clamp(ramp_up_opacity - ramp_down_opacity, 0.0f, 1.0f);
return ramp_down_opacity_;
} else {
base::TimeDelta time_since_first_frame = timestamp - first_frame_time_;
return std::clamp(
static_cast<float>(ramp_down_opacity_ +
(time_since_first_frame.InMillisecondsF() /
ramp_up_duration.InMillisecondsF())),
0.0f, 1.0f);
}
}
void GlicAnimatedEffectView::StartRampingDown() {
CHECK(compositor_);
// From now on the opacity will be decreased until it reaches 0.
record_first_ramp_down_frame_ = true;
if (!compositor_->HasAnimationObserver(this)) {
compositor_->AddAnimationObserver(this);
}
if (tester_) [[unlikely]] {
tester_->RampDownStarted();
}
}
float GlicAnimatedEffectView::GetEffectTime() const {
if (last_animation_step_time_.is_null()) {
return 0;
}
// Returns a constant duration so the effect states don't jump around when
// switching tabs.
if (skip_animation_cycle_) {
auto time_since_creation =
(first_frame_time_ - GetCreationTime()) % kMaxTime;
return time_since_creation.InSecondsF();
}
auto time_since_creation =
((last_animation_step_time_ - GetCreationTime()) - total_steady_time_) %
kMaxTime;
return time_since_creation.InSecondsF();
}
float GlicAnimatedEffectView::GetEffectProgress(
base::TimeTicks timestamp) const {
if (skip_animation_cycle_) {
return 0.0;
}
base::TimeDelta time_since_first_frame = timestamp - first_cycle_frame_;
return std::clamp(
static_cast<float>(time_since_first_frame.InMillisecondsF() /
GetTotalDuration().InMillisecondsF()),
0.0f, 1.0f);
}
base::TimeTicks GlicAnimatedEffectView::GetCreationTime() const {
if (tester_ && !tester_->GetTestCreationTime().is_null()) [[unlikely]] {
return tester_->GetTestCreationTime();
}
return creation_time_;
}
bool GlicAnimatedEffectView::ForceSimplifiedShader() const {
return base::FeatureList::IsEnabled(features::kGlicForceSimplifiedBorder) ||
!has_hardware_acceleration_;
}
GlicKeyedService* GlicAnimatedEffectView::GetGlicService() const {
auto* service =
GlicKeyedServiceFactory::GetGlicKeyedService(browser_->GetProfile());
CHECK(service);
return service;
}
void GlicAnimatedEffectView::UpdateShader() {
if (base::FeatureList::IsEnabled(features::kGlicParameterizedShader) &&
!colors_.empty() && !floats_.empty()) {
shader_ =
ForceSimplifiedShader()
? ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
IDR_GLIC_SIMPLIFIED_PARAMETERIZED_BORDER_SHADER)
: ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
IDR_GLIC_PARAMETERIZED_BORDER_SHADER);
} else {
shader_ =
ForceSimplifiedShader()
? ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
IDR_GLIC_SIMPLIFIED_BORDER_SHADER)
: ui::ResourceBundle::GetSharedInstance().LoadDataResourceString(
IDR_GLIC_BORDER_SHADER);
}
}
BEGIN_METADATA(GlicAnimatedEffectView)
END_METADATA
} // namespace glic