#include "ash/wm/snap_group/snap_group.h"
#include "ash/screen_util.h"
#include "ash/shell.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/overview/overview_utils.h"
#include "ash/wm/scoped_windows_mover.h"
#include "ash/wm/snap_group/snap_group_controller.h"
#include "ash/wm/snap_group/snap_group_metrics.h"
#include "ash/wm/splitview/split_view_constants.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/splitview/split_view_types.h"
#include "ash/wm/splitview/split_view_utils.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/metrics/user_metrics.h"
#include "base/time/time.h"
#include "chromeos/ui/base/window_state_type.h"
#include "ui/base/hit_test.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/public/activation_client.h"
namespace ash {
namespace {
using chromeos::WindowStateType;
SnapGroupExitPoint GetWindowStateChangeExitPoint(WindowState* window_state) {
WindowStateType state_type = window_state->GetStateType();
switch (state_type) {
case WindowStateType::kDefault:
return SnapGroupExitPoint::kWindowStateChangedDefault;
case WindowStateType::kNormal:
return SnapGroupExitPoint::kWindowStateChangedNormal;
case WindowStateType::kMinimized:
return SnapGroupExitPoint::kWindowStateChangedMinimized;
case WindowStateType::kMaximized:
return SnapGroupExitPoint::kWindowStateChangedMaximized;
case WindowStateType::kInactive:
return SnapGroupExitPoint::kWindowStateChangedInactive;
case WindowStateType::kFullscreen:
return SnapGroupExitPoint::kWindowStateChangedFullscreen;
case WindowStateType::kPrimarySnapped:
return SnapGroupExitPoint::kWindowStateChangedPrimarySnapped;
case WindowStateType::kSecondarySnapped:
return SnapGroupExitPoint::kWindowStateChangedSecondarySnapped;
case WindowStateType::kPinned:
return SnapGroupExitPoint::kWindowStateChangedPinned;
case WindowStateType::kLockedFullscreen:
return SnapGroupExitPoint::kWindowStateChangedTrustedPinned;
case WindowStateType::kPip:
return SnapGroupExitPoint::kWindowStateChangedPip;
case WindowStateType::kFloated:
return SnapGroupExitPoint::kWindowStateChangedFloated;
}
}
int CalculateDividerPosition(aura::Window* root_window,
float primary_snap_ratio) {
const int upper_limit = GetDividerPositionUpperLimit(root_window);
const int requested_divider_position =
upper_limit * primary_snap_ratio - kSplitviewDividerShortSideLength / 2.f;
return requested_divider_position;
}
}
SnapGroup::SnapGroup(aura::Window* window1,
aura::Window* window2,
std::optional<base::TimeTicks> sticky_creation_time)
: snap_group_divider_(this),
carry_over_creation_time_(
sticky_creation_time.value_or(base::TimeTicks().Now())),
actual_creation_time_(base::TimeTicks().Now()) {
CHECK_EQ(window1->parent(), window2->parent());
auto* window_state1 = WindowState::Get(window1);
auto* window_state2 = WindowState::Get(window2);
CHECK(window_state1->IsSnapped() && window_state2->IsSnapped() &&
window_state1->GetStateType() != window_state2->GetStateType());
if (window_state1->GetStateType() ==
chromeos::WindowStateType::kPrimarySnapped) {
window1_ = window1;
window2_ = window2;
} else {
window1_ = window2;
window2_ = window1;
}
StartObservingWindows();
ShowDivider();
display::Screen::Get()->AddObserver(this);
Shell::Get()->activation_client()->AddObserver(this);
}
SnapGroup::~SnapGroup() {
if (!is_shutting_down_) {
Shutdown();
}
}
void SnapGroup::Shutdown() {
is_shutting_down_ = true;
window_to_target_snap_position_map_.clear();
Shell::Get()->activation_client()->RemoveObserver(this);
display::Screen::Get()->RemoveObserver(this);
UpdateGroupWindowsBounds(false);
StopObservingWindows();
}
gfx::Rect SnapGroup::GetSnappedWindowBoundsInRoot(
aura::Window* window,
const chromeos::WindowStateType state_type,
float snap_ratio) {
const float primary_snap_ratio =
state_type == chromeos::WindowStateType::kPrimarySnapped
? snap_ratio
: 1.f - snap_ratio;
snap_group_divider_.SetDividerPosition(
CalculateDividerPosition(GetRootWindow(), primary_snap_ratio));
gfx::Rect bounds_in_parent = GetSnappedWindowBoundsInScreen(
ToSnapPosition(state_type), window, snap_ratio,
snap_group_divider_.IsDividerWidgetVisible());
wm::ConvertRectFromScreen(window->GetRootWindow(), &bounds_in_parent);
return bounds_in_parent;
}
aura::Window* SnapGroup::GetPhysicallyLeftOrTopWindow() {
return IsPhysicallyLeftOrTop(window1_) ? window1_ : window2_;
}
aura::Window* SnapGroup::GetPhysicallyRightOrBottomWindow() {
return IsPhysicallyLeftOrTop(window1_) ? window2_ : window1_;
}
void SnapGroup::ShowDivider() {
if (snap_group_divider_.IsDividerWidgetVisible()) {
return;
}
const bool is_left_or_top = IsPhysicallyLeftOrTop(window1_);
aura::Window* primary_window = is_left_or_top ? window1_ : window2_;
aura::Window* secondary_window = is_left_or_top ? window2_ : window1_;
const gfx::Rect window1_bounds = primary_window->GetTargetBounds();
const gfx::Rect window2_bounds = secondary_window->GetTargetBounds();
int edge_gap = 0;
if (IsSnapGroupLayoutHorizontal()) {
edge_gap = window2_bounds.x() - window1_bounds.right();
} else {
edge_gap = window2_bounds.y() - window1_bounds.bottom();
}
const bool account_for_divider_width =
edge_gap < kSplitviewDividerShortSideLength;
snap_group_divider_.SetDividerPosition(
GetEquivalentDividerPosition(primary_window, account_for_divider_width));
snap_group_divider_.SetVisible(true);
}
void SnapGroup::HideDivider() {
snap_group_divider_.SetVisible(false);
}
bool SnapGroup::IsSnapGroupLayoutHorizontal() const {
return IsLayoutHorizontal(GetRootWindow());
}
aura::Window* SnapGroup::GetTopMostWindowInGroup() const {
aura::Window* window1_root_window = window1_->GetRootWindow();
aura::Window* window2_root_window = window2_->GetRootWindow();
if (window1_root_window != window2_root_window) {
aura::Window* cursor_root_window = window_util::GetRootWindowAt(
display::Screen::Get()->GetCursorScreenPoint());
return window1_root_window == cursor_root_window ? window1_root_window
: window2_root_window;
}
if (window1_->parent() != window2_->parent()) {
return desks_util::BelongsToActiveDesk(window1_) ? window1_ : window2_;
}
return window_util::IsStackedBelow(window1_, window2_) ? window2_ : window1_;
}
void SnapGroup::RefreshSnapGroup() {
if (is_shutting_down_) {
return;
}
if (!IsSnapped(window1_) || !IsSnapped(window2_)) {
return;
}
CHECK_EQ(window1_->GetRootWindow(), window2_->GetRootWindow());
if (!CanWindowsFitInWorkArea(window1_, window2_)) {
SnapGroupController::Get()->RemoveSnapGroup(
this, SnapGroupExitPoint::kCanNotFitInWorkArea);
return;
}
ApplyPrimarySnapRatio(WindowState::Get(GetPhysicallyLeftOrTopWindow())
->snap_ratio()
.value_or(chromeos::kDefaultSnapRatio));
}
void SnapGroup::OnWindowDestroying(aura::Window* window) {
if (is_shutting_down_) {
return;
}
DCHECK(window == window1_ || window == window2_);
SnapGroupController::Get()->RemoveSnapGroup(
this, SnapGroupExitPoint::kWindowDestruction);
}
void SnapGroup::OnWindowParentChanged(aura::Window* window,
aura::Window* parent) {
if (parent == nullptr || is_moving_snap_group_) {
return;
}
DCHECK(window == window1_ || window == window2_);
base::AutoReset<bool> lock(&is_moving_snap_group_, true);
const bool cached_divider_visibility =
snap_group_divider_.target_visibility();
snap_group_divider_.SetVisible(false);
aura::Window* to_be_moved_window = window == window1_ ? window2_ : window1_;
bool did_parent_change = false;
bool did_ungroup = false;
ScopedWindowsMover mover(
display::Screen::Get()->GetDisplayNearestWindow(parent).id());
if (window->GetRootWindow() != to_be_moved_window->GetRootWindow()) {
base::RecordAction(
base::UserMetricsAction("SnapGroups_MoveSnapGroupToDisplay"));
SnapGroupController::Get()->RemoveSnapGroup(
this, SnapGroupExitPoint::kMoveToAnotherDisplay);
mover.add_window(to_be_moved_window);
did_parent_change = true;
did_ungroup = true;
} else if (parent != to_be_moved_window->parent()) {
base::RecordAction(
base::UserMetricsAction("SnapGroups_MoveSnapGroupToDesk"));
parent->AddChild(to_be_moved_window);
did_parent_change = true;
}
if (did_parent_change && desks_util::IsDeskContainer(parent) &&
!desks_util::IsWindowVisibleOnAllWorkspaces(to_be_moved_window)) {
mover.set_callback(
base::BindOnce(&window_util::FixWindowStackingAccordingToGlobalMru,
to_be_moved_window));
}
if (!did_ungroup) {
snap_group_divider_.SetVisible(cached_divider_visibility);
RefreshSnapGroup();
}
}
void SnapGroup::OnPreWindowStateTypeChange(WindowState* window_state,
chromeos::WindowStateType old_type) {
if (is_shutting_down_) {
return;
}
if (swapping_windows_) {
return;
}
CHECK(old_type == WindowStateType::kPrimarySnapped ||
old_type == WindowStateType::kSecondarySnapped);
const chromeos::WindowStateType new_type = window_state->GetStateType();
if (new_type != old_type) {
SnapGroupController::Get()->RemoveSnapGroup(
this, GetWindowStateChangeExitPoint(window_state));
}
}
void SnapGroup::OnPostWindowStateTypeChange(
WindowState* window_state,
chromeos::WindowStateType old_type) {
if (window_to_target_snap_position_map_.empty()) {
return;
}
aura::Window* window = window_state->window();
auto iter = window_to_target_snap_position_map_.find(window);
if (iter == window_to_target_snap_position_map_.end()) {
return;
}
const WindowState* window1_state = WindowState::Get(window1_);
const WindowState* window2_state = WindowState::Get(window2_);
if (window_state->GetStateType() ==
GetWindowStateTypeFromSnapPosition(iter->second)) {
window_to_target_snap_position_map_.erase(iter);
}
if (window_to_target_snap_position_map_.empty() &&
window1_state->GetStateType() == WindowStateType::kSecondarySnapped &&
window2_state->GetStateType() == WindowStateType::kPrimarySnapped) {
std::swap(window1_, window2_);
auto new_window1_snap_ratio = WindowState::Get(window1_)->snap_ratio();
CHECK(new_window1_snap_ratio);
ApplyPrimarySnapRatio(*new_window1_snap_ratio);
base::RecordAction(
base::UserMetricsAction("SnapGroups_DoubleTapWindowSwapSuccess"));
swapping_windows_ = false;
}
}
aura::Window* SnapGroup::GetRootWindow() const {
return window1_->GetRootWindow();
}
void SnapGroup::StartResizeWithDivider(const gfx::Point& location_in_screen) {
base::RecordAction(base::UserMetricsAction("SnapGroups_ResizeSnapGroup"));
}
void SnapGroup::UpdateResizeWithDivider(const gfx::Point& location_in_screen) {
CHECK(snap_group_divider_.is_resizing_with_divider());
UpdateGroupWindowsBounds(true);
}
bool SnapGroup::EndResizeWithDivider(const gfx::Point& location_in_screen) {
CHECK(!snap_group_divider_.is_resizing_with_divider());
UpdateGroupWindowsBounds(true);
return true;
}
void SnapGroup::OnResizeEnding() {}
void SnapGroup::OnResizeEnded() {}
void SnapGroup::SwapWindows() {
if (swapping_windows_) {
return;
}
swapping_windows_ = true;
WindowState* window1_state = WindowState::Get(window1_);
const auto window1_snap_ratio = window1_state->snap_ratio();
CHECK(window1_snap_ratio);
WindowState* window2_state = WindowState::Get(window2_);
const auto window2_snap_ratio = window2_state->snap_ratio();
CHECK(window2_snap_ratio);
window_to_target_snap_position_map_[window1_.get()] =
SnapPosition::kSecondary;
window_to_target_snap_position_map_[window2_.get()] = SnapPosition::kPrimary;
const WindowSnapWMEvent secondary_snap_event(WM_EVENT_SNAP_SECONDARY,
*window1_snap_ratio);
window1_state->OnWMEvent(&secondary_snap_event);
const WindowSnapWMEvent primary_snap_event(WM_EVENT_SNAP_PRIMARY,
*window2_snap_ratio);
window2_state->OnWMEvent(&primary_snap_event);
base::RecordAction(
base::UserMetricsAction("SnapGroups_DoubleTapWindowSwapAttempts"));
}
gfx::Rect SnapGroup::GetSnappedWindowBoundsInScreen(
SnapPosition snap_position,
aura::Window* window_for_minimum_size,
float snap_ratio,
bool account_for_divider_width) const {
const int original_divider_position = snap_group_divider_.divider_position();
const int divider_position =
account_for_divider_width
? original_divider_position
: original_divider_position + kSplitviewDividerShortSideLength / 2.f;
return CalculateSnappedWindowBoundsInScreen(
snap_position, window_for_minimum_size->GetRootWindow(),
window_for_minimum_size, account_for_divider_width, divider_position,
snap_group_divider_.is_resizing_with_divider());
}
SnapPosition SnapGroup::GetPositionOfSnappedWindow(
const aura::Window* window) const {
const auto state_type = WindowState::Get(window)->GetStateType();
if (!chromeos::IsSnappedWindowStateType(state_type)) {
return window == window1_ ? SnapPosition::kPrimary
: SnapPosition::kSecondary;
}
return ToSnapPosition(state_type);
}
void SnapGroup::OnDisplayMetricsChanged(const display::Display& display,
uint32_t metrics) {
aura::Window* display_root = Shell::GetRootWindowForDisplayId(display.id());
if (window1_->GetRootWindow() != display_root ||
window2_->GetRootWindow() != display_root) {
return;
}
auto* divider_widget = snap_group_divider_.divider_widget();
if (!divider_widget || !divider_widget->IsVisible()) {
return;
}
if (!(metrics &
(DISPLAY_METRIC_BOUNDS | DISPLAY_METRIC_ROTATION |
DISPLAY_METRIC_DEVICE_SCALE_FACTOR | DISPLAY_METRIC_WORK_AREA))) {
return;
}
RefreshSnapGroup();
}
void SnapGroup::OnWindowActivated(ActivationReason reason,
aura::Window* gained_active,
aura::Window* lost_active) {
if (lost_active == window1_ || lost_active == window2_) {
return;
}
if (gained_active == window1_ || gained_active == window2_) {
base::RecordAction(base::UserMetricsAction("SnapGroups_RecallSnapGroup"));
}
}
void SnapGroup::StartObservingWindows() {
CHECK(window1_);
CHECK(window2_);
for (aura::Window* window : {window1_, window2_}) {
window->AddObserver(this);
WindowState::Get(window)->AddObserver(this);
snap_group_divider_.MaybeAddObservedWindow(window);
}
}
void SnapGroup::StopObservingWindows() {
HideDivider();
for (aura::Window* window : {window1_, window2_}) {
if (window) {
window->RemoveObserver(this);
WindowState::Get(window)->RemoveObserver(this);
snap_group_divider_.MaybeRemoveObservedWindow(window);
}
}
window1_ = nullptr;
window2_ = nullptr;
}
void SnapGroup::UpdateGroupWindowsBounds(bool account_for_divider_width) {
if (display::Screen::Get()->InTabletMode()) {
return;
}
for (aura::Window* window : {window1_, window2_}) {
if (IsSnapped(window)) {
UpdateSnappedWindowBounds(window, account_for_divider_width,
std::nullopt);
}
}
}
void SnapGroup::UpdateSnappedWindowBounds(aura::Window* window,
bool account_for_divider_width,
std::optional<float> snap_ratio) {
gfx::Rect requested_bounds = GetSnappedWindowBoundsInScreen(
GetPositionOfSnappedWindow(window), window,
snap_ratio.value_or(window_util::GetSnapRatioForWindow(window)),
account_for_divider_width);
wm::ConvertRectFromScreen(window->GetRootWindow(), &requested_bounds);
const SetBoundsWMEvent event(requested_bounds, false);
WindowState::Get(window)->OnWMEvent(&event);
}
void SnapGroup::ApplyPrimarySnapRatio(float primary_snap_ratio) {
CHECK(CanWindowsFitInWorkArea(window1_, window2_));
snap_group_divider_.SetDividerPosition(
CalculateDividerPosition(GetRootWindow(), primary_snap_ratio));
UpdateSnappedWindowBounds(window1_, true,
primary_snap_ratio);
UpdateSnappedWindowBounds(window2_, true,
1 - primary_snap_ratio);
}
}