chromium/ash/wm/tile_group/window_tiling_controller.cc

// Copyright 2024 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/tile_group/window_tiling_controller.h"

#include "ash/wm/window_state.h"
#include "ash/wm/wm_event.h"
#include "base/containers/adapters.h"
#include "base/numerics/safe_conversions.h"
#include "ui/aura/window.h"
#include "ui/aura/window_delegate.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect.h"

namespace ash {

namespace {

constexpr float kTilingRatios[] = {
    0, 1.0f / 4, 1.0f / 3, 1.0f / 2, 2.0f / 3, 3.0f / 4, 1,
};
constexpr float kInitialTilingRatio = 1.0f / 2;

bool CanUseWindowBounds(const aura::Window& window,
                        const gfx::Rect& screen_bounds) {
  if (screen_bounds.IsEmpty()) {
    return false;
  }
  if (const auto* delegate = window.delegate()) {
    const gfx::Size min_size = delegate->GetMinimumSize();
    // Empty min_size is ok.
    return min_size.width() <= screen_bounds.width() &&
           min_size.height() <= screen_bounds.height();
  }
  return true;
}

void SetWindowBoundsInScreen(aura::Window* window,
                             const gfx::Rect& screen_bounds,
                             const display::Display& display) {
  if (!CanUseWindowBounds(*window, screen_bounds)) {
    return;
  }

  auto* window_state = WindowState::Get(window);
  if (!window_state->IsNormalStateType()) {
    // TODO(b/308194482): Disable animation, e.g. if this would unmaximize.
    // But having animation may be ok, so need UX input.
    window_state->SetRestoreBoundsInScreen(screen_bounds);
    const WMEvent event(WM_EVENT_NORMAL);
    window_state->OnWMEvent(&event);
    return;
  }
  window->SetBoundsInScreen(screen_bounds, display);
}

// Applies insets to the window.
void ApplyInsets(aura::Window* window,
                 const gfx::Insets& insets,
                 const display::Display& display) {
  gfx::Rect screen_bounds = window->GetBoundsInScreen();
  screen_bounds.Inset(insets);

  SetWindowBoundsInScreen(window, screen_bounds, display);
}

}  // namespace

bool WindowTilingController::CanTilingResize(aura::Window* window) const {
  WindowState* window_state = WindowState::Get(window);
  return window_state &&
         (window_state->IsNormalStateType() || window_state->IsSnapped() ||
          window_state->IsMaximized());
}

void WindowTilingController::OnTilingResizeLeft(aura::Window* window) {
  const gfx::Rect window_bounds = window->GetBoundsInScreen();
  const display::Display display =
      display::Screen::GetScreen()->GetDisplayNearestWindow(window);
  const gfx::Rect work_area = display.work_area();

  // "Snap" left if neither left nor right edges are against work area bounds.
  // TODO(b/316962035): Design behavior if window is off screen vertically.
  // Same for the other directions.
  if (window_bounds.x() != work_area.x() &&
      window_bounds.right() != work_area.right()) {
    SetWindowBoundsInScreen(
        window,
        gfx::Rect(work_area.x(), work_area.y(),
                  work_area.width() * kInitialTilingRatio, work_area.height()),
        display);
    return;
  }

  // Resizing left decreases X, so reverse iterate through ratios.
  for (float ratio : base::Reversed(kTilingRatios)) {
    int new_x = work_area.x() + base::ClampRound(ratio * work_area.width());
    if (window_bounds.x() == work_area.x()) {
      if (new_x < window_bounds.right()) {
        ApplyInsets(window,
                    gfx::Insets().set_right(window_bounds.right() - new_x),
                    display);
        break;
      }
    } else if (new_x < window_bounds.x()) {
      ApplyInsets(window, gfx::Insets().set_left(new_x - window_bounds.x()),
                  display);
      break;
    }
  }
}

void WindowTilingController::OnTilingResizeRight(aura::Window* window) {
  const gfx::Rect window_bounds = window->GetBoundsInScreen();
  const display::Display display =
      display::Screen::GetScreen()->GetDisplayNearestWindow(window);
  const gfx::Rect work_area = display.work_area();

  // "Snap" right if neither left nor right edges are against work area bounds.
  if (window_bounds.x() != work_area.x() &&
      window_bounds.right() != work_area.right()) {
    SetWindowBoundsInScreen(
        window,
        gfx::Rect(work_area.x() + work_area.width() * kInitialTilingRatio,
                  work_area.y(), work_area.width() * kInitialTilingRatio,
                  work_area.height()),
        display);
    return;
  }

  // Resizing right increases X, so forward iterate through ratios.
  for (float ratio : kTilingRatios) {
    int new_x = work_area.x() + base::ClampRound(ratio * work_area.width());
    if (window_bounds.right() == work_area.right()) {
      if (new_x > window_bounds.x()) {
        ApplyInsets(window, gfx::Insets().set_left(new_x - window_bounds.x()),
                    display);
        break;
      }
    } else if (new_x > window_bounds.right()) {
      ApplyInsets(window,
                  gfx::Insets().set_right(window_bounds.right() - new_x),
                  display);
      break;
    }
  }
}

void WindowTilingController::OnTilingResizeUp(aura::Window* window) {
  const gfx::Rect window_bounds = window->GetBoundsInScreen();
  const display::Display display =
      display::Screen::GetScreen()->GetDisplayNearestWindow(window);
  const gfx::Rect work_area = display.work_area();

  // "Snap" up if neither top nor bottom edges are against work area bounds.
  if (window_bounds.y() != work_area.y() &&
      window_bounds.bottom() != work_area.bottom()) {
    SetWindowBoundsInScreen(
        window,
        gfx::Rect(work_area.x(), work_area.y(), work_area.width(),
                  work_area.height() * kInitialTilingRatio),
        display);
    return;
  }

  // Resizing up decreases Y, so reverse iterate through ratios.
  for (float ratio : base::Reversed(kTilingRatios)) {
    int new_y = work_area.y() + base::ClampRound(ratio * work_area.height());
    if (window_bounds.y() == work_area.y()) {
      if (new_y < window_bounds.bottom()) {
        ApplyInsets(window,
                    gfx::Insets().set_bottom(window_bounds.bottom() - new_y),
                    display);
        break;
      }
    } else if (new_y < window_bounds.y()) {
      ApplyInsets(window, gfx::Insets().set_top(new_y - window_bounds.y()),
                  display);
      break;
    }
  }
}

void WindowTilingController::OnTilingResizeDown(aura::Window* window) {
  const gfx::Rect window_bounds = window->GetBoundsInScreen();
  const display::Display display =
      display::Screen::GetScreen()->GetDisplayNearestWindow(window);
  const gfx::Rect work_area = display.work_area();

  // "Snap" down if neither top nor bottom edges are against work area bounds.
  if (window_bounds.y() != work_area.y() &&
      window_bounds.bottom() != work_area.bottom()) {
    SetWindowBoundsInScreen(
        window,
        gfx::Rect(work_area.x(),
                  work_area.y() + work_area.height() * kInitialTilingRatio,
                  work_area.width(), work_area.height() * kInitialTilingRatio),
        display);
    return;
  }

  // Resizing down increases Y, so forward iterate through ratios.
  for (float ratio : kTilingRatios) {
    int new_y = work_area.y() + base::ClampRound(ratio * work_area.height());
    if (window_bounds.bottom() == work_area.bottom()) {
      if (new_y > window_bounds.y()) {
        ApplyInsets(window, gfx::Insets().set_top(new_y - window_bounds.y()),
                    display);
        break;
      }
    } else if (new_y > window_bounds.bottom()) {
      ApplyInsets(window,
                  gfx::Insets().set_bottom(window_bounds.bottom() - new_y),
                  display);
      break;
    }
  }
}

}  // namespace ash