blob: 3938a5b8fc8c4f2c836b1b34f143790f2c986ea2 [file] [log] [blame]
// Copyright 2018 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/pip/pip_positioner.h"
#include <algorithm>
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/root_window_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "base/logging.h"
#include "ui/aura/window.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/keyboard/keyboard_controller.h"
namespace ash {
namespace {
const int kPipWorkAreaInsetsDp = 8;
const float kPipDismissMovementProportion = 1.5f;
enum { GRAVITY_LEFT, GRAVITY_RIGHT, GRAVITY_TOP, GRAVITY_BOTTOM };
// Returns the result of adjusting |bounds| according to |gravity| inside
// |region|.
gfx::Rect GetAdjustedBoundsByGravity(const gfx::Rect& bounds,
const gfx::Rect& region,
int gravity) {
switch (gravity) {
case GRAVITY_LEFT:
return gfx::Rect(region.x(), bounds.y(), bounds.width(), bounds.height());
case GRAVITY_RIGHT:
return gfx::Rect(region.right() - bounds.width(), bounds.y(),
bounds.width(), bounds.height());
case GRAVITY_TOP:
return gfx::Rect(bounds.x(), region.y(), bounds.width(), bounds.height());
case GRAVITY_BOTTOM:
return gfx::Rect(bounds.x(), region.bottom() - bounds.height(),
bounds.width(), bounds.height());
default:
NOTREACHED();
}
return bounds;
}
// Returns the gravity that would make |bounds| fall to the closest edge of
// |region|. If |bounds| is outside of |region| then it will return the gravity
// as if |bounds| had fallen outside of |region|. See the below diagram for what
// the gravity regions look like for a point.
// \ TOP /
// \____/ R
// L |\ /| I
// E | \/ | G
// F | /\ | H
// T |/__\| T
// / \
// /BOTTOM
int 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 GRAVITY_LEFT;
} else if (insets.right() == minimum_edge_dist) {
return GRAVITY_RIGHT;
} else if (insets.top() == minimum_edge_dist) {
return GRAVITY_TOP;
} else {
return GRAVITY_BOTTOM;
}
}
std::vector<gfx::Rect> CollectCollisionRects(const display::Display& display) {
std::vector<gfx::Rect> rects;
auto* root_window = Shell::GetRootWindowForDisplayId(display.id());
if (root_window) {
auto* settings_bubble_container =
root_window->GetChildById(kShellWindowId_SettingBubbleContainer);
for (auto* window : settings_bubble_container->children()) {
if (!window->IsVisible() && !window->GetTargetBounds().IsEmpty())
continue;
// Use the target bounds in case an animation is in progress.
rects.push_back(window->GetTargetBounds());
rects.back().Inset(-kPipWorkAreaInsetsDp, -kPipWorkAreaInsetsDp);
}
}
auto* keyboard_controller = keyboard::KeyboardController::Get();
if (keyboard_controller->IsEnabled() &&
keyboard_controller->GetActiveContainerType() ==
keyboard::ContainerType::FLOATING &&
keyboard_controller->GetRootWindow() == root_window &&
!keyboard_controller->visual_bounds_in_screen().IsEmpty()) {
rects.push_back(keyboard_controller->visual_bounds_in_screen());
rects.back().Inset(-kPipWorkAreaInsetsDp, -kPipWorkAreaInsetsDp);
}
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 PIP window will move.
// Case #1: Touches 0 obstacles. This corresponds to not moving.
// Case #2: Touches 1 obstacle.
// The PIP window will be touching one edge of the obstacle.
// Case #3: Touches 2 obstacles.
// The PIP 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 PIP 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 PipPositioner::GetMovementArea(const display::Display& display) {
gfx::Rect work_area = display.work_area();
auto* keyboard_controller = keyboard::KeyboardController::Get();
// Include keyboard if it's not floating.
if (keyboard_controller->IsEnabled() &&
keyboard_controller->GetActiveContainerType() !=
keyboard::ContainerType::FLOATING) {
gfx::Rect keyboard_bounds = keyboard_controller->visual_bounds_in_screen();
work_area.Subtract(keyboard_bounds);
}
work_area.Inset(kPipWorkAreaInsetsDp, kPipWorkAreaInsetsDp);
return work_area;
}
gfx::Rect PipPositioner::GetBoundsForDrag(const display::Display& display,
const gfx::Rect& bounds) {
gfx::Rect drag_bounds = bounds;
drag_bounds.AdjustToFit(GetMovementArea(display));
drag_bounds = AvoidObstacles(display, drag_bounds);
return drag_bounds;
}
gfx::Rect PipPositioner::GetRestingPosition(const display::Display& display,
const gfx::Rect& bounds) {
gfx::Rect resting_bounds = bounds;
gfx::Rect area = GetMovementArea(display);
resting_bounds.AdjustToFit(area);
const int gravity = GetGravityToClosestEdge(resting_bounds, area);
resting_bounds = GetAdjustedBoundsByGravity(resting_bounds, area, gravity);
return AvoidObstacles(display, resting_bounds);
}
gfx::Rect PipPositioner::GetDismissedPosition(const display::Display& display,
const gfx::Rect& bounds) {
gfx::Rect work_area = GetMovementArea(display);
const int gravity = GetGravityToClosestEdge(bounds, work_area);
// Allow the bounds to move at most |kPipDismissMovementProportion| of the
// length of the bounds in the direction of movement.
gfx::Rect bounds_movement_area = bounds;
bounds_movement_area.Inset(-bounds.width() * kPipDismissMovementProportion,
-bounds.height() * kPipDismissMovementProportion);
gfx::Rect dismissed_bounds =
GetAdjustedBoundsByGravity(bounds, bounds_movement_area, gravity);
// If the PIP window isn't close enough to the edge of the screen, don't slide
// it out.
return work_area.Intersects(dismissed_bounds) ? bounds : dismissed_bounds;
}
gfx::Rect PipPositioner::GetPositionAfterMovementAreaChange(
wm::WindowState* window_state) {
// Restore to previous bounds if we have them. This lets us move the PIP
// window back to its original bounds after transient movement area changes,
// like the keyboard popping up and pushing the PIP window up.
const gfx::Rect bounds = window_state->HasRestoreBounds()
? window_state->GetRestoreBoundsInScreen()
: window_state->window()->GetBoundsInScreen();
return GetRestingPosition(window_state->GetDisplay(), bounds);
}
gfx::Rect PipPositioner::AvoidObstacles(const display::Display& display,
const gfx::Rect& bounds) {
gfx::Rect work_area = GetMovementArea(display);
auto rects = CollectCollisionRects(display);
// The worst case for this should be: floating keyboard + one system tray +
// the volume shifter, which is 3 windows.
DCHECK(rects.size() <= 15) << "PipPositioner::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);
}
gfx::Rect PipPositioner::AvoidObstaclesInternal(
const gfx::Rect& work_area,
const std::vector<gfx::Rect>& rects,
const gfx::Rect& bounds) {
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(bounds.width() / 2, bounds.height() / 2,
(bounds.width() - 1) / 2, (bounds.height() - 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| size.
rect.Inset(-(bounds.width() - 1) / 2, -(bounds.height() - 1) / 2,
-bounds.width() / 2, -bounds.height() / 2);
}
gfx::Point moved_center = ComputeBestCandidatePoint(
bounds.CenterPoint(), inset_work_area, inset_rects);
gfx::Rect moved_bounds = bounds;
moved_bounds.Offset(moved_center - bounds.CenterPoint());
return moved_bounds;
}
} // namespace ash