1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "ash/wm/collision_detection/collision_detection_utils.h"
6 
7 #include "ash/keyboard/ui/keyboard_ui_controller.h"
8 #include "ash/public/cpp/shelf_types.h"
9 #include "ash/public/cpp/shell_window_ids.h"
10 #include "ash/shelf/shelf.h"
11 #include "ash/shelf/shelf_layout_manager.h"
12 #include "ash/shell.h"
13 #include "ash/wm/work_area_insets.h"
14 #include "base/macros.h"
15 #include "ui/base/class_property.h"
16 #include "ui/wm/core/coordinate_conversion.h"
17 
18 DEFINE_UI_CLASS_PROPERTY_TYPE(ash::CollisionDetectionUtils::RelativePriority)
19 
20 namespace ash {
21 
22 namespace {
23 
24 // A property key to store whether the a window should be ignored for window
25 // collision detection. For example, StatusBubble windows.
DEFINE_UI_CLASS_PROPERTY_KEY(bool,kIgnoreForWindowCollisionDetection,false)26 DEFINE_UI_CLASS_PROPERTY_KEY(bool, kIgnoreForWindowCollisionDetection, false)
27 
28 // A property key to store a window's collision detection priority, used to
29 // resolve collisions between multiple windows which are both using
30 // CollisionDetectionUtils.
31 DEFINE_UI_CLASS_PROPERTY_KEY(
32     CollisionDetectionUtils::RelativePriority,
33     kCollisionDetectionRelativePriority,
34     CollisionDetectionUtils::RelativePriority::kDefault)
35 
36 bool ShouldIgnoreWindowForCollision(
37     const aura::Window* window,
38     CollisionDetectionUtils::RelativePriority priority) {
39   if (window->GetProperty(kIgnoreForWindowCollisionDetection))
40     return true;
41 
42   DCHECK_NE(CollisionDetectionUtils::RelativePriority::kDefault, priority);
43 
44   // Only ignore a window if our priority is greater than theirs. Lower priority
45   // windows need to move for higher priority windows, and windows do not need
46   // to move for a window of their own priority.
47   return static_cast<int>(priority) >=
48          static_cast<int>(
49              window->GetProperty(kCollisionDetectionRelativePriority));
50 }
51 
ComputeCollisionRectFromBounds(const gfx::Rect & bounds,const aura::Window * parent,bool inset=true)52 gfx::Rect ComputeCollisionRectFromBounds(const gfx::Rect& bounds,
53                                          const aura::Window* parent,
54                                          bool inset = true) {
55   gfx::Rect collision_rect = bounds;
56   ::wm::ConvertRectToScreen(parent, &collision_rect);
57   if (inset) {
58     collision_rect.Inset(-kCollisionWindowWorkAreaInsetsDp,
59                          -kCollisionWindowWorkAreaInsetsDp);
60   }
61   return collision_rect;
62 }
63 
CollectCollisionRects(const display::Display & display,CollisionDetectionUtils::RelativePriority priority)64 std::vector<gfx::Rect> CollectCollisionRects(
65     const display::Display& display,
66     CollisionDetectionUtils::RelativePriority priority) {
67   DCHECK_NE(CollisionDetectionUtils::RelativePriority::kDefault, priority);
68   std::vector<gfx::Rect> rects;
69   auto* root_window = Shell::GetRootWindowForDisplayId(display.id());
70   if (root_window) {
71     // Check SettingsBubbleContainer windows.
72     auto* settings_bubble_container =
73         root_window->GetChildById(kShellWindowId_SettingBubbleContainer);
74     for (auto* window : settings_bubble_container->children()) {
75       if (!window->IsVisible() && !window->GetTargetBounds().IsEmpty())
76         continue;
77       if (ShouldIgnoreWindowForCollision(window, priority))
78         continue;
79       // Use the target bounds in case an animation is in progress.
80       rects.push_back(ComputeCollisionRectFromBounds(window->GetTargetBounds(),
81                                                      window->parent()));
82     }
83 
84     // Check auto-hide shelf, which isn't included normally in the work area:
85     auto* shelf = Shelf::ForWindow(root_window);
86     auto* shelf_window = shelf->GetWindow();
87     if (shelf->IsVisible() &&
88         !ShouldIgnoreWindowForCollision(shelf_window, priority))
89       rects.push_back(ComputeCollisionRectFromBounds(
90           shelf_window->GetTargetBounds(), shelf_window->parent()));
91 
92     // The hotseat doesn't span the whole width of the display, but to allow
93     // a PIP window to be slided horizontally along the hotseat, we extend the
94     // width of the hotseat to that of the display.
95     auto* hotseat_widget = shelf->hotseat_widget();
96     if (hotseat_widget) {
97       auto* hotseat_window = hotseat_widget->GetNativeWindow();
98       gfx::Rect hotseat_rect{root_window->bounds().x(),
99                              hotseat_window->GetTargetBounds().y(),
100                              root_window->bounds().width(),
101                              hotseat_window->GetTargetBounds().height()};
102       if (hotseat_widget->state() != HotseatState::kHidden &&
103           hotseat_widget->state() != HotseatState::kNone &&
104           !ShouldIgnoreWindowForCollision(hotseat_window, priority)) {
105         rects.push_back(ComputeCollisionRectFromBounds(
106             hotseat_rect, hotseat_window->parent()));
107       }
108     }
109 
110     // Check the Automatic Clicks windows.
111     // TODO(Katie): The PIP isn't re-triggered to check the accessibility bubble
112     // windows when the autoclick window moves, just when the PIP moves or
113     // another system window. Need to ensure that changing the autoclick menu
114     // position triggers the PIP to re-check its bounds. crbug.com/954546.
115     auto* accessibility_bubble_container =
116         root_window->GetChildById(kShellWindowId_AccessibilityBubbleContainer);
117     for (auto* window : accessibility_bubble_container->children()) {
118       if (!window->IsVisible() && !window->GetTargetBounds().IsEmpty())
119         continue;
120       if (ShouldIgnoreWindowForCollision(window, priority))
121         continue;
122       // Use the target bounds in case an animation is in progress.
123       if (priority ==
124               CollisionDetectionUtils::RelativePriority::kAutomaticClicksMenu &&
125           window->GetProperty(kCollisionDetectionRelativePriority) ==
126               CollisionDetectionUtils::RelativePriority::
127                   kAutomaticClicksScrollMenu) {
128         // The autoclick menu widget and autoclick scroll menu widget are 0dip
129         // apart because they must be drawn at 8dips apart including drop
130         // shadow. This special case means we should not add an inset if we
131         // are calculating collision rects for the autoclick menu.
132         rects.push_back(ComputeCollisionRectFromBounds(
133             window->GetTargetBounds(), window->parent(),
134             false /* do not inset */));
135       } else {
136         rects.push_back(ComputeCollisionRectFromBounds(
137             window->GetTargetBounds(), window->parent()));
138       }
139     }
140   }
141 
142   auto* keyboard_controller = keyboard::KeyboardUIController::Get();
143   if (keyboard_controller->IsEnabled() &&
144       keyboard_controller->GetActiveContainerType() ==
145           keyboard::ContainerType::kFloating &&
146       keyboard_controller->GetRootWindow() == root_window &&
147       !keyboard_controller->GetVisualBoundsInScreen().IsEmpty() &&
148       !ShouldIgnoreWindowForCollision(keyboard_controller->GetKeyboardWindow(),
149                                       priority)) {
150     rects.push_back(ComputeCollisionRectFromBounds(
151         keyboard_controller->visual_bounds_in_root(),
152         /*parent=*/root_window));
153   }
154 
155   return rects;
156 }
157 
158 // Finds the candidate points |center| could be moved to. One of these points
159 // is guaranteed to be the minimum distance to move |center| to make it not
160 // intersect any of the rectangles in |collision_rects|.
CollectCandidatePoints(const gfx::Point & center,const std::vector<gfx::Rect> & collision_rects)161 std::vector<gfx::Point> CollectCandidatePoints(
162     const gfx::Point& center,
163     const std::vector<gfx::Rect>& collision_rects) {
164   std::vector<gfx::Point> candidate_points;
165   candidate_points.reserve(4 * collision_rects.size() * collision_rects.size() +
166                            4 * collision_rects.size() + 1);
167   // There are four cases for how the window will move.
168   // Case #1: Touches 0 obstacles. This corresponds to not moving.
169   // Case #2: Touches 1 obstacle.
170   //   The result window will be touching one edge of the obstacle.
171   // Case #3: Touches 2 obstacles.
172   //   The result window will be touching one horizontal and one vertical edge
173   //   from two different obstacles.
174   // Case #4: Touches more than 2 obstacles. This is handled in case #3.
175 
176   // Case #2.
177   // Rects include the left and top edges, so subtract 1 for those.
178   // Prioritize horizontal movement before vertical movement.
179   bool intersects = false;
180   for (const auto& rectA : collision_rects) {
181     intersects = intersects || rectA.Contains(center);
182     candidate_points.emplace_back(rectA.x() - 1, center.y());
183     candidate_points.emplace_back(rectA.right(), center.y());
184     candidate_points.emplace_back(center.x(), rectA.y() - 1);
185     candidate_points.emplace_back(center.x(), rectA.bottom());
186   }
187   // Case #1: Touching zero obstacles, so don't move the window.
188   if (!intersects)
189     return {};
190 
191   // Case #3: Add candidate points corresponding to each pair of horizontal
192   // and vertical edges.
193   for (const auto& rectA : collision_rects) {
194     for (const auto& rectB : collision_rects) {
195       // Continuing early here isn't necessary but makes fewer candidate points.
196       if (&rectA == &rectB)
197         continue;
198       candidate_points.emplace_back(rectA.x() - 1, rectB.y() - 1);
199       candidate_points.emplace_back(rectA.x() - 1, rectB.bottom());
200       candidate_points.emplace_back(rectA.right(), rectB.y() - 1);
201       candidate_points.emplace_back(rectA.right(), rectB.bottom());
202     }
203   }
204   return candidate_points;
205 }
206 
207 // Finds the candidate point with the shortest distance to |center| that is
208 // inside |work_area| and does not intersect any gfx::Rect in |rects|.
ComputeBestCandidatePoint(const gfx::Point & center,const gfx::Rect & work_area,const std::vector<gfx::Rect> & rects)209 gfx::Point ComputeBestCandidatePoint(const gfx::Point& center,
210                                      const gfx::Rect& work_area,
211                                      const std::vector<gfx::Rect>& rects) {
212   auto candidate_points = CollectCandidatePoints(center, rects);
213   int64_t best_dist = -1;
214   gfx::Point best_point = center;
215   for (const auto& point : candidate_points) {
216     if (!work_area.Contains(point))
217       continue;
218     bool viable = true;
219     for (const auto& rect : rects) {
220       if (rect.Contains(point)) {
221         viable = false;
222         break;
223       }
224     }
225     if (!viable)
226       continue;
227     int64_t dist = (point - center).LengthSquared();
228     if (dist < best_dist || best_dist == -1) {
229       best_dist = dist;
230       best_point = point;
231     }
232   }
233   return best_point;
234 }
235 
236 }  // namespace
237 
GetMovementArea(const display::Display & display)238 gfx::Rect CollisionDetectionUtils::GetMovementArea(
239     const display::Display& display) {
240   gfx::Rect work_area =
241       WorkAreaInsets::ForWindow(Shell::GetRootWindowForDisplayId(display.id()))
242           ->user_work_area_bounds();
243 
244   work_area.Inset(kCollisionWindowWorkAreaInsetsDp,
245                   kCollisionWindowWorkAreaInsetsDp);
246   return work_area;
247 }
248 
AdjustToFitMovementAreaByGravity(const display::Display & display,const gfx::Rect & bounds_in_screen)249 gfx::Rect CollisionDetectionUtils::AdjustToFitMovementAreaByGravity(
250     const display::Display& display,
251     const gfx::Rect& bounds_in_screen) {
252   gfx::Rect resting_bounds = bounds_in_screen;
253   gfx::Rect area = GetMovementArea(display);
254   resting_bounds.AdjustToFit(area);
255   const CollisionDetectionUtils::Gravity gravity =
256       GetGravityToClosestEdge(resting_bounds, area);
257   return GetAdjustedBoundsByGravity(resting_bounds, area, gravity);
258 }
259 
GetRestingPosition(const display::Display & display,const gfx::Rect & bounds_in_screen,CollisionDetectionUtils::RelativePriority priority)260 gfx::Rect CollisionDetectionUtils::GetRestingPosition(
261     const display::Display& display,
262     const gfx::Rect& bounds_in_screen,
263     CollisionDetectionUtils::RelativePriority priority) {
264   gfx::Rect resting_bounds =
265       AdjustToFitMovementAreaByGravity(display, bounds_in_screen);
266   return AvoidObstacles(display, resting_bounds, priority);
267 }
268 
IgnoreWindowForCollisionDetection(aura::Window * window)269 void CollisionDetectionUtils::IgnoreWindowForCollisionDetection(
270     aura::Window* window) {
271   window->SetProperty(kIgnoreForWindowCollisionDetection, true);
272 }
273 
MarkWindowPriorityForCollisionDetection(aura::Window * window,RelativePriority priority)274 void CollisionDetectionUtils::MarkWindowPriorityForCollisionDetection(
275     aura::Window* window,
276     RelativePriority priority) {
277   window->SetProperty(kCollisionDetectionRelativePriority, priority);
278 }
279 
AvoidObstacles(const display::Display & display,const gfx::Rect & bounds_in_screen,RelativePriority priority)280 gfx::Rect CollisionDetectionUtils::AvoidObstacles(
281     const display::Display& display,
282     const gfx::Rect& bounds_in_screen,
283     RelativePriority priority) {
284   gfx::Rect work_area = GetMovementArea(display);
285   auto rects = CollectCollisionRects(display, priority);
286   // The worst case for this should be: floating keyboard + one system tray +
287   // the volume shifter + autoclick bubble menu, which is 4 windows.
288   DCHECK(rects.size() <= 15)
289       << "CollisionDetectionUtils::AvoidObstacles is N^3 and "
290          "should be optimized if there are a lot of "
291          "windows. Please see crrev.com/c/1221427 for a "
292          "description of an N^2 algorithm.";
293   return AvoidObstaclesInternal(work_area, rects, bounds_in_screen, priority);
294 }
295 
296 // Returns the result of adjusting |bounds| according to |gravity| inside
297 // |region|.
GetAdjustedBoundsByGravity(const gfx::Rect & bounds,const gfx::Rect & region,CollisionDetectionUtils::Gravity gravity)298 gfx::Rect CollisionDetectionUtils::GetAdjustedBoundsByGravity(
299     const gfx::Rect& bounds,
300     const gfx::Rect& region,
301     CollisionDetectionUtils::Gravity gravity) {
302   switch (gravity) {
303     case CollisionDetectionUtils::Gravity::kGravityLeft:
304       return gfx::Rect(region.x(), bounds.y(), bounds.width(), bounds.height());
305     case CollisionDetectionUtils::Gravity::kGravityRight:
306       return gfx::Rect(region.right() - bounds.width(), bounds.y(),
307                        bounds.width(), bounds.height());
308     case CollisionDetectionUtils::Gravity::kGravityTop:
309       return gfx::Rect(bounds.x(), region.y(), bounds.width(), bounds.height());
310     case CollisionDetectionUtils::Gravity::kGravityBottom:
311       return gfx::Rect(bounds.x(), region.bottom() - bounds.height(),
312                        bounds.width(), bounds.height());
313     default:
314       NOTREACHED();
315   }
316   return bounds;
317 }
318 
319 CollisionDetectionUtils::Gravity
GetGravityToClosestEdge(const gfx::Rect & bounds,const gfx::Rect & region)320 CollisionDetectionUtils::GetGravityToClosestEdge(const gfx::Rect& bounds,
321                                                  const gfx::Rect& region) {
322   const gfx::Insets insets = region.InsetsFrom(bounds);
323   int minimum_edge_dist = std::min(insets.left(), insets.right());
324   minimum_edge_dist = std::min(minimum_edge_dist, insets.top());
325   minimum_edge_dist = std::min(minimum_edge_dist, insets.bottom());
326 
327   if (insets.left() == minimum_edge_dist) {
328     return CollisionDetectionUtils::Gravity::kGravityLeft;
329   } else if (insets.right() == minimum_edge_dist) {
330     return CollisionDetectionUtils::Gravity::kGravityRight;
331   } else if (insets.top() == minimum_edge_dist) {
332     return CollisionDetectionUtils::Gravity::kGravityTop;
333   } else {
334     return CollisionDetectionUtils::Gravity::kGravityBottom;
335   }
336 }
337 
AvoidObstaclesInternal(const gfx::Rect & work_area,const std::vector<gfx::Rect> & rects,const gfx::Rect & bounds_in_screen,RelativePriority priority)338 gfx::Rect CollisionDetectionUtils::AvoidObstaclesInternal(
339     const gfx::Rect& work_area,
340     const std::vector<gfx::Rect>& rects,
341     const gfx::Rect& bounds_in_screen,
342     RelativePriority priority) {
343   gfx::Rect inset_work_area = work_area;
344 
345   // For even sized bounds, there is no 'center' integer point, so we need
346   // to adjust the obstacles and work area to account for this.
347   inset_work_area.Inset(
348       bounds_in_screen.width() / 2, bounds_in_screen.height() / 2,
349       (bounds_in_screen.width() - 1) / 2, (bounds_in_screen.height() - 1) / 2);
350   std::vector<gfx::Rect> inset_rects(rects);
351   for (auto& rect : inset_rects) {
352     // Reduce the collision resolution problem from rectangles-rectangle
353     // resolution to rectangles-point resolution, by expanding each obstacle
354     // by |bounds_in_screen| size.
355     rect.Inset(-(bounds_in_screen.width() - 1) / 2,
356                -(bounds_in_screen.height() - 1) / 2,
357                -bounds_in_screen.width() / 2, -bounds_in_screen.height() / 2);
358   }
359 
360   gfx::Point moved_center = ComputeBestCandidatePoint(
361       bounds_in_screen.CenterPoint(), inset_work_area, inset_rects);
362   gfx::Rect moved_bounds = bounds_in_screen;
363   moved_bounds.Offset(moved_center - bounds_in_screen.CenterPoint());
364   return moved_bounds;
365 }
366 
367 }  // namespace ash
368