chromium/ash/shelf/swipe_home_to_overview_controller.cc

// Copyright 2019 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/swipe_home_to_overview_controller.h"

#include <algorithm>
#include <optional>

#include "ash/app_list/app_list_controller_impl.h"
#include "ash/constants/ash_features.h"
#include "ash/controls/contextual_tooltip.h"
#include "ash/public/cpp/app_list/app_list_config.h"
#include "ash/public/cpp/shelf_config.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shelf/drag_window_from_shelf_controller.h"
#include "ash/shelf/shelf_metrics.h"
#include "ash/shell.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_session.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_macros.h"
#include "base/time/default_tick_clock.h"
#include "ui/compositor/layer_animation_element.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/gfx/animation/tween.h"

namespace ash {

namespace {

// The target/min home launcher view scale.
constexpr float kTargetHomeScale = 0.92f;

// The home UI will be scaled down towards center of the screen as drag location
// moves upwards. The target threshold for scaling is extended above the actual
// threshold for transition so the UI keeps changing even when the gesture goes
// over the threshold. This is the target home screen scaling threshold in terms
// of ratio of the display height.
constexpr float kHomeScalingThresholdDisplayHeightRatio = 0.5f;

// The amount of time the drag has to remain bellow velocity threshold before
// the transition to the overview starts.
constexpr base::TimeDelta kOverviewTransitionDelay = base::Milliseconds(150);

// The duration of transition from the home screen current scaled state to the
// initial (unscaled) state when the gesture is canceled.
constexpr base::TimeDelta kGestureCancelationDuration = base::Milliseconds(350);

// The duration of transition from the home screen current scaled state to the
// initial (unscaled) state when the gesture is canceled due to a back gesture.
constexpr base::TimeDelta kGestureCancelationForBackDuration =
    base::Milliseconds(250);

void UpdateHomeAnimationForGestureCancel(
    bool going_back,
    ui::ScopedLayerAnimationSettings* settings) {
  settings->SetTransitionDuration(going_back
                                      ? kGestureCancelationForBackDuration
                                      : kGestureCancelationDuration);
  settings->SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN);
  settings->SetPreemptionStrategy(
      ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
}

}  // namespace

SwipeHomeToOverviewController::SwipeHomeToOverviewController(int64_t display_id)
    : SwipeHomeToOverviewController(display_id,
                                    base::DefaultTickClock::GetInstance()) {}

SwipeHomeToOverviewController::SwipeHomeToOverviewController(
    int64_t display_id,
    const base::TickClock* tick_clock)
    : display_id_(display_id), overview_transition_timer_(tick_clock) {}

SwipeHomeToOverviewController::~SwipeHomeToOverviewController() {
  CancelDrag();
}

void SwipeHomeToOverviewController::Drag(const gfx::PointF& location_in_screen,
                                         float scroll_x,
                                         float scroll_y) {
  if (state_ == State::kFinished)
    return;

  display::Display display;
  if (!display::Screen::GetScreen()->GetDisplayWithDisplayId(display_id_,
                                                             &display)) {
    CancelDrag();
    return;
  }

  const int shelf_top =
      display.bounds().bottom() - ShelfConfig::Get()->shelf_size();
  if (state_ == State::kInitial) {
    // Do not start drag until the drag goes above the shelf.
    if (location_in_screen.y() > shelf_top)
      return;

    overview_transition_threshold_y_ =
        shelf_top - kVerticalThresholdForOverviewTransition;
    scaling_threshold_y_ =
        display.bounds().y() +
        display.bounds().height() * kHomeScalingThresholdDisplayHeightRatio;
    state_ = State::kTrackingDrag;
  } else {
    if (location_in_screen.y() <= overview_transition_threshold_y_ &&
        std::abs(scroll_x) + std::abs(scroll_y) <= kMovementVelocityThreshold) {
      ScheduleFinalizeDragAndShowOverview();
    } else {
      overview_transition_timer_.Stop();
    }
  }

  // Update the home screen scale to match progress during the drag.
  // Use extended threshold as the projected final transition position - UI
  // changing even after the user gets over the threshold should make the user
  // more likely to keep dragging up when they get really close to the threshold
  // for transition to overview (and reduce false negatives for detecting
  // transition to overview).
  const float distance = location_in_screen.y() - scaling_threshold_y_;
  const float target_distance = overview_transition_threshold_y_ -
                                scaling_threshold_y_ +
                                kVerticalThresholdForOverviewTransition;

  const float progress = gfx::Tween::CalculateValue(
      gfx::Tween::FAST_OUT_SLOW_IN,
      std::clamp(1.f - distance / target_distance, 0.0f, 1.0f));

  float scale = gfx::Tween::FloatValueBetween(progress, 1.0f, kTargetHomeScale);
  Shell::Get()->app_list_controller()->UpdateScaleAndOpacityForHomeLauncher(
      scale, 1.0f /*opacity*/, std::nullopt /*animation_info*/,
      base::NullCallback());
}

void SwipeHomeToOverviewController::EndDrag(
    const gfx::PointF& location_in_screen,
    std::optional<float> velocity_y) {
  if (state_ != State::kTrackingDrag) {
    state_ = State::kFinished;
    return;
  }

  // Upward swipe should return to the home screen's initial state.
  const bool go_back =
      velocity_y &&
      *velocity_y <
          -DragWindowFromShelfController::kVelocityToHomeScreenThreshold;

  // Overview is triggered by |overview_transition_timer_|. If EndDrag()
  // is called before the timer fires, the result of the gesture should be
  // staying on the home screen.
  FinalizeDragAndStayOnHomeScreen(go_back);
}

void SwipeHomeToOverviewController::CancelDrag() {
  if (state_ != State::kTrackingDrag) {
    state_ = State::kFinished;
    return;
  }

  FinalizeDragAndStayOnHomeScreen(/*go_back=*/false);
}

void SwipeHomeToOverviewController::ScheduleFinalizeDragAndShowOverview() {
  if (overview_transition_timer_.IsRunning())
    return;

  overview_transition_timer_.Start(
      FROM_HERE, kOverviewTransitionDelay,
      base::BindOnce(
          &SwipeHomeToOverviewController::FinalizeDragAndShowOverview,
          base::Unretained(this)));
}

void SwipeHomeToOverviewController::FinalizeDragAndShowOverview() {
  state_ = State::kFinished;
  overview_transition_threshold_y_ = 0;

  if (features::IsHideShelfControlsInTabletModeEnabled()) {
    contextual_tooltip::HandleGesturePerformed(
        Shell::Get()->session_controller()->GetActivePrefService(),
        contextual_tooltip::TooltipType::kHomeToOverview);
  }

  UMA_HISTOGRAM_ENUMERATION(kEnterOverviewHistogramName,
                            EnterOverviewFromHomeLauncher::kOverview);

  // NOTE: No need to update the home launcher opacity and scale here - the
  // AppListControllerImpl will update the home launcher state when it detects
  // that the overview is starting.
  Shell::Get()->overview_controller()->StartOverview(
      OverviewStartAction::kExitHomeLauncher);
}

void SwipeHomeToOverviewController::FinalizeDragAndStayOnHomeScreen(
    bool go_back) {
  overview_transition_timer_.Stop();
  overview_transition_threshold_y_ = 0;
  state_ = State::kFinished;

  // App list controller may get destroyed before shelf during shutdown.
  auto* const app_list_controller = Shell::Get()->app_list_controller();
  if (!app_list_controller) {
    return;
  }

  if (go_back) {
    app_list_controller->Back();
    UMA_HISTOGRAM_ENUMERATION(kEnterOverviewHistogramName,
                              EnterOverviewFromHomeLauncher::kBack);
  } else {
    UMA_HISTOGRAM_ENUMERATION(kEnterOverviewHistogramName,
                              EnterOverviewFromHomeLauncher::kCanceled);
  }

  // Make sure the home launcher scale and opacity return to the initial state.
  // Note that this is needed even if the gesture ended up in a fling, as early
  // gesture handling might have updated the launcher scale.
  app_list_controller->UpdateScaleAndOpacityForHomeLauncher(
      1.0f /*scale*/, 1.0f /*opacity*/, std::nullopt /*animation_info*/,
      base::BindRepeating(&UpdateHomeAnimationForGestureCancel, go_back));
}

}  // namespace ash