| // 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/wm/collision_detection/collision_detection_utils.h" |
| |
| #include "ash/app_list/app_list_controller_impl.h" |
| #include "ash/capture_mode/capture_mode_camera_controller.h" |
| #include "ash/capture_mode/capture_mode_controller.h" |
| #include "ash/capture_mode/capture_mode_session.h" |
| #include "ash/constants/ash_features.h" |
| #include "ash/keyboard/ui/keyboard_ui_controller.h" |
| #include "ash/public/cpp/shelf_types.h" |
| #include "ash/public/cpp/shell_window_ids.h" |
| #include "ash/shelf/shelf.h" |
| #include "ash/shelf/shelf_layout_manager.h" |
| #include "ash/shell.h" |
| #include "ash/system/message_center/ash_message_popup_collection.h" |
| #include "ash/wm/work_area_insets.h" |
| #include "ui/base/class_property.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/wm/core/coordinate_conversion.h" |
| |
| DEFINE_UI_CLASS_PROPERTY_TYPE(ash::CollisionDetectionUtils::RelativePriority) |
| |
| namespace ash { |
| |
| namespace { |
| |
| // A property key to store whether the a window should be ignored for window |
| // collision detection. For example, StatusBubble windows. |
| DEFINE_UI_CLASS_PROPERTY_KEY(bool, kIgnoreForWindowCollisionDetection, false) |
| |
| // A property key to store a window's collision detection priority, used to |
| // resolve collisions between multiple windows which are both using |
| // CollisionDetectionUtils. |
| DEFINE_UI_CLASS_PROPERTY_KEY( |
| CollisionDetectionUtils::RelativePriority, |
| kCollisionDetectionRelativePriority, |
| CollisionDetectionUtils::RelativePriority::kDefault) |
| |
| bool ShouldIgnoreWindowForCollision( |
| const aura::Window* window, |
| CollisionDetectionUtils::RelativePriority priority) { |
| if (window->GetProperty(kIgnoreForWindowCollisionDetection)) |
| return true; |
| |
| DCHECK_NE(CollisionDetectionUtils::RelativePriority::kDefault, priority); |
| |
| // Only ignore a window if our priority is greater than theirs. Lower priority |
| // windows need to move for higher priority windows, and windows do not need |
| // to move for a window of their own priority. |
| return static_cast<int>(priority) >= |
| static_cast<int>( |
| window->GetProperty(kCollisionDetectionRelativePriority)); |
| } |
| |
| gfx::Rect ComputeCollisionRectFromBounds(const gfx::Rect& bounds, |
| const aura::Window* parent, |
| bool inset = true) { |
| gfx::Rect collision_rect = bounds; |
| ::wm::ConvertRectToScreen(parent, &collision_rect); |
| if (inset) { |
| collision_rect.Inset(-kCollisionWindowWorkAreaInsetsDp); |
| } |
| return collision_rect; |
| } |
| |
| std::vector<gfx::Rect> CollectCollisionRects( |
| const display::Display& display, |
| CollisionDetectionUtils::RelativePriority priority) { |
| DCHECK_NE(CollisionDetectionUtils::RelativePriority::kDefault, priority); |
| std::vector<gfx::Rect> rects; |
| auto* root_window = Shell::GetRootWindowForDisplayId(display.id()); |
| if (root_window) { |
| // Check SettingsBubbleContainer windows. |
| auto* settings_bubble_container = |
| root_window->GetChildById(kShellWindowId_SettingBubbleContainer); |
| for (auto* window : settings_bubble_container->children()) { |
| if (!window->IsVisible() && !window->GetTargetBounds().IsEmpty()) |
| continue; |
| if (ShouldIgnoreWindowForCollision(window, priority)) |
| continue; |
| // Use the target bounds in case an animation is in progress. |
| rects.push_back(ComputeCollisionRectFromBounds(window->GetTargetBounds(), |
| window->parent())); |
| } |
| |
| // Check auto-hide shelf, which isn't included normally in the work area: |
| auto* shelf = Shelf::ForWindow(root_window); |
| auto* shelf_window = shelf->GetWindow(); |
| if (shelf->IsVisible() && |
| !ShouldIgnoreWindowForCollision(shelf_window, priority)) { |
| rects.push_back(ComputeCollisionRectFromBounds( |
| shelf_window->GetTargetBounds(), shelf_window->parent())); |
| } |
| |
| // Explicitly add popup notifications as they are not in the notification |
| // tray. |
| auto* shelf_container = |
| root_window->GetChildById(kShellWindowId_ShelfContainer); |
| for (auto* window : shelf_container->children()) { |
| if (window->IsVisible() && !window->GetTargetBounds().IsEmpty() && |
| window->GetName() == |
| AshMessagePopupCollection::kMessagePopupWidgetName && |
| !ShouldIgnoreWindowForCollision(window, priority)) { |
| rects.push_back(ComputeCollisionRectFromBounds( |
| window->GetTargetBounds(), window->parent())); |
| } |
| } |
| |
| // The hotseat doesn't span the whole width of the display, but to allow |
| // a PIP window to be slided horizontally along the hotseat, we extend the |
| // width of the hotseat to that of the display. |
| auto* hotseat_widget = shelf->hotseat_widget(); |
| if (hotseat_widget) { |
| auto* hotseat_window = hotseat_widget->GetNativeWindow(); |
| gfx::Rect hotseat_rect = |
| shelf->IsHorizontalAlignment() |
| ? gfx::Rect(root_window->bounds().x(), |
| hotseat_window->GetTargetBounds().y(), |
| root_window->bounds().width(), |
| hotseat_window->GetTargetBounds().height()) |
| : gfx::Rect(hotseat_window->GetTargetBounds().x(), |
| root_window->bounds().y(), |
| hotseat_window->GetTargetBounds().width(), |
| root_window->bounds().height()); |
| if (hotseat_widget->state() != HotseatState::kHidden && |
| hotseat_widget->state() != HotseatState::kNone && |
| !ShouldIgnoreWindowForCollision(hotseat_window, priority)) { |
| rects.push_back(ComputeCollisionRectFromBounds( |
| hotseat_rect, hotseat_window->parent())); |
| } |
| } |
| |
| // Check the Automatic Clicks windows. |
| // TODO(Katie): The PIP isn't re-triggered to check the accessibility bubble |
| // windows when the autoclick window moves, just when the PIP moves or |
| // another system window. Need to ensure that changing the autoclick menu |
| // position triggers the PIP to re-check its bounds. crbug.com/954546. |
| auto* accessibility_bubble_container = |
| root_window->GetChildById(kShellWindowId_AccessibilityBubbleContainer); |
| for (auto* window : accessibility_bubble_container->children()) { |
| if (!window->IsVisible() && !window->GetTargetBounds().IsEmpty()) |
| continue; |
| if (ShouldIgnoreWindowForCollision(window, priority)) |
| continue; |
| // Use the target bounds in case an animation is in progress. |
| if (priority == |
| CollisionDetectionUtils::RelativePriority::kAutomaticClicksMenu && |
| window->GetProperty(kCollisionDetectionRelativePriority) == |
| CollisionDetectionUtils::RelativePriority:: |
| kAutomaticClicksScrollMenu) { |
| // The autoclick menu widget and autoclick scroll menu widget are 0dip |
| // apart because they must be drawn at 8dips apart including drop |
| // shadow. This special case means we should not add an inset if we |
| // are calculating collision rects for the autoclick menu. |
| rects.push_back(ComputeCollisionRectFromBounds( |
| window->GetTargetBounds(), window->parent(), |
| false /* do not inset */)); |
| } else { |
| rects.push_back(ComputeCollisionRectFromBounds( |
| window->GetTargetBounds(), window->parent())); |
| } |
| } |
| } |
| |
| auto* keyboard_controller = keyboard::KeyboardUIController::Get(); |
| if (keyboard_controller->IsEnabled() && |
| keyboard_controller->GetActiveContainerType() == |
| keyboard::ContainerType::kFloating && |
| keyboard_controller->GetRootWindow() == root_window && |
| !keyboard_controller->GetVisualBoundsInScreen().IsEmpty() && |
| !ShouldIgnoreWindowForCollision(keyboard_controller->GetKeyboardWindow(), |
| priority)) { |
| rects.push_back(ComputeCollisionRectFromBounds( |
| keyboard_controller->visual_bounds_in_root(), |
| /*parent=*/root_window)); |
| } |
| |
| // Check the capture bar if capture mode is active. |
| auto* capture_mode_controller = CaptureModeController::Get(); |
| if (capture_mode_controller->IsActive()) { |
| aura::Window* capture_bar_window = |
| capture_mode_controller->capture_mode_session() |
| ->capture_mode_bar_widget() |
| ->GetNativeWindow(); |
| rects.push_back(ComputeCollisionRectFromBounds( |
| capture_bar_window->GetTargetBounds(), capture_bar_window->parent())); |
| } |
| |
| // Check the camera preview if it exists. |
| auto* camera_preview_widget = |
| capture_mode_controller->camera_controller()->camera_preview_widget(); |
| if (camera_preview_widget && camera_preview_widget->IsVisible()) { |
| aura::Window* camera_preview_window = |
| camera_preview_widget->GetNativeWindow(); |
| rects.push_back( |
| ComputeCollisionRectFromBounds(camera_preview_window->GetTargetBounds(), |
| camera_preview_window->parent())); |
| } |
| |
| // Avoid clamshell-mode launcher bubble. |
| auto* app_list_controller = Shell::Get()->app_list_controller(); |
| if (!Shell::Get()->IsInTabletMode() && |
| app_list_controller->GetTargetVisibility(display.id())) { |
| aura::Window* window = app_list_controller->GetWindow(); |
| if (window) { |
| rects.push_back(ComputeCollisionRectFromBounds(window->GetTargetBounds(), |
| window->parent())); |
| } |
| } |
| |
| return rects; |
| } |
| |
| // Finds the candidate points |center| could be moved to. One of these points |
| // is guaranteed to be the minimum distance to move |center| to make it not |
| // intersect any of the rectangles in |collision_rects|. |
| std::vector<gfx::Point> CollectCandidatePoints( |
| const gfx::Point& center, |
| const std::vector<gfx::Rect>& collision_rects) { |
| std::vector<gfx::Point> candidate_points; |
| candidate_points.reserve(4 * collision_rects.size() * collision_rects.size() + |
| 4 * collision_rects.size() + 1); |
| // There are four cases for how the window will move. |
| // Case #1: Touches 0 obstacles. This corresponds to not moving. |
| // Case #2: Touches 1 obstacle. |
| // The result window will be touching one edge of the obstacle. |
| // Case #3: Touches 2 obstacles. |
| // The result window will be touching one horizontal and one vertical edge |
| // from two different obstacles. |
| // Case #4: Touches more than 2 obstacles. This is handled in case #3. |
| |
| // Case #2. |
| // Rects include the left and top edges, so subtract 1 for those. |
| // Prioritize horizontal movement before vertical movement. |
| bool intersects = false; |
| for (const auto& rectA : collision_rects) { |
| intersects = intersects || rectA.Contains(center); |
| candidate_points.emplace_back(rectA.x() - 1, center.y()); |
| candidate_points.emplace_back(rectA.right(), center.y()); |
| candidate_points.emplace_back(center.x(), rectA.y() - 1); |
| candidate_points.emplace_back(center.x(), rectA.bottom()); |
| } |
| // Case #1: Touching zero obstacles, so don't move the window. |
| if (!intersects) |
| return {}; |
| |
| // Case #3: Add candidate points corresponding to each pair of horizontal |
| // and vertical edges. |
| for (const auto& rectA : collision_rects) { |
| for (const auto& rectB : collision_rects) { |
| // Continuing early here isn't necessary but makes fewer candidate points. |
| if (&rectA == &rectB) |
| continue; |
| candidate_points.emplace_back(rectA.x() - 1, rectB.y() - 1); |
| candidate_points.emplace_back(rectA.x() - 1, rectB.bottom()); |
| candidate_points.emplace_back(rectA.right(), rectB.y() - 1); |
| candidate_points.emplace_back(rectA.right(), rectB.bottom()); |
| } |
| } |
| return candidate_points; |
| } |
| |
| // Finds the candidate point with the shortest distance to |center| that is |
| // inside |work_area| and does not intersect any gfx::Rect in |rects|. |
| gfx::Point ComputeBestCandidatePoint(const gfx::Point& center, |
| const gfx::Rect& work_area, |
| const std::vector<gfx::Rect>& rects) { |
| auto candidate_points = CollectCandidatePoints(center, rects); |
| int64_t best_dist = -1; |
| gfx::Point best_point = center; |
| for (const auto& point : candidate_points) { |
| if (!work_area.Contains(point)) |
| continue; |
| bool viable = true; |
| for (const auto& rect : rects) { |
| if (rect.Contains(point)) { |
| viable = false; |
| break; |
| } |
| } |
| if (!viable) |
| continue; |
| int64_t dist = (point - center).LengthSquared(); |
| if (dist < best_dist || best_dist == -1) { |
| best_dist = dist; |
| best_point = point; |
| } |
| } |
| return best_point; |
| } |
| |
| } // namespace |
| |
| gfx::Rect CollisionDetectionUtils::GetMovementArea( |
| const display::Display& display) { |
| gfx::Rect work_area = |
| WorkAreaInsets::ForWindow(Shell::GetRootWindowForDisplayId(display.id())) |
| ->user_work_area_bounds(); |
| |
| work_area.Inset(kCollisionWindowWorkAreaInsetsDp); |
| return work_area; |
| } |
| |
| gfx::Rect CollisionDetectionUtils::AdjustToFitMovementAreaByGravity( |
| const display::Display& display, |
| const gfx::Rect& bounds_in_screen) { |
| gfx::Rect resting_bounds = bounds_in_screen; |
| gfx::Rect area = GetMovementArea(display); |
| resting_bounds.AdjustToFit(area); |
| const CollisionDetectionUtils::Gravity gravity = |
| GetGravityToClosestEdge(resting_bounds, area); |
| return GetAdjustedBoundsByGravity(resting_bounds, area, gravity); |
| } |
| |
| gfx::Rect CollisionDetectionUtils::GetRestingPosition( |
| const display::Display& display, |
| const gfx::Rect& bounds_in_screen, |
| CollisionDetectionUtils::RelativePriority priority) { |
| gfx::Rect resting_bounds = |
| AdjustToFitMovementAreaByGravity(display, bounds_in_screen); |
| return AvoidObstacles(display, resting_bounds, priority); |
| } |
| |
| void CollisionDetectionUtils::IgnoreWindowForCollisionDetection( |
| aura::Window* window) { |
| window->SetProperty(kIgnoreForWindowCollisionDetection, true); |
| } |
| |
| void CollisionDetectionUtils::MarkWindowPriorityForCollisionDetection( |
| aura::Window* window, |
| RelativePriority priority) { |
| window->SetProperty(kCollisionDetectionRelativePriority, priority); |
| } |
| |
| gfx::Rect CollisionDetectionUtils::AvoidObstacles( |
| const display::Display& display, |
| const gfx::Rect& bounds_in_screen, |
| RelativePriority priority) { |
| gfx::Rect work_area = GetMovementArea(display); |
| auto rects = CollectCollisionRects(display, priority); |
| // The worst case for this should be: floating keyboard + one system tray + |
| // the volume shifter + autoclick bubble menu, which is 4 windows. |
| DCHECK(rects.size() <= 15) |
| << "CollisionDetectionUtils::AvoidObstacles is N^3 and " |
| "should be optimized if there are a lot of " |
| "windows. Please see crrev.com/c/1221427 for a " |
| "description of an N^2 algorithm."; |
| return AvoidObstaclesInternal(work_area, rects, bounds_in_screen, priority); |
| } |
| |
| // Returns the result of adjusting |bounds| according to |gravity| inside |
| // |region|. |
| gfx::Rect CollisionDetectionUtils::GetAdjustedBoundsByGravity( |
| const gfx::Rect& bounds, |
| const gfx::Rect& region, |
| CollisionDetectionUtils::Gravity gravity) { |
| switch (gravity) { |
| case CollisionDetectionUtils::Gravity::kGravityLeft: |
| return gfx::Rect(region.x(), bounds.y(), bounds.width(), bounds.height()); |
| case CollisionDetectionUtils::Gravity::kGravityRight: |
| return gfx::Rect(region.right() - bounds.width(), bounds.y(), |
| bounds.width(), bounds.height()); |
| case CollisionDetectionUtils::Gravity::kGravityTop: |
| return gfx::Rect(bounds.x(), region.y(), bounds.width(), bounds.height()); |
| case CollisionDetectionUtils::Gravity::kGravityBottom: |
| return gfx::Rect(bounds.x(), region.bottom() - bounds.height(), |
| bounds.width(), bounds.height()); |
| default: |
| NOTREACHED(); |
| } |
| return bounds; |
| } |
| |
| CollisionDetectionUtils::Gravity |
| CollisionDetectionUtils::GetGravityToClosestEdge(const gfx::Rect& bounds, |
| const gfx::Rect& region) { |
| const gfx::Insets insets = region.InsetsFrom(bounds); |
| int minimum_edge_dist = std::min(insets.left(), insets.right()); |
| minimum_edge_dist = std::min(minimum_edge_dist, insets.top()); |
| minimum_edge_dist = std::min(minimum_edge_dist, insets.bottom()); |
| |
| if (insets.left() == minimum_edge_dist) { |
| return CollisionDetectionUtils::Gravity::kGravityLeft; |
| } else if (insets.right() == minimum_edge_dist) { |
| return CollisionDetectionUtils::Gravity::kGravityRight; |
| } else if (insets.top() == minimum_edge_dist) { |
| return CollisionDetectionUtils::Gravity::kGravityTop; |
| } else { |
| return CollisionDetectionUtils::Gravity::kGravityBottom; |
| } |
| } |
| |
| gfx::Rect CollisionDetectionUtils::AvoidObstaclesInternal( |
| const gfx::Rect& work_area, |
| const std::vector<gfx::Rect>& rects, |
| const gfx::Rect& bounds_in_screen, |
| RelativePriority priority) { |
| gfx::Rect inset_work_area = work_area; |
| |
| // For even sized bounds, there is no 'center' integer point, so we need |
| // to adjust the obstacles and work area to account for this. |
| inset_work_area.Inset(gfx::Insets::TLBR( |
| bounds_in_screen.height() / 2, bounds_in_screen.width() / 2, |
| (bounds_in_screen.height() - 1) / 2, (bounds_in_screen.width() - 1) / 2)); |
| std::vector<gfx::Rect> inset_rects(rects); |
| for (auto& rect : inset_rects) { |
| // Reduce the collision resolution problem from rectangles-rectangle |
| // resolution to rectangles-point resolution, by expanding each obstacle |
| // by |bounds_in_screen| size. |
| rect.Inset(gfx::Insets::TLBR(-(bounds_in_screen.height() - 1) / 2, |
| -(bounds_in_screen.width() - 1) / 2, |
| -bounds_in_screen.height() / 2, |
| -bounds_in_screen.width() / 2)); |
| } |
| |
| gfx::Point moved_center = ComputeBestCandidatePoint( |
| bounds_in_screen.CenterPoint(), inset_work_area, inset_rects); |
| gfx::Rect moved_bounds = bounds_in_screen; |
| moved_bounds.Offset(moved_center - bounds_in_screen.CenterPoint()); |
| return moved_bounds; |
| } |
| |
| } // namespace ash |