// Copyright 2023 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/wm/scoped_window_tucker.h"
#include "ash/app_list/app_list_controller_impl.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/shell.h"
#include "ash/wm/tablet_mode/tablet_mode_window_state.h"
#include "base/metrics/user_metrics.h"
#include "ui/aura/window_targeter.h"
#include "ui/compositor/layer_animator.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/wm/core/scoped_animation_disabler.h"
#include "ui/wm/core/window_util.h"
#include "ui/wm/public/activation_client.h"
namespace ash {
namespace {
// The tuck handle can be tapped slightly outside its bounds.
constexpr gfx::Insets kTuckHandleExtraTapInset = gfx::Insets::VH(-8, -16);
// The distance from the edge of the tucked window to the edge of the screen
// during the bounce.
constexpr int kTuckOffscreenPaddingDp = 20;
// The duration for the tucked window to slide offscreen during the bounce.
constexpr base::TimeDelta kTuckWindowBounceStartDuration =
base::Milliseconds(400);
// The duration for the tucked window to bounce back to the edge of the
// screen.
constexpr base::TimeDelta kTuckWindowBounceEndDuration =
base::Milliseconds(533);
constexpr base::TimeDelta kUntuckWindowAnimationDuration =
base::Milliseconds(400);
constexpr base::TimeDelta kSlideHandleForOverviewDuration =
base::Milliseconds(200);
} // namespace
ScopedWindowTucker::Delegate::Delegate() {}
ScopedWindowTucker::Delegate::~Delegate() {}
// Represents a tuck handle that untucks floated windows from offscreen.
ScopedWindowTucker::TuckHandleView::TuckHandleView(
base::WeakPtr<Delegate> delegate,
base::RepeatingClosure callback,
bool left)
: views::Button(callback),
scoped_window_tucker_delegate_(delegate),
left_(left) {
SetFlipCanvasOnPaintForRTLUI(false);
SetFocusBehavior(FocusBehavior::NEVER);
SetEventTargeter(std::make_unique<views::ViewTargeter>(this));
}
ScopedWindowTucker::TuckHandleView::~TuckHandleView() {}
void ScopedWindowTucker::TuckHandleView::OnThemeChanged() {
views::View::OnThemeChanged();
SchedulePaint();
}
void ScopedWindowTucker::TuckHandleView::PaintButtonContents(
gfx::Canvas* canvas) {
if (scoped_window_tucker_delegate_) {
scoped_window_tucker_delegate_->PaintTuckHandle(canvas, width(), left_);
}
}
void ScopedWindowTucker::TuckHandleView::OnGestureEvent(
ui::GestureEvent* event) {
if (event->type() != ui::EventType::kGestureScrollBegin) {
views::Button::OnGestureEvent(event);
return;
}
const ui::GestureEventDetails details = event->details();
const float detail_x = details.scroll_x_hint(),
detail_y = details.scroll_y_hint();
// Ignore vertical gestures.
if (std::fabs(detail_x) <= std::fabs(detail_y)) {
return;
}
// Handle like a normal button press for events on the tuck handle that are
// obvious inward gestures.
if ((left_ && detail_x > 0) || (!left_ && detail_x < 0)) {
NotifyClick(*event);
event->SetHandled();
event->StopPropagation();
}
}
bool ScopedWindowTucker::TuckHandleView::DoesIntersectRect(
const views::View* target,
const gfx::Rect& rect) const {
return true;
}
ScopedWindowTucker::ScopedWindowTucker(std::unique_ptr<Delegate> delegate,
aura::Window* window,
bool left)
: delegate_(std::move(delegate)),
window_(window),
left_(left),
event_blocker_(window) {
InitializeTuckHandleWidget();
window_observation_.Observe(window);
}
ScopedWindowTucker::~ScopedWindowTucker() {
Shell::Get()->activation_client()->RemoveObserver(this);
if (!window_->IsVisible()) {
window_->Show();
return;
}
wm::ActivateWindow(window_);
}
void ScopedWindowTucker::AnimateTuck() {
const gfx::Rect initial_bounds(window_->bounds());
// Sets the destination tucked bounds after the animation.
delegate_->UpdateWindowPosition(window_, left_);
const gfx::Rect final_bounds(window_->bounds());
// Align the tuck handle with the window.
aura::Window* tuck_handle = tuck_handle_widget_->GetNativeWindow();
tuck_handle->SetBounds(delegate_->GetTuckHandleBounds(left_, final_bounds));
// Set the window back to its initial floated bounds.
const gfx::Transform initial_transform = gfx::TransformBetweenRects(
gfx::RectF(final_bounds), gfx::RectF(initial_bounds));
// Set the transform during the bounce.
const gfx::Transform offset_transform = gfx::Transform::MakeTranslation(
left_ ? -kTuckOffscreenPaddingDp : kTuckOffscreenPaddingDp, 0);
views::AnimationBuilder()
.OnEnded(base::BindOnce(&ScopedWindowTucker::OnAnimateTuckEnded,
weak_factory_.GetWeakPtr()))
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.Once()
.SetDuration(base::TimeDelta())
.SetTransform(window_, initial_transform)
.SetTransform(tuck_handle, initial_transform)
.Then()
.SetDuration(kTuckWindowBounceStartDuration)
.SetTransform(window_, offset_transform, gfx::Tween::ACCEL_30_DECEL_20_85)
.SetTransform(tuck_handle, offset_transform,
gfx::Tween::ACCEL_30_DECEL_20_85)
.Then()
.SetDuration(kTuckWindowBounceEndDuration)
.SetTransform(window_, gfx::Transform(), gfx::Tween::ACCEL_20_DECEL_100)
.SetTransform(tuck_handle, gfx::Transform(),
gfx::Tween::ACCEL_20_DECEL_100);
base::RecordAction(base::UserMetricsAction(kTuckUserAction));
}
void ScopedWindowTucker::AnimateUntuck(base::OnceClosure callback) {
wm::ScopedAnimationDisabler disable(window_);
window_->Show();
const gfx::RectF initial_bounds(window_->bounds());
delegate_->UpdateWindowPosition(window_, left_);
const gfx::Rect final_bounds(window_->bounds());
const gfx::Transform transform =
gfx::TransformBetweenRects(gfx::RectF(final_bounds), initial_bounds);
aura::Window* tuck_handle = tuck_handle_widget_->GetNativeWindow();
tuck_handle->SetBounds(delegate_->GetTuckHandleBounds(left_, final_bounds));
views::AnimationBuilder()
// TODO(sammiequon|sophiewen): Should we handle the case where the
// animation gets aborted?
.OnEnded(std::move(callback))
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.Once()
.SetDuration(base::TimeDelta())
.SetTransform(window_, transform)
.SetTransform(tuck_handle, transform)
.Then()
.SetDuration(kUntuckWindowAnimationDuration)
.SetTransform(window_, gfx::Transform(), gfx::Tween::ACCEL_5_70_DECEL_90)
.SetTransform(tuck_handle, gfx::Transform(),
gfx::Tween::ACCEL_5_70_DECEL_90);
base::RecordAction(base::UserMetricsAction(kUntuckUserAction));
}
void ScopedWindowTucker::UntuckWindow() {
delegate_->UntuckWindow(window_);
}
void ScopedWindowTucker::OnAnimateTuckEnded() {
delegate_->OnAnimateTuckEnded(window_);
}
void ScopedWindowTucker::OnWindowActivated(ActivationReason reason,
aura::Window* gained_active,
aura::Window* lost_active) {
// Note that `UntuckWindow()` destroys `this`.
if (gained_active == window_) {
delegate_->UntuckWindow(window_);
}
}
void ScopedWindowTucker::OnOverviewModeStarting() {
OnOverviewModeChanged(/*in_overview=*/true);
}
void ScopedWindowTucker::OnOverviewModeEndingAnimationComplete(bool canceled) {
OnOverviewModeChanged(/*in_overview=*/false);
}
void ScopedWindowTucker::OnWindowBoundsChanged(
aura::Window* window,
const gfx::Rect& old_bounds,
const gfx::Rect& new_bounds,
ui::PropertyChangeReason reason) {
aura::Window* tuck_handle = tuck_handle_widget_->GetNativeWindow();
tuck_handle->SetBounds(delegate_->GetTuckHandleBounds(left_, new_bounds));
}
void ScopedWindowTucker::InitializeTuckHandleWidget() {
views::Widget::InitParams params(
views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
views::Widget::InitParams::TYPE_POPUP);
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
params.parent =
window()->GetRootWindow()->GetChildById(delegate_->ParentContainerId());
params.init_properties_container.SetProperty(kHideInOverviewKey, true);
params.init_properties_container.SetProperty(kForceVisibleInMiniViewKey,
false);
params.name = "TuckHandleWidget";
tuck_handle_widget_->Init(std::move(params));
tuck_handle_widget_->SetContentsView(std::make_unique<TuckHandleView>(
delegate_->GetWeakPtr(),
base::BindRepeating(&ScopedWindowTucker::UntuckWindow,
base::Unretained(this)),
left_));
tuck_handle_widget_->Show();
auto targeter = std::make_unique<aura::WindowTargeter>();
targeter->SetInsets(kTuckHandleExtraTapInset);
tuck_handle_widget_->GetNativeWindow()->SetEventTargeter(std::move(targeter));
// Activate the most recent window that is not minimized and not the
// tucked `window_`, if it exists. If no such window exists in tablet
// mode, activate the app list.
auto mru_windows =
Shell::Get()->mru_window_tracker()->BuildMruWindowList(kActiveDesk);
auto app_window_it =
base::ranges::find_if(mru_windows, [this](aura::Window* w) {
CHECK(WindowState::Get(w));
return w != window() && !WindowState::Get(w)->IsMinimized();
});
aura::Window* window_to_activate = nullptr;
if (app_window_it == mru_windows.end()) {
if (display::Screen::GetScreen()->InTabletMode()) {
window_to_activate = Shell::Get()->app_list_controller()->GetWindow();
}
} else {
window_to_activate = *app_window_it;
}
if (window_to_activate) {
wm::ActivateWindow(window_to_activate);
}
Shell::Get()->activation_client()->AddObserver(this);
overview_observer_.Observe(Shell::Get()->overview_controller());
}
void ScopedWindowTucker::OnOverviewModeChanged(bool in_overview) {
// Slide the tuck handle offscreen if entering overview mode, or back onscreen
// if exiting overview mode.
aura::Window* tuck_handle = tuck_handle_widget_->GetNativeWindow();
const gfx::Rect bounds = tuck_handle->bounds();
gfx::Rect target_bounds =
delegate_->GetTuckHandleBounds(left_, window_->GetTargetBounds());
if (in_overview) {
const int x_offset = left_ ? -kTuckHandleWidth : kTuckHandleWidth;
target_bounds.Offset(x_offset, 0);
}
if (target_bounds == bounds) {
return;
}
tuck_handle->SetBounds(target_bounds);
const gfx::Transform transform =
gfx::TransformBetweenRects(gfx::RectF(target_bounds), gfx::RectF(bounds));
views::AnimationBuilder()
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.Once()
.SetDuration(base::TimeDelta())
.SetTransform(tuck_handle, transform)
.Then()
.SetDuration(kSlideHandleForOverviewDuration)
.SetTransform(tuck_handle, gfx::Transform(),
gfx::Tween::ACCEL_20_DECEL_100);
}
} // namespace ash