chromium/ash/shelf/home_to_overview_nudge_controller.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/shelf/home_to_overview_nudge_controller.h"

#include "ash/controls/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/shelf/hotseat_widget.h"
#include "ash/shelf/scrollable_shelf_view.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_provider.h"
#include "ash/wm/mru_window_tracker.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/geometry/transform.h"

namespace ash {

namespace {

// The amount of time after home shelf is shown before showing the nudge.
constexpr base::TimeDelta kShowDelay = base::Seconds(2);

// The duration of nudge opacity animations.
constexpr base::TimeDelta kNudgeFadeDuration = base::Milliseconds(300);

// The duration of the nudge opacity and transform animations when the nudge
// gets hidden on user tap.
constexpr base::TimeDelta kNudgeHideOnTapDuration = base::Milliseconds(150);

// The duration of a single component of the nudge position animation - the
// nudge is transformed vertically up and down for a preset number of
// iterations.
constexpr base::TimeDelta kNudgeTransformComponentDuration =
    base::Milliseconds(600);

// The baseline vertical offset from default kShown state bounds added to
// hotseat position when the nudge is shown - this is the offset that the
// hotseat will have once show throb animation completes.
constexpr int kHotseatBaselineNudgeOffset = -22;

// The number of times the nudge should be moved up and down when the nudge is
// shown.
constexpr int kNudgeShowThrobIterations = 5;

// The vertical max vertical ofsset from the baseline position during nudge show
// animation.
constexpr int kNudgeShowThrobAmplitude = 6;

// The vertical distance between the nudge widget and the hotseat.
constexpr int kNudgeMargins = 4;

gfx::Tween::Type GetHideTransformTween(
    HomeToOverviewNudgeController::HideTransition transition) {
  switch (transition) {
    case HomeToOverviewNudgeController::HideTransition::kShelfStateChange:
    case HomeToOverviewNudgeController::HideTransition::kNudgeTimeout:
      return gfx::Tween::EASE_OUT_2;
    case HomeToOverviewNudgeController::HideTransition::kUserTap:
      return gfx::Tween::FAST_OUT_LINEAR_IN;
  }
}

base::TimeDelta GetHideTransformDuration(
    HomeToOverviewNudgeController::HideTransition transition) {
  switch (transition) {
    case HomeToOverviewNudgeController::HideTransition::kShelfStateChange:
    case HomeToOverviewNudgeController::HideTransition::kNudgeTimeout:
      return kNudgeTransformComponentDuration;
    case HomeToOverviewNudgeController::HideTransition::kUserTap:
      return kNudgeHideOnTapDuration;
  }
}

base::TimeDelta GetHideFadeDuration(
    HomeToOverviewNudgeController::HideTransition transition) {
  switch (transition) {
    case HomeToOverviewNudgeController::HideTransition::kShelfStateChange:
      return base::TimeDelta();
    case HomeToOverviewNudgeController::HideTransition::kUserTap:
      return kNudgeHideOnTapDuration;
    case HomeToOverviewNudgeController::HideTransition::kNudgeTimeout:
      return kNudgeFadeDuration;
  }
}

class ObserverToCloseWidget : public ui::ImplicitAnimationObserver {
 public:
  explicit ObserverToCloseWidget(views::Widget* widget) : widget_(widget) {}

  ObserverToCloseWidget(const ObserverToCloseWidget& other) = delete;
  ObserverToCloseWidget& operator=(const ObserverToCloseWidget& other) = delete;

  ~ObserverToCloseWidget() override { StopObservingImplicitAnimations(); }

  // ui::ImplicitAnimationObserver:
  void OnImplicitAnimationsCompleted() override {
    widget_->Close();
    delete this;
  }

 private:
  const raw_ptr<views::Widget> widget_;
};

}  // namespace

HomeToOverviewNudgeController::HomeToOverviewNudgeController(
    HotseatWidget* hotseat_widget)
    : hotseat_widget_(hotseat_widget) {}

HomeToOverviewNudgeController::~HomeToOverviewNudgeController() = default;

void HomeToOverviewNudgeController::SetNudgeAllowedForCurrentShelf(
    bool allowed) {
  if (nudge_allowed_for_shelf_state_ == allowed)
    return;
  nudge_allowed_for_shelf_state_ = allowed;

  if (!nudge_allowed_for_shelf_state_) {
    nudge_show_timer_.Stop();
    nudge_hide_timer_.Stop();
    HideNudge(HideTransition::kShelfStateChange);
    return;
  }

  // Make sure that the overview, if opened, would show at least two app
  // windows.
  MruWindowTracker::WindowList windows =
      Shell::Get()->mru_window_tracker()->BuildMruWindowList(kActiveDesk);
  if (windows.size() < 2)
    return;

  DCHECK(!nudge_);

  PrefService* pref_service =
      Shell::Get()->session_controller()->GetLastActiveUserPrefService();
  if (!contextual_tooltip::ShouldShowNudge(
          pref_service, contextual_tooltip::TooltipType::kHomeToOverview,
          nullptr)) {
    return;
  }

  nudge_hide_timer_.Stop();
  nudge_show_timer_.Start(
      FROM_HERE, kShowDelay,
      base::BindOnce(&HomeToOverviewNudgeController::ShowNudge,
                     base::Unretained(this)));
}

void HomeToOverviewNudgeController::OnWidgetDestroying(views::Widget* widget) {
  nudge_ = nullptr;
  widget_observations_.RemoveAllObservations();
}

void HomeToOverviewNudgeController::OnWidgetBoundsChanged(
    views::Widget* widget,
    const gfx::Rect& new_bounds) {
  if (widget == hotseat_widget_)
    UpdateNudgeAnchorBounds();
}

bool HomeToOverviewNudgeController::HasShowTimerForTesting() const {
  return nudge_show_timer_.IsRunning();
}

void HomeToOverviewNudgeController::FireShowTimerForTesting() {
  nudge_show_timer_.FireNow();
}

bool HomeToOverviewNudgeController::HasHideTimerForTesting() const {
  return nudge_hide_timer_.IsRunning();
}

void HomeToOverviewNudgeController::FireHideTimerForTesting() {
  nudge_hide_timer_.FireNow();
}

void HomeToOverviewNudgeController::ShowNudge() {
  DCHECK(!nudge_);

  // The nudge is effectively anchored below the hotseat widget, but the nudge
  // center is not generally aligned with the hotseat widget center. The nudge
  // should be horizontally centered in the screen, which might not be the
  // case for the hotseat widget bounds on home screen.
  // To work around this, HomeToOverviewNudgeController will update the anchor
  // bounds directly - see UpdateNudgeAnchorBounds().
  nudge_ = new ContextualNudge(
      nullptr, hotseat_widget_->GetNativeWindow()->parent(),
      ContextualNudge::Position::kBottom, gfx::Insets(kNudgeMargins),
      l10n_util::GetStringUTF16(IDS_ASH_HOME_TO_OVERVIEW_CONTEXTUAL_NUDGE),
      base::BindRepeating(&HomeToOverviewNudgeController::HandleNudgeTap,
                          weak_factory_.GetWeakPtr()));

  UpdateNudgeAnchorBounds();

  widget_observations_.AddObservation(nudge_->GetWidget());
  widget_observations_.AddObservation(hotseat_widget_.get());

  nudge_->GetWidget()->Show();
  nudge_->GetWidget()->GetLayer()->SetTransform(gfx::Transform());
  nudge_->label()->layer()->SetOpacity(0.0f);

  hotseat_widget_->GetLayerForNudgeAnimation()->SetTransform(gfx::Transform());

  base::TimeDelta total_animation_duration;

  // Initial animation - nudge slides in form the bottom, and hotseat moves up.
  auto animate_initial_transform = [](ui::Layer* layer) -> base::TimeDelta {
    ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
    settings.SetTweenType(gfx::Tween::LINEAR_OUT_SLOW_IN);
    settings.SetTransitionDuration(kNudgeTransformComponentDuration);
    settings.SetPreemptionStrategy(
        ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);

    gfx::Transform transform;
    transform.Translate(0, kHotseatBaselineNudgeOffset);
    layer->SetTransform(transform);

    return layer->GetAnimator()->GetTransitionDuration();
  };

  total_animation_duration +=
      animate_initial_transform(hotseat_widget_->GetLayerForNudgeAnimation());
  animate_initial_transform(nudge_->GetWidget()->GetLayer());

  // Additionally the nudge label should fade in.
  {
    ui::ScopedLayerAnimationSettings settings(
        nudge_->label()->layer()->GetAnimator());
    settings.SetTweenType(gfx::Tween::LINEAR);
    settings.SetTransitionDuration(kNudgeFadeDuration);
    settings.SetPreemptionStrategy(
        ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);

    nudge_->label()->layer()->SetOpacity(1.0f);
  }

  auto enqueue_loop_transform = [](ui::Layer* layer,
                                   bool up) -> base::TimeDelta {
    ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
    settings.SetTweenType(gfx::Tween::EASE_IN_OUT_2);
    settings.SetTransitionDuration(kNudgeTransformComponentDuration);
    // Use enqueue preemption strategy, as the animation is expected to run
    // after other previously scheduled animations.
    settings.SetPreemptionStrategy(ui::LayerAnimator::ENQUEUE_NEW_ANIMATION);

    gfx::Transform transform;
    transform.Translate(0, kHotseatBaselineNudgeOffset +
                               (up ? 0 : 1) * kNudgeShowThrobAmplitude);
    layer->SetTransform(transform);

    return layer->GetAnimator()->GetTransitionDuration();
  };

  // Enqueue series of animated up-down transforms on the nudge and the hotseat.
  // The final position should match the position after the initial animated
  // transform.
  for (int i = 0; i < kNudgeShowThrobIterations; ++i) {
    total_animation_duration += enqueue_loop_transform(
        hotseat_widget_->GetLayerForNudgeAnimation(), false /*up*/);
    enqueue_loop_transform(nudge_->GetWidget()->GetLayer(), false /*up*/);

    total_animation_duration += enqueue_loop_transform(
        hotseat_widget_->GetLayerForNudgeAnimation(), true /*up*/);
    enqueue_loop_transform(nudge_->GetWidget()->GetLayer(), true /*up*/);
  }

  PrefService* pref_service =
      Shell::Get()->session_controller()->GetLastActiveUserPrefService();
  base::TimeDelta nudge_duration = contextual_tooltip::GetNudgeTimeout(
      pref_service, contextual_tooltip::TooltipType::kHomeToOverview);
  contextual_tooltip::HandleNudgeShown(
      pref_service, contextual_tooltip::TooltipType::kHomeToOverview);

  // If the nudge has a timeout, schedule a task to hide it. The timeout should
  // start when the animation sequence finishes.
  if (!nudge_duration.is_zero()) {
    nudge_hide_timer_.Start(
        FROM_HERE, nudge_duration + total_animation_duration,
        base::BindOnce(&HomeToOverviewNudgeController::HideNudge,
                       base::Unretained(this), HideTransition::kNudgeTimeout));
  }
}

void HomeToOverviewNudgeController::HideNudge(HideTransition transition) {
  if (!nudge_)
    return;

  auto animate_hide_transform = [](HideTransition transition,
                                   ui::Layer* layer) {
    ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
    settings.SetTweenType(GetHideTransformTween(transition));
    settings.SetTransitionDuration(GetHideTransformDuration(transition));
    settings.SetPreemptionStrategy(
        ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);

    layer->SetTransform(gfx::Transform());
  };

  animate_hide_transform(transition,
                         hotseat_widget_->GetLayerForNudgeAnimation());
  animate_hide_transform(transition, nudge_->GetWidget()->GetLayer());

  {
    ui::ScopedLayerAnimationSettings settings(
        nudge_->label()->layer()->GetAnimator());
    settings.SetTweenType(gfx::Tween::LINEAR);
    settings.SetTransitionDuration(GetHideFadeDuration(transition));
    settings.SetPreemptionStrategy(
        ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
    settings.AddObserver(new ObserverToCloseWidget(nudge_->GetWidget()));

    nudge_->label()->layer()->SetOpacity(0.0f);
  }

  widget_observations_.RemoveAllObservations();
  nudge_ = nullptr;

  // Invalidated nudge tap handler callbacks.
  weak_factory_.InvalidateWeakPtrs();
}

void HomeToOverviewNudgeController::UpdateNudgeAnchorBounds() {
  // Update the nudge anchor bounds - use the hotseat bounds vertical
  // coordinates, so the nudge follows the vertical hotseat position, but the
  // shelf window horizontal coordinates to center nudge horizontally in the
  // shelf widget bounds (the hotseat widget parent).
  const gfx::Rect hotseat_bounds =
      hotseat_widget_->GetNativeWindow()->GetTargetBounds();
  const gfx::Rect shelf_bounds =
      hotseat_widget_->GetNativeWindow()->parent()->GetTargetBounds();
  nudge_->UpdateAnchorRect(
      gfx::Rect(gfx::Point(shelf_bounds.x(), hotseat_bounds.y()),
                gfx::Size(shelf_bounds.width(), hotseat_bounds.height())));
}

void HomeToOverviewNudgeController::HandleNudgeTap() {
  HideNudge(HideTransition::kUserTap);
}

}  // namespace ash