// Copyright 2013 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/client_controlled_state.h"
#include <queue>
#include "ash/display/screen_orientation_controller.h"
#include "ash/display/screen_orientation_controller_test_api.h"
#include "ash/frame/non_client_frame_view_ash.h"
#include "ash/public/cpp/shelf_config.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/test/shell_test_api.h"
#include "ash/root_window_controller.h"
#include "ash/screen_util.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "ash/test/ash_test_util.h"
#include "ash/test/test_widget_builder.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/float/float_controller.h"
#include "ash/wm/float/float_test_api.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_item.h"
#include "ash/wm/overview/overview_test_util.h"
#include "ash/wm/pip/pip_positioner.h"
#include "ash/wm/screen_pinning_controller.h"
#include "ash/wm/snap_group/snap_group.h"
#include "ash/wm/snap_group/snap_group_controller.h"
#include "ash/wm/snap_group/snap_group_test_util.h"
#include "ash/wm/splitview/split_view_constants.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/splitview/split_view_divider.h"
#include "ash/wm/splitview/split_view_test_util.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ash/wm/test/fake_window_state.h"
#include "ash/wm/window_positioning_utils.h"
#include "ash/wm/window_resizer.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_state_delegate.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_event.h"
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/ui/base/app_types.h"
#include "chromeos/ui/base/display_util.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/base/window_state_type.h"
#include "chromeos/ui/frame/caption_buttons/frame_caption_button_container_view.h"
#include "chromeos/ui/frame/caption_buttons/snap_controller.h"
#include "chromeos/ui/frame/header_view.h"
#include "chromeos/ui/wm/constants.h"
#include "chromeos/ui/wm/window_util.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/display/screen.h"
#include "ui/display/test/display_manager_test_api.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/point_conversions.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/wm/core/window_util.h"
namespace ash {
namespace {
using ::chromeos::WindowStateType;
using BoundsRequestCallback =
base::RepeatingCallback<void(const gfx::Rect& bounds)>;
using WindowStateRequestCallback =
base::RepeatingCallback<void(WindowStateType new_state)>;
constexpr gfx::Rect kInitialBounds(0, 0, 100, 100);
class TestClientControlledStateDelegate
: public ClientControlledState::Delegate {
public:
TestClientControlledStateDelegate() = default;
TestClientControlledStateDelegate(const TestClientControlledStateDelegate&) =
delete;
TestClientControlledStateDelegate& operator=(
const TestClientControlledStateDelegate&) = delete;
~TestClientControlledStateDelegate() override = default;
void HandleWindowStateRequest(WindowState* window_state,
WindowStateType next_state) override {
EXPECT_FALSE(deleted_);
old_state_ = window_state->GetStateType();
new_state_ = next_state;
if (window_state_request_callback_) {
window_state_request_callback_.Run(next_state);
}
}
void HandleBoundsRequest(WindowState* window_state,
WindowStateType requested_state,
const gfx::Rect& bounds,
int64_t display_id) override {
requested_bounds_ = bounds;
if (requested_state != window_state->GetStateType()) {
DCHECK(requested_state == WindowStateType::kPrimarySnapped ||
requested_state == WindowStateType::kSecondarySnapped ||
requested_state == WindowStateType::kFloated);
old_state_ = window_state->GetStateType();
new_state_ = requested_state;
}
display_id_ = display_id;
if (bounds_request_callback_) {
bounds_request_callback_.Run(bounds);
}
}
WindowStateType old_state() const { return old_state_; }
WindowStateType new_state() const { return new_state_; }
const gfx::Rect& requested_bounds() const { return requested_bounds_; }
void set_bounds_request_callback(BoundsRequestCallback callback) {
bounds_request_callback_ = std::move(callback);
}
void set_window_state_request_callback(WindowStateRequestCallback callback) {
window_state_request_callback_ = std::move(callback);
}
int64_t display_id() const { return display_id_; }
void Reset() {
old_state_ = WindowStateType::kDefault;
new_state_ = WindowStateType::kDefault;
requested_bounds_.SetRect(0, 0, 0, 0);
display_id_ = display::kInvalidDisplayId;
}
void mark_as_deleted() { deleted_ = true; }
private:
WindowStateType old_state_ = WindowStateType::kDefault;
WindowStateType new_state_ = WindowStateType::kDefault;
int64_t display_id_ = display::kInvalidDisplayId;
gfx::Rect requested_bounds_;
bool deleted_ = false;
BoundsRequestCallback bounds_request_callback_;
WindowStateRequestCallback window_state_request_callback_;
};
class TestWidgetDelegate : public views::WidgetDelegateView {
public:
TestWidgetDelegate() = default;
TestWidgetDelegate(const TestWidgetDelegate&) = delete;
TestWidgetDelegate& operator=(const TestWidgetDelegate&) = delete;
~TestWidgetDelegate() override = default;
void EnableSnap() {
SetCanMaximize(true);
SetCanResize(true);
GetWidget()->OnSizeConstraintsChanged();
}
void EnableFloat() {
SetCanResize(true);
GetWidget()->OnSizeConstraintsChanged();
}
std::unique_ptr<views::NonClientFrameView> CreateNonClientFrameView(
views::Widget* widget) override {
return std::make_unique<NonClientFrameViewAsh>(widget);
}
};
class TestEmptyState : public WindowState::State {
public:
void OnWMEvent(WindowState* window_state, const WMEvent* event) override {}
chromeos::WindowStateType GetType() const override {
return chromeos::WindowStateType::kDefault;
}
void AttachState(WindowState* window_state, State* previous_state) override {}
void DetachState(WindowState* window_state) override {}
void OnWindowDestroying(WindowState* window_state) override {}
};
void VerifySnappedBounds(aura::Window* window, float expected_snap_ratio) {
const WindowState* window_state = WindowState::Get(window);
// `window` must be in any snapped state to use this method.
ASSERT_TRUE(window_state->IsSnapped());
const bool in_tablet = display::Screen::GetScreen()->InTabletMode();
const auto display =
display::Screen::GetScreen()->GetDisplayNearestWindow(window);
const gfx::Rect work_area = display.work_area();
const auto rotation = display.rotation();
const bool is_primary =
window_state->GetStateType() == WindowStateType::kPrimarySnapped;
// Following conditions assume that the natural display orientation is
// landscape.
ASSERT_TRUE(chromeos::IsLandscapeOrientation(
chromeos::GetDisplayNaturalOrientation(display)));
const bool is_landscape = rotation == display::Display::ROTATE_0 ||
rotation == display::Display::ROTATE_180;
const bool is_top_or_left =
(rotation == display::Display::ROTATE_0 && is_primary) ||
(rotation == display::Display::ROTATE_90 && !is_primary) ||
(rotation == display::Display::ROTATE_180 && !is_primary) ||
(rotation == display::Display::ROTATE_270 && is_primary);
// Also consider the divider width if the window is in a snap group.
const bool in_snap_group = [&]() {
auto* snap_group_controller = SnapGroupController::Get();
return snap_group_controller &&
snap_group_controller->GetSnapGroupForGivenWindow(window);
}();
const int divider_margin =
(in_tablet || in_snap_group) ? kSplitviewDividerShortSideLength / 2 : 0;
const gfx::Size expected_size =
is_landscape
? gfx::Size(work_area.width() * expected_snap_ratio - divider_margin,
work_area.height())
: gfx::Size(
work_area.width(),
work_area.height() * expected_snap_ratio - divider_margin);
const gfx::Point expected_origin =
is_landscape
? gfx::Point(is_top_or_left
? work_area.x()
: work_area.right() - expected_size.width(),
work_area.y())
: gfx::Point(work_area.x(),
is_top_or_left
? work_area.y()
: work_area.bottom() - expected_size.height());
const gfx::Rect bounds = window->GetTargetBounds();
// Allow 1px (3px in clamshell) rounding errors for partial snap. Note even if
// `SnapGroup` is enabled, the window may not be in a snap group, so allow 3px
// rounding errors.
// TODO(b/319342277): Investigate why eps can't be 1 when clamshell mode.
const int eps = in_tablet ? 1 : 3;
EXPECT_NEAR(expected_size.width(), bounds.width(), is_landscape ? eps : 0);
EXPECT_NEAR(expected_size.height(), bounds.height(), !is_landscape ? eps : 0);
EXPECT_NEAR(expected_origin.x(), bounds.x(), is_landscape ? eps : 0);
EXPECT_NEAR(expected_origin.y(), bounds.y(), !is_landscape ? eps : 0);
}
} // namespace
class ClientControlledStateTest : public AshTestBase {
public:
ClientControlledStateTest() = default;
ClientControlledStateTest(const ClientControlledStateTest&) = delete;
ClientControlledStateTest& operator=(const ClientControlledStateTest&) =
delete;
~ClientControlledStateTest() override = default;
void SetUp() override {
AshTestBase::SetUp();
widget_delegate_ = new TestWidgetDelegate();
views::Widget::InitParams params(
views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
params.parent = Shell::GetPrimaryRootWindow()->GetChildById(
desks_util::GetActiveDeskContainerId());
params.bounds = kInitialBounds;
params.delegate = widget_delegate_.get();
widget_ = std::make_unique<views::Widget>();
widget_->Init(std::move(params));
WindowState* window_state = WindowState::Get(window());
window_state->set_allow_set_bounds_direct(true);
auto delegate = std::make_unique<TestClientControlledStateDelegate>();
state_delegate_ = delegate.get();
auto state = std::make_unique<ClientControlledState>(std::move(delegate));
state_ = state.get();
window_state->SetStateObject(std::move(state));
auto window_state_delegate = std::make_unique<FakeWindowStateDelegate>();
window_state_delegate_ = window_state_delegate.get();
window_state->SetDelegate(std::move(window_state_delegate));
widget_->Show();
}
void TearDown() override {
widget_ = nullptr;
AshTestBase::TearDown();
}
TestWidgetDelegate* widget_delegate() { return widget_delegate_; }
protected:
aura::Window* window() { return widget_->GetNativeWindow(); }
WindowState* window_state() { return WindowState::Get(window()); }
ClientControlledState* state() { return state_; }
TestClientControlledStateDelegate* delegate() { return state_delegate_; }
views::Widget* widget() { return widget_.get(); }
ScreenPinningController* GetScreenPinningController() {
return Shell::Get()->screen_pinning_controller();
}
FakeWindowStateDelegate* window_state_delegate() {
return window_state_delegate_;
}
chromeos::HeaderView* GetHeaderView() {
auto* const frame = NonClientFrameViewAsh::Get(window());
DCHECK(frame);
return frame->GetHeaderView();
}
void ApplyPendingRequestedBounds() {
state()->set_bounds_locally(true);
widget()->SetBounds(delegate()->requested_bounds());
state()->set_bounds_locally(false);
}
void ClickOnOverviewItem(aura::Window* window) {
auto* const overview_controller = OverviewController::Get();
ASSERT_TRUE(overview_controller->InOverviewSession());
auto* const overview_item = GetOverviewItemForWindow(window);
auto* const event_generator = GetEventGenerator();
event_generator->set_current_screen_location(
gfx::ToRoundedPoint(overview_item->target_bounds().CenterPoint()));
event_generator->ClickLeftButton();
}
void SimulateUnminimizeViaShelfIcon(views::Widget* widget) {
// When clicking an app icon on the hotseat to unminimize the window,
// `ChromeShelfController` shows and activates the widget.
// We here simulate the behavior because //ash should not use any component
// from //chrome/browser/ui.
widget->Show();
widget->Activate();
}
void DragResizeSnappedWindow(aura::Window* window, int target_x) {
ASSERT_TRUE(WindowState::Get(window)->IsSnapped());
ui::test::EventGenerator* const generator = GetEventGenerator();
const bool in_tablet = display::Screen::GetScreen()->InTabletMode();
if (in_tablet) {
auto* split_view_controller = SplitViewController::Get(window);
const gfx::Rect divider_bounds =
split_view_controller->split_view_divider()->GetDividerBoundsInScreen(
false);
generator->set_current_screen_location(divider_bounds.CenterPoint());
} else {
generator->set_current_screen_location(
window->GetBoundsInScreen().right_center());
}
generator->DragMouseTo(gfx::Point(target_x, 0));
}
void DragOverviewItemToSnap(aura::Window* window, bool to_left) {
auto* const overview_controller = OverviewController::Get();
ASSERT_TRUE(overview_controller->InOverviewSession());
auto* const overview_item = GetOverviewItemForWindow(window);
auto* const event_generator = GetEventGenerator();
event_generator->set_current_screen_location(
gfx::ToRoundedPoint(overview_item->target_bounds().CenterPoint()));
const gfx::Rect work_area = display::Screen::GetScreen()
->GetDisplayNearestWindow(window)
.work_area();
event_generator->DragMouseTo(to_left ? work_area.left_center()
: work_area.right_center());
}
private:
raw_ptr<ClientControlledState, DanglingUntriaged> state_ = nullptr;
raw_ptr<TestClientControlledStateDelegate, DanglingUntriaged>
state_delegate_ = nullptr;
raw_ptr<TestWidgetDelegate, DanglingUntriaged> widget_delegate_ =
nullptr; // owned by itself.
raw_ptr<FakeWindowStateDelegate, DanglingUntriaged> window_state_delegate_ =
nullptr;
std::unique_ptr<views::Widget> widget_;
};
class SnapGroupClientControlledStateTest : public ClientControlledStateTest {
public:
SnapGroupClientControlledStateTest() = default;
SnapGroupClientControlledStateTest(
const SnapGroupClientControlledStateTest&) = delete;
SnapGroupClientControlledStateTest& operator=(
const SnapGroupClientControlledStateTest&) = delete;
~SnapGroupClientControlledStateTest() override = default;
private:
base::test::ScopedFeatureList scoped_feature_list_{features::kSnapGroup};
};
// This suite runs test cases both in clamshell mode and tablet mode.
class ClientControlledStateTestClamshellAndTablet
: public ClientControlledStateTest,
public testing::WithParamInterface<bool> {
public:
ClientControlledStateTestClamshellAndTablet() = default;
ClientControlledStateTestClamshellAndTablet(
const ClientControlledStateTestClamshellAndTablet&) = delete;
ClientControlledStateTestClamshellAndTablet& operator=(
const ClientControlledStateTestClamshellAndTablet&) = delete;
~ClientControlledStateTestClamshellAndTablet() override = default;
void SetUp() override {
ClientControlledStateTest::SetUp();
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(InTabletMode());
}
protected:
bool InTabletMode() { return GetParam(); }
};
// The parameter indicates whether the tablet mode is enabled.
INSTANTIATE_TEST_SUITE_P(All,
ClientControlledStateTestClamshellAndTablet,
testing::Bool());
TEST_F(ClientControlledStateTest, ClientControlledFlag) {
ASSERT_TRUE(window_state()->is_client_controlled());
// Attach `TestEmptyState` to detach `ClientControlledState`.
window_state()->SetStateObject(std::make_unique<TestEmptyState>());
EXPECT_FALSE(window_state()->is_client_controlled());
// Attach `ClientControlledState` to detach `TestEmptyState`.
window_state()->SetStateObject(std::make_unique<ClientControlledState>(
std::make_unique<TestClientControlledStateDelegate>()));
EXPECT_TRUE(window_state()->is_client_controlled());
}
// Make sure that calling Maximize()/Minimize()/Fullscreen() result in
// sending the state change request and won't change the state immediately.
// The state will be updated when ClientControlledState::EnterToNextState
// is called.
TEST_F(ClientControlledStateTest, Maximize) {
widget()->Maximize();
// The state shouldn't be updated until EnterToNextState is called.
EXPECT_FALSE(widget()->IsMaximized());
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
EXPECT_EQ(WindowStateType::kDefault, delegate()->old_state());
EXPECT_EQ(WindowStateType::kMaximized, delegate()->new_state());
// Now enters the new state.
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_TRUE(widget()->IsMaximized());
// Bounds is controlled by client.
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
// Maximized request should be also sent. It is up to client impl
// how to handle it.
widget()->SetBounds(gfx::Rect(0, 0, 100, 100));
EXPECT_EQ(gfx::Rect(0, 0, 100, 100), delegate()->requested_bounds());
widget()->Restore();
EXPECT_TRUE(widget()->IsMaximized());
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
EXPECT_EQ(WindowStateType::kMaximized, delegate()->old_state());
EXPECT_EQ(WindowStateType::kNormal, delegate()->new_state());
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_FALSE(widget()->IsMaximized());
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
}
TEST_F(ClientControlledStateTest, Minimize) {
widget()->Minimize();
EXPECT_FALSE(widget()->IsMinimized());
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
EXPECT_EQ(WindowStateType::kDefault, delegate()->old_state());
EXPECT_EQ(WindowStateType::kMinimized, delegate()->new_state());
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_TRUE(widget()->IsMinimized());
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
widget()->Restore();
EXPECT_TRUE(widget()->IsMinimized());
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
EXPECT_EQ(WindowStateType::kMinimized, delegate()->old_state());
EXPECT_EQ(WindowStateType::kNormal, delegate()->new_state());
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_FALSE(widget()->IsMinimized());
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
// use wm::Unminimize to unminimize.
widget()->Minimize();
EXPECT_FALSE(widget()->IsMinimized());
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
EXPECT_EQ(WindowStateType::kNormal, delegate()->old_state());
EXPECT_EQ(WindowStateType::kMinimized, delegate()->new_state());
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_TRUE(widget()->IsMinimized());
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
::wm::Unminimize(widget()->GetNativeWindow());
EXPECT_TRUE(widget()->IsMinimized());
EXPECT_EQ(ui::SHOW_STATE_NORMAL, widget()->GetNativeWindow()->GetProperty(
aura::client::kRestoreShowStateKey));
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
EXPECT_EQ(WindowStateType::kMinimized, delegate()->old_state());
EXPECT_EQ(WindowStateType::kNormal, delegate()->new_state());
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_FALSE(widget()->IsMinimized());
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
}
TEST_F(ClientControlledStateTest, Fullscreen) {
widget()->SetFullscreen(true);
EXPECT_FALSE(widget()->IsFullscreen());
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
EXPECT_EQ(WindowStateType::kDefault, delegate()->old_state());
EXPECT_EQ(WindowStateType::kFullscreen, delegate()->new_state());
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_TRUE(widget()->IsFullscreen());
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
widget()->SetFullscreen(false);
EXPECT_TRUE(widget()->IsFullscreen());
EXPECT_EQ(WindowStateType::kFullscreen, delegate()->old_state());
EXPECT_EQ(WindowStateType::kNormal, delegate()->new_state());
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_FALSE(widget()->IsFullscreen());
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
}
// Make sure toggle fullscreen from maximized state goes back to
// maximized state.
TEST_F(ClientControlledStateTest, MaximizeToFullscreen) {
widget()->Maximize();
EXPECT_FALSE(widget()->IsMaximized());
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
EXPECT_EQ(WindowStateType::kDefault, delegate()->old_state());
EXPECT_EQ(WindowStateType::kMaximized, delegate()->new_state());
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_TRUE(widget()->IsMaximized());
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
widget()->SetFullscreen(true);
EXPECT_TRUE(widget()->IsMaximized());
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
EXPECT_EQ(WindowStateType::kMaximized, delegate()->old_state());
EXPECT_EQ(WindowStateType::kFullscreen, delegate()->new_state());
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_TRUE(widget()->IsFullscreen());
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
widget()->SetFullscreen(false);
EXPECT_TRUE(widget()->IsFullscreen());
EXPECT_EQ(WindowStateType::kFullscreen, delegate()->old_state());
EXPECT_EQ(WindowStateType::kMaximized, delegate()->new_state());
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_TRUE(widget()->IsMaximized());
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
widget()->Restore();
EXPECT_TRUE(widget()->IsMaximized());
EXPECT_EQ(WindowStateType::kMaximized, delegate()->old_state());
EXPECT_EQ(WindowStateType::kNormal, delegate()->new_state());
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_FALSE(widget()->IsMaximized());
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
}
TEST_F(ClientControlledStateTest, IgnoreWorkspace) {
widget()->Maximize();
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_TRUE(widget()->IsMaximized());
delegate()->Reset();
UpdateDisplay("1000x800");
// Client is responsible to handle workspace change, so
// no action should be taken.
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
EXPECT_EQ(WindowStateType::kDefault, delegate()->old_state());
EXPECT_EQ(WindowStateType::kDefault, delegate()->new_state());
EXPECT_EQ(gfx::Rect(), delegate()->requested_bounds());
}
TEST_F(ClientControlledStateTest, SetBounds) {
constexpr gfx::Rect new_bounds(100, 100, 100, 100);
widget()->SetBounds(new_bounds);
EXPECT_EQ(kInitialBounds, widget()->GetWindowBoundsInScreen());
EXPECT_EQ(new_bounds, delegate()->requested_bounds());
state()->set_bounds_locally(true);
widget()->SetBounds(delegate()->requested_bounds());
state()->set_bounds_locally(false);
EXPECT_EQ(new_bounds, widget()->GetWindowBoundsInScreen());
}
TEST_F(ClientControlledStateTest, CenterWindow) {
display::Screen* screen = display::Screen::GetScreen();
const gfx::Rect bounds = screen->GetPrimaryDisplay().work_area();
gfx::Rect center_bounds = bounds;
center_bounds.ClampToCenteredSize(window()->bounds().size());
window()->SetBoundsInScreen(center_bounds, screen->GetPrimaryDisplay());
EXPECT_NEAR(bounds.CenterPoint().x(),
delegate()->requested_bounds().CenterPoint().x(), 1);
EXPECT_NEAR(bounds.CenterPoint().y(),
delegate()->requested_bounds().CenterPoint().y(), 1);
}
TEST_F(ClientControlledStateTest, CycleSnapWindow) {
// Snap disabled.
ASSERT_FALSE(window_state()->CanResize());
ASSERT_FALSE(window_state()->CanSnap());
// The event should be ignored.
const WindowSnapWMEvent snap_left_event(WM_EVENT_CYCLE_SNAP_PRIMARY);
window_state()->OnWMEvent(&snap_left_event);
EXPECT_FALSE(window_state()->IsSnapped());
EXPECT_TRUE(delegate()->requested_bounds().IsEmpty());
const WindowSnapWMEvent snap_right_event(WM_EVENT_CYCLE_SNAP_SECONDARY);
window_state()->OnWMEvent(&snap_right_event);
EXPECT_FALSE(window_state()->IsSnapped());
EXPECT_TRUE(delegate()->requested_bounds().IsEmpty());
// Snap enabled.
widget_delegate()->EnableSnap();
ASSERT_TRUE(window_state()->CanResize());
ASSERT_TRUE(window_state()->CanSnap());
window_state()->OnWMEvent(&snap_left_event);
// No actual state/bounds should be changed until the client applies the
// changes.
EXPECT_NE(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
EXPECT_EQ(kInitialBounds, window()->GetTargetBounds());
EXPECT_EQ(WindowStateType::kDefault, delegate()->old_state());
EXPECT_EQ(WindowStateType::kPrimarySnapped, delegate()->new_state());
// Apply pending requests.
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
delegate()->Reset();
window_state()->OnWMEvent(&snap_right_event);
// No actual state/bounds should be changed until the client applies the
// changes.
EXPECT_NE(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
EXPECT_EQ(WindowStateType::kPrimarySnapped, delegate()->old_state());
EXPECT_EQ(WindowStateType::kSecondarySnapped, delegate()->new_state());
// Apply pending requests.
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
}
// Tests the entry point via selecting a window from partial overview.
TEST_F(SnapGroupClientControlledStateTest, SelectFromOverviewEntryPoint) {
UpdateDisplay("800x600");
// Set the client-controlled window app type so it can be recognized in
// `GetActiveDeskAppWindowsInZOrder()`.
window()->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::ARC_APP);
// Create at least 1 other app window so we can start faster splitview.
widget_delegate()->EnableSnap();
auto non_client_controlled_window = CreateAppWindow();
// Snap the client-controlled window using a snap action source that can start
// faster splitview. Note `SnapOneTestWindow()` would not work here since it
// expects the state type to be updated immediately.
const WindowSnapWMEvent snap_primary_event(
WM_EVENT_SNAP_PRIMARY, chromeos::kDefaultSnapRatio,
WindowSnapActionSource::kSnapByWindowLayoutMenu);
window_state()->OnWMEvent(&snap_primary_event);
// Apply pending requests.
ApplyPendingRequestedBounds();
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
// Test we start faster splitview, then select the normal window.
VerifySplitViewOverviewSession(window());
ClickOnOverviewItem(non_client_controlled_window.get());
EXPECT_EQ(
WindowStateType::kSecondarySnapped,
WindowState::Get(non_client_controlled_window.get())->GetStateType());
// Apply pending bounds changes and verify the state doesn't change.
ApplyPendingRequestedBounds();
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
// Test a snap group is created.
auto* snap_group_controller = SnapGroupController::Get();
ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(
window(), non_client_controlled_window.get()));
UnionBoundsEqualToWorkAreaBounds(
snap_group_controller->GetSnapGroupForGivenWindow(window()));
}
// Tests the entry point via auto grouping on window snapped.
TEST_F(SnapGroupClientControlledStateTest, AutoGroupEntryPoint) {
UpdateDisplay("800x600");
// Set the client-controlled window app type so it can be recognized in
// `GetActiveDeskAppWindowsInZOrder()`.
window()->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::ARC_APP);
widget_delegate()->EnableSnap();
// Snap the client-controlled window. Since it's the only window, we don't
// start faster splitview.
const WindowSnapWMEvent snap_primary_event(
WM_EVENT_SNAP_PRIMARY, chromeos::kDefaultSnapRatio,
WindowSnapActionSource::kSnapByWindowLayoutMenu);
window_state()->OnWMEvent(&snap_primary_event);
// Apply pending requests.
ApplyPendingRequestedBounds();
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
VerifyNotSplitViewOrOverviewSession(window());
// Open a normal window, then snap it to the opposite side of `window()`.
auto non_client_controlled_window = CreateAppWindow();
SnapOneTestWindow(non_client_controlled_window.get(),
WindowStateType::kSecondarySnapped,
chromeos::kDefaultSnapRatio,
WindowSnapActionSource::kSnapByWindowLayoutMenu);
// Apply pending bounds changes and verify the state doesn't change.
ApplyPendingRequestedBounds();
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
// Test a snap group is created.
auto* snap_group_controller = SnapGroupController::Get();
ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(
window(), non_client_controlled_window.get()));
UnionBoundsEqualToWorkAreaBounds(
snap_group_controller->GetSnapGroupForGivenWindow(window()));
}
// Tests basic snap group divider resizing.
TEST_F(SnapGroupClientControlledStateTest, ResizeViaDivider) {
UpdateDisplay("900x600");
// Create a snap group with a client-controlled and normal state window.
widget_delegate()->EnableSnap();
auto non_client_controlled_window = CreateAppWindow();
SnapOneTestWindow(non_client_controlled_window.get(),
WindowStateType::kSecondarySnapped,
chromeos::kDefaultSnapRatio,
WindowSnapActionSource::kSnapByWindowLayoutMenu);
VerifySplitViewOverviewSession(non_client_controlled_window.get());
ClickOnOverviewItem(window());
// Apply pending requests.
ApplyPendingRequestedBounds();
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
SnapGroupController* snap_group_controller = SnapGroupController::Get();
auto* snap_group =
snap_group_controller->GetSnapGroupForGivenWindow(window());
ASSERT_TRUE(snap_group);
auto* snap_group_divider = snap_group->snap_group_divider();
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
// Start a drag on the divider.
auto* event_generator = GetEventGenerator();
// Resize to arbitrary locations with the divider.
for (const float target_width : {300, 450, 600}) {
const gfx::Point divider_center(snap_group_divider
->GetDividerBoundsInScreen(
/*is_dragging=*/false)
.CenterPoint());
event_generator->MoveMouseTo(divider_center);
event_generator->PressLeftButton();
const gfx::Rect bounds_before_resizing(delegate()->requested_bounds());
delegate()->set_bounds_request_callback(
base::BindLambdaForTesting([&](const gfx::Rect& bounds) {
if (bounds == bounds_before_resizing) {
return;
}
// When any new bounds is requested, `OnDragStarted()` should be
// called already.
EXPECT_TRUE(window_state_delegate()->drag_in_progress());
EXPECT_TRUE(window_state()->drag_details()->bounds_change &
WindowResizer::kBoundsChange_Resizes);
delegate()->set_bounds_request_callback(base::NullCallback());
}));
ApplyPendingRequestedBounds();
// Resize with at least 2 steps to simulate the real CUJ of dragging the
// mouse. The default test EventGenerator sends only the start and end
// points which is an abrupt jump between points.
event_generator->MoveMouseTo(gfx::Point(target_width, divider_center.y()),
/*count=*/2);
ASSERT_TRUE(snap_group_divider->is_resizing_with_divider());
EXPECT_TRUE(window_state_delegate()->drag_in_progress());
EXPECT_TRUE(window_state()->drag_details()->bounds_change &
WindowResizer::kBoundsChange_Resizes);
// Apply pending requests.
ApplyPendingRequestedBounds();
const float expected_snap_ratio = target_width / 900;
VerifySnappedBounds(window(), expected_snap_ratio);
EXPECT_NEAR(target_width, window()->GetTargetBounds().width(),
/*abs_error=*/kSplitviewDividerShortSideLength / 2);
event_generator->ReleaseLeftButton();
VerifySnappedBounds(window(), expected_snap_ratio);
// The following drag info is used by client to determine how to handle the
// bounds change.
EXPECT_FALSE(window_state_delegate()->drag_in_progress());
}
}
// Tests the basic functionalities of snap-to-replace.
TEST_F(SnapGroupClientControlledStateTest, SnapToReplace) {
// Create a snap group with 2 normal windows.
auto w1 = CreateAppWindow();
auto w2 = CreateAppWindow();
SnapOneTestWindow(w1.get(), WindowStateType::kPrimarySnapped,
chromeos::kDefaultSnapRatio);
SnapOneTestWindow(w2.get(), WindowStateType::kSecondarySnapped,
chromeos::kDefaultSnapRatio);
SnapGroupController* snap_group_controller = SnapGroupController::Get();
ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
// Snap `window()` on top of `w1`.
widget_delegate()->EnableSnap();
const WindowSnapWMEvent snap_primary_event(
WM_EVENT_SNAP_PRIMARY, chromeos::kDefaultSnapRatio,
WindowSnapActionSource::kSnapByWindowLayoutMenu);
window_state()->OnWMEvent(&snap_primary_event);
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
// Test it replaces `w1` in the group.
EXPECT_FALSE(
snap_group_controller->AreWindowsInSnapGroup(w1.get(), w2.get()));
EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(window(), w2.get()));
}
// Tests that double click on the divider swaps the windows.
TEST_F(SnapGroupClientControlledStateTest, DoubleClickToSwap) {
// Create a snap group.
widget_delegate()->EnableSnap();
auto non_client_controlled_window = CreateAppWindow();
SnapOneTestWindow(non_client_controlled_window.get(),
WindowStateType::kSecondarySnapped,
chromeos::kDefaultSnapRatio,
WindowSnapActionSource::kSnapByWindowLayoutMenu);
VerifySplitViewOverviewSession(non_client_controlled_window.get());
ClickOnOverviewItem(window());
ApplyPendingRequestedBounds();
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
SnapGroupController* snap_group_controller = SnapGroupController::Get();
ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(
window(), non_client_controlled_window.get()));
auto* snap_group =
snap_group_controller->GetSnapGroupForGivenWindow(window());
ASSERT_TRUE(snap_group);
EXPECT_EQ(window(), snap_group->window1());
EXPECT_EQ(non_client_controlled_window.get(), snap_group->window2());
UnionBoundsEqualToWorkAreaBounds(snap_group);
// Double click on the divider.
const gfx::Rect divider_bounds(
snap_group->snap_group_divider()->GetDividerBoundsInScreen(
/*is_dragging=*/false));
auto* event_generator = GetEventGenerator();
event_generator->MoveMouseTo(divider_bounds.CenterPoint());
event_generator->DoubleClickLeftButton();
// Apply pending requests.
ApplyPendingRequestedBounds();
state()->EnterNextState(window_state(), delegate()->new_state());
// Test the state types and windows are swapped.
ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(
window(), non_client_controlled_window.get()));
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
EXPECT_EQ(
WindowStateType::kPrimarySnapped,
WindowState::Get(non_client_controlled_window.get())->GetStateType());
EXPECT_EQ(non_client_controlled_window.get(), snap_group->window1());
EXPECT_EQ(window(), snap_group->window2());
// TODO(b/352621475): Verify `UnionBoundsEqualToWorkAreaBounds()`. Currently
// there may be a 1-px overlap, likely due to rounding.
}
// Tests the snap group window bounds are correct after minimize then
// unminimize.
TEST_F(SnapGroupClientControlledStateTest, SnapThenMinimize) {
UpdateDisplay("800x600");
// Create a snap group with a client-controlled and normal state window.
widget_delegate()->EnableSnap();
auto non_client_controlled_window = CreateAppWindow();
SnapOneTestWindow(non_client_controlled_window.get(),
WindowStateType::kSecondarySnapped,
chromeos::kDefaultSnapRatio,
WindowSnapActionSource::kSnapByWindowLayoutMenu);
VerifySplitViewOverviewSession(non_client_controlled_window.get());
ClickOnOverviewItem(window());
// Apply pending requests. Test the bounds are at 1/2.
ApplyPendingRequestedBounds();
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
SnapGroupController* snap_group_controller = SnapGroupController::Get();
ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(
window(), non_client_controlled_window.get()));
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
// Minimize the client-controlled window.
window_state()->Minimize();
ApplyPendingRequestedBounds();
state()->EnterNextState(window_state(), delegate()->new_state());
// Test the group is broken.
ASSERT_FALSE(snap_group_controller->AreWindowsInSnapGroup(
window(), non_client_controlled_window.get()));
// Unminimize the client-controlled window. Test the bounds are back at 1/2.
window_state()->Unminimize();
ApplyPendingRequestedBounds();
state()->EnterNextState(window_state(), delegate()->new_state());
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
}
// Tests that a client-controlled window in a snap group, when snapped to the
// opposite side, will set the correct bounds. Regression test for
// http://b/349774996.
TEST_F(SnapGroupClientControlledStateTest, SnapToOppositeSide) {
UpdateDisplay("800x600");
// Create a snap group with a client-controlled and normal state window.
widget_delegate()->EnableSnap();
auto non_client_controlled_window = CreateAppWindow();
SnapOneTestWindow(non_client_controlled_window.get(),
WindowStateType::kSecondarySnapped,
chromeos::kDefaultSnapRatio,
WindowSnapActionSource::kSnapByWindowLayoutMenu);
VerifySplitViewOverviewSession(non_client_controlled_window.get());
ClickOnOverviewItem(window());
// Apply pending requests. Test the bounds are at 1/2.
ApplyPendingRequestedBounds();
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
SnapGroupController* snap_group_controller = SnapGroupController::Get();
ASSERT_TRUE(snap_group_controller->AreWindowsInSnapGroup(
window(), non_client_controlled_window.get()));
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
VerifySnappedBounds(non_client_controlled_window.get(),
chromeos::kDefaultSnapRatio);
auto* snap_group =
snap_group_controller->GetSnapGroupForGivenWindow(window());
ASSERT_TRUE(snap_group);
UnionBoundsEqualToWorkAreaBounds(window(), non_client_controlled_window.get(),
snap_group->snap_group_divider());
// Snap to secondary 1/3.
const WindowSnapWMEvent snap_partial_secondary(
WM_EVENT_SNAP_SECONDARY, chromeos::kOneThirdSnapRatio,
WindowSnapActionSource::kSnapByWindowLayoutMenu);
window_state()->OnWMEvent(&snap_partial_secondary);
// Apply pending requests. Test the bounds are at 1/3.
ApplyPendingRequestedBounds();
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
VerifySnappedBounds(window(), chromeos::kOneThirdSnapRatio);
}
TEST_P(ClientControlledStateTestClamshellAndTablet, SnapWindow) {
// Snap disabled.
ASSERT_FALSE(window_state()->CanResize());
ASSERT_FALSE(window_state()->CanSnap());
// The event should be ignored.
const WindowSnapWMEvent snap_primary_event(WM_EVENT_SNAP_PRIMARY);
window_state()->OnWMEvent(&snap_primary_event);
EXPECT_FALSE(window_state()->IsSnapped());
EXPECT_TRUE(delegate()->requested_bounds().IsEmpty());
const WindowSnapWMEvent snap_secondary_event(WM_EVENT_SNAP_SECONDARY);
window_state()->OnWMEvent(&snap_secondary_event);
EXPECT_FALSE(window_state()->IsSnapped());
EXPECT_TRUE(delegate()->requested_bounds().IsEmpty());
// Snap enabled.
widget_delegate()->EnableSnap();
ASSERT_TRUE(window_state()->CanResize());
ASSERT_TRUE(window_state()->CanSnap());
// Snap to primary.
window_state()->OnWMEvent(&snap_primary_event);
// No actual state/bounds should be changed until the client applies the
// changes.
EXPECT_NE(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
EXPECT_EQ(kInitialBounds, window()->GetTargetBounds());
EXPECT_EQ(WindowStateType::kDefault, delegate()->old_state());
EXPECT_EQ(WindowStateType::kPrimarySnapped, delegate()->new_state());
// Apply pending requests.
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
delegate()->Reset();
// Snap to secondary.
window_state()->OnWMEvent(&snap_secondary_event);
// No actual state/bounds should be changed until the client applies the
// changes.
EXPECT_NE(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
EXPECT_EQ(WindowStateType::kPrimarySnapped, delegate()->old_state());
EXPECT_EQ(WindowStateType::kSecondarySnapped, delegate()->new_state());
// Apply pending requests.
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
}
TEST_P(ClientControlledStateTestClamshellAndTablet, PartialSnap) {
// Snap enabled.
widget_delegate()->EnableSnap();
// Test that snap from half to partial works.
const WindowSnapWMEvent snap_left_half(WM_EVENT_SNAP_PRIMARY);
window_state()->OnWMEvent(&snap_left_half);
// No actual state/bounds should be changed until the client applies the
// changes.
EXPECT_NE(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
EXPECT_EQ(kInitialBounds, window()->GetTargetBounds());
EXPECT_EQ(WindowStateType::kDefault, delegate()->old_state());
EXPECT_EQ(WindowStateType::kPrimarySnapped, delegate()->new_state());
// Apply pending requests.
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
const WindowSnapWMEvent snap_left_partial(WM_EVENT_SNAP_PRIMARY,
chromeos::kTwoThirdSnapRatio);
window_state()->OnWMEvent(&snap_left_partial);
// No actual state/bounds should be changed until the client applies the
// changes.
EXPECT_EQ(WindowStateType::kPrimarySnapped, delegate()->new_state());
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
// Apply pending requests.
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
VerifySnappedBounds(window(), chromeos::kTwoThirdSnapRatio);
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
// Test that snap from primary to secondary works.
const WindowSnapWMEvent snap_right_half(WM_EVENT_SNAP_SECONDARY);
window_state()->OnWMEvent(&snap_right_half);
// No actual state/bounds should be changed until the client applies the
// changes.
EXPECT_NE(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
EXPECT_EQ(WindowStateType::kSecondarySnapped, delegate()->new_state());
VerifySnappedBounds(window(), chromeos::kTwoThirdSnapRatio);
// Apply pending requests.
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
const WindowSnapWMEvent snap_right_partial(WM_EVENT_SNAP_SECONDARY,
chromeos::kOneThirdSnapRatio);
window_state()->OnWMEvent(&snap_right_partial);
// No actual state/bounds should be changed until the client applies the
// changes.
EXPECT_EQ(WindowStateType::kSecondarySnapped, delegate()->new_state());
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
// Apply pending requests.
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
VerifySnappedBounds(window(), chromeos::kOneThirdSnapRatio);
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
}
TEST_F(ClientControlledStateTest, SnapInSecondaryDisplay) {
UpdateDisplay("800x600, 600x500");
widget()->SetBounds(gfx::Rect(800, 0, 100, 200));
display::Screen* screen = display::Screen::GetScreen();
const int64_t second_display_id = screen->GetAllDisplays()[1].id();
EXPECT_EQ(second_display_id, screen->GetDisplayNearestWindow(window()).id());
widget_delegate()->EnableSnap();
// Make sure the requested bounds for snapped window is local to display.
const WindowSnapWMEvent snap_left_event(WM_EVENT_CYCLE_SNAP_PRIMARY);
window_state()->OnWMEvent(&snap_left_event);
EXPECT_EQ(second_display_id, delegate()->display_id());
EXPECT_EQ(gfx::Rect(0, 0, 300, 500 - ShelfConfig::Get()->shelf_size()),
delegate()->requested_bounds());
state()->EnterNextState(window_state(), delegate()->new_state());
// Make sure moving to another display tries to update the bounds.
auto first_display = screen->GetAllDisplays()[0];
delegate()->Reset();
state()->set_bounds_locally(true);
window()->SetBoundsInScreen(delegate()->requested_bounds(), first_display);
state()->set_bounds_locally(false);
EXPECT_EQ(first_display.id(), delegate()->display_id());
EXPECT_EQ(gfx::Rect(0, 0, 400, 600 - ShelfConfig::Get()->shelf_size()),
delegate()->requested_bounds());
}
TEST_P(ClientControlledStateTestClamshellAndTablet, SnapMinimizeAndUnminimize) {
UpdateDisplay("900x600");
widget_delegate()->EnableSnap();
const WindowSnapWMEvent snap_left_event(WM_EVENT_SNAP_PRIMARY);
window_state()->OnWMEvent(&snap_left_event);
// Apply pending requests.
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_EQ(WindowStateType::kPrimarySnapped, delegate()->new_state());
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
const float target_width = 300;
const float expected_snap_ratio = target_width / 900;
DragResizeSnappedWindow(window(), target_width);
// Apply pending requests.
ApplyPendingRequestedBounds();
VerifySnappedBounds(window(), expected_snap_ratio);
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
// Minimize.
widget()->Minimize();
state()->EnterNextState(window_state(), delegate()->new_state());
// Unminimize via the `Unminimize` method.
::wm::Unminimize(widget()->GetNativeWindow());
// Apply pending requests.
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
VerifySnappedBounds(window(), expected_snap_ratio);
EXPECT_EQ(WindowStateType::kPrimarySnapped, delegate()->new_state());
// Minimize again.
widget()->Minimize();
state()->EnterNextState(window_state(), delegate()->new_state());
// Unminimize via drag-to-snap to the opposite side.
if (!InTabletMode()) {
ToggleOverview();
}
DragOverviewItemToSnap(window(), /*to_left=*/false);
// The client may activate the widget before accepting the snap request.
widget()->Activate();
// Apply pending requests.
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
// Minimize again.
widget()->Minimize();
state()->EnterNextState(window_state(), delegate()->new_state());
// Unminimize via overview mode.
if (!InTabletMode()) {
ToggleOverview();
}
ClickOnOverviewItem(window());
// The client may activate the widget before accepting the snap request.
widget()->Activate();
// Apply pending requests.
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
// Minimize again.
widget()->Minimize();
state()->EnterNextState(window_state(), delegate()->new_state());
// Unminimize via shelf icon.
SimulateUnminimizeViaShelfIcon(widget());
// The client may activate the widget before accepting the snap request.
widget()->Activate();
// Apply pending requests.
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
}
// Tests that auto snapping from maximized/minimized via overview/shelf works
// for ClientControlledState.
TEST_F(ClientControlledStateTest, AutoSnap) {
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
ASSERT_TRUE(display::Screen::GetScreen()->InTabletMode());
// Snap enabled.
widget_delegate()->EnableSnap();
ASSERT_TRUE(window_state()->CanResize());
ASSERT_TRUE(window_state()->CanSnap());
// Create a normal (non-client-controlled) window in addition to `window()`
// (client-controlled window) to fill the one side of the split view.
auto non_client_controlled_window = CreateAppWindow();
// Snap `non_client_controlled_window` to left.
const WindowSnapWMEvent snap_primary(WM_EVENT_SNAP_PRIMARY);
WindowState::Get(non_client_controlled_window.get())
->OnWMEvent(&snap_primary);
// Click `window()`'s overview item to snap to right.
ClickOnOverviewItem(window());
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
VerifySnappedBounds(non_client_controlled_window.get(),
chromeos::kDefaultSnapRatio);
// Minimize `window()`.
const WMEvent minimize(WM_EVENT_MINIMIZE);
window_state()->OnWMEvent(&minimize);
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_TRUE(window_state()->IsMinimized());
EXPECT_FALSE(window()->IsVisible());
// Click `window()`'s overview item to snap to right.
ClickOnOverviewItem(window());
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
VerifySnappedBounds(non_client_controlled_window.get(),
chromeos::kDefaultSnapRatio);
// Minimize `window()`.
window_state()->OnWMEvent(&minimize);
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_TRUE(window_state()->IsMinimized());
EXPECT_FALSE(window()->IsVisible());
// Unminimize `window()` by clicking the app icon on the shelf.
SimulateUnminimizeViaShelfIcon(widget());
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
VerifySnappedBounds(non_client_controlled_window.get(),
chromeos::kDefaultSnapRatio);
}
// Tests that auto partial-snapping from maximized/minimized via overview/shelf
// works for ClientControlledState.
TEST_F(ClientControlledStateTest, AutoPartialSnap) {
UpdateDisplay("900x600");
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
ASSERT_TRUE(display::Screen::GetScreen()->InTabletMode());
// Snap enabled.
widget_delegate()->EnableSnap();
ASSERT_TRUE(window_state()->CanResize());
ASSERT_TRUE(window_state()->CanSnap());
// Create a normal (non-client-controlled) window in addition to `window()`
// (client-controlled window) to fill the one side of the split view.
auto non_client_controlled_window = CreateAppWindow();
// Snap `non_client_controlled_window` to 1/3 left.
const WindowSnapWMEvent snap_primary(WM_EVENT_SNAP_PRIMARY,
chromeos::kOneThirdSnapRatio);
WindowState::Get(non_client_controlled_window.get())
->OnWMEvent(&snap_primary);
// Click `window()`'s overview item to snap to 2/3 right.
ClickOnOverviewItem(window());
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
VerifySnappedBounds(window(), chromeos::kTwoThirdSnapRatio);
VerifySnappedBounds(non_client_controlled_window.get(),
chromeos::kOneThirdSnapRatio);
// Minimize `window()`.
const WMEvent minimize(WM_EVENT_MINIMIZE);
window_state()->OnWMEvent(&minimize);
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_TRUE(window_state()->IsMinimized());
EXPECT_FALSE(window()->IsVisible());
// Click `window()`'s overview item to snap to 2/3 right.
ClickOnOverviewItem(window());
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
VerifySnappedBounds(window(), chromeos::kTwoThirdSnapRatio);
VerifySnappedBounds(non_client_controlled_window.get(),
chromeos::kOneThirdSnapRatio);
// Minimize `window()`.
window_state()->OnWMEvent(&minimize);
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_TRUE(window_state()->IsMinimized());
EXPECT_FALSE(window()->IsVisible());
// Unminimize `window()` by clicking the app icon on the shelf.
SimulateUnminimizeViaShelfIcon(widget());
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
VerifySnappedBounds(window(), chromeos::kTwoThirdSnapRatio);
VerifySnappedBounds(non_client_controlled_window.get(),
chromeos::kOneThirdSnapRatio);
// Minimize `window()`.
window_state()->OnWMEvent(&minimize);
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_TRUE(window_state()->IsMinimized());
EXPECT_FALSE(window()->IsVisible());
// Resize `non_client_controlled_window` to 2/3 left.
DragResizeSnappedWindow(non_client_controlled_window.get(), 600);
VerifySnappedBounds(non_client_controlled_window.get(),
chromeos::kTwoThirdSnapRatio);
// Click `window()`'s overview item to snap to 1/3 right.
ClickOnOverviewItem(window());
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
VerifySnappedBounds(window(), chromeos::kOneThirdSnapRatio);
VerifySnappedBounds(non_client_controlled_window.get(),
chromeos::kTwoThirdSnapRatio);
// Minimize `window()`.
window_state()->OnWMEvent(&minimize);
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_TRUE(window_state()->IsMinimized());
EXPECT_FALSE(window()->IsVisible());
// Resize `non_client_controlled_window` to 1/3 left.
DragResizeSnappedWindow(non_client_controlled_window.get(), 300);
VerifySnappedBounds(non_client_controlled_window.get(),
chromeos::kOneThirdSnapRatio);
// Unminimize `window()` by clicking the app icon on the shelf.
SimulateUnminimizeViaShelfIcon(widget());
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
VerifySnappedBounds(window(), chromeos::kTwoThirdSnapRatio);
VerifySnappedBounds(non_client_controlled_window.get(),
chromeos::kOneThirdSnapRatio);
}
TEST_P(ClientControlledStateTestClamshellAndTablet, SnapAndRotate) {
// Rotation animation needs an internal display.
const int64_t internal_display_id =
display::test::DisplayManagerTestApi(display_manager())
.SetFirstDisplayAsInternalDisplay();
ScreenOrientationControllerTestApi orientation_test_api(
Shell::Get()->screen_orientation_controller());
// Snap enabled.
widget_delegate()->EnableSnap();
ASSERT_TRUE(window_state()->CanSnap());
for (const bool is_primary : {true, false}) {
SCOPED_TRACE(::testing::Message() << "Testing in primary: " << is_primary);
const auto target_state_type = is_primary
? WindowStateType::kPrimarySnapped
: WindowStateType::kSecondarySnapped;
for (const float snap_ratio :
{chromeos::kDefaultSnapRatio, chromeos::kOneThirdSnapRatio,
chromeos::kTwoThirdSnapRatio}) {
SCOPED_TRACE(::testing::Message()
<< "Testing in snap ratio: " << snap_ratio);
const WindowSnapWMEvent snap_event(
is_primary ? WM_EVENT_SNAP_PRIMARY : WM_EVENT_SNAP_SECONDARY,
snap_ratio);
window_state()->OnWMEvent(&snap_event);
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
VerifySnappedBounds(window(), snap_ratio);
EXPECT_EQ(target_state_type, window_state()->GetStateType());
for (const auto& rotation :
{display::Display::ROTATE_90, display::Display::ROTATE_180,
display::Display::ROTATE_270, display::Display::ROTATE_0}) {
SCOPED_TRACE(::testing::Message()
<< "Testing in rotation: "
<< display::Display::RotationToDegrees(rotation));
// Rotate the display.
orientation_test_api.SetDisplayRotation(
rotation, display::Display::RotationSource::USER);
ASSERT_EQ(Shell::Get()
->display_manager()
->GetDisplayInfo(internal_display_id)
.GetActiveRotation(),
rotation);
// Apply pending requests.
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
VerifySnappedBounds(window(), snap_ratio);
EXPECT_EQ(target_state_type, window_state()->GetStateType());
}
}
}
}
// Tests that resize-to-dismiss split view works for client-controlled windows.
TEST_F(ClientControlledStateTest, ResizeToDismissSplitView) {
UpdateDisplay("900x600");
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
ASSERT_TRUE(display::Screen::GetScreen()->InTabletMode());
auto* const split_view_controller = SplitViewController::Get(window());
widget_delegate()->EnableSnap();
ASSERT_TRUE(window_state()->CanResize());
ASSERT_TRUE(window_state()->CanSnap());
// Create a normal (non-client-controlled) window in addition to `window()`
// (client-controlled window) to fill the one side of the split view.
auto non_client_controlled_window = CreateAppWindow();
for (const bool resize_to_left : {false, true}) {
SCOPED_TRACE(::testing::Message()
<< "Testing in resize-to-left: " << resize_to_left);
// Snap `non_client_controlled_window` to left.
const WindowSnapWMEvent snap_primary(WM_EVENT_SNAP_PRIMARY);
WindowState::Get(non_client_controlled_window.get())
->OnWMEvent(&snap_primary);
// Snap `window()` to right.
const WindowSnapWMEvent snap_secondary(WM_EVENT_SNAP_SECONDARY);
window_state()->OnWMEvent(&snap_secondary);
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_EQ(WindowStateType::kSecondarySnapped,
window_state()->GetStateType());
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
VerifySnappedBounds(non_client_controlled_window.get(),
chromeos::kDefaultSnapRatio);
EXPECT_TRUE(split_view_controller->InSplitViewMode());
views::test::WidgetDestroyedWaiter divider_destroyed_waiter(
split_view_controller->split_view_divider()->divider_widget());
// Move the divider to the left/right edge. It should dismiss the split view
// and move the expanded window to front.
DragResizeSnappedWindow(window(), resize_to_left ? 0 : 900);
// Wait until the divider gets destroyed.
divider_destroyed_waiter.Wait();
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_FALSE(split_view_controller->InSplitViewMode());
EXPECT_TRUE(window_state()->IsMaximized());
EXPECT_TRUE(window()->IsVisible());
EXPECT_EQ(widget()->IsActive(), resize_to_left);
}
}
// Tests that drag-caption-to-snap works for client-controlled windows. The
// order of emitted drag events and state change events matters for a client so
// this test strictly verifies the order of events.
TEST_F(ClientControlledStateTest, DragCaptionToSnap) {
auto* const event_generator = GetEventGenerator();
widget_delegate()->EnableSnap();
ASSERT_TRUE(window_state()->CanResize());
ASSERT_TRUE(window_state()->CanSnap());
const gfx::Rect normal_state_bounds(200, 200, 400, 300);
const SetBoundsWMEvent set_bounds_event(normal_state_bounds);
window_state()->OnWMEvent(&set_bounds_event);
ApplyPendingRequestedBounds();
// First, tests that dragging the caption to snap to primary, and then tests
// that dragging it to secondary.
for (const auto target_state :
{WindowStateType::kPrimarySnapped, WindowStateType::kSecondarySnapped}) {
SCOPED_TRACE(::testing::Message()
<< "Testing in drag-cation-to-snap: from "
<< window_state()->GetStateType() << " to " << target_state);
// Start dragging in the center of the header.
auto* const header_view = GetHeaderView();
gfx::Point next_cursor_point =
header_view->GetBoundsInScreen().CenterPoint();
event_generator->set_current_screen_location(next_cursor_point);
event_generator->PressLeftButton();
// Keep slightly (5px) dragging...
delegate()->set_bounds_request_callback(
base::BindLambdaForTesting([&](const gfx::Rect& bounds) {
// When any new bounds is requested, `OnDragStarted()` should be
// called already.
EXPECT_TRUE(window_state_delegate()->drag_in_progress());
EXPECT_TRUE(window_state()->drag_details()->bounds_change &
WindowResizer::kBoundsChange_Repositions);
}));
next_cursor_point.Offset(-5, 0);
event_generator->MoveMouseTo(next_cursor_point);
// The following drag info is used by client to determine how to handle the
// bounds change.
EXPECT_TRUE(window_state_delegate()->drag_in_progress());
EXPECT_TRUE(window_state()->drag_details()->bounds_change &
WindowResizer::kBoundsChange_Repositions);
ApplyPendingRequestedBounds();
delegate()->set_bounds_request_callback(base::NullCallback());
// Drag it to the left edge of the screen.
const gfx::Rect work_area =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
next_cursor_point = target_state == WindowStateType::kPrimarySnapped
? work_area.left_center()
: work_area.right_center();
event_generator->MoveMouseTo(next_cursor_point);
delegate()->set_window_state_request_callback(
base::BindLambdaForTesting([&](WindowStateType new_state) {
if (new_state != target_state) {
return;
}
// When a new state (i.e., snapped) is requested, `OnDragFinished()`
// should be called already.
EXPECT_FALSE(window_state_delegate()->drag_in_progress());
}));
event_generator->ReleaseLeftButton();
// The following drag info is used by client to determine how to handle the
// bounds change.
EXPECT_FALSE(window_state_delegate()->drag_in_progress());
// Accept the snap request.
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
EXPECT_EQ(target_state, window_state()->GetStateType());
}
}
// Tests that drag-caption-to-unsnap works for client-controlled windows. The
// order of emitted drag events and state change events matters for a client so
// this test strictly verifies the order of events.
TEST_F(ClientControlledStateTest, DragCaptionToUnsnap) {
auto* const event_generator = GetEventGenerator();
widget_delegate()->EnableSnap();
ASSERT_TRUE(window_state()->CanResize());
ASSERT_TRUE(window_state()->CanSnap());
// Snap `window()` to left.
const WindowSnapWMEvent snap_primary(WM_EVENT_SNAP_PRIMARY);
window_state()->OnWMEvent(&snap_primary);
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
// Start dragging in the center of the header.
auto* const header_view = GetHeaderView();
gfx::Point next_cursor_point = header_view->GetBoundsInScreen().CenterPoint();
event_generator->set_current_screen_location(next_cursor_point);
event_generator->PressLeftButton();
// Keep slightly (5px) dragging...
delegate()->set_bounds_request_callback(
base::BindLambdaForTesting([&](const gfx::Rect& bounds) {
// When any new bounds is requested, `OnDragStarted()` should be
// called already.
EXPECT_TRUE(window_state_delegate()->drag_in_progress());
EXPECT_TRUE(window_state()->drag_details()->bounds_change &
WindowResizer::kBoundsChange_Repositions);
}));
next_cursor_point.Offset(5, 0);
event_generator->MoveMouseTo(next_cursor_point);
// The following drag info is used by client to determine how to handle the
// bounds change.
EXPECT_TRUE(window_state_delegate()->drag_in_progress());
EXPECT_TRUE(window_state()->drag_details()->bounds_change &
WindowResizer::kBoundsChange_Repositions);
ApplyPendingRequestedBounds();
delegate()->set_bounds_request_callback(base::NullCallback());
// Drag it to the center of the screen.
const auto work_area =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
next_cursor_point = work_area.CenterPoint();
event_generator->MoveMouseTo(next_cursor_point);
delegate()->set_window_state_request_callback(
base::BindLambdaForTesting([&](WindowStateType new_state) {
if (new_state != chromeos::WindowStateType::kPrimarySnapped) {
return;
}
// When a new state (i.e., normal) is requested, `OnDragFinished()`
// should be called already.
EXPECT_FALSE(window_state_delegate()->drag_in_progress());
}));
event_generator->ReleaseLeftButton();
// The following drag info is used by client to determine how to handle the
// bounds change.
EXPECT_FALSE(window_state_delegate()->drag_in_progress());
// Accept the restore request.
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_EQ(chromeos::WindowStateType::kNormal, window_state()->GetStateType());
}
// Tests that swapping snapped windows works for client-controlled windows
TEST_F(ClientControlledStateTest, SwapSnappedWindows) {
ShellTestApi().SetTabletModeEnabledForTest(true);
ASSERT_TRUE(display::Screen::GetScreen()->InTabletMode());
UpdateDisplay("900x600");
auto* const split_view_controller = SplitViewController::Get(window());
widget_delegate()->EnableSnap();
ASSERT_TRUE(window_state()->CanResize());
ASSERT_TRUE(window_state()->CanSnap());
// Create a normal (non-client-controlled) window in addition to `window()`
// (client-controlled window) to fill the one side of the split view.
auto non_client_controlled_window = CreateAppWindow();
auto* const non_client_controlled_window_state =
WindowState::Get(non_client_controlled_window.get());
// Snap `window()` to 1/3 left.
const WindowSnapWMEvent snap_primary(WM_EVENT_SNAP_PRIMARY,
chromeos::kOneThirdSnapRatio);
window_state()->OnWMEvent(&snap_primary);
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
// Snap `non_client_controlled_window` to 2/3 right.
const WindowSnapWMEvent snap_secondary(WM_EVENT_SNAP_SECONDARY,
chromeos::kTwoThirdSnapRatio);
non_client_controlled_window_state->OnWMEvent(&snap_secondary);
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
EXPECT_EQ(WindowStateType::kSecondarySnapped,
non_client_controlled_window_state->GetStateType());
VerifySnappedBounds(window(), chromeos::kOneThirdSnapRatio);
VerifySnappedBounds(non_client_controlled_window.get(),
chromeos::kTwoThirdSnapRatio);
EXPECT_TRUE(split_view_controller->InSplitViewMode());
// Swap windows.
split_view_controller->SwapWindows();
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
EXPECT_EQ(WindowStateType::kPrimarySnapped,
non_client_controlled_window_state->GetStateType());
VerifySnappedBounds(window(), chromeos::kOneThirdSnapRatio);
VerifySnappedBounds(non_client_controlled_window.get(),
chromeos::kTwoThirdSnapRatio);
EXPECT_TRUE(split_view_controller->InSplitViewMode());
}
// Tests that to-tablet/clamshell conversion carries over the snapped ratio.
TEST_F(ClientControlledStateTest, ClamshellTabletConversionWithSnappedWindow) {
UpdateDisplay("900x600");
auto* const split_view_controller = SplitViewController::Get(window());
widget_delegate()->EnableSnap();
ASSERT_TRUE(window_state()->CanResize());
ASSERT_TRUE(window_state()->CanSnap());
// The scenario starts in clamshell mode.
ShellTestApi().SetTabletModeEnabledForTest(false);
ASSERT_FALSE(display::Screen::GetScreen()->InTabletMode());
// Create a normal (non-client-controlled) window in addition to `window()`
// (client-controlled window) to fill the one side of the split view.
auto non_client_controlled_window = CreateAppWindow();
auto* const non_client_controlled_window_state =
WindowState::Get(non_client_controlled_window.get());
// Snap `window()` to 1/3 left.
const WindowSnapWMEvent snap_primary(WM_EVENT_SNAP_PRIMARY,
chromeos::kOneThirdSnapRatio);
window_state()->OnWMEvent(&snap_primary);
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
// Snap `non_client_controlled_window` to 2/3 right.
const WindowSnapWMEvent snap_secondary(WM_EVENT_SNAP_SECONDARY,
chromeos::kTwoThirdSnapRatio);
non_client_controlled_window_state->OnWMEvent(&snap_secondary);
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
EXPECT_EQ(WindowStateType::kSecondarySnapped,
non_client_controlled_window_state->GetStateType());
VerifySnappedBounds(window(), chromeos::kOneThirdSnapRatio);
VerifySnappedBounds(non_client_controlled_window.get(),
chromeos::kTwoThirdSnapRatio);
EXPECT_FALSE(split_view_controller->InSplitViewMode());
// Clamshell-to-tablet transition should carry over the bounds.
ShellTestApi().SetTabletModeEnabledForTest(true);
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
EXPECT_EQ(WindowStateType::kSecondarySnapped,
non_client_controlled_window_state->GetStateType());
VerifySnappedBounds(window(), chromeos::kOneThirdSnapRatio);
VerifySnappedBounds(non_client_controlled_window.get(),
chromeos::kTwoThirdSnapRatio);
EXPECT_TRUE(split_view_controller->InSplitViewMode());
// Tablet-to-clamshell transition should carry over the bounds.
ShellTestApi().SetTabletModeEnabledForTest(false);
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
EXPECT_EQ(WindowStateType::kSecondarySnapped,
non_client_controlled_window_state->GetStateType());
VerifySnappedBounds(window(), chromeos::kOneThirdSnapRatio);
VerifySnappedBounds(non_client_controlled_window.get(),
chromeos::kTwoThirdSnapRatio);
EXPECT_FALSE(split_view_controller->InSplitViewMode());
}
// Pin events should not be applied immediately. The request should be sent
// to delegate.
TEST_F(ClientControlledStateTest, Pinned) {
ASSERT_FALSE(window_state()->IsPinned());
ASSERT_FALSE(GetScreenPinningController()->IsPinned());
const WMEvent pin_event(WM_EVENT_PIN);
window_state()->OnWMEvent(&pin_event);
EXPECT_FALSE(window_state()->IsPinned());
EXPECT_EQ(WindowStateType::kDefault, delegate()->old_state());
EXPECT_EQ(WindowStateType::kPinned, delegate()->new_state());
state()->EnterNextState(window_state(), WindowStateType::kPinned);
EXPECT_TRUE(window_state()->IsPinned());
EXPECT_TRUE(GetScreenPinningController()->IsPinned());
EXPECT_EQ(WindowStateType::kPinned, window_state()->GetStateType());
EXPECT_EQ(WindowStateType::kDefault, delegate()->old_state());
EXPECT_EQ(WindowStateType::kPinned, delegate()->new_state());
// All state transition events are ignored except for NORMAL.
widget()->Maximize();
EXPECT_EQ(WindowStateType::kPinned, window_state()->GetStateType());
EXPECT_TRUE(GetScreenPinningController()->IsPinned());
widget()->Minimize();
EXPECT_EQ(WindowStateType::kPinned, window_state()->GetStateType());
EXPECT_TRUE(GetScreenPinningController()->IsPinned());
EXPECT_TRUE(window()->IsVisible());
widget()->SetFullscreen(true);
EXPECT_EQ(WindowStateType::kPinned, window_state()->GetStateType());
EXPECT_TRUE(GetScreenPinningController()->IsPinned());
// WM/User cannot change the bounds of the pinned window.
constexpr gfx::Rect new_bounds(100, 100, 200, 100);
widget()->SetBounds(new_bounds);
EXPECT_TRUE(delegate()->requested_bounds().IsEmpty());
// But client can change the bounds of the pinned window.
state()->set_bounds_locally(true);
widget()->SetBounds(new_bounds);
state()->set_bounds_locally(false);
EXPECT_EQ(new_bounds, widget()->GetWindowBoundsInScreen());
widget()->Restore();
EXPECT_TRUE(window_state()->IsPinned());
EXPECT_EQ(WindowStateType::kPinned, delegate()->old_state());
EXPECT_EQ(WindowStateType::kNormal, delegate()->new_state());
state()->EnterNextState(window_state(), WindowStateType::kNormal);
EXPECT_FALSE(window_state()->IsPinned());
EXPECT_EQ(WindowStateType::kNormal, window_state()->GetStateType());
EXPECT_FALSE(GetScreenPinningController()->IsPinned());
// Two windows cannot be pinned simultaneously.
auto widget2 =
CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
WindowState* window_state_2 = WindowState::Get(widget2->GetNativeWindow());
window_state_2->OnWMEvent(&pin_event);
EXPECT_TRUE(window_state_2->IsPinned());
EXPECT_TRUE(GetScreenPinningController()->IsPinned());
// Pin request should fail.
EXPECT_FALSE(window_state()->IsPinned());
window_state()->OnWMEvent(&pin_event);
EXPECT_NE(WindowStateType::kPinned, delegate()->new_state());
}
TEST_F(ClientControlledStateTest, TrustedPinnedBasic) {
EXPECT_FALSE(window_state()->IsPinned());
EXPECT_FALSE(GetScreenPinningController()->IsPinned());
const WMEvent trusted_pin_event(WM_EVENT_TRUSTED_PIN);
window_state()->OnWMEvent(&trusted_pin_event);
EXPECT_FALSE(window_state()->IsPinned());
EXPECT_EQ(WindowStateType::kDefault, delegate()->old_state());
EXPECT_EQ(WindowStateType::kTrustedPinned, delegate()->new_state());
state()->EnterNextState(window_state(), WindowStateType::kTrustedPinned);
EXPECT_TRUE(window_state()->IsPinned());
EXPECT_TRUE(GetScreenPinningController()->IsPinned());
EXPECT_EQ(WindowStateType::kTrustedPinned, window_state()->GetStateType());
EXPECT_EQ(WindowStateType::kDefault, delegate()->old_state());
EXPECT_EQ(WindowStateType::kTrustedPinned, delegate()->new_state());
// All state transition events are ignored except for NORMAL.
widget()->Maximize();
EXPECT_EQ(WindowStateType::kTrustedPinned, window_state()->GetStateType());
EXPECT_TRUE(GetScreenPinningController()->IsPinned());
widget()->Minimize();
EXPECT_EQ(WindowStateType::kTrustedPinned, window_state()->GetStateType());
EXPECT_TRUE(GetScreenPinningController()->IsPinned());
EXPECT_TRUE(window()->IsVisible());
widget()->SetFullscreen(true);
EXPECT_EQ(WindowStateType::kTrustedPinned, window_state()->GetStateType());
EXPECT_TRUE(GetScreenPinningController()->IsPinned());
// WM/User cannot change the bounds of the trusted-pinned window.
constexpr gfx::Rect new_bounds(100, 100, 200, 100);
widget()->SetBounds(new_bounds);
EXPECT_TRUE(delegate()->requested_bounds().IsEmpty());
// But client can change the bounds of the trusted-pinned window.
state()->set_bounds_locally(true);
widget()->SetBounds(new_bounds);
state()->set_bounds_locally(false);
EXPECT_EQ(new_bounds, widget()->GetWindowBoundsInScreen());
widget()->Restore();
EXPECT_TRUE(window_state()->IsPinned());
EXPECT_EQ(WindowStateType::kTrustedPinned, delegate()->old_state());
EXPECT_EQ(WindowStateType::kNormal, delegate()->new_state());
state()->EnterNextState(window_state(), WindowStateType::kNormal);
EXPECT_FALSE(window_state()->IsPinned());
EXPECT_EQ(WindowStateType::kNormal, window_state()->GetStateType());
EXPECT_FALSE(GetScreenPinningController()->IsPinned());
// Two windows cannot be trusted-pinned simultaneously.
auto widget2 =
CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
WindowState* window_state_2 = WindowState::Get(widget2->GetNativeWindow());
window_state_2->OnWMEvent(&trusted_pin_event);
EXPECT_TRUE(window_state_2->IsTrustedPinned());
EXPECT_TRUE(GetScreenPinningController()->IsPinned());
EXPECT_FALSE(window_state()->IsTrustedPinned());
window_state()->OnWMEvent(&trusted_pin_event);
EXPECT_NE(WindowStateType::kTrustedPinned, delegate()->new_state());
EXPECT_TRUE(window_state_2->IsTrustedPinned());
}
TEST_F(ClientControlledStateTest, ClosePinned) {
EXPECT_FALSE(window_state()->IsPinned());
EXPECT_FALSE(GetScreenPinningController()->IsPinned());
const WMEvent trusted_pin_event(WM_EVENT_TRUSTED_PIN);
window_state()->OnWMEvent(&trusted_pin_event);
EXPECT_FALSE(window_state()->IsPinned());
EXPECT_EQ(WindowStateType::kDefault, delegate()->old_state());
EXPECT_EQ(WindowStateType::kTrustedPinned, delegate()->new_state());
state()->EnterNextState(window_state(), WindowStateType::kTrustedPinned);
EXPECT_TRUE(window_state()->IsPinned());
EXPECT_TRUE(GetScreenPinningController()->IsPinned());
delegate()->mark_as_deleted();
widget()->CloseNow();
}
TEST_F(ClientControlledStateTest, MoveWindowToDisplay) {
UpdateDisplay("600x500, 600x500");
display::Screen* screen = display::Screen::GetScreen();
const int64_t first_display_id = screen->GetAllDisplays()[0].id();
const int64_t second_display_id = screen->GetAllDisplays()[1].id();
EXPECT_EQ(first_display_id, screen->GetDisplayNearestWindow(window()).id());
window_util::MoveWindowToDisplay(window(), second_display_id);
// Make sure that the boundsChange request has correct destination
// information.
EXPECT_EQ(second_display_id, delegate()->display_id());
EXPECT_EQ(window()->bounds(), delegate()->requested_bounds());
}
TEST_F(ClientControlledStateTest, MoveWindowToDisplayOutOfBounds) {
UpdateDisplay("1000x500, 600x500");
state()->set_bounds_locally(true);
constexpr int kWidth = 100;
widget()->SetBounds(gfx::Rect(700, 0, kWidth, 200));
state()->set_bounds_locally(false);
EXPECT_EQ(gfx::Rect(700, 0, kWidth, 200),
widget()->GetWindowBoundsInScreen());
display::Screen* screen = display::Screen::GetScreen();
const int64_t first_display_id = screen->GetAllDisplays()[0].id();
const int64_t second_display_id = screen->GetAllDisplays()[1].id();
EXPECT_EQ(first_display_id, screen->GetDisplayNearestWindow(window()).id());
window_util::MoveWindowToDisplay(window(), second_display_id);
// Make sure that the boundsChange request has correct destination
// information.
EXPECT_EQ(second_display_id, delegate()->display_id());
// The bounds is constrained by
// |AdjustBoundsToEnsureMinimumWindowVisibility| in the secondary
// display.
constexpr int kMinVisibleWidth = kWidth * kMinimumPercentOnScreenArea;
EXPECT_EQ(gfx::Rect(600 - kMinVisibleWidth, 0, kWidth, 200),
delegate()->requested_bounds());
}
// Make sure disconnecting primary notifies the display id change.
TEST_F(ClientControlledStateTest, DisconnectPrimary) {
UpdateDisplay("600x500,600x500");
SwapPrimaryDisplay();
auto* screen = display::Screen::GetScreen();
auto old_primary_id = screen->GetPrimaryDisplay().id();
EXPECT_EQ(old_primary_id, window_state()->GetDisplay().id());
gfx::Rect bounds = window()->bounds();
UpdateDisplay("600x500");
ASSERT_NE(old_primary_id, screen->GetPrimaryDisplay().id());
EXPECT_EQ(delegate()->display_id(), screen->GetPrimaryDisplay().id());
EXPECT_EQ(bounds, delegate()->requested_bounds());
}
TEST_F(ClientControlledStateTest,
WmEventNormalIsResolvedToMaximizeInTabletMode) {
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
ASSERT_TRUE(display::Screen::GetScreen()->InTabletMode());
window_state()->window()->SetProperty(
aura::client::kResizeBehaviorKey,
aura::client::kResizeBehaviorCanMaximize);
const WMEvent normal_event(WM_EVENT_NORMAL);
window_state()->OnWMEvent(&normal_event);
EXPECT_EQ(WindowStateType::kMaximized, delegate()->new_state());
}
TEST_F(ClientControlledStateTest,
IgnoreWmEventWhenWindowIsInTransitionalSnappedState) {
auto* split_view_controller =
SplitViewController::Get(window_state()->window());
widget_delegate()->EnableSnap();
split_view_controller->SnapWindow(window_state()->window(),
SnapPosition::kSecondary);
EXPECT_EQ(WindowStateType::kSecondarySnapped, delegate()->new_state());
EXPECT_FALSE(window_state()->IsSnapped());
// Ensures the window is in a transitional snapped state.
EXPECT_TRUE(split_view_controller->IsWindowInTransitionalState(
window_state()->window()));
EXPECT_EQ(WindowStateType::kSecondarySnapped, delegate()->new_state());
EXPECT_FALSE(window_state()->IsSnapped());
// Ignores WMEvent if in a transitional state.
widget()->Maximize();
EXPECT_NE(WindowStateType::kMaximized, delegate()->new_state());
// Applies snap request.
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_TRUE(window_state()->IsSnapped());
// After exiting the transitional state, works normally.
widget()->Maximize();
EXPECT_EQ(WindowStateType::kMaximized, delegate()->new_state());
}
TEST_P(ClientControlledStateTestClamshellAndTablet, ResizeSnappedWindow) {
// Set screen width.
UpdateDisplay("1200x600");
ASSERT_EQ(chromeos::OrientationType::kLandscapePrimary,
GetCurrentScreenOrientation());
// Snap a window
widget_delegate()->EnableSnap();
const WindowSnapWMEvent snap_primary(WM_EVENT_SNAP_PRIMARY);
window_state()->OnWMEvent(&snap_primary);
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_TRUE(window_state()->IsSnapped());
const gfx::Rect bounds_before_resizing(delegate()->requested_bounds());
// Start drag-resizing from the center point of the work area.
auto* const event_generator = GetEventGenerator();
gfx::Point next_cursor_point = display::Screen::GetScreen()
->GetPrimaryDisplay()
.work_area()
.CenterPoint();
event_generator->set_current_screen_location(next_cursor_point);
event_generator->PressLeftButton();
// Test the requested bounds do not change.
EXPECT_EQ(bounds_before_resizing, delegate()->requested_bounds());
// Keep dragging...
delegate()->set_bounds_request_callback(
base::BindLambdaForTesting([&](const gfx::Rect& bounds) {
if (bounds == bounds_before_resizing) {
return;
}
// When any new bounds is requested, `OnDragStarted()` should be called
// already.
EXPECT_TRUE(window_state_delegate()->drag_in_progress());
EXPECT_TRUE(window_state()->drag_details()->bounds_change &
WindowResizer::kBoundsChange_Resizes);
}));
next_cursor_point.Offset(-50, 0);
event_generator->MoveMouseTo(next_cursor_point);
// The following drag info is used by client to determine how to handle the
// bounds change.
EXPECT_TRUE(window_state_delegate()->drag_in_progress());
EXPECT_TRUE(window_state()->drag_details()->bounds_change &
WindowResizer::kBoundsChange_Resizes);
ApplyPendingRequestedBounds();
delegate()->set_bounds_request_callback(base::NullCallback());
// Drag to 1/3 (i.e. make the width 400).
const float target_width = 400;
next_cursor_point.set_x(target_width);
event_generator->MoveMouseTo(next_cursor_point);
event_generator->ReleaseLeftButton();
// The following drag info is used by client to determine how to handle the
// bounds change.
EXPECT_FALSE(window_state_delegate()->drag_in_progress());
ApplyPendingRequestedBounds();
VerifySnappedBounds(window(), target_width / 1200);
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
// Changing display size should keep the current snap ratio.
UpdateDisplay("900x600");
ApplyPendingRequestedBounds();
VerifySnappedBounds(window(), target_width / 1200);
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
}
// Tests that a window leaves the snapped state when the client sets a new
// window state.
TEST_P(ClientControlledStateTestClamshellAndTablet,
LeaveSnappedStateByNewStateChange) {
auto* const split_view_controller = SplitViewController::Get(window());
widget_delegate()->EnableSnap();
for (const auto new_state_type :
{WindowStateType::kMaximized, WindowStateType::kFullscreen}) {
// Snap a window.
const WindowSnapWMEvent snap_primary(WM_EVENT_SNAP_PRIMARY);
window_state()->OnWMEvent(&snap_primary);
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
if (InTabletMode()) {
EXPECT_TRUE(split_view_controller->InSplitViewMode());
}
EXPECT_EQ(window_state()->GetStateType(), WindowStateType::kPrimarySnapped);
// The client sets a new state.
state()->EnterNextState(window_state(), new_state_type);
ApplyPendingRequestedBounds();
if (InTabletMode()) {
EXPECT_FALSE(split_view_controller->InSplitViewMode());
}
EXPECT_EQ(window_state()->GetStateType(), new_state_type);
}
}
TEST_F(ClientControlledStateTest, FlingFloatedWindowInTabletMode) {
// The AppType must be set to any except `chromeos::AppType::NON_APP` (default
// value) to make it floatable.
window()->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::ARC_APP);
widget_delegate()->EnableFloat();
ASSERT_TRUE(chromeos::wm::CanFloatWindow(window()));
// Enter tablet mode
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
ASSERT_TRUE(display::Screen::GetScreen()->InTabletMode());
// Float window.
const WindowFloatWMEvent float_event(
chromeos::FloatStartLocation::kBottomRight);
window_state()->OnWMEvent(&float_event);
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_TRUE(window_state()->IsFloated());
EXPECT_EQ(kShellWindowId_FloatContainer, window()->parent()->GetId());
// Start dragging in the center of the header and fling it to the top left.
const auto initial_bounds = delegate()->requested_bounds();
auto* const header_view = GetHeaderView();
auto* const event_generator = GetEventGenerator();
const auto start = header_view->GetBoundsInScreen().CenterPoint();
const gfx::Vector2d offset(-20, -20);
EXPECT_FALSE(window_state_delegate()->drag_in_progress());
event_generator->GestureScrollSequenceWithCallback(
start, start + offset, base::Milliseconds(10), /*steps=*/2,
base::BindLambdaForTesting(
[&](ui::EventType event_type, const gfx::Vector2dF& delta) {
if (event_type != ui::EventType::kGestureScrollUpdate) {
return;
}
EXPECT_TRUE(window_state_delegate()->drag_in_progress());
}));
EXPECT_FALSE(window_state_delegate()->drag_in_progress());
// In tablet mode, `FloatController` magnetize the window so the
// drag-to-top-left operation should result in placing the window at the top
// left with padding.
const int padding = chromeos::wm::kFloatedWindowPaddingDp;
EXPECT_EQ(delegate()->requested_bounds(),
gfx::Rect(gfx::Point(padding, padding), initial_bounds.size()));
}
TEST_F(ClientControlledStateTest, TuckAndUntuckFloatedWindowInTabletMode) {
ui::ScopedAnimationDurationScaleMode test_duration_mode(
ui::ScopedAnimationDurationScaleMode::NORMAL_DURATION);
// This test checks the window animation state, but not interested in the
// animation by the education.
FloatTestApi::ScopedTuckEducationDisabler scoped_tuck_education_disabler;
auto* const float_controller = Shell::Get()->float_controller();
// The AppType must be set to any except `chromeos::AppType::NON_APP` (default
// value) to make it floatable.
window()->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::ARC_APP);
widget_delegate()->EnableFloat();
ASSERT_TRUE(chromeos::wm::CanFloatWindow(window()));
// Enter tablet mode
Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
ASSERT_TRUE(display::Screen::GetScreen()->InTabletMode());
// Float window.
const WindowFloatWMEvent float_event(
chromeos::FloatStartLocation::kBottomRight);
window_state()->OnWMEvent(&float_event);
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_TRUE(window_state()->IsFloated());
EXPECT_EQ(kShellWindowId_FloatContainer, window()->parent()->GetId());
// Test tucking.
// Start dragging in the center of the header and fling it to offscreen.
auto* const header_view = GetHeaderView();
auto* const event_generator = GetEventGenerator();
const gfx::Point start = header_view->GetBoundsInScreen().CenterPoint();
const gfx::Vector2d offset(10, 10);
event_generator->GestureScrollSequence(start, start + offset,
base::Milliseconds(10), /*steps=*/1);
EXPECT_TRUE(window()->layer()->GetAnimator()->is_animating());
// Client-requested bounds change should be blocked while animating.
const auto start_bounds = window()->GetBoundsInScreen();
const gfx::Rect client_requested_bounds(0, 0, 256, 256);
state()->set_bounds_locally(true);
widget()->SetBounds(client_requested_bounds);
state()->set_bounds_locally(false);
EXPECT_EQ(window()->GetBoundsInScreen(), start_bounds);
EXPECT_TRUE(window()->IsVisible());
ShellTestApi().WaitForWindowFinishAnimating(window());
EXPECT_FALSE(window()->IsVisible());
EXPECT_TRUE(float_controller->IsFloatedWindowTuckedForTablet(window()));
EXPECT_FALSE(window()->layer()->GetAnimator()->is_animating());
// Bounds change should be blocked while tucked.
const auto tucked_bounds = window()->GetBoundsInScreen();
state()->set_bounds_locally(true);
widget()->SetBounds(client_requested_bounds);
state()->set_bounds_locally(false);
EXPECT_EQ(window()->GetBoundsInScreen(), tucked_bounds);
// Rotation should update the bounds.
Shell::Get()->display_manager()->SetDisplayRotation(
display::Screen::GetScreen()->GetPrimaryDisplay().id(),
display::Display::ROTATE_90, display::Display::RotationSource::USER);
// Manually call the rotation animation callback here as the animator is only
// used when a wallpaper is set, and there is no easy way to fake a wallpaper
// in ash_unittests.
float_controller->OnScreenRotationAnimationFinished(
Shell::GetPrimaryRootWindowController()->GetScreenRotationAnimator(),
/*canceled=*/false);
EXPECT_FALSE(window()->IsVisible());
EXPECT_TRUE(float_controller->IsFloatedWindowTuckedForTablet(window()));
EXPECT_EQ(FloatController::GetFloatWindowTabletBounds(window()),
window()->GetBoundsInScreen());
// Test untucking.
float_controller->MaybeUntuckFloatedWindowForTablet(window());
ShellTestApi().WaitForWindowFinishAnimating(window());
EXPECT_TRUE(window()->IsVisible());
EXPECT_FALSE(float_controller->IsFloatedWindowTuckedForTablet(window()));
EXPECT_EQ(FloatController::GetFloatWindowTabletBounds(window()),
delegate()->requested_bounds());
// Bounds change should NOT be blocked after untucked.
state()->set_bounds_locally(true);
widget()->SetBounds(client_requested_bounds);
state()->set_bounds_locally(false);
EXPECT_EQ(window()->GetBoundsInScreen(), client_requested_bounds);
}
TEST_P(ClientControlledStateTestClamshellAndTablet, MoveFloatedWindow) {
// The AppType must be set to any except `chromeos::AppType::NON_APP` (default
// value) to make it floatable.
window()->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::ARC_APP);
if (InTabletMode()) {
// Resizing must be enabled in tablet mode to float.
widget_delegate()->EnableFloat();
}
ASSERT_TRUE(chromeos::wm::CanFloatWindow(window()));
// Float window.
const WindowFloatWMEvent float_event(
chromeos::FloatStartLocation::kBottomRight);
window_state()->OnWMEvent(&float_event);
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_TRUE(window_state()->IsFloated());
EXPECT_EQ(kShellWindowId_FloatContainer, window()->parent()->GetId());
// Start dragging on the left of the minimize button.
auto* const header_view = GetHeaderView();
auto* const event_generator = GetEventGenerator();
chromeos::FrameCaptionButtonContainerView::TestApi test_api(
header_view->caption_button_container());
event_generator->set_current_screen_location(
gfx::Point(test_api.minimize_button()->GetBoundsInScreen().x() - 5,
// Minimize button y coordinate is at the top of the header, so
// use the center point of the header instead.
header_view->GetBoundsInScreen().CenterPoint().y()));
event_generator->PressLeftButton();
EXPECT_TRUE(window_state_delegate()->drag_in_progress());
gfx::Rect expected_bounds = delegate()->requested_bounds();
// Drag to the top left with some interval points. Verify the window is
// aligned with the new cursor point.
for (const gfx::Vector2d& diff :
{gfx::Vector2d(-10, -10), gfx::Vector2d(-100, -10),
gfx::Vector2d(-400, -400)}) {
event_generator->MoveMouseBy(diff.x(), diff.y());
expected_bounds.Offset(diff);
EXPECT_TRUE(window_state_delegate()->drag_in_progress());
EXPECT_EQ(delegate()->requested_bounds(), expected_bounds);
ApplyPendingRequestedBounds();
}
event_generator->ReleaseLeftButton();
EXPECT_FALSE(window_state_delegate()->drag_in_progress());
if (InTabletMode()) {
// In tablet mode, we have magnetism so the drag-to-top-left operation
// should result in placing the window at the top left with padding.
const int padding = chromeos::wm::kFloatedWindowPaddingDp;
expected_bounds.set_origin(gfx::Point(padding, padding));
EXPECT_EQ(delegate()->requested_bounds(), expected_bounds);
} else {
// In clamshell mode, we don't have magnetism so the window bounds should
// persist after releasing the mouse button.
EXPECT_EQ(delegate()->requested_bounds(), expected_bounds);
}
// Minimize and unminimize the window. Test that its bounds are restored.
window_state()->Minimize();
window_state()->Restore();
ApplyPendingRequestedBounds();
EXPECT_EQ(delegate()->requested_bounds(), expected_bounds);
}
TEST_P(ClientControlledStateTestClamshellAndTablet, FloatWindow) {
// The AppType must be set to any except `chromeos::AppType::NON_APP` (default
// value) to make it floatable.
window()->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::ARC_APP);
if (InTabletMode()) {
// Resizing must be enabled in tablet mode to float.
widget_delegate()->EnableFloat();
}
ASSERT_TRUE(chromeos::wm::CanFloatWindow(window()));
// Test float.
const WindowFloatWMEvent float_event(
chromeos::FloatStartLocation::kBottomRight);
window_state()->OnWMEvent(&float_event);
EXPECT_EQ(InTabletMode()
? FloatController::GetFloatWindowTabletBounds(window())
: FloatController::GetFloatWindowClamshellBounds(
window(), chromeos::FloatStartLocation::kBottomRight),
delegate()->requested_bounds());
EXPECT_EQ(WindowStateType::kDefault, delegate()->old_state());
EXPECT_EQ(WindowStateType::kFloated, delegate()->new_state());
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_TRUE(window_state()->IsFloated());
EXPECT_EQ(kShellWindowId_FloatContainer, window()->parent()->GetId());
// Test rotate.
ASSERT_TRUE(chromeos::wm::IsLandscapeOrientationForWindow(window()));
Shell::Get()->display_manager()->SetDisplayRotation(
display::Screen::GetScreen()->GetPrimaryDisplay().id(),
display::Display::ROTATE_90, display::Display::RotationSource::USER);
ASSERT_FALSE(chromeos::wm::IsLandscapeOrientationForWindow(window()));
EXPECT_EQ(InTabletMode()
? FloatController::GetFloatWindowTabletBounds(window())
: FloatController::GetFloatWindowClamshellBounds(
window(), chromeos::FloatStartLocation::kBottomRight),
delegate()->requested_bounds());
// Test minimize.
const WMEvent minimize_event(WM_EVENT_MINIMIZE);
window_state()->OnWMEvent(&minimize_event);
EXPECT_EQ(WindowStateType::kFloated, delegate()->old_state());
EXPECT_EQ(WindowStateType::kMinimized, delegate()->new_state());
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_TRUE(window_state()->IsMinimized());
EXPECT_FALSE(window()->IsVisible());
// Test unminimize.
const WMEvent unminimize_event(WM_EVENT_RESTORE);
window_state()->OnWMEvent(&unminimize_event);
EXPECT_EQ(InTabletMode()
? FloatController::GetFloatWindowTabletBounds(window())
: FloatController::GetFloatWindowClamshellBounds(
window(), chromeos::FloatStartLocation::kBottomRight),
delegate()->requested_bounds());
EXPECT_EQ(WindowStateType::kMinimized, delegate()->old_state());
EXPECT_EQ(WindowStateType::kFloated, delegate()->new_state());
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_TRUE(window_state()->IsFloated());
EXPECT_EQ(kShellWindowId_FloatContainer, window()->parent()->GetId());
// Test unfloat.
const WMEvent restore_event(WM_EVENT_RESTORE);
window_state()->OnWMEvent(&restore_event);
EXPECT_EQ(WindowStateType::kFloated, delegate()->old_state());
EXPECT_EQ(WindowStateType::kNormal, delegate()->new_state());
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_FALSE(window_state()->IsFloated());
EXPECT_NE(kShellWindowId_FloatContainer, window()->parent()->GetId());
}
TEST_P(ClientControlledStateTestClamshellAndTablet,
DragOverviewWindowToSnapOneSide) {
auto* const overview_controller = OverviewController::Get();
auto* const split_view_controller = SplitViewController::Get(window());
widget_delegate()->EnableSnap();
// Create a fake normal window in addition to `window()` (client-controlled
// window) because we need at least two windows to keep overview mode active
// after snapping one of them.
auto fake_uninterested_window = CreateAppWindow();
// Enter overview.
ToggleOverview();
EXPECT_TRUE(overview_controller->InOverviewSession());
EXPECT_FALSE(split_view_controller->InSplitViewMode());
// Drag `window()`'s overview item to snap to left.
DragOverviewItemToSnap(window(), /*to_left=*/true);
// Ensures the window is in a transitional snapped state.
EXPECT_TRUE(split_view_controller->IsWindowInTransitionalState(window()));
EXPECT_EQ(WindowStateType::kPrimarySnapped, delegate()->new_state());
EXPECT_FALSE(window_state()->IsSnapped());
// Activating window just before accepting the request shouldn't end the
// overview.
widget()->Activate();
EXPECT_TRUE(overview_controller->InOverviewSession());
// Accept the snap request.
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
EXPECT_TRUE(window_state()->IsSnapped());
EXPECT_TRUE(split_view_controller->InSplitViewMode());
EXPECT_EQ(split_view_controller->state(),
SplitViewController::State::kPrimarySnapped);
EXPECT_EQ(split_view_controller->primary_window(), window());
EXPECT_TRUE(overview_controller->InOverviewSession());
}
TEST_P(ClientControlledStateTestClamshellAndTablet,
DragOverviewWindowToSnapBothSide) {
auto* const overview_controller = OverviewController::Get();
auto* const split_view_controller = SplitViewController::Get(window());
widget_delegate()->EnableSnap();
// Create a normal (non-client-controlled) window in addition to `window()`
// (client-controlled window) to fill the one side of the split view.
auto non_client_controlled_window = CreateAppWindow();
// Enter overview.
ToggleOverview();
EXPECT_TRUE(overview_controller->InOverviewSession());
EXPECT_FALSE(split_view_controller->InSplitViewMode());
// Drag `non_client_controlled_window`'s overview item to snap to left.
DragOverviewItemToSnap(non_client_controlled_window.get(), /*to_left=*/true);
// Click `window()`'s overview item to snap to right.
ClickOnOverviewItem(window());
// Ensures the window is in a transitional snapped state.
EXPECT_TRUE(split_view_controller->IsWindowInTransitionalState(window()));
EXPECT_EQ(WindowStateType::kSecondarySnapped, delegate()->new_state());
EXPECT_FALSE(window_state()->IsSnapped());
// Activating window just before accepting the request shouldn't end the
// overview.
widget()->Activate();
EXPECT_TRUE(overview_controller->InOverviewSession());
// Accept the snap request.
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_TRUE(window_state()->IsSnapped());
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
VerifySnappedBounds(non_client_controlled_window.get(),
chromeos::kDefaultSnapRatio);
EXPECT_EQ(WindowStateType::kSecondarySnapped, window_state()->GetStateType());
EXPECT_EQ(
WindowStateType::kPrimarySnapped,
WindowState::Get(non_client_controlled_window.get())->GetStateType());
if (InTabletMode()) {
// In tablet mode, we should keep splitview while overview should end.
EXPECT_TRUE(split_view_controller->InSplitViewMode());
EXPECT_EQ(split_view_controller->state(),
SplitViewController::State::kBothSnapped);
EXPECT_EQ(split_view_controller->secondary_window(), window());
EXPECT_FALSE(overview_controller->InOverviewSession());
} else {
// In clamshell mode, we should end both splitview and overview.
EXPECT_FALSE(split_view_controller->InSplitViewMode());
EXPECT_EQ(split_view_controller->state(),
SplitViewController::State::kNoSnap);
EXPECT_FALSE(overview_controller->InOverviewSession());
}
}
// Tests that a client-controlled window works with dragging the window to the
// edge of the screen to replace an snapped window with the dragged window.
TEST_P(ClientControlledStateTestClamshellAndTablet,
DragOverviewWindowToReplaceSnappedWindow) {
auto* const overview_controller = OverviewController::Get();
auto* const split_view_controller = SplitViewController::Get(window());
widget_delegate()->EnableSnap();
// Create a normal (non-client-controlled) window in addition to `window()`.
auto non_client_controlled_window = CreateAppWindow();
// Enter overview.
ToggleOverview();
EXPECT_TRUE(overview_controller->InOverviewSession());
EXPECT_FALSE(split_view_controller->InSplitViewMode());
// Drag `non_client_controlled_window`'s overview item to snap to left.
DragOverviewItemToSnap(non_client_controlled_window.get(), /*to_left=*/true);
EXPECT_EQ(
WindowStateType::kPrimarySnapped,
WindowState::Get(non_client_controlled_window.get())->GetStateType());
EXPECT_TRUE(overview_controller->InOverviewSession());
// Drag `window()`'s overview item to snap to left.
DragOverviewItemToSnap(window(), /*to_left=*/true);
// Ensures the window is in a transitional snapped state.
EXPECT_TRUE(split_view_controller->IsWindowInTransitionalState(window()));
EXPECT_EQ(WindowStateType::kPrimarySnapped, delegate()->new_state());
EXPECT_FALSE(window_state()->IsSnapped());
// Activating window just before accepting the request shouldn't trigger
// another auto snapping.
widget()->Activate();
EXPECT_TRUE(overview_controller->InOverviewSession());
// Accept the snap request.
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
EXPECT_TRUE(window_state()->IsSnapped());
// `window()` should be snapped to left. And `non_client_controlled_window`
// should be kicked out of snapped state and be in overview.
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
EXPECT_TRUE(overview_controller->InOverviewSession());
EXPECT_TRUE(GetOverviewItemForWindow(non_client_controlled_window.get()));
}
TEST_P(ClientControlledStateTestClamshellAndTablet,
SnapBeforePreviousEventIsApplied) {
auto* const overview_controller = OverviewController::Get();
auto* const split_view_controller = SplitViewController::Get(window());
widget_delegate()->EnableSnap();
std::queue<WindowStateType> new_state_queue;
std::queue<gfx::Rect> requested_bounds_queue;
// Send a maximize request.
const WMEvent maximize(WM_EVENT_MAXIMIZE);
window_state()->OnWMEvent(&maximize);
new_state_queue.push(delegate()->new_state());
requested_bounds_queue.push(delegate()->requested_bounds());
// Send a snap request.
const WindowSnapWMEvent snap(WM_EVENT_SNAP_PRIMARY);
window_state()->OnWMEvent(&snap);
new_state_queue.push(delegate()->new_state());
requested_bounds_queue.push(delegate()->requested_bounds());
// Process requests sequentially.
ASSERT_EQ(new_state_queue.size(), requested_bounds_queue.size());
while (!new_state_queue.empty() && !requested_bounds_queue.empty()) {
state()->EnterNextState(window_state(), new_state_queue.front());
state()->set_bounds_locally(true);
widget()->SetBounds(requested_bounds_queue.front());
state()->set_bounds_locally(false);
new_state_queue.pop();
requested_bounds_queue.pop();
}
// The window should be snapped as it's the last requested state.
EXPECT_TRUE(window_state()->IsSnapped());
// In tablet mode, split view mode should be activated.
if (InTabletMode()) {
EXPECT_TRUE(split_view_controller->InSplitViewMode());
EXPECT_EQ(split_view_controller->state(),
SplitViewController::State::kPrimarySnapped);
EXPECT_EQ(split_view_controller->primary_window(), window());
EXPECT_TRUE(overview_controller->InOverviewSession());
}
}
TEST_P(ClientControlledStateTestClamshellAndTablet, SnapFloatedWindow) {
// The AppType must be set to any except `chromeos::AppType::NON_APP` (default
// value) to make it floatable.
window()->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::ARC_APP);
widget_delegate()->EnableFloat();
ASSERT_TRUE(chromeos::wm::CanFloatWindow(window()));
widget_delegate()->EnableSnap();
ASSERT_TRUE(window_state()->CanSnap());
// Send a float request and accepts it.
const WindowFloatWMEvent float_event(
chromeos::FloatStartLocation::kBottomRight);
window_state()->OnWMEvent(&float_event);
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
ASSERT_TRUE(window_state()->IsFloated());
// Send a snap request but don't accept it yet.
const WindowSnapWMEvent snap(WM_EVENT_SNAP_PRIMARY);
window_state()->OnWMEvent(&snap);
ASSERT_EQ(WindowStateType::kPrimarySnapped, delegate()->new_state());
ASSERT_FALSE(window_state()->IsSnapped());
// Emit the size constraints changed event.
widget()->OnSizeConstraintsChanged();
// The requested bounds should be the snapped one (not floated bounds).
EXPECT_EQ(WindowStateType::kPrimarySnapped, delegate()->new_state());
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
VerifySnappedBounds(window(), chromeos::kDefaultSnapRatio);
EXPECT_EQ(WindowStateType::kPrimarySnapped, window_state()->GetStateType());
}
// Tests that floating a fullscreen window to replace a floated window works
// properly without any crash. Regression test for b/322374826.
TEST_P(ClientControlledStateTestClamshellAndTablet,
ReplaceFloatedWindowWithFullscreenWindow) {
// The AppType must be set to any except `chromeos::AppType::NON_APP` (default
// value) to make it floatable.
window()->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::ARC_APP);
widget_delegate()->EnableFloat();
ASSERT_TRUE(chromeos::wm::CanFloatWindow(window()));
// Make `window()` fullscreen to hide shelf.
const WMEvent enter_fullscreen(WM_EVENT_FULLSCREEN);
window_state()->OnWMEvent(&enter_fullscreen);
state()->EnterNextState(window_state(), delegate()->new_state());
EXPECT_TRUE(widget()->IsFullscreen());
// Create another client-controlled window.
auto widget2 =
TestWidgetBuilder()
.SetParent(Shell::GetPrimaryRootWindow()->GetChildById(
desks_util::GetActiveDeskContainerId()))
.SetBounds(kInitialBounds)
.SetTestWidgetDelegate()
.SetWindowProperty(chromeos::kAppTypeKey, chromeos::AppType::ARC_APP)
.SetShow(false)
.BuildOwnsNativeWidget();
auto* const window_state2 = WindowState::Get(widget2->GetNativeWindow());
window_state2->set_allow_set_bounds_direct(true);
auto delegate2 = std::make_unique<TestClientControlledStateDelegate>();
auto* const state_delegate2_ptr = delegate2.get();
auto state2 = std::make_unique<ClientControlledState>(std::move(delegate2));
auto* state2_ptr = state2.get();
window_state2->SetStateObject(std::move(state2));
widget2->Show();
// Float `widget2`.
const WindowFloatWMEvent float_event(
chromeos::FloatStartLocation::kBottomRight);
window_state2->OnWMEvent(&float_event);
state2_ptr->EnterNextState(window_state2, state_delegate2_ptr->new_state());
ASSERT_TRUE(window_state2->IsFloated());
// Float `window`.
window_state()->OnWMEvent(&float_event);
state()->EnterNextState(window_state(), delegate()->new_state());
ApplyPendingRequestedBounds();
ASSERT_TRUE(window_state()->IsFloated());
// Floating `window` should result in unfloating `widget2`.
state2_ptr->EnterNextState(window_state2, state_delegate2_ptr->new_state());
EXPECT_FALSE(window_state2->IsFloated());
}
} // namespace ash