chromium/ash/wm/collision_detection/collision_detection_utils.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/wm/collision_detection/collision_detection_utils.h"

#include "ash/app_list/app_list_controller_impl.h"
#include "ash/capture_mode/capture_mode_controller.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/shell.h"
#include "ash/system/notification_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->IsVisible() && !window->GetTargetBounds().IsEmpty()) {
    return true;
  }
  if (window->is_destroying()) {
    return true;
  }
  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 (aura::Window* 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 (aura::Window* 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 (aura::Window* 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));
  }

  for (auto* window :
       CaptureModeController::Get()->GetWindowsForCollisionAvoidance()) {
    rects.push_back(ComputeCollisionRectFromBounds(window->GetTargetBounds(),
                                                   window->parent()));
  }

  // Avoid clamshell-mode launcher bubble.
  auto* app_list_controller = Shell::Get()->app_list_controller();
  if (app_list_controller && !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();
  }
}

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