#include "ash/wm/gestures/back_gesture/back_gesture_contextual_nudge.h"
#include "ash/controls/contextual_tooltip.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/wm/gestures/back_gesture/back_gesture_util.h"
#include "base/i18n/rtl.h"
#include "base/timer/timer.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/shadow_value.h"
#include "ui/gfx/skia_paint_util.h"
#include "ui/views/controls/label.h"
#include "ui/views/view.h"
namespace ash {
namespace {
constexpr int kBackgroundWidth = 320;
constexpr int kCircleRadius = 20;
constexpr int kCircleInsideScreenWidth = 12;
constexpr int kPaddingBetweenCircleAndLabel = 8;
constexpr int kLabelLineHeight = 18;
constexpr int kLabelCornerRadius = 16;
constexpr int kLabelTopBottomInset = 6;
constexpr base::TimeDelta kPauseBeforeShowAnimationDuration = base::Seconds(10);
constexpr base::TimeDelta kNudgeShowAnimationDuration = base::Milliseconds(600);
constexpr base::TimeDelta kNudgeHideAnimationDuration = base::Milliseconds(400);
constexpr base::TimeDelta kSuggestionDismissDuration = base::Milliseconds(100);
constexpr base::TimeDelta kSuggestionBounceAnimationDuration =
base::Milliseconds(600);
constexpr int kSuggestionAnimationRepeatTimes = 4;
std::unique_ptr<views::Widget> CreateWidget() {
auto widget = std::make_unique<views::Widget>();
views::Widget::InitParams params(
views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
params.z_order = ui::ZOrderLevel::kFloatingWindow;
params.accept_events = false;
params.activatable = views::Widget::InitParams::Activatable::kNo;
params.name = "BackGestureContextualNudge";
params.layer_type = ui::LAYER_NOT_DRAWN;
params.parent = Shell::GetPrimaryRootWindow()->GetChildById(
kShellWindowId_OverlayContainer);
widget->Init(std::move(params));
const gfx::Rect display_bounds =
display::Screen::Get()->GetPrimaryDisplay().bounds();
gfx::Rect widget_bounds;
if (base::i18n::IsRTL()) {
widget_bounds = gfx::Rect(display_bounds.right(), display_bounds.y(),
kBackgroundWidth, display_bounds.height());
} else {
widget_bounds =
gfx::Rect(display_bounds.x() - kBackgroundWidth, display_bounds.y(),
kBackgroundWidth, display_bounds.height());
}
widget->SetBounds(widget_bounds);
return widget;
}
}
class BackGestureContextualNudge::ContextualNudgeView
: public views::View,
public ui::ImplicitAnimationObserver {
METADATA_HEADER(ContextualNudgeView, views::View)
public:
explicit ContextualNudgeView(base::OnceCallback<void(bool)> callback)
: callback_(std::move(callback)) {
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
suggestion_view_ = AddChildView(std::make_unique<SuggestionView>(this));
show_timer_.Start(
FROM_HERE, kPauseBeforeShowAnimationDuration, this,
&ContextualNudgeView::ScheduleOffScreenToStartPositionAnimation);
}
ContextualNudgeView(const ContextualNudgeView&) = delete;
ContextualNudgeView& operator=(const ContextualNudgeView&) = delete;
~ContextualNudgeView() override { StopObservingImplicitAnimations(); }
void CancelAnimationOrFadeOutToHide() {
if (animation_stage_ == AnimationStage::kWaitingCancelled ||
animation_stage_ == AnimationStage::kFadingOut) {
return;
}
if (animation_stage_ == AnimationStage::kWaiting) {
animation_stage_ = AnimationStage::kWaitingCancelled;
DCHECK(show_timer_.IsRunning());
show_timer_.Stop();
std::move(callback_).Run(false);
} else if (animation_stage_ == AnimationStage::kSlidingIn ||
animation_stage_ == AnimationStage::kBouncing ||
animation_stage_ == AnimationStage::kSlidingOut) {
layer()->GetAnimator()->AbortAllAnimations();
suggestion_view_->layer()->GetAnimator()->AbortAllAnimations();
animation_stage_ = AnimationStage::kFadingOut;
suggestion_view_->FadeOutForDismiss();
}
}
void SetNudgeShownForTesting() { SetNudgeCountsAsShown(); }
bool count_as_shown() const { return count_as_shown_; }
private:
enum class AnimationStage {
kWaiting,
kSlidingIn,
kBouncing,
kSlidingOut,
kWaitingCancelled,
kFadingOut,
};
class SuggestionView : public views::View,
public ui::ImplicitAnimationObserver {
METADATA_HEADER(SuggestionView, views::View)
public:
explicit SuggestionView(ContextualNudgeView* nudge_view)
: nudge_view_(nudge_view) {
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
label_ = AddChildView(std::make_unique<views::Label>());
label_->SetBackgroundColor(SK_ColorTRANSPARENT);
label_->SetEnabledColor(cros_tokens::kTextColorPrimary);
label_->SetText(l10n_util::GetStringUTF16(
base::i18n::IsRTL() ? IDS_ASH_BACK_GESTURE_CONTEXTUAL_NUDGE_RTL
: IDS_ASH_BACK_GESTURE_CONTEXTUAL_NUDGE));
label_->SetLineHeight(kLabelLineHeight);
label_->SetFontList(
gfx::FontList().DeriveWithWeight(gfx::Font::Weight::MEDIUM));
}
SuggestionView(const SuggestionView&) = delete;
SuggestionView& operator=(const SuggestionView&) = delete;
~SuggestionView() override { StopObservingImplicitAnimations(); }
void ScheduleBounceAnimation() {
const bool is_rtl = base::i18n::IsRTL();
gfx::Transform transform;
const int x_offset = kCircleRadius - kCircleInsideScreenWidth;
transform.Translate(is_rtl ? x_offset : -x_offset, 0);
ui::ScopedLayerAnimationSettings animation(layer()->GetAnimator());
animation.AddObserver(this);
animation.SetTransitionDuration(kSuggestionBounceAnimationDuration);
animation.SetTweenType(gfx::Tween::EASE_IN_OUT_2);
animation.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
layer()->SetTransform(transform);
transform.Translate(is_rtl ? -x_offset : x_offset, 0);
animation.SetTransitionDuration(kSuggestionBounceAnimationDuration);
animation.SetTweenType(gfx::Tween::EASE_IN_OUT_2);
animation.SetPreemptionStrategy(ui::LayerAnimator::ENQUEUE_NEW_ANIMATION);
layer()->SetTransform(transform);
}
void FadeOutForDismiss() {
ui::ScopedLayerAnimationSettings animation(layer()->GetAnimator());
animation.SetTransitionDuration(kSuggestionDismissDuration);
animation.SetTweenType(gfx::Tween::LINEAR);
animation.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
animation.AddObserver(nudge_view_);
layer()->SetOpacity(0.f);
}
private:
void Layout(PassKey) override {
const gfx::Rect bounds = GetLocalBounds();
gfx::Rect label_rect(bounds);
label_rect.ClampToCenteredSize(
label_->GetPreferredSize(views::SizeBounds(label_->width(), {})));
label_rect.set_x(bounds.x() + 2 * kCircleRadius +
kPaddingBetweenCircleAndLabel + kLabelCornerRadius);
label_->SetBoundsRect(label_rect);
}
void OnPaint(gfx::Canvas* canvas) override {
const auto* color_provider = GetColorProvider();
cc::PaintFlags circle_flags;
circle_flags.setAntiAlias(true);
circle_flags.setStyle(cc::PaintFlags::kFill_Style);
circle_flags.setColor(
color_provider->GetColor(kColorAshShieldAndBaseOpaque));
gfx::PointF center_point;
if (base::i18n::IsRTL()) {
const gfx::Point right_center = GetLocalBounds().right_center();
center_point =
gfx::PointF(right_center.x() - kCircleRadius, right_center.y());
} else {
const gfx::Point left_center = GetLocalBounds().left_center();
center_point =
gfx::PointF(left_center.x() + kCircleRadius, left_center.y());
}
canvas->DrawCircle(center_point, kCircleRadius, circle_flags);
DrawCircleHighlightBorder(this, canvas, center_point, kCircleRadius);
cc::PaintFlags round_rect_flags;
round_rect_flags.setStyle(cc::PaintFlags::kFill_Style);
round_rect_flags.setAntiAlias(true);
round_rect_flags.setColor(
color_provider->GetColor(kColorAshShieldAndBaseOpaque));
gfx::Rect label_bounds(label_->GetMirroredBounds());
label_bounds.Inset(
gfx::Insets::VH(-kLabelTopBottomInset, -kLabelCornerRadius));
canvas->DrawRoundRect(label_bounds, kLabelCornerRadius, round_rect_flags);
DrawRoundRectHighlightBorder(this, canvas, label_bounds,
kLabelCornerRadius);
}
void OnImplicitAnimationsCompleted() override {
if (WasAnimationAbortedForProperty(ui::LayerAnimationElement::TRANSFORM))
return;
if (current_animation_times_ < (kSuggestionAnimationRepeatTimes - 1)) {
current_animation_times_++;
ScheduleBounceAnimation();
} else {
nudge_view_->ScheduleStartPositionToOffScreenAnimation();
}
}
raw_ptr<views::Label> label_ = nullptr;
int current_animation_times_ = 0;
raw_ptr<ContextualNudgeView> nudge_view_ = nullptr;
};
void ScheduleOffScreenToStartPositionAnimation() {
animation_stage_ = AnimationStage::kSlidingIn;
gfx::Transform transform;
transform.Translate(base::i18n::IsRTL() ? -kBackgroundWidth + kCircleRadius
: kBackgroundWidth - kCircleRadius,
0);
ui::ScopedLayerAnimationSettings animation(layer()->GetAnimator());
animation.AddObserver(this);
animation.SetTransitionDuration(kNudgeShowAnimationDuration);
animation.SetTweenType(gfx::Tween::LINEAR_OUT_SLOW_IN);
layer()->SetTransform(transform);
}
void ScheduleStartPositionToOffScreenAnimation() {
animation_stage_ = AnimationStage::kSlidingOut;
gfx::Transform transform;
transform.Translate(base::i18n::IsRTL() ? kBackgroundWidth - kCircleRadius
: -kBackgroundWidth + kCircleRadius,
0);
ui::ScopedLayerAnimationSettings animation(layer()->GetAnimator());
animation.SetTransitionDuration(kNudgeHideAnimationDuration);
animation.SetTweenType(gfx::Tween::EASE_OUT_2);
animation.AddObserver(this);
layer()->SetTransform(transform);
}
void SetNudgeCountsAsShown() {
count_as_shown_ = true;
contextual_tooltip::HandleNudgeShown(
Shell::Get()->session_controller()->GetActivePrefService(),
contextual_tooltip::TooltipType::kBackGesture);
}
void Layout(PassKey) override {
suggestion_view_->SetBoundsRect(GetLocalBounds());
}
void OnImplicitAnimationsCompleted() override {
if (animation_stage_ == AnimationStage::kFadingOut ||
(animation_stage_ == AnimationStage::kSlidingOut &&
!WasAnimationAbortedForProperty(
ui::LayerAnimationElement::TRANSFORM))) {
std::move(callback_).Run(animation_stage_ ==
AnimationStage::kSlidingOut);
return;
}
if (animation_stage_ == AnimationStage::kSlidingIn &&
!WasAnimationAbortedForProperty(ui::LayerAnimationElement::TRANSFORM)) {
SetNudgeCountsAsShown();
animation_stage_ = AnimationStage::kBouncing;
suggestion_view_->ScheduleBounceAnimation();
}
}
raw_ptr<SuggestionView> suggestion_view_ = nullptr;
base::OneShotTimer show_timer_;
AnimationStage animation_stage_ = AnimationStage::kWaiting;
bool count_as_shown_ = false;
base::OnceCallback<void(bool)> callback_;
};
BEGIN_METADATA(BackGestureContextualNudge, ContextualNudgeView)
END_METADATA
BEGIN_METADATA(BackGestureContextualNudge::ContextualNudgeView, SuggestionView)
END_METADATA
BackGestureContextualNudge::BackGestureContextualNudge(
base::OnceCallback<void(bool)> callback) {
widget_ = CreateWidget();
nudge_view_ = widget_->SetContentsView(
std::make_unique<ContextualNudgeView>(std::move(callback)));
widget_->Show();
}
BackGestureContextualNudge::~BackGestureContextualNudge() = default;
void BackGestureContextualNudge::CancelAnimationOrFadeOutToHide() {
nudge_view_->CancelAnimationOrFadeOutToHide();
}
bool BackGestureContextualNudge::ShouldNudgeCountAsShown() const {
return nudge_view_->count_as_shown();
}
void BackGestureContextualNudge::SetNudgeShownForTesting() {
nudge_view_->SetNudgeShownForTesting();
}
}