| // Copyright 2016 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/window_positioning_utils.h" |
| |
| #include <algorithm> |
| |
| #include "ash/constants/ash_features.h" |
| #include "ash/display/display_util.h" |
| #include "ash/public/cpp/window_properties.h" |
| #include "ash/root_window_controller.h" |
| #include "ash/screen_util.h" |
| #include "ash/shell.h" |
| #include "ash/wm/pip/pip_controller.h" |
| #include "ash/wm/system_modal_container_layout_manager.h" |
| #include "ash/wm/window_properties.h" |
| #include "ash/wm/window_restore/window_restore_controller.h" |
| #include "ash/wm/window_state.h" |
| #include "ash/wm/window_util.h" |
| #include "ash/wm/wm_event.h" |
| #include "base/notreached.h" |
| #include "base/numerics/ranges.h" |
| #include "components/app_restore/window_properties.h" |
| #include "ui/aura/client/focus_client.h" |
| #include "ui/aura/window.h" |
| #include "ui/aura/window_delegate.h" |
| #include "ui/aura/window_tracker.h" |
| #include "ui/display/display.h" |
| #include "ui/display/screen.h" |
| #include "ui/display/types/display_constants.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/wm/core/window_util.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| int GetSnappedWindowAxisLength(float snap_ratio, |
| int work_area_axis_length, |
| int min_axis_length, |
| bool is_primary_snap) { |
| DCHECK_GT(snap_ratio, 0); |
| DCHECK_LE(snap_ratio, 1.f); |
| min_axis_length = std::min(min_axis_length, work_area_axis_length); |
| // The primary snap size is proportional to |snap_ratio|. |
| if (is_primary_snap) { |
| return std::clamp(static_cast<int>(snap_ratio * work_area_axis_length), |
| min_axis_length, work_area_axis_length); |
| } |
| |
| // The secondary snap size is proportional to the |snap_ratio|, but |
| // we want to make sure there is no gap between the primary and secondary |
| // windows when their |snap_ratio|'s sum up to 1. Thus to avoid a gap from |
| // integer rounding up issue, we compute the empty-space size and subtracted |
| // it from |work_area_axis_length|. An example test is |
| // `WindowPositioningUtilsTest.SnapBoundsWithOddNumberedScreenWidth`. |
| const int empty_space_axis_length = |
| static_cast<int>((1 - snap_ratio) * work_area_axis_length); |
| return std::clamp(work_area_axis_length - empty_space_axis_length, |
| min_axis_length, work_area_axis_length); |
| } |
| |
| // Return true if the window or one of its ancestor returns true from |
| // IsLockedToRoot(). |
| bool IsWindowOrAncestorLockedToRoot(const aura::Window* window) { |
| return window && (window->GetProperty(kLockedToRootKey) || |
| IsWindowOrAncestorLockedToRoot(window->parent())); |
| } |
| |
| } // namespace |
| |
| void AdjustBoundsSmallerThan(const gfx::Size& max_size, gfx::Rect* bounds) { |
| bounds->set_width(std::min(bounds->width(), max_size.width())); |
| bounds->set_height(std::min(bounds->height(), max_size.height())); |
| } |
| |
| void AdjustBoundsToEnsureMinimumWindowVisibility(const gfx::Rect& visible_area, |
| bool client_controlled, |
| gfx::Rect* bounds) { |
| if (Shell::Get()->pip_controller()->is_tucked()) { |
| // PiP is allowed to be positioned beyond the threshold while it's tucked. |
| // TODO(http://b/337113950): Check if this is also needed for float windows. |
| return; |
| } |
| |
| int min_width = |
| std::max(kMinimumOnScreenArea, |
| static_cast<int>(bounds->width() * kMinimumPercentOnScreenArea)); |
| int min_height = std::max( |
| kMinimumOnScreenArea, |
| static_cast<int>(bounds->height() * kMinimumPercentOnScreenArea)); |
| if (client_controlled) { |
| min_width++; |
| min_height++; |
| } |
| |
| AdjustBoundsSmallerThan(visible_area.size(), bounds); |
| |
| min_width = std::min(min_width, visible_area.width()); |
| min_height = std::min(min_height, visible_area.height()); |
| |
| if (bounds->right() < visible_area.x() + min_width) { |
| bounds->set_x(visible_area.x() + std::min(bounds->width(), min_width) - |
| bounds->width()); |
| } else if (bounds->x() > visible_area.right() - min_width) { |
| bounds->set_x(visible_area.right() - std::min(bounds->width(), min_width)); |
| } |
| if (bounds->bottom() < visible_area.y() + min_height) { |
| bounds->set_y(visible_area.y() + std::min(bounds->height(), min_height) - |
| bounds->height()); |
| } else if (bounds->y() > visible_area.bottom() - min_height) { |
| bounds->set_y(visible_area.bottom() - |
| std::min(bounds->height(), min_height)); |
| } |
| if (bounds->y() < visible_area.y()) { |
| bounds->set_y(visible_area.y()); |
| } |
| } |
| |
| gfx::Rect GetSnappedWindowBoundsInParent(aura::Window* window, |
| SnapViewType type, |
| float snap_ratio) { |
| return GetSnappedWindowBounds( |
| screen_util::GetDisplayWorkAreaBoundsInParent(window), |
| display::Screen::GetScreen()->GetDisplayNearestWindow(window), window, |
| type, snap_ratio); |
| } |
| |
| gfx::Rect GetDefaultSnappedWindowBoundsInParent(aura::Window* window, |
| SnapViewType type) { |
| return GetSnappedWindowBoundsInParent(window, type, |
| chromeos::kDefaultSnapRatio); |
| } |
| |
| gfx::Rect GetSnappedWindowBounds(const gfx::Rect& work_area, |
| const display::Display display, |
| aura::Window* window, |
| SnapViewType type, |
| float snap_ratio) { |
| chromeos::OrientationType orientation = GetSnapDisplayOrientation(display); |
| enum class SnapRegion { kLeft, kRight, kBottom, kTop, kInvalid }; |
| SnapRegion snap_region = SnapRegion::kInvalid; |
| const bool is_primary_snap = type == SnapViewType::kPrimary; |
| bool is_horizontal = true; |
| |
| // Find the actual snap position that the `window` should be snapped to based |
| // on `orientation` and `type`. |
| switch (orientation) { |
| case chromeos::OrientationType::kLandscapePrimary: |
| snap_region = is_primary_snap ? SnapRegion::kLeft : SnapRegion::kRight; |
| break; |
| case chromeos::OrientationType::kLandscapeSecondary: |
| snap_region = is_primary_snap ? SnapRegion::kRight : SnapRegion::kLeft; |
| break; |
| case chromeos::OrientationType::kPortraitPrimary: |
| snap_region = is_primary_snap ? SnapRegion::kTop : SnapRegion::kBottom; |
| is_horizontal = false; |
| break; |
| case chromeos::OrientationType::kPortraitSecondary: |
| snap_region = is_primary_snap ? SnapRegion::kBottom : SnapRegion::kTop; |
| is_horizontal = false; |
| break; |
| default: |
| snap_region = SnapRegion::kInvalid; |
| NOTREACHED(); |
| } |
| |
| // Compute size of the side of the window bound that should be proportional |
| // |WindowState::snap_ratio_| to that of the work area, i.e. width for |
| // horizontal layout and height for vertical layout. |
| gfx::Rect snap_bounds = gfx::Rect(work_area); |
| const int work_area_axis_length = |
| is_horizontal ? work_area.width() : work_area.height(); |
| int min_size = 0; |
| if (window->delegate()) { |
| const gfx::Size minimum_size = window->delegate()->GetMinimumSize(); |
| min_size = is_horizontal ? minimum_size.width() : minimum_size.height(); |
| } |
| |
| int axis_length = GetSnappedWindowAxisLength( |
| snap_ratio, work_area_axis_length, min_size, is_primary_snap); |
| const gfx::Size* preferred_size = |
| window->GetProperty(kUnresizableSnappedSizeKey); |
| if (preferred_size && !WindowState::Get(window)->CanResize()) { |
| DCHECK(preferred_size->width() == 0 || preferred_size->height() == 0); |
| if (is_horizontal && preferred_size->width() > 0) |
| axis_length = preferred_size->width(); |
| if (!is_horizontal && preferred_size->height() > 0) |
| axis_length = preferred_size->height(); |
| } else if (window == WindowRestoreController::Get()->to_be_snapped_window()) { |
| // Edit `axis_length` if window restore is currently restoring a snapped |
| // window; take into account the snap percentage saved by the window. |
| app_restore::WindowInfo* window_info = |
| window->GetProperty(app_restore::kWindowInfoKey); |
| if (window_info && window_info->snap_percentage) { |
| const int snap_percentage = *window_info->snap_percentage; |
| axis_length = snap_percentage * work_area_axis_length / 100; |
| } |
| } |
| |
| // Set the size of such side and the window position based on a given snap |
| // position. |
| switch (snap_region) { |
| case SnapRegion::kLeft: |
| snap_bounds.set_width(axis_length); |
| break; |
| case SnapRegion::kRight: |
| snap_bounds.set_width(axis_length); |
| // Snap to the right. |
| snap_bounds.set_x(work_area.right() - axis_length); |
| break; |
| case SnapRegion::kTop: |
| snap_bounds.set_height(axis_length); |
| break; |
| case SnapRegion::kBottom: |
| snap_bounds.set_height(axis_length); |
| // Snap to the bottom. |
| snap_bounds.set_y(work_area.bottom() - axis_length); |
| break; |
| case SnapRegion::kInvalid: |
| NOTREACHED(); |
| } |
| return snap_bounds; |
| } |
| |
| chromeos::OrientationType GetSnapDisplayOrientation( |
| const display::Display& display) { |
| // This function is used by `GetSnappedWindowBounds()` for clamshell mode |
| // only. Tablet mode uses a different function |
| // `SplitViewController::GetSnappedWindowBoundsInScreen()`. |
| DCHECK(!display::Screen::GetScreen()->InTabletMode()); |
| |
| const display::Display::Rotation& rotation = |
| Shell::Get() |
| ->display_manager() |
| ->GetDisplayInfo(display.id()) |
| .GetActiveRotation(); |
| |
| return RotationToOrientation(chromeos::GetDisplayNaturalOrientation(display), |
| rotation); |
| } |
| |
| void SetBoundsInScreen(aura::Window* window, |
| const gfx::Rect& bounds_in_screen, |
| const display::Display& display) { |
| // Don't move a window to other root window if: |
| // a) the window is a transient window. It moves when its |
| // transient parent moves. |
| // b) if the window or its ancestor has IsLockedToRoot(). It's intentionally |
| // kept in the same root window even if the bounds is outside of the |
| // display. |
| if (!::wm::GetTransientParent(window) && |
| !IsWindowOrAncestorLockedToRoot(window)) { |
| RootWindowController* dst_root_window_controller = |
| Shell::GetRootWindowControllerWithDisplayId(display.id()); |
| DCHECK(dst_root_window_controller); |
| aura::Window* dst_root = dst_root_window_controller->GetRootWindow(); |
| DCHECK(dst_root); |
| aura::Window* dst_container = nullptr; |
| if (dst_root != window->GetRootWindow()) { |
| int container_id = window->parent()->GetId(); |
| // All containers that use screen coordinates must have valid window ids. |
| DCHECK_GE(container_id, 0); |
| // Don't move modal background. |
| if (!SystemModalContainerLayoutManager::IsModalBackground(window)) |
| dst_container = dst_root->GetChildById(container_id); |
| } |
| |
| if (dst_container && window->parent() != dst_container) { |
| aura::Window* focused = window_util::GetFocusedWindow(); |
| aura::Window* active = window_util::GetActiveWindow(); |
| |
| aura::WindowTracker tracker; |
| if (focused) |
| tracker.Add(focused); |
| if (active && focused != active) |
| tracker.Add(active); |
| |
| // Client controlled window will have its own logic on client side |
| // to adjust bounds. |
| // TODO(oshima): Use WM_EVENT_SET_BOUNDS with target display id. |
| auto* window_state = WindowState::Get(window); |
| if (!window_state || !window_state->allow_set_bounds_direct()) { |
| gfx::Point origin = bounds_in_screen.origin(); |
| const gfx::Point display_origin = display.bounds().origin(); |
| origin.Offset(-display_origin.x(), -display_origin.y()); |
| gfx::Rect new_bounds = gfx::Rect(origin, bounds_in_screen.size()); |
| // Set new bounds now so that the container's layout manager can adjust |
| // the bounds if necessary. |
| if (window_state) |
| window_state->set_is_moving_to_another_display(true); |
| window->SetBounds(new_bounds); |
| } |
| dst_container->AddChild(window); |
| |
| if (window_state) |
| window_state->set_is_moving_to_another_display(false); |
| |
| // Restore focused/active window. |
| if (focused && tracker.Contains(focused)) { |
| aura::client::GetFocusClient(focused)->FocusWindow(focused); |
| Shell::SetRootWindowForNewWindows(focused->GetRootWindow()); |
| } else if (active && tracker.Contains(active)) { |
| wm::ActivateWindow(active); |
| } |
| // TODO(oshima): We should not have to update the bounds again |
| // below in theory, but we currently do need as there is a code |
| // that assumes that the bounds will never be overridden by the |
| // layout mananger. We should have more explicit control how |
| // constraints are applied by the layout manager. |
| } |
| } |
| gfx::Point origin(bounds_in_screen.origin()); |
| const gfx::Point display_origin = display::Screen::GetScreen() |
| ->GetDisplayNearestWindow(window) |
| .bounds() |
| .origin(); |
| origin.Offset(-display_origin.x(), -display_origin.y()); |
| window->SetBounds(gfx::Rect(origin, bounds_in_screen.size())); |
| } |
| |
| } // namespace ash |