| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/wm/overview/overview_window_drag_controller.h" |
| |
| #include <memory> |
| |
| #include "ash/screen_util.h" |
| #include "ash/shell.h" |
| #include "ash/wm/overview/overview_grid.h" |
| #include "ash/wm/overview/overview_item.h" |
| #include "ash/wm/overview/overview_session.h" |
| #include "ash/wm/overview/overview_utils.h" |
| #include "ash/wm/splitview/split_view_constants.h" |
| #include "ash/wm/splitview/split_view_drag_indicators.h" |
| #include "ash/wm/splitview/split_view_utils.h" |
| #include "ash/wm/window_positioning_utils.h" |
| #include "base/numerics/ranges.h" |
| #include "ui/aura/window.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 int kMinimumDragDistanceDp = 5; |
| // Items dragged to within |kDistanceFromEdgeDp| of the screen will get snapped |
| // even if they have not moved by |kMinimumDragDistanceDp|. |
| constexpr int kDistanceFromEdgeDp = 16; |
| // The minimum distance that an item must be moved before it is snapped. This |
| // prevents accidental snaps. |
| constexpr int kMinimumDragToSnapDistanceDp = 96; |
| // The minimum distance that an item must be moved before it is considered a |
| // drag event, if the drag starts in one of the snap regions. |
| constexpr int kMinimumDragDistanceAlreadyInSnapRegionDp = 48; |
| |
| // Flings with less velocity than this will not close the dragged item. |
| constexpr float kFlingToCloseVelocityThreshold = 2000.f; |
| constexpr float kItemMinOpacity = 0.4f; |
| |
| } // namespace |
| |
| OverviewWindowDragController::OverviewWindowDragController( |
| OverviewSession* overview_session) |
| : overview_session_(overview_session), |
| split_view_controller_(Shell::Get()->split_view_controller()) {} |
| |
| OverviewWindowDragController::~OverviewWindowDragController() = default; |
| |
| void OverviewWindowDragController::InitiateDrag( |
| OverviewItem* item, |
| const gfx::Point& location_in_screen) { |
| item_ = item; |
| previous_event_location_ = location_in_screen; |
| initial_event_location_ = location_in_screen; |
| if (ShouldAllowSplitView()) { |
| started_in_snap_region_ = |
| GetSnapPosition(location_in_screen) != SplitViewController::NONE; |
| } |
| current_drag_behavior_ = DragBehavior::kUndefined; |
| } |
| |
| void OverviewWindowDragController::Drag(const gfx::Point& location_in_screen) { |
| if (!did_move_) { |
| gfx::Vector2d distance = location_in_screen - previous_event_location_; |
| // Do not start dragging if the distance from |location_in_screen| to |
| // |previous_event_location_| is not greater than |kMinimumDragDistanceDp|. |
| if (std::abs(distance.x()) < kMinimumDragDistanceDp && |
| std::abs(distance.y()) < kMinimumDragDistanceDp) { |
| return; |
| } |
| |
| if (std::abs(distance.x()) < std::abs(distance.y())) { |
| current_drag_behavior_ = DragBehavior::kDragToClose; |
| original_opacity_ = item_->GetOpacity(); |
| overview_session_->GetGridWithRootWindow(item_->root_window()) |
| ->StartNudge(item_); |
| did_move_ = true; |
| } else if (ShouldAllowSplitView()) { |
| StartSplitViewDragMode(location_in_screen); |
| } |
| } |
| |
| int x_offset = 0; |
| // Update the state based on the drag behavior. |
| if (current_drag_behavior_ == DragBehavior::kDragToClose) { |
| // 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(static_cast<float>(location_in_screen.y()) - |
| initial_event_location_.y()) / |
| kDragToCloseDistanceThresholdDp; |
| overview_session_->GetGridWithRootWindow(item_->root_window()) |
| ->UpdateNudge(item_, val); |
| val = base::ClampToRange(val, 0.f, 1.f); |
| float opacity = original_opacity_; |
| if (opacity > kItemMinOpacity) |
| opacity = original_opacity_ - val * (original_opacity_ - kItemMinOpacity); |
| item_->SetOpacity(opacity); |
| } else if (current_drag_behavior_ == DragBehavior::kDragToSnap) { |
| UpdateDragIndicatorsAndOverviewGrid(location_in_screen); |
| x_offset = location_in_screen.x() - previous_event_location_.x(); |
| } |
| |
| // Update the split view divider bar status if necessary. If splitview is |
| // active when dragging the overview window, the split divider bar should be |
| // placed below the dragged window during dragging. |
| if (ShouldAllowSplitView()) |
| split_view_controller_->OnWindowDragStarted(item_->GetWindow()); |
| |
| // Update the dragged |item_|'s bounds accordingly. |
| gfx::Rect bounds(item_->target_bounds()); |
| bounds.Offset(x_offset, |
| location_in_screen.y() - previous_event_location_.y()); |
| item_->SetBounds(bounds, OVERVIEW_ANIMATION_NONE); |
| previous_event_location_ = location_in_screen; |
| } |
| |
| void OverviewWindowDragController::CompleteDrag( |
| const gfx::Point& location_in_screen) { |
| // Update the split view divider bar stuatus if necessary. The divider bar |
| // should be placed above the dragged window after drag ends. Note here the |
| // passed paramters |snap_position_| and |location_in_screen| won't be used in |
| // this function for this case, but they are passed in as placeholders. |
| if (ShouldAllowSplitView()) { |
| split_view_controller_->OnWindowDragEnded( |
| item_->GetWindow(), snap_position_, location_in_screen); |
| } |
| |
| // Update window grid bounds and |snap_position_| in case the screen |
| // orientation was changed. |
| if (current_drag_behavior_ == DragBehavior::kDragToSnap) { |
| UpdateDragIndicatorsAndOverviewGrid(location_in_screen); |
| overview_session_->SetSplitViewDragIndicatorsIndicatorState( |
| IndicatorState::kNone, gfx::Point()); |
| } |
| |
| if (!did_move_) { |
| ActivateDraggedWindow(); |
| } else if (current_drag_behavior_ == DragBehavior::kDragToClose) { |
| // If we are in drag to close mode 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(); |
| if (std::abs((location_in_screen - initial_event_location_).y()) > |
| kDragToCloseDistanceThresholdDp) { |
| item_->AnimateAndCloseWindow( |
| (location_in_screen - initial_event_location_).y() < 0); |
| } else { |
| item_->SetOpacity(original_opacity_); |
| overview_session_->PositionWindows(/*animate=*/true); |
| } |
| } else if (current_drag_behavior_ == DragBehavior::kDragToSnap) { |
| // If the window was dragged around but should not be snapped, move it back |
| // to overview window grid. |
| if (!ShouldUpdateDragIndicatorsOrSnap(location_in_screen) || |
| snap_position_ == SplitViewController::NONE) { |
| item_->set_should_restack_on_animation_end(true); |
| overview_session_->PositionWindows(/*animate=*/true); |
| } else { |
| SnapWindow(snap_position_); |
| } |
| } |
| did_move_ = false; |
| item_ = nullptr; |
| current_drag_behavior_ = DragBehavior::kNoDrag; |
| } |
| |
| void OverviewWindowDragController::StartSplitViewDragMode( |
| const gfx::Point& location_in_screen) { |
| DCHECK(ShouldAllowSplitView()); |
| |
| item_->ScaleUpSelectedItem( |
| OVERVIEW_ANIMATION_LAY_OUT_SELECTOR_ITEMS_IN_OVERVIEW); |
| |
| did_move_ = true; |
| current_drag_behavior_ = DragBehavior::kDragToSnap; |
| overview_session_->SetSplitViewDragIndicatorsIndicatorState( |
| CanSnapInSplitview(item_->GetWindow()) ? IndicatorState::kDragArea |
| : IndicatorState::kCannotSnap, |
| location_in_screen); |
| } |
| |
| void OverviewWindowDragController::Fling(const gfx::Point& 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) { |
| if (ShouldAllowSplitView()) { |
| split_view_controller_->OnWindowDragEnded( |
| item_->GetWindow(), snap_position_, location_in_screen); |
| } |
| |
| item_->AnimateAndCloseWindow( |
| (location_in_screen - initial_event_location_).y() < 0); |
| did_move_ = false; |
| item_ = nullptr; |
| current_drag_behavior_ = DragBehavior::kNoDrag; |
| return; |
| } |
| } |
| |
| // If the fling velocity was not high enough, or flings should be ignored, |
| // treat it as a scroll end event. |
| 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::State split_state = split_view_controller_->state(); |
| if (!ShouldAllowSplitView() || split_state == SplitViewController::NO_SNAP) { |
| overview_session_->SelectWindow(item_); |
| } else if (CanSnapInSplitview(item_->GetWindow())) { |
| SnapWindow(split_state == SplitViewController::LEFT_SNAPPED |
| ? SplitViewController::RIGHT |
| : SplitViewController::LEFT); |
| } else { |
| split_view_controller_->EndSplitView(); |
| overview_session_->SelectWindow(item_); |
| split_view_controller_->ShowAppCannotSnapToast(); |
| } |
| current_drag_behavior_ = DragBehavior::kNoDrag; |
| } |
| |
| void OverviewWindowDragController::ResetGesture() { |
| overview_session_->PositionWindows(/*animate=*/true); |
| if (ShouldAllowSplitView()) { |
| overview_session_->SetSplitViewDragIndicatorsIndicatorState( |
| IndicatorState::kNone, gfx::Point()); |
| } |
| // This function gets called after a long press release, which bypasses |
| // CompleteDrag but stops dragging as well, so reset |item_|. |
| item_ = nullptr; |
| current_drag_behavior_ = DragBehavior::kNoDrag; |
| } |
| |
| void OverviewWindowDragController::ResetOverviewSession() { |
| overview_session_ = nullptr; |
| } |
| |
| void OverviewWindowDragController::UpdateDragIndicatorsAndOverviewGrid( |
| const gfx::Point& location_in_screen) { |
| DCHECK(ShouldAllowSplitView()); |
| if (!ShouldUpdateDragIndicatorsOrSnap(location_in_screen)) |
| return; |
| |
| // Attempt to update the drag indicators and move the window grid only if the |
| // window is snappable. |
| if (!CanSnapInSplitview(item_->GetWindow())) { |
| snap_position_ = SplitViewController::NONE; |
| return; |
| } |
| |
| SplitViewController::SnapPosition last_snap_position = snap_position_; |
| snap_position_ = GetSnapPosition(location_in_screen); |
| |
| // If there is no current snapped window, update the window grid size if the |
| // dragged window can be snapped if dropped. |
| if (split_view_controller_->state() == SplitViewController::NO_SNAP && |
| snap_position_ != last_snap_position) { |
| // Do not reposition the item that is currently being dragged. |
| overview_session_->SetBoundsForOverviewGridsInScreenIgnoringWindow( |
| GetGridBounds(snap_position_), item_); |
| } |
| |
| // Show the cannot snap ui on the split view drag indicators if the window |
| // cannot be snapped, otherwise show the drag ui. |
| if (snap_position_ == SplitViewController::NONE) { |
| overview_session_->SetSplitViewDragIndicatorsIndicatorState( |
| CanSnapInSplitview(item_->GetWindow()) ? IndicatorState::kDragArea |
| : IndicatorState::kCannotSnap, |
| gfx::Point()); |
| return; |
| } |
| |
| // Display the preview area on the split view drag indicators. The split |
| // view drag indicators will calculate the preview area bounds. |
| overview_session_->SetSplitViewDragIndicatorsIndicatorState( |
| snap_position_ == SplitViewController::LEFT |
| ? IndicatorState::kPreviewAreaLeft |
| : IndicatorState::kPreviewAreaRight, |
| gfx::Point()); |
| } |
| |
| bool OverviewWindowDragController::ShouldUpdateDragIndicatorsOrSnap( |
| const gfx::Point& event_location) { |
| auto snap_position = GetSnapPosition(event_location); |
| const bool inverted = !IsCurrentScreenOrientationPrimary(); |
| // Note: in some orientations SplitViewController::LEFT is not physically on |
| // the left/top. |
| const bool on_the_left_or_top = |
| (!inverted && snap_position == SplitViewController::LEFT) || |
| (inverted && snap_position == SplitViewController::RIGHT); |
| |
| // Snap the window if it is less than |kDistanceFromEdgeDp| from the edge. |
| const bool landscape = IsCurrentScreenOrientationLandscape(); |
| gfx::Rect area( |
| screen_util::GetDisplayWorkAreaBoundsInParent(item_->GetWindow())); |
| ::wm::ConvertRectToScreen(item_->GetWindow()->GetRootWindow(), &area); |
| area.Inset(kDistanceFromEdgeDp, kDistanceFromEdgeDp); |
| if ((landscape && |
| (event_location.x() < area.x() || event_location.x() > area.right())) || |
| (!landscape && |
| (event_location.y() < area.y() || event_location.y() > area.bottom()))) { |
| return true; |
| } |
| |
| // The drag indicators can update or the item can snap even if the drag events |
| // are in the snap region, if the event has travelled past the threshold in |
| // the direction of the attempted snap region. |
| const gfx::Vector2d distance = event_location - initial_event_location_; |
| // Check the x-axis distance for landscape, y-axis distance for portrait. |
| const int distance_scalar = landscape ? distance.x() : distance.y(); |
| |
| // If not started in a snap region, snap if the item has been dragged |
| // |kMinimumDragDistanceDp|. This prevents accidental snaps. |
| if (!started_in_snap_region_ && |
| std::abs(distance_scalar) > kMinimumDragToSnapDistanceDp) { |
| return true; |
| } |
| |
| if (snap_position == SplitViewController::NONE) { |
| // If the event started in a snap region, but has since moved out set |
| // |started_in_snap_region_| to false. |event_location| is guarenteed to not |
| // be in a snap region so that the drag indicators are shown correctly and |
| // the snap mechanism works normally for the rest of the drag. |
| started_in_snap_region_ = false; |
| return true; |
| } |
| |
| // If the snap region is physically on the left/top side of the device, check |
| // that |distance_scalar| is less than |
| // -|kMinimumDragDistanceAlreadyInSnapRegionDp|. If the snap region is |
| // physically on the right/bottom side of the device, check that |
| // |distance_scalar| is greater than |
| // |kMinimumDragDistanceAlreadyInSnapRegionDp|. |
| return on_the_left_or_top |
| ? distance_scalar <= -kMinimumDragDistanceAlreadyInSnapRegionDp |
| : distance_scalar >= kMinimumDragDistanceAlreadyInSnapRegionDp; |
| } |
| |
| SplitViewController::SnapPosition OverviewWindowDragController::GetSnapPosition( |
| const gfx::Point& location_in_screen) const { |
| DCHECK(item_); |
| DCHECK(ShouldAllowSplitView()); |
| gfx::Rect area( |
| screen_util::GetDisplayWorkAreaBoundsInParent(item_->GetWindow())); |
| ::wm::ConvertRectToScreen(item_->GetWindow()->GetRootWindow(), &area); |
| |
| const bool is_landscape = IsCurrentScreenOrientationLandscape(); |
| const bool is_primary = IsCurrentScreenOrientationPrimary(); |
| |
| // 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. |
| if (split_view_controller_->IsSplitViewModeActive()) { |
| const int position = |
| is_landscape ? location_in_screen.x() : location_in_screen.y(); |
| SplitViewController::SnapPosition default_snap_position = |
| split_view_controller_->default_snap_position(); |
| // If we're trying to snap to a position that already has a snapped window: |
| const bool re_snap = |
| is_primary == (position < split_view_controller_->divider_position() == |
| (default_snap_position == SplitViewController::LEFT)); |
| if (re_snap) |
| return default_snap_position; |
| } |
| |
| if (is_landscape) { |
| // The window can be snapped if it reaches close enough to the screen |
| // edge of the screen (on primary axis). The edge insets are a fixed ratio |
| // of the screen plus some padding. This matches the drag indicators ui. |
| const int screen_edge_inset_for_drag = |
| area.width() * kHighlightScreenPrimaryAxisRatio + |
| kHighlightScreenEdgePaddingDp; |
| area.Inset(screen_edge_inset_for_drag, 0); |
| if (location_in_screen.x() <= area.x()) { |
| return is_primary ? SplitViewController::LEFT |
| : SplitViewController::RIGHT; |
| } |
| if (location_in_screen.x() >= area.right() - 1) { |
| return is_primary ? SplitViewController::RIGHT |
| : SplitViewController::LEFT; |
| } |
| return SplitViewController::NONE; |
| } else { |
| const int screen_edge_inset_for_drag = |
| area.height() * kHighlightScreenPrimaryAxisRatio + |
| kHighlightScreenEdgePaddingDp; |
| area.Inset(0, screen_edge_inset_for_drag); |
| if (location_in_screen.y() <= area.y()) { |
| return is_primary ? SplitViewController::LEFT |
| : SplitViewController::RIGHT; |
| } |
| if (location_in_screen.y() >= area.bottom() - 1) { |
| return is_primary ? SplitViewController::RIGHT |
| : SplitViewController::LEFT; |
| } |
| return SplitViewController::NONE; |
| } |
| } |
| |
| gfx::Rect OverviewWindowDragController::GetGridBounds( |
| SplitViewController::SnapPosition snap_position) { |
| aura::Window* pending_snapped_window = item_->GetWindow(); |
| switch (snap_position) { |
| case SplitViewController::NONE: |
| return gfx::Rect( |
| screen_util::GetDisplayWorkAreaBoundsInParentForDefaultContainer( |
| pending_snapped_window)); |
| case SplitViewController::LEFT: |
| return split_view_controller_->GetSnappedWindowBoundsInScreen( |
| pending_snapped_window, SplitViewController::RIGHT); |
| case SplitViewController::RIGHT: |
| return split_view_controller_->GetSnappedWindowBoundsInScreen( |
| pending_snapped_window, SplitViewController::LEFT); |
| } |
| |
| NOTREACHED(); |
| return gfx::Rect(); |
| } |
| |
| void OverviewWindowDragController::SnapWindow( |
| SplitViewController::SnapPosition snap_position) { |
| DCHECK_NE(snap_position, SplitViewController::NONE); |
| |
| // |item_| will be deleted after SplitViewController::SnapWindow(). |
| split_view_controller_->SnapWindow(item_->GetWindow(), snap_position); |
| item_ = nullptr; |
| } |
| |
| } // namespace ash |