chromium/ash/wm/window_positioning_utils.cc

// 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