// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "ash/wm/overview/overview_window_drag_controller.h"
#include <algorithm>
#include "ash/display/mouse_cursor_event_filter.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/root_window_controller.h"
#include "ash/screen_util.h"
#include "ash/shell.h"
#include "ash/wm/desks/desk_icon_button.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/desks/overview_desk_bar_view.h"
#include "ash/wm/float/float_controller.h"
#include "ash/wm/overview/overview_constants.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_grid.h"
#include "ash/wm/overview/overview_item.h"
#include "ash/wm/overview/overview_item_base.h"
#include "ash/wm/overview/overview_item_view.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/overview/overview_utils.h"
#include "ash/wm/overview/scoped_float_container_stacker.h"
#include "ash/wm/snap_group/snap_group_metrics.h"
#include "ash/wm/splitview/split_view_constants.h"
#include "ash/wm/splitview/split_view_drag_indicators.h"
#include "ash/wm/splitview/split_view_types.h"
#include "ash/wm/splitview/split_view_utils.h"
#include "ash/wm/window_positioning_utils.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_constants.h"
#include "base/auto_reset.h"
#include "base/metrics/histogram_functions.h"
#include "base/numerics/safe_conversions.h"
#include "chromeos/ui/frame/caption_buttons/snap_controller.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"
#include "ui/aura/window.h"
#include "ui/aura/window_observer.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/presentation_time_recorder.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/wm/core/coordinate_conversion.h"
namespace ash {
namespace {
// The amount of distance from the start of drag the item needs to be dragged
// vertically for it to be closed on release.
constexpr float kDragToCloseDistanceThresholdDp = 160.f;
// The minimum distance that will be considered as a drag event.
constexpr float kMinimumDragDistanceDp = 5.f;
// Items dragged to within |kDistanceFromEdgeDp| of the screen will get snapped
// even if they have not moved by |kMinimumDragToSnapDistanceDp|.
constexpr float kDistanceFromEdgeDp = 16.f;
// The minimum distance that an item must be moved before it is snapped. This
// prevents accidental snaps.
constexpr float kMinimumDragToSnapDistanceDp = 96.f;
// Flings with less velocity than this will not close the dragged item.
constexpr float kFlingToCloseVelocityThreshold = 2000.f;
constexpr float kItemMinOpacity = 0.4f;
// The scale factor used to calculate the minimum side length for the overview
// item bounds on the desks bar.
constexpr float kScaleFactorForMinimumSideLength = 0.5f;
// The minimum vertical overlapped length between the overview item and new desk
// button in order to activate the new desk button.
constexpr int kVerticalOverlappedLengthToActivateNewDeskButton = 15;
// Amount of time we wait to unpause the occlusion tracker after a overview item
// is finished dragging. Waits a bit longer than the overview item animation.
constexpr base::TimeDelta kOcclusionPauseDurationForDrag =
base::Milliseconds(300);
constexpr base::TimeDelta kScaleUpNewDeskButtonGracePeriod =
base::Milliseconds(500);
// The UMA histogram that records presentation time for window dragging
// operation in overview mode.
constexpr char kOverviewWindowDragHistogram[] =
"Ash.Overview.WindowDrag.PresentationTime.TabletMode";
constexpr char kOverviewWindowDragMaxLatencyHistogram[] =
"Ash.Overview.WindowDrag.PresentationTime.MaxLatency.TabletMode";
bool g_skip_new_desk_button_scale_up_for_test = false;
bool GetVirtualDesksBarEnabled(OverviewItemBase* item) {
return desks_util::ShouldDesksBarBeCreated() &&
item->overview_grid()->desks_bar_view();
}
// Returns whether |item|'s window is visible on all desks.
bool DraggedItemIsVisibleOnAllDesks(OverviewItemBase* item) {
aura::Window* const dragged_window = item->GetWindow();
return dragged_window &&
desks_util::IsWindowVisibleOnAllWorkspaces(dragged_window);
}
// Returns the scaled-down size of the dragged item that should be used when
// it's dragged over the OverviewDeskBarView that belongs to |overview_grid|.
// |window_original_size| is the size of the item's window before it was scaled
// up for dragging.
gfx::SizeF GetItemSizeWhenOnDesksBar(OverviewGrid* overview_grid,
const gfx::SizeF& window_original_size) {
DCHECK(overview_grid);
const OverviewDeskBarView* desks_bar_view = overview_grid->desks_bar_view();
DCHECK(desks_bar_view);
const int expanded_desks_bar_height = DeskBarViewBase::GetPreferredBarHeight(
overview_grid->root_window(), DeskBarViewBase::Type::kOverview,
DeskBarViewBase::State::kExpanded);
// We should always use the expanded desks bar height here even if the desks
// bar is actually in zero state to calculate `scale_factor`. Because if zero
// state bar height is used here, the dragged window could become too small
// during the drag.
const float scale_factor = static_cast<float>(expanded_desks_bar_height) /
overview_grid->root_window()->bounds().height();
gfx::SizeF scaled_size = gfx::ScaleSize(window_original_size, scale_factor);
// Adjust the scaled size to ensure that its smaller side length is equal or
// larger than the `minimum_size_length`, and then adjust the larger size
// length to preserve the ratio of the original size.
const float minimum_size_length =
expanded_desks_bar_height * kScaleFactorForMinimumSideLength;
const float scaled_size_height = scaled_size.height();
const float scaled_size_width = scaled_size.width();
if (scaled_size_height < minimum_size_length ||
scaled_size_width < minimum_size_length) {
if (scaled_size_height < scaled_size_width) {
scaled_size.set_height(minimum_size_length);
scaled_size.set_width(scaled_size_width / scaled_size_height *
minimum_size_length);
} else {
scaled_size.set_width(minimum_size_length);
scaled_size.set_height(scaled_size_height / scaled_size_width *
minimum_size_length);
}
}
// Add the margins overview mode adds around the window's contents.
scaled_size.Enlarge(kDraggingEnlargeDp,
kDraggingEnlargeDp + kWindowMiniViewHeaderHeight);
return scaled_size;
}
float GetManhattanDistanceX(float point_x, const gfx::RectF& rect) {
return std::max(rect.x() - point_x, point_x - rect.right());
}
float GetManhattanDistanceY(float point_y, const gfx::RectF& rect) {
return std::max(rect.y() - point_y, point_y - rect.bottom());
}
void RecordDrag(OverviewDragAction action) {
base::UmaHistogramEnumeration("Ash.Overview.WindowDrag.Workflow", action);
}
// Restores the new desk button state back to the
// `DeskIconButton::State::kExpanded` on drag ended on all `OverviewGrid`s.
void MaybeRestoreNewDeskButtonState() {
OverviewSession* overview_session =
OverviewController::Get()->overview_session();
if (!overview_session || overview_session->is_shutting_down()) {
return;
}
for (aura::Window* root : Shell::GetAllRootWindows()) {
OverviewGrid* overview_grid = overview_session->GetGridWithRootWindow(root);
if (auto* desks_bar_view = overview_grid->desks_bar_view()) {
desks_bar_view->UpdateDeskIconButtonState(
desks_bar_view->new_desk_button(), DeskIconButton::State::kExpanded);
}
}
}
// Helps with handling the workflow where you drag an overview item from one
// grid and drop into another grid. The challenge is that if the item represents
// an ARC window, that window will be moved to the target root asynchronously.
// |OverviewItemMoveHelper| observes the window until it moves to the target
// root. Then |OverviewItemMoveHelper| self destructs and adds a new item to
// represent the window on the target root.
class OverviewItemMoveHelper : public aura::WindowObserver {
public:
// |target_item_bounds| is the bounds of the dragged overview item when the
// drag ends. |target_item_bounds| is used to put the new item where the old
// item ended, so it looks like it is the same item. Then the item is animated
// from there to its proper position in the grid.
OverviewItemMoveHelper(aura::Window* window,
const gfx::RectF& target_item_bounds)
: window_(window), target_item_bounds_(target_item_bounds) {
window->AddObserver(this);
}
OverviewItemMoveHelper(const OverviewItemMoveHelper&) = delete;
OverviewItemMoveHelper& operator=(const OverviewItemMoveHelper&) = delete;
~OverviewItemMoveHelper() override {
OverviewController* overview_controller = OverviewController::Get();
if (overview_controller->InOverviewSession()) {
overview_controller->overview_session()->PositionWindows(
/*animate=*/true);
}
}
// aura::WindowObserver:
void OnWindowDestroyed(aura::Window* window) override {
DCHECK_EQ(window_, window);
delete this;
}
void OnWindowAddedToRootWindow(aura::Window* window) override {
DCHECK_EQ(window_, window);
window->RemoveObserver(this);
OverviewController* overview_controller = OverviewController::Get();
if (overview_controller->InOverviewSession()) {
// OverviewSession::AddItemInMruOrder() will add |window| to the grid
// associated with |window|'s root. Do not reposition or restack as we
// will soon handle them both anyway.
OverviewSession* session = overview_controller->overview_session();
session->AddItemInMruOrder(window, /*reposition=*/false,
/*animate=*/false, /*restack=*/false,
/*use_spawn_animation=*/false);
OverviewItemBase* item = session->GetOverviewItemForWindow(window);
DCHECK(item);
item->SetBounds(target_item_bounds_, OVERVIEW_ANIMATION_NONE);
item->set_should_restack_on_animation_end(true);
// The destructor will call OverviewSession::PositionWindows().
}
delete this;
}
private:
const raw_ptr<aura::Window> window_;
const gfx::RectF target_item_bounds_;
};
} // namespace
OverviewWindowDragController::OverviewWindowDragController(
OverviewSession* overview_session,
OverviewItemBase* item,
bool is_touch_dragging,
OverviewItemBase* event_source_item)
: overview_session_(overview_session),
item_(item),
event_source_item_(event_source_item),
display_count_(Shell::GetAllRootWindows().size()),
is_touch_dragging_(is_touch_dragging),
is_eligible_for_drag_to_snap_(
IsEligibleForDraggingToSnapInOverview(item)),
virtual_desks_bar_enabled_(GetVirtualDesksBarEnabled(item)) {
CHECK(!OverviewController::Get()->IsInStartAnimation());
CHECK(!SplitViewController::Get(item_->root_window())->IsDividerAnimating());
}
OverviewWindowDragController::~OverviewWindowDragController() {
// This object is deleted using `DeleteSoon()`, so the shell may be destroyed
// already during shutdown.
if (Shell::HasInstance()) {
Shell::Get()->mouse_cursor_filter()->HideSharedEdgeIndicator();
}
}
// static
base::AutoReset<bool>
OverviewWindowDragController::SkipNewDeskButtonScaleUpDurationForTesting() {
return {&g_skip_new_desk_button_scale_up_for_test, true};
}
void OverviewWindowDragController::InitiateDrag(
const gfx::PointF& location_in_screen) {
initial_event_location_ = location_in_screen;
initial_centerpoint_ = item_->target_bounds().CenterPoint();
original_opacity_ = item_->GetOpacity();
current_drag_behavior_ = DragBehavior::kUndefined;
occlusion_pauser_ = OverviewController::Get()->PauseOcclusionTracker(
kOcclusionPauseDurationForDrag);
DCHECK(!presentation_time_recorder_);
presentation_time_recorder_ = CreatePresentationTimeHistogramRecorder(
item_->root_window()->layer()->GetCompositor(),
kOverviewWindowDragHistogram, kOverviewWindowDragMaxLatencyHistogram);
}
void OverviewWindowDragController::Drag(const gfx::PointF& location_in_screen) {
if (!did_move_) {
gfx::Vector2dF distance = location_in_screen - initial_event_location_;
// Do not start dragging if the distance from |location_in_screen| to
// |initial_event_location_| is not greater than |kMinimumDragDistanceDp|.
if (std::abs(distance.x()) < kMinimumDragDistanceDp &&
std::abs(distance.y()) < kMinimumDragDistanceDp) {
return;
}
if (is_touch_dragging_ && std::abs(distance.x()) < std::abs(distance.y())) {
StartDragToCloseMode();
} else if (is_eligible_for_drag_to_snap_ || virtual_desks_bar_enabled_) {
StartNormalDragMode(location_in_screen);
} else {
return;
}
}
if (current_drag_behavior_ == DragBehavior::kDragToClose)
ContinueDragToClose(location_in_screen);
else if (current_drag_behavior_ == DragBehavior::kNormalDrag)
ContinueNormalDrag(location_in_screen);
if (presentation_time_recorder_)
presentation_time_recorder_->RequestNext();
}
OverviewWindowDragController::DragResult
OverviewWindowDragController::CompleteDrag(
const gfx::PointF& location_in_screen) {
per_grid_desks_bar_data_.clear();
DragResult result = DragResult::kNeverDisambiguated;
switch (current_drag_behavior_) {
case DragBehavior::kNoDrag:
NOTREACHED();
case DragBehavior::kUndefined:
ActivateDraggedWindow();
break;
case DragBehavior::kNormalDrag:
result = CompleteNormalDrag(location_in_screen);
break;
case DragBehavior::kDragToClose:
result = CompleteDragToClose(location_in_screen);
break;
}
did_move_ = false;
// `item_` may be null if `CompleteNormalDrag()` resulted in moving the
// window into another desk. At this point, we can just pass in a nullptr and
// the `FloatContainerStacker` will reset the stacking. Also,
// `ActivateDraggedWindow()` above may have started the session shutdown, so
// the `FloatContainerStacker` may be null.
if (auto* float_container_stacker =
overview_session_->float_container_stacker()) {
float_container_stacker->OnDragFinished(item_ ? item_->GetWindow()
: nullptr);
}
item_ = nullptr;
event_source_item_ = nullptr;
current_drag_behavior_ = DragBehavior::kNoDrag;
occlusion_pauser_.reset();
presentation_time_recorder_.reset();
return result;
}
void OverviewWindowDragController::StartNormalDragMode(
const gfx::PointF& location_in_screen) {
CHECK(is_eligible_for_drag_to_snap_ || virtual_desks_bar_enabled_);
did_move_ = true;
current_drag_behavior_ = DragBehavior::kNormalDrag;
Shell::Get()->mouse_cursor_filter()->ShowSharedEdgeIndicator(
item_->root_window());
const gfx::SizeF window_original_size(item_->GetWindow()->bounds().size());
item_->ScaleUpSelectedItem(
OVERVIEW_ANIMATION_LAYOUT_OVERVIEW_ITEMS_IN_OVERVIEW);
original_scaled_size_ = item_->target_bounds().size();
auto* overview_grid = item_->overview_grid();
overview_grid->AddDropTargetForDraggingFromThisGrid(item_);
// Expand all desks bars on all displays when normal drag starts if it is in
// zero state.
for (const std::unique_ptr<OverviewGrid>& grid :
overview_session_->grid_list()) {
// The bar may be null if we have no desks in tablet mode.
if (auto* desks_bar_view = grid->desks_bar_view();
desks_bar_view && desks_bar_view->IsZeroState()) {
desks_bar_view->UpdateNewMiniViews(/*initializing_bar_view=*/false,
/*expanding_bar_view=*/true);
}
}
item_->UpdateShadowTypeForDrag(/*is_dragging=*/true);
aura::Window* dragged_window = item_->GetWindow();
if (is_eligible_for_drag_to_snap_) {
overview_session_->SetSplitViewDragIndicatorsDraggedWindow(dragged_window);
overview_session_->UpdateSplitViewDragIndicatorsWindowDraggingStates(
GetRootWindowBeingDraggedIn(),
SplitViewDragIndicators::ComputeWindowDraggingState(
/*is_dragging=*/true,
SplitViewDragIndicators::WindowDraggingState::kFromOverview,
SnapPosition::kNone));
item_->HideCannotSnapWarning(/*animate=*/true);
// Update the split view divider bar status if necessary. If splitview is
// active when dragging the `dragged_window`, the split divider bar should
// be placed below the dragged window during dragging.
SplitViewController::Get(item_->root_window())
->OnWindowDragStarted(dragged_window);
}
if (virtual_desks_bar_enabled_) {
// Calculate the item bounds minus the header and margins (which are
// invisible). Use this for the shrink bounds so that the item starts
// shrinking when the visible top-edge of the item aligns with the
// bottom-edge of the desks bar (may be different edges if we are dragging
// from different directions).
gfx::SizeF item_no_header_size = original_scaled_size_;
item_no_header_size.Enlarge(
float{-kDraggingEnlargeDp},
float{-kDraggingEnlargeDp - kWindowMiniViewHeaderHeight});
// We must update the desks bar widget bounds before we cache its bounds
// below, in case it needs to be pushed down due to splitview indicators.
// Note that when drag is just getting started, the window hasn't moved to
// another display, so it's ok to use the item's |overview_grid|.
overview_grid->MaybeUpdateDesksWidgetBounds();
// Calculate cached values for usage during drag for each grid.
for (const auto& grid : overview_session_->grid_list()) {
GridDesksBarData& grid_desks_bar_data =
per_grid_desks_bar_data_[grid.get()];
grid_desks_bar_data.on_desks_bar_item_size =
GetItemSizeWhenOnDesksBar(grid.get(), window_original_size);
grid_desks_bar_data.desks_bar_bounds = grid_desks_bar_data.shrink_bounds =
gfx::RectF(grid->desks_bar_view()->GetBoundsInScreen());
const int expanded_height = DeskBarViewBase::GetPreferredBarHeight(
grid->root_window(), DeskBarViewBase::Type::kOverview,
DeskBarViewBase::State::kExpanded);
grid_desks_bar_data.desks_bar_bounds.set_height(expanded_height);
grid_desks_bar_data.shrink_bounds.set_height(expanded_height);
grid_desks_bar_data.shrink_bounds.Inset(gfx::InsetsF::VH(
-item_no_header_size.height() / 2, -item_no_header_size.width() / 2));
grid_desks_bar_data.shrink_region_distance =
grid_desks_bar_data.desks_bar_bounds.origin() -
grid_desks_bar_data.shrink_bounds.origin();
}
}
overview_session_->float_container_stacker()->OnDragStarted(dragged_window);
}
OverviewWindowDragController::DragResult OverviewWindowDragController::Fling(
const gfx::PointF& location_in_screen,
float velocity_x,
float velocity_y) {
if (current_drag_behavior_ == DragBehavior::kDragToClose ||
current_drag_behavior_ == DragBehavior::kUndefined) {
if (std::abs(velocity_y) > kFlingToCloseVelocityThreshold) {
item_->AnimateAndCloseItem(
(location_in_screen - initial_event_location_).y() < 0);
did_move_ = false;
item_ = nullptr;
event_source_item_ = nullptr;
current_drag_behavior_ = DragBehavior::kNoDrag;
occlusion_pauser_.reset();
RecordDragToClose(kFlingToClose);
return DragResult::kSuccessfulDragToClose;
}
}
// If the fling velocity was not high enough, or flings should be ignored,
// treat it as a scroll end event.
return CompleteDrag(location_in_screen);
}
void OverviewWindowDragController::ActivateDraggedWindow() {
// If no drag was initiated (e.g., a click/tap on the overview window),
// activate the window. If the split view is active and has a left window,
// snap the current window to right. If the split view is active and has a
// right window, snap the current window to left. If split view is active
// and the selected window cannot be snapped, exit splitview and activate
// the selected window, and also exit the overview.
SplitViewController* split_view_controller =
SplitViewController::Get(item_->root_window());
SplitViewController::State split_state = split_view_controller->state();
if (!is_eligible_for_drag_to_snap_ ||
split_state == SplitViewController::State::kNoSnap) {
overview_session_->SelectWindow(event_source_item_);
// Explicitly set `item_` to null to avoid being accessed after been
// released in `OverviewGrid::RemoveItem()`. See UaF reported in
// b/301368132.
item_ = nullptr;
event_source_item_ = nullptr;
} else if (auto* split_view_overview_session =
RootWindowController::ForWindow(item_->GetWindow())
->split_view_overview_session();
split_view_overview_session) {
// If `SplitViewOverviewSession` is active, activate the window;
// `AutoSnapController` will handle the autosnap.
RecordPartialOverviewMetrics(item_);
overview_session_->SelectWindow(event_source_item_);
item_ = nullptr;
event_source_item_ = nullptr;
} else if (split_view_controller->CanSnapWindow(
item_->GetWindow(), chromeos::kDefaultSnapRatio)) {
// Used for overview items that are being dragged to snap. Since the
// window is already activated, `AutoSnapController::OnWindowActivating()`
// will not work above.
RecordPartialOverviewMetrics(item_);
SnapWindow(split_view_controller,
split_state == SplitViewController::State::kPrimarySnapped
? SnapPosition::kSecondary
: SnapPosition::kPrimary);
} else {
split_view_controller->EndSplitView();
overview_session_->SelectWindow(event_source_item_);
// Same as above, explicitly set `item_` to nullptr to avoid UaF.
item_ = nullptr;
event_source_item_ = nullptr;
ShowAppCannotSnapToast();
}
current_drag_behavior_ = DragBehavior::kNoDrag;
occlusion_pauser_.reset();
}
void OverviewWindowDragController::ResetGesture() {
if (current_drag_behavior_ == DragBehavior::kNormalDrag) {
CHECK(item_->overview_grid()->drop_target());
Shell::Get()->mouse_cursor_filter()->HideSharedEdgeIndicator();
item_->DestroyMirrorsForDragging();
overview_session_->RemoveDropTargets();
if (is_eligible_for_drag_to_snap_) {
SplitViewController::Get(item_->root_window())->OnWindowDragCanceled();
overview_session_->ResetSplitViewDragIndicatorsWindowDraggingStates();
item_->UpdateCannotSnapWarningVisibility(/*animate=*/true);
}
}
// No need to position windows that are being destroyed.
base::flat_set<OverviewItemBase*> ignored_items;
if (item_->GetWindow()->is_destroying()) {
ignored_items.insert(item_);
}
overview_session_->PositionWindows(/*animate=*/true, ignored_items);
overview_session_->float_container_stacker()->OnDragFinished(
item_->GetWindow());
// This function gets called after a long press release, which bypasses
// CompleteDrag but stops dragging as well, so reset |item_|.
item_ = nullptr;
event_source_item_ = nullptr;
current_drag_behavior_ = DragBehavior::kNoDrag;
occlusion_pauser_.reset();
}
void OverviewWindowDragController::ResetOverviewSession() {
overview_session_ = nullptr;
new_desk_button_scale_up_timer_.Stop();
}
void OverviewWindowDragController::StartDragToCloseMode() {
DCHECK(is_touch_dragging_);
did_move_ = true;
current_drag_behavior_ = DragBehavior::kDragToClose;
overview_session_->GetGridWithRootWindow(item_->root_window())
->StartNudge(item_);
item_->UpdateShadowTypeForDrag(/*is_dragging=*/true);
overview_session_->float_container_stacker()->OnDragStarted(
item_->GetWindow());
}
void OverviewWindowDragController::ContinueDragToClose(
const gfx::PointF& location_in_screen) {
DCHECK_EQ(current_drag_behavior_, DragBehavior::kDragToClose);
// Update the dragged |item_|'s bounds accordingly. The distance from the new
// location to the new centerpoint should be the same it was initially.
gfx::RectF bounds(item_->target_bounds());
const gfx::PointF centerpoint =
location_in_screen - (initial_event_location_ - initial_centerpoint_);
// If the drag location intersects with the desk bar, then we should cancel
// the drag-to-close mode and start the normal drag mode.
if (virtual_desks_bar_enabled_ &&
item_->overview_grid()->IntersectsWithDesksBar(
gfx::ToRoundedPoint(location_in_screen),
/*update_desks_bar_drag_details=*/false, /*for_drop=*/false)) {
item_->SetOpacity(original_opacity_);
StartNormalDragMode(location_in_screen);
ContinueNormalDrag(location_in_screen);
return;
}
// Update |item_|'s opacity based on its distance. |item_|'s x coordinate
// should not change while in drag to close state.
float val = std::abs(location_in_screen.y() - initial_event_location_.y()) /
kDragToCloseDistanceThresholdDp;
overview_session_->GetGridWithRootWindow(item_->root_window())
->UpdateNudge(item_, val);
val = std::clamp(val, 0.f, 1.f);
float opacity = original_opacity_;
if (opacity > kItemMinOpacity)
opacity = original_opacity_ - val * (original_opacity_ - kItemMinOpacity);
item_->SetOpacity(opacity);
// When dragging to close, only update the y component.
bounds.set_y(centerpoint.y() - bounds.height() / 2.f);
item_->SetBounds(bounds, OVERVIEW_ANIMATION_NONE);
}
OverviewWindowDragController::DragResult
OverviewWindowDragController::CompleteDragToClose(
const gfx::PointF& location_in_screen) {
DCHECK_EQ(current_drag_behavior_, DragBehavior::kDragToClose);
// Close the window if it has been dragged enough, otherwise reposition it and
// set its opacity back to its original value.
overview_session_->GetGridWithRootWindow(item_->root_window())->EndNudge();
const float y_distance = (location_in_screen - initial_event_location_).y();
if (std::abs(y_distance) > kDragToCloseDistanceThresholdDp) {
item_->AnimateAndCloseItem(/*up=*/y_distance < 0);
RecordDragToClose(kSwipeToCloseSuccessful);
return DragResult::kSuccessfulDragToClose;
}
item_->UpdateShadowTypeForDrag(/*is_dragging=*/false);
item_->SetOpacity(original_opacity_);
overview_session_->PositionWindows(/*animate=*/true);
RecordDragToClose(kSwipeToCloseCanceled);
return DragResult::kCanceledDragToClose;
}
void OverviewWindowDragController::ContinueNormalDrag(
const gfx::PointF& location_in_screen) {
DCHECK_EQ(current_drag_behavior_, DragBehavior::kNormalDrag);
// Update the dragged |item_|'s bounds accordingly. The distance from the new
// location to the new centerpoint should be the same it was initially unless
// the item is over the DeskBarView, in which case we scale it down and center
// it around the drag location.
gfx::RectF bounds(item_->target_bounds());
gfx::PointF centerpoint =
location_in_screen - (initial_event_location_ - initial_centerpoint_);
auto* overview_grid = GetCurrentGrid();
// If virtual desks is enabled, we want to gradually shrink the dragged item
// as it gets closer to get dropped into a desk mini view.
if (virtual_desks_bar_enabled_) {
// TODO(sammiequon): There is a slight jump especially if we drag from the
// corner of a larger overview item, but this is necessary for the time
// being to prevent jumps from happening while shrinking. Investigate if we
// can satisfy all cases.
centerpoint = location_in_screen;
const auto iter = per_grid_desks_bar_data_.find(overview_grid);
DCHECK(iter != per_grid_desks_bar_data_.end());
const GridDesksBarData& desks_bar_data = iter->second;
if (desks_bar_data.shrink_bounds.Contains(location_in_screen)) {
// Update the mini views borders by checking if |location_in_screen|
// intersects. Only update the borders if the dragged item is not visible
// on all desks.
overview_grid->IntersectsWithDesksBar(
gfx::ToRoundedPoint(location_in_screen),
/*update_desks_bar_drag_details=*/
!DraggedItemIsVisibleOnAllDesks(item_), /*for_drop=*/false);
float value = 0.f;
if (centerpoint.y() < desks_bar_data.desks_bar_bounds.y() ||
centerpoint.y() > desks_bar_data.desks_bar_bounds.bottom()) {
// Coming vertically, this is the main use case. This is a ratio of the
// distance from |centerpoint| to the closest edge of |desk_bar_bounds|
// to the distance from |shrink_bounds| to |desk_bar_bounds|.
value = GetManhattanDistanceY(centerpoint.y(),
desks_bar_data.desks_bar_bounds) /
desks_bar_data.shrink_region_distance.y();
} else if (centerpoint.x() < desks_bar_data.desks_bar_bounds.x() ||
centerpoint.x() > desks_bar_data.desks_bar_bounds.right()) {
// Coming horizontally, this only happens if we are in landscape split
// view and someone drags an item to the other half, then up, then into
// the desks bar. Works same as vertically except using x-coordinates.
value = GetManhattanDistanceX(centerpoint.x(),
desks_bar_data.desks_bar_bounds) /
desks_bar_data.shrink_region_distance.x();
}
value = std::clamp(value, 0.f, 1.f);
const gfx::SizeF size_value =
gfx::Tween::SizeFValueBetween(1.f - value, original_scaled_size_,
desks_bar_data.on_desks_bar_item_size);
bounds.set_size(size_value);
} else {
bounds.set_size(original_scaled_size_);
}
}
if (is_eligible_for_drag_to_snap_) {
UpdateDragIndicatorsAndOverviewGrid(location_in_screen);
// The newly updated indicator state may cause the desks widget to be pushed
// down to make room for the top splitview guidance indicator when in
// portrait orientation in tablet mode.
overview_grid->MaybeUpdateDesksWidgetBounds();
}
if (!overview_grid->drop_target() &&
(!is_eligible_for_drag_to_snap_ ||
SplitViewDragIndicators::GetSnapPosition(
overview_grid->split_view_drag_indicators()
->current_window_dragging_state()) == SnapPosition::kNone)) {
overview_grid->AddDropTargetNotForDraggingFromThisGrid(item_->GetWindow(),
/*animate=*/true);
}
overview_session_->UpdateDropTargetsBackgroundVisibilities(
item_, location_in_screen);
bounds.set_x(centerpoint.x() - bounds.width() / 2.f);
bounds.set_y(centerpoint.y() - bounds.height() / 2.f);
item_->SetBounds(bounds, OVERVIEW_ANIMATION_NONE);
// The bar may be null if we have no desks in tablet mode.
if (auto* desks_bar_view = overview_grid->desks_bar_view()) {
auto* new_desk_button = desks_bar_view->new_desk_button();
// The header of window is shown during dragging. Overview item should be
// hovered on the new desk button with
// `kVerticalOverlappedLengthToActivateNewDeskButton` overlapped vertical
// area in order to activate the new desk button. There could be a lot of
// mistriggers with header shown if the new desk button is activated when
// the overview item intersects with it.
gfx::Rect effective_hovered_bounds(gfx::ToEnclosedRect(bounds));
effective_hovered_bounds.Inset(gfx::Insets::TLBR(
kVerticalOverlappedLengthToActivateNewDeskButton, 0, 0, 0));
const bool is_hovered_on_new_desk_button =
new_desk_button->GetBoundsInScreen().Intersects(
effective_hovered_bounds);
if (!is_hovered_on_new_desk_button) {
new_desk_button_scale_up_timer_.Stop();
} else if (!new_desk_button_scale_up_timer_.IsRunning() &&
new_desk_button->state() == DeskIconButton::State::kExpanded) {
if (g_skip_new_desk_button_scale_up_for_test) {
MaybeScaleUpNewDeskButton();
} else {
new_desk_button_scale_up_timer_.Start(
FROM_HERE, kScaleUpNewDeskButtonGracePeriod, this,
&OverviewWindowDragController::MaybeScaleUpNewDeskButton);
}
}
}
if (display_count_ > 1u)
item_->UpdateMirrorsForDragging(is_touch_dragging_);
}
OverviewWindowDragController::DragResult
OverviewWindowDragController::CompleteNormalDrag(
const gfx::PointF& location_in_screen) {
DCHECK_EQ(current_drag_behavior_, DragBehavior::kNormalDrag);
auto* item_overview_grid = item_->overview_grid();
CHECK(item_overview_grid->drop_target());
Shell::Get()->mouse_cursor_filter()->HideSharedEdgeIndicator();
item_->DestroyMirrorsForDragging();
overview_session_->RemoveDropTargets();
item_->UpdateShadowTypeForDrag(/*is_dragging=*/false);
const gfx::Point rounded_screen_point =
gfx::ToRoundedPoint(location_in_screen);
if (is_eligible_for_drag_to_snap_) {
// Update the split view divider bar status if necessary. The divider bar
// should be placed above the dragged window after drag ends. Note here the
// passed parameters |snap_position_| and |location_in_screen| won't be used
// in this function for this case, but they are passed in as placeholders.
aura::Window* window = item_->GetWindow();
SplitViewController::Get(item_->root_window())
->OnWindowDragEnded(
window, snap_position_, rounded_screen_point,
WindowSnapActionSource::kDragOrSelectOverviewWindowToSnap);
// Update window grid bounds and |snap_position_| in case the screen
// orientation was changed.
UpdateDragIndicatorsAndOverviewGrid(location_in_screen);
overview_session_->ResetSplitViewDragIndicatorsWindowDraggingStates();
item_->UpdateCannotSnapWarningVisibility(/*animate=*/true);
}
// This function has multiple exit positions, at each we must update the desks
// bar widget bounds. We can't do this before we attempt dropping the window
// on a desk mini_view, since this will change where it is relative to the
// current |location_in_screen|.
absl::Cleanup at_exit_runner = [] {
// Overview might have exited if we snapped windows on both sides.
auto* overview_controller = OverviewController::Get();
if (!overview_controller->InOverviewSession())
return;
for (auto& grid : overview_controller->overview_session()->grid_list())
grid->MaybeUpdateDesksWidgetBounds();
};
aura::Window* target_root = GetRootWindowBeingDraggedIn();
const bool is_dragged_to_other_display = target_root != item_->root_window();
auto* current_grid = GetCurrentGrid();
if (virtual_desks_bar_enabled_) {
item_->SetOpacity(original_opacity_);
// Attempt to move a window to a different desk.
if (current_grid->MaybeDropItemOnDeskMiniViewOrNewDeskButton(
rounded_screen_point, item_)) {
// Window was successfully moved to another desk, and |item_| was
// removed from the grid. It may never be accessed after this.
item_ = nullptr;
event_source_item_ = nullptr;
overview_session_->PositionWindows(/*animate=*/true);
RecordNormalDrag(kToDesk, is_dragged_to_other_display);
return DragResult::kDragToDesk;
}
}
// Snap a window if appropriate.
if (is_eligible_for_drag_to_snap_ && snap_position_ != SnapPosition::kNone) {
// Overview grid will be updated after window is snapped in splitview.
SnapWindow(SplitViewController::Get(target_root), snap_position_);
RecordNormalDrag(kToSnap, is_dragged_to_other_display);
MaybeRestoreNewDeskButtonState();
return DragResult::kSnap;
}
DCHECK(item_);
const bool dragged_item_is_visible_on_all_desks =
DraggedItemIsVisibleOnAllDesks(item_);
const bool item_intersects_other_display_desk_bar =
virtual_desks_bar_enabled_ &&
current_grid->IntersectsWithDesksBar(
gfx::ToRoundedPoint(location_in_screen),
/*update_desks_bar_drag_details=*/false, /*for_drop=*/false);
// Drop a window into overview because we have not done anything else with it.
// If the window is visible on all desks, only move it to another display if
// it doesn't intersect the other grid's desk bar.
if (is_dragged_to_other_display &&
!(dragged_item_is_visible_on_all_desks &&
item_intersects_other_display_desk_bar)) {
// Get the window and bounds from |item_| before removing it from its grid.
aura::Window* window = item_->GetWindow();
const gfx::RectF target_item_bounds = item_->target_bounds();
// Remove |item_| from overview. Leave the repositioning to the
// |OverviewItemMoveHelper|.
overview_session_->RemoveItem(item_, /*item_destroying=*/false,
/*reposition=*/false);
item_ = nullptr;
event_source_item_ = nullptr;
// The |OverviewItemMoveHelper| will self destruct when we move |window| to
// |target_root|.
new OverviewItemMoveHelper(window, target_item_bounds);
// Move |window| to |target_root|. The |OverviewItemMoveHelper| will take
// care of the rest.
window_util::MoveWindowToDisplay(window,
display::Screen::GetScreen()
->GetDisplayNearestWindow(target_root)
.id());
} else {
item_->set_should_restack_on_animation_end(true);
overview_session_->PositionWindows(/*animate=*/true);
MaybeRestoreNewDeskButtonState();
}
RecordNormalDrag(kToGrid, is_dragged_to_other_display);
return DragResult::kDropIntoOverview;
}
void OverviewWindowDragController::UpdateDragIndicatorsAndOverviewGrid(
const gfx::PointF& location_in_screen) {
CHECK(is_eligible_for_drag_to_snap_);
snap_position_ = GetSnapPosition(location_in_screen);
overview_session_->UpdateSplitViewDragIndicatorsWindowDraggingStates(
GetRootWindowBeingDraggedIn(),
SplitViewDragIndicators::ComputeWindowDraggingState(
/*is_dragging=*/true,
SplitViewDragIndicators::WindowDraggingState::kFromOverview,
snap_position_));
overview_session_->RearrangeDuringDrag(item_);
}
aura::Window* OverviewWindowDragController::GetRootWindowBeingDraggedIn()
const {
if (is_touch_dragging_) {
return item_->root_window();
}
auto* screen = display::Screen::GetScreen();
CHECK(screen);
auto display = screen->GetDisplayNearestPoint(screen->GetCursorScreenPoint());
return Shell::GetRootWindowForDisplayId(display.id());
}
SnapPosition OverviewWindowDragController::GetSnapPosition(
const gfx::PointF& location_in_screen) const {
CHECK(item_);
CHECK(is_eligible_for_drag_to_snap_);
gfx::Rect area =
screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
GetRootWindowBeingDraggedIn());
// If split view mode is active at the moment, and dragging an overview window
// to snap it to a position that already has a snapped window in place, we
// should show the preview window as soon as the window past the split divider
// bar.
aura::Window* root_window = GetRootWindowBeingDraggedIn();
SplitViewController* split_view_controller =
SplitViewController::Get(root_window);
if (!split_view_controller->CanSnapWindow(item_->GetWindow(),
chromeos::kDefaultSnapRatio)) {
return SnapPosition::kNone;
}
if (split_view_controller->InSplitViewMode()) {
// If we're trying to snap to a position that already has a snapped window:
aura::Window* default_snapped_window =
split_view_controller->GetDefaultSnappedWindow();
if (gfx::RectF(default_snapped_window->GetBoundsInScreen())
.Contains(location_in_screen)) {
return split_view_controller->GetPositionOfSnappedWindow(
default_snapped_window);
}
}
return ::ash::GetSnapPosition(
root_window, item_->GetWindow(), gfx::ToRoundedPoint(location_in_screen),
gfx::ToRoundedPoint(initial_event_location_),
/*snap_distance_from_edge=*/kDistanceFromEdgeDp,
/*minimum_drag_distance=*/kMinimumDragToSnapDistanceDp,
/*horizontal_edge_inset=*/area.width() *
kHighlightScreenPrimaryAxisRatio +
kHighlightScreenEdgePaddingDp,
/*vertical_edge_inset=*/area.height() * kHighlightScreenPrimaryAxisRatio +
kHighlightScreenEdgePaddingDp);
}
void OverviewWindowDragController::SnapWindow(
SplitViewController* split_view_controller,
SnapPosition snap_position) {
DCHECK_NE(snap_position, SnapPosition::kNone);
CHECK(!SplitViewController::Get(item_->root_window())->IsDividerAnimating());
aura::Window* window = item_->GetWindow();
// If `window` is currently fullscreen, snapping it will trigger a work area
// change, which triggers `OverviewSession::OnDisplayMetricsChanged`. Display
// changes normally end dragging for simplicity, but we need `item` to be
// nullptr before that happens so we can skip resetting the window gesture.
// See crbug.com/1330042 for more details. `item_` will be deleted after
// SplitViewController::SnapWindow().
item_ = nullptr;
event_source_item_ = nullptr;
split_view_controller->SnapWindow(
window, snap_position,
WindowSnapActionSource::kDragOrSelectOverviewWindowToSnap,
/*activate_window=*/true);
}
OverviewGrid* OverviewWindowDragController::GetCurrentGrid() const {
return overview_session_->GetGridWithRootWindow(
GetRootWindowBeingDraggedIn());
}
void OverviewWindowDragController::RecordNormalDrag(
NormalDragAction action,
bool is_dragged_to_other_display) const {
const bool is_tablet = display::Screen::GetScreen()->InTabletMode();
if (is_dragged_to_other_display) {
DCHECK(!is_touch_dragging_);
if (!is_tablet) {
constexpr OverviewDragAction kDrag[kNormalDragActionEnumSize] = {
OverviewDragAction::kToGridOtherDisplayClamshellMouse,
OverviewDragAction::kToDeskOtherDisplayClamshellMouse,
OverviewDragAction::kToSnapOtherDisplayClamshellMouse};
RecordDrag(kDrag[action]);
}
} else if (is_tablet) {
if (is_touch_dragging_) {
constexpr OverviewDragAction kDrag[kNormalDragActionEnumSize] = {
OverviewDragAction::kToGridSameDisplayTabletTouch,
OverviewDragAction::kToDeskSameDisplayTabletTouch,
OverviewDragAction::kToSnapSameDisplayTabletTouch};
RecordDrag(kDrag[action]);
}
} else {
constexpr OverviewDragAction kMouseDrag[kNormalDragActionEnumSize] = {
OverviewDragAction::kToGridSameDisplayClamshellMouse,
OverviewDragAction::kToDeskSameDisplayClamshellMouse,
OverviewDragAction::kToSnapSameDisplayClamshellMouse};
constexpr OverviewDragAction kTouchDrag[kNormalDragActionEnumSize] = {
OverviewDragAction::kToGridSameDisplayClamshellTouch,
OverviewDragAction::kToDeskSameDisplayClamshellTouch,
OverviewDragAction::kToSnapSameDisplayClamshellTouch};
RecordDrag(is_touch_dragging_ ? kTouchDrag[action] : kMouseDrag[action]);
}
}
void OverviewWindowDragController::RecordDragToClose(
DragToCloseAction action) const {
DCHECK(is_touch_dragging_);
constexpr OverviewDragAction kClamshellDrag[kDragToCloseActionEnumSize] = {
OverviewDragAction::kSwipeToCloseSuccessfulClamshellTouch,
OverviewDragAction::kSwipeToCloseCanceledClamshellTouch,
OverviewDragAction::kFlingToCloseClamshellTouch};
constexpr OverviewDragAction kTabletDrag[kDragToCloseActionEnumSize] = {
OverviewDragAction::kSwipeToCloseSuccessfulTabletTouch,
OverviewDragAction::kSwipeToCloseCanceledTabletTouch,
OverviewDragAction::kFlingToCloseTabletTouch};
RecordDrag(display::Screen::GetScreen()->InTabletMode()
? kTabletDrag[action]
: kClamshellDrag[action]);
}
void OverviewWindowDragController::MaybeScaleUpNewDeskButton() {
if (!item_ || !item_->overview_grid()) {
return;
}
// When there's only one window and it's snapped, overview mode will be
// ended. Thus we need to check whether `overview_session_` is being
// shutting down or not here before triggering `UpdateDeskIconButtonState`.
if (!overview_session_) {
return;
}
auto* overview_grid =
overview_session_->GetGridWithRootWindow(GetRootWindowBeingDraggedIn());
auto* desks_bar_view = overview_grid->desks_bar_view();
auto* new_desk_button = desks_bar_view->new_desk_button();
if (!new_desk_button->GetEnabled()) {
return;
}
// Do not reposition the windows while changing the desk icon button. This
// could cause items to shift around mid drag.
overview_session_->SuspendReposition();
desks_bar_view->UpdateDeskIconButtonState(
new_desk_button, /*target_state=*/DeskIconButton::State::kActive);
overview_session_->ResumeReposition();
}
} // namespace ash