// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/exo/client_controlled_shell_surface.h"
#include "ash/display/screen_orientation_controller.h"
#include "ash/frame/non_client_frame_view_ash.h"
#include "ash/frame/wide_frame_view.h"
#include "ash/public/cpp/arc_resize_lock_type.h"
#include "ash/public/cpp/shelf_types.h"
#include "ash/public/cpp/test/shell_test_api.h"
#include "ash/shelf/shelf.h"
#include "ash/shell.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ash/test/test_widget_builder.h"
#include "ash/wm/pip/pip_controller.h"
#include "ash/wm/pip/pip_positioner.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h"
#include "ash/wm/window_resizer.h"
#include "ash/wm/window_restore/window_restore_controller.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_event.h"
#include "ash/wm/work_area_insets.h"
#include "ash/wm/workspace_controller_test_api.h"
#include "base/memory/raw_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "cc/paint/display_item_list.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/ui/base/window_pin_type.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/frame/caption_buttons/caption_button_model.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 "components/app_restore/window_properties.h"
#include "components/exo/buffer.h"
#include "components/exo/display.h"
#include "components/exo/permission.h"
#include "components/exo/sub_surface.h"
#include "components/exo/surface.h"
#include "components/exo/test/exo_test_base.h"
#include "components/exo/test/exo_test_helper.h"
#include "components/exo/test/shell_surface_builder.h"
#include "components/exo/test/surface_tree_host_test_util.h"
#include "third_party/skia/include/utils/SkNoDrawCanvas.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/client/window_parenting_client.h"
#include "ui/aura/env.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/aura/window_targeter.h"
#include "ui/aura/window_tree_host.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/compositor/test/layer_animation_stopped_waiter.h"
#include "ui/compositor_extra/shadow.h"
#include "ui/display/display.h"
#include "ui/display/test/display_manager_test_api.h"
#include "ui/display/types/display_constants.h"
#include "ui/display/util/display_util.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event_targeter.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/views/paint_info.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/caption_button_types.h"
#include "ui/wm/core/shadow_controller.h"
#include "ui/wm/core/shadow_types.h"
using chromeos::WindowStateType;
namespace exo {
namespace {
class ClientControlledShellSurfaceTest
: public test::ExoTestBase,
public testing::WithParamInterface<test::FrameSubmissionType> {
public:
ClientControlledShellSurfaceTest() {
test::SetFrameSubmissionFeatureFlags(&feature_list_, GetParam());
}
private:
base::test::ScopedFeatureList feature_list_;
};
bool HasBackdrop() {
ash::WorkspaceController* wc = ash::ShellTestApi().workspace_controller();
return !!ash::WorkspaceControllerTestApi(wc).GetBackdropWindow();
}
bool IsWidgetPinned(views::Widget* widget) {
return ash::WindowState::Get(widget->GetNativeWindow())->IsPinned();
}
int GetShadowElevation(aura::Window* window) {
return window->GetProperty(wm::kShadowElevationKey);
}
void EnableTabletMode(bool enable) {
if (enable) {
ash::TabletModeControllerTestApi().EnterTabletMode();
} else {
ash::TabletModeControllerTestApi().LeaveTabletMode();
}
}
// A canvas that just logs when a text blob is drawn.
class TestCanvas : public SkNoDrawCanvas {
public:
TestCanvas() : SkNoDrawCanvas(100, 100) {}
TestCanvas(const TestCanvas&) = delete;
TestCanvas& operator=(const TestCanvas&) = delete;
~TestCanvas() override {}
void onDrawTextBlob(const SkTextBlob*,
SkScalar,
SkScalar,
const SkPaint&) override {
text_was_drawn_ = true;
}
bool text_was_drawn() const { return text_was_drawn_; }
private:
bool text_was_drawn_ = false;
};
} // namespace
// Instantiate the values of frame submission types in the parameterized tests.
INSTANTIATE_TEST_SUITE_P(All,
ClientControlledShellSurfaceTest,
testing::Values(test::FrameSubmissionType::kNoReactive,
test::FrameSubmissionType::kReactive));
TEST_P(ClientControlledShellSurfaceTest, SetPinned) {
auto shell_surface = exo::test::ShellSurfaceBuilder({256, 256})
.BuildClientControlledShellSurface();
shell_surface->SetPinned(chromeos::WindowPinType::kTrustedPinned);
EXPECT_FALSE(IsWidgetPinned(shell_surface->GetWidget()));
shell_surface->root_surface()->Commit();
EXPECT_TRUE(IsWidgetPinned(shell_surface->GetWidget()));
shell_surface->SetRestored();
EXPECT_TRUE(IsWidgetPinned(shell_surface->GetWidget()));
shell_surface->root_surface()->Commit();
EXPECT_FALSE(IsWidgetPinned(shell_surface->GetWidget()));
shell_surface->SetPinned(chromeos::WindowPinType::kPinned);
EXPECT_FALSE(IsWidgetPinned(shell_surface->GetWidget()));
shell_surface->root_surface()->Commit();
EXPECT_TRUE(IsWidgetPinned(shell_surface->GetWidget()));
shell_surface->SetRestored();
EXPECT_TRUE(IsWidgetPinned(shell_surface->GetWidget()));
shell_surface->root_surface()->Commit();
EXPECT_FALSE(IsWidgetPinned(shell_surface->GetWidget()));
}
TEST_P(ClientControlledShellSurfaceTest, SetSystemUiVisibility) {
auto shell_surface = exo::test::ShellSurfaceBuilder({256, 256})
.BuildClientControlledShellSurface();
shell_surface->SetSystemUiVisibility(true);
EXPECT_TRUE(
ash::WindowState::Get(shell_surface->GetWidget()->GetNativeWindow())
->autohide_shelf_when_maximized_or_fullscreen());
shell_surface->SetSystemUiVisibility(false);
EXPECT_FALSE(
ash::WindowState::Get(shell_surface->GetWidget()->GetNativeWindow())
->autohide_shelf_when_maximized_or_fullscreen());
}
TEST_P(ClientControlledShellSurfaceTest, SetTopInset) {
auto shell_surface = exo::test::ShellSurfaceBuilder({64, 64})
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
ASSERT_TRUE(window);
EXPECT_EQ(0, window->GetProperty(aura::client::kTopViewInset));
constexpr int kTopInsetHeight = 20;
shell_surface->SetTopInset(kTopInsetHeight);
surface->Commit();
EXPECT_EQ(kTopInsetHeight, window->GetProperty(aura::client::kTopViewInset));
}
TEST_P(ClientControlledShellSurfaceTest, UpdateModalWindow) {
auto shell_surface = exo::test::ShellSurfaceBuilder({640, 480})
.SetUseSystemModalContainer()
.SetInputRegion(cc::Region())
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
EXPECT_FALSE(ash::Shell::IsSystemModalWindowOpen());
EXPECT_FALSE(shell_surface->GetWidget()->IsActive());
// Creating a surface without input region should not make it modal.
std::unique_ptr<Display> display(new Display);
std::unique_ptr<Surface> child = display->CreateSurface();
constexpr gfx::Size kBufferSize(128, 128);
auto child_buffer = test::ExoTestHelper::CreateBuffer(kBufferSize);
child->Attach(child_buffer.get());
std::unique_ptr<SubSurface> sub_surface(
display->CreateSubSurface(child.get(), surface));
surface->SetSubSurfacePosition(child.get(), gfx::PointF(10, 10));
child->Commit();
surface->Commit();
EXPECT_FALSE(ash::Shell::IsSystemModalWindowOpen());
EXPECT_FALSE(shell_surface->GetWidget()->IsActive());
// Making the surface opaque shouldn't make it modal either.
child->SetBlendMode(SkBlendMode::kSrc);
child->Commit();
surface->Commit();
EXPECT_FALSE(ash::Shell::IsSystemModalWindowOpen());
EXPECT_FALSE(shell_surface->GetWidget()->IsActive());
// Setting input regions won't make it modal either.
surface->SetInputRegion(gfx::Rect(10, 10, 100, 100));
surface->Commit();
EXPECT_FALSE(ash::Shell::IsSystemModalWindowOpen());
EXPECT_FALSE(shell_surface->GetWidget()->IsActive());
// Only SetSystemModal changes modality.
shell_surface->SetSystemModal(true);
EXPECT_TRUE(ash::Shell::IsSystemModalWindowOpen());
EXPECT_TRUE(shell_surface->GetWidget()->IsActive());
shell_surface->SetSystemModal(false);
EXPECT_FALSE(ash::Shell::IsSystemModalWindowOpen());
EXPECT_FALSE(shell_surface->GetWidget()->IsActive());
// If the non modal system window was active,
shell_surface->GetWidget()->Activate();
EXPECT_TRUE(shell_surface->GetWidget()->IsActive());
shell_surface->SetSystemModal(true);
EXPECT_TRUE(ash::Shell::IsSystemModalWindowOpen());
EXPECT_TRUE(shell_surface->GetWidget()->IsActive());
shell_surface->SetSystemModal(false);
EXPECT_FALSE(ash::Shell::IsSystemModalWindowOpen());
EXPECT_TRUE(shell_surface->GetWidget()->IsActive());
}
TEST_P(ClientControlledShellSurfaceTest,
ModalWindowSetSystemModalBeforeCommit) {
auto shell_surface = exo::test::ShellSurfaceBuilder({640, 480})
.SetUseSystemModalContainer()
.SetInputRegion(cc::Region())
.SetNoCommit()
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
// Set SetSystemModal before any commit happens. Widget is not created at
// this time.
EXPECT_FALSE(shell_surface->GetWidget());
shell_surface->SetSystemModal(true);
surface->Commit();
// It is expected that modal window is shown.
EXPECT_TRUE(shell_surface->GetWidget());
EXPECT_TRUE(ash::Shell::IsSystemModalWindowOpen());
// Now widget is created and setting modal state should be applied
// immediately.
shell_surface->SetSystemModal(false);
EXPECT_FALSE(ash::Shell::IsSystemModalWindowOpen());
}
TEST_P(ClientControlledShellSurfaceTest,
NonSystemModalContainerCantChangeModality) {
auto shell_surface = exo::test::ShellSurfaceBuilder({640, 480})
.SetInputRegion(cc::Region())
.EnableSystemModal()
.BuildClientControlledShellSurface();
// It is expected that a non system modal container is unable to set a system
// modal.
EXPECT_FALSE(ash::Shell::IsSystemModalWindowOpen());
}
TEST_P(ClientControlledShellSurfaceTest, SurfaceShadow) {
auto shell_surface = exo::test::ShellSurfaceBuilder({128, 128})
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
// 1) Initial state, no shadow (SurfaceFrameType is NONE);
EXPECT_FALSE(wm::ShadowController::GetShadowForWindow(window));
std::unique_ptr<Display> display(new Display);
// 2) Just creating a sub surface won't create a shadow.
auto* child =
test::ShellSurfaceBuilder::AddChildSurface(surface, {0, 0, 128, 128});
surface->Commit();
EXPECT_FALSE(wm::ShadowController::GetShadowForWindow(window));
// 3) Create a shadow.
surface->SetFrame(SurfaceFrameType::SHADOW);
shell_surface->SetShadowBounds(gfx::Rect(10, 10, 100, 100));
surface->Commit();
ui::Shadow* shadow = wm::ShadowController::GetShadowForWindow(window);
ASSERT_TRUE(shadow);
EXPECT_TRUE(shadow->layer()->visible());
gfx::Rect before = shadow->layer()->bounds();
// 4) Shadow bounds is independent of the sub surface.
constexpr gfx::Size kNewBufferSize(256, 256);
auto new_child_buffer = test::ExoTestHelper::CreateBuffer(kNewBufferSize);
child->Attach(new_child_buffer.get());
child->Commit();
surface->Commit();
EXPECT_EQ(before, shadow->layer()->bounds());
// 4) Updating the widget's window bounds should not change the shadow bounds.
// TODO(oshima): The following scenario only worked with Xdg/ShellSurface,
// which never uses SetShadowBounds. This is broken with correct scenario, and
// will be fixed when the bounds control is delegated to the client.
//
// window->SetBounds(gfx::Rect(10, 10, 100, 100));
// EXPECT_EQ(before, shadow->layer()->bounds());
// 5) This should disable shadow.
shell_surface->SetShadowBounds(gfx::Rect());
surface->Commit();
EXPECT_EQ(wm::kShadowElevationNone, GetShadowElevation(window));
EXPECT_FALSE(shadow->layer()->visible());
// 6) This should enable non surface shadow again.
shell_surface->SetShadowBounds(gfx::Rect(10, 10, 100, 100));
surface->Commit();
EXPECT_EQ(wm::kShadowElevationDefault, GetShadowElevation(window));
EXPECT_TRUE(shadow->layer()->visible());
}
TEST_P(ClientControlledShellSurfaceTest, ShadowWithStateChange) {
constexpr gfx::Size kContentSize(100, 100);
// Position the widget at 10,10 so that we get non zero offset.
auto shell_surface = exo::test::ShellSurfaceBuilder(kContentSize)
.SetGeometry({gfx::Point(10, 10), kContentSize})
.SetFrame(SurfaceFrameType::SHADOW)
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
// In parent coordinates.
constexpr gfx::Rect kShadowBounds(gfx::Point(-10, -10), kContentSize);
views::Widget* widget = shell_surface->GetWidget();
aura::Window* window = widget->GetNativeWindow();
ui::Shadow* shadow = wm::ShadowController::GetShadowForWindow(window);
shell_surface->SetShadowBounds(kShadowBounds);
surface->Commit();
EXPECT_EQ(wm::kShadowElevationDefault, GetShadowElevation(window));
EXPECT_TRUE(shadow->layer()->visible());
// Origin must be in sync.
EXPECT_EQ(kShadowBounds.origin(), shadow->content_bounds().origin());
const gfx::Rect work_area =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
// Maximizing window hides the shadow.
widget->Maximize();
ASSERT_TRUE(widget->IsMaximized());
EXPECT_FALSE(shadow->layer()->visible());
shell_surface->SetShadowBounds(work_area);
surface->Commit();
EXPECT_FALSE(shadow->layer()->visible());
// Restoring bounds will re-enable shadow. It's content size is set to work
// area,/ thus not visible until new bounds is committed.
widget->Restore();
EXPECT_TRUE(shadow->layer()->visible());
EXPECT_EQ(work_area, shadow->content_bounds());
// The bounds is updated.
shell_surface->SetShadowBounds(kShadowBounds);
surface->Commit();
EXPECT_EQ(kShadowBounds, shadow->content_bounds());
}
TEST_P(ClientControlledShellSurfaceTest, ShadowWithTransform) {
constexpr gfx::Size kContentSize(100, 100);
// Position the widget at 10,10 so that we get non zero offset.
auto shell_surface = exo::test::ShellSurfaceBuilder(kContentSize)
.SetGeometry({gfx::Point(10, 10), kContentSize})
.SetFrame(SurfaceFrameType::SHADOW)
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
ui::Shadow* shadow = wm::ShadowController::GetShadowForWindow(window);
// In parent coordinates.
constexpr gfx::Rect kShadowBounds(gfx::Point(-10, -10), kContentSize);
// Shadow bounds relative to its parent should not be affected by a transform.
gfx::Transform transform;
transform.Translate(50, 50);
window->SetTransform(transform);
shell_surface->SetShadowBounds(kShadowBounds);
surface->Commit();
EXPECT_TRUE(shadow->layer()->visible());
EXPECT_EQ(gfx::Rect(-10, -10, 100, 100), shadow->content_bounds());
}
TEST_P(ClientControlledShellSurfaceTest, ShadowStartMaximized) {
auto shell_surface =
exo::test::ShellSurfaceBuilder({256, 256})
.SetWindowState(chromeos::WindowStateType::kMaximized)
.SetFrame(SurfaceFrameType::SHADOW)
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
views::Widget* widget = shell_surface->GetWidget();
aura::Window* window = widget->GetNativeWindow();
// There is no shadow when started in maximized state.
EXPECT_FALSE(wm::ShadowController::GetShadowForWindow(window));
// Sending a shadow bounds in maximized state won't create a shadow.
shell_surface->SetShadowBounds(gfx::Rect(10, 10, 100, 100));
surface->Commit();
EXPECT_FALSE(wm::ShadowController::GetShadowForWindow(window));
// Restore the window and make sure the shadow is created, visible and
// has the latest bounds.
widget->Restore();
ui::Shadow* shadow = wm::ShadowController::GetShadowForWindow(window);
ASSERT_TRUE(shadow);
EXPECT_TRUE(shadow->layer()->visible());
EXPECT_EQ(gfx::Rect(10, 10, 100, 100), shadow->content_bounds());
}
TEST_P(ClientControlledShellSurfaceTest, Frame) {
UpdateDisplay("800x600");
constexpr gfx::Rect kClientBounds(20, 50, 300, 200);
constexpr gfx::Rect kFullscreenBounds(0, 0, 800, 600);
// The window bounds is the client bounds + frame size.
constexpr gfx::Rect kNormalWindowBounds(20, 18, 300, 232);
auto shell_surface = exo::test::ShellSurfaceBuilder({kClientBounds.size()})
.SetGeometry(kClientBounds)
.SetFrame(SurfaceFrameType::NORMAL)
.SetNoCommit()
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
shell_surface->SetSystemUiVisibility(true); // disable shelf.
surface->Commit();
int64_t display_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
display::DisplayManager* display_manager =
ash::Shell::Get()->display_manager();
views::Widget* widget = shell_surface->GetWidget();
ash::NonClientFrameViewAsh* frame_view =
static_cast<ash::NonClientFrameViewAsh*>(
widget->non_client_view()->frame_view());
// Normal state.
widget->LayoutRootViewIfNecessary();
EXPECT_TRUE(frame_view->GetFrameEnabled());
EXPECT_EQ(kNormalWindowBounds, widget->GetWindowBoundsInScreen());
EXPECT_EQ(kClientBounds,
frame_view->GetClientBoundsForWindowBounds(kNormalWindowBounds));
// Maximized
shell_surface->SetMaximized();
shell_surface->SetGeometry(gfx::Rect(0, 0, 800, 568));
surface->Commit();
widget->LayoutRootViewIfNecessary();
EXPECT_TRUE(frame_view->GetFrameEnabled());
EXPECT_EQ(kFullscreenBounds, widget->GetWindowBoundsInScreen());
EXPECT_EQ(
gfx::Size(800, 568),
frame_view->GetClientBoundsForWindowBounds(kFullscreenBounds).size());
// With work area top insets.
display_manager->UpdateWorkAreaOfDisplay(display_id,
gfx::Insets::TLBR(200, 0, 0, 0));
shell_surface->SetGeometry(gfx::Rect(0, 0, 800, 368));
surface->Commit();
widget->LayoutRootViewIfNecessary();
EXPECT_TRUE(frame_view->GetFrameEnabled());
EXPECT_EQ(gfx::Rect(0, 200, 800, 400), widget->GetWindowBoundsInScreen());
display_manager->UpdateWorkAreaOfDisplay(display_id, gfx::Insets());
// AutoHide
surface->SetFrame(SurfaceFrameType::AUTOHIDE);
shell_surface->SetGeometry(kFullscreenBounds);
surface->Commit();
widget->LayoutRootViewIfNecessary();
EXPECT_TRUE(frame_view->GetFrameEnabled());
EXPECT_EQ(kFullscreenBounds, widget->GetWindowBoundsInScreen());
EXPECT_EQ(kFullscreenBounds,
frame_view->GetClientBoundsForWindowBounds(kFullscreenBounds));
// Fullscreen state.
shell_surface->SetFullscreen(true, display::kInvalidDisplayId);
surface->Commit();
widget->LayoutRootViewIfNecessary();
EXPECT_TRUE(frame_view->GetFrameEnabled());
EXPECT_EQ(kFullscreenBounds, widget->GetWindowBoundsInScreen());
EXPECT_EQ(kFullscreenBounds,
frame_view->GetClientBoundsForWindowBounds(kFullscreenBounds));
// Updating frame, then window state should still update the frame state.
surface->SetFrame(SurfaceFrameType::NORMAL);
surface->Commit();
widget->LayoutRootViewIfNecessary();
EXPECT_FALSE(frame_view->GetHeaderView()->GetVisible());
shell_surface->SetMaximized();
surface->Commit();
widget->LayoutRootViewIfNecessary();
EXPECT_TRUE(frame_view->GetHeaderView()->GetVisible());
// Restore to normal state.
shell_surface->SetRestored();
shell_surface->SetGeometry(kClientBounds);
surface->SetFrame(SurfaceFrameType::NORMAL);
surface->Commit();
widget->LayoutRootViewIfNecessary();
EXPECT_TRUE(frame_view->GetFrameEnabled());
EXPECT_EQ(kNormalWindowBounds, widget->GetWindowBoundsInScreen());
EXPECT_EQ(kClientBounds,
frame_view->GetClientBoundsForWindowBounds(kNormalWindowBounds));
// No frame. The all bounds are same as client bounds.
shell_surface->SetRestored();
shell_surface->SetGeometry(kClientBounds);
surface->SetFrame(SurfaceFrameType::NONE);
surface->Commit();
widget->LayoutRootViewIfNecessary();
EXPECT_FALSE(frame_view->GetFrameEnabled());
EXPECT_EQ(kClientBounds, widget->GetWindowBoundsInScreen());
EXPECT_EQ(kClientBounds,
frame_view->GetClientBoundsForWindowBounds(kClientBounds));
// Test NONE -> AUTOHIDE -> NONE
shell_surface->SetMaximized();
shell_surface->SetGeometry(kFullscreenBounds);
surface->SetFrame(SurfaceFrameType::AUTOHIDE);
surface->Commit();
widget->LayoutRootViewIfNecessary();
EXPECT_TRUE(frame_view->GetFrameEnabled());
EXPECT_TRUE(frame_view->GetHeaderView()->in_immersive_mode());
surface->SetFrame(SurfaceFrameType::NONE);
surface->Commit();
widget->LayoutRootViewIfNecessary();
EXPECT_FALSE(frame_view->GetFrameEnabled());
EXPECT_FALSE(frame_view->GetHeaderView()->in_immersive_mode());
// Fullscreen (AUTOHIDE) to normal with a single commit.
shell_surface->SetGeometry(kFullscreenBounds);
shell_surface->SetMaximized();
shell_surface->SetFullscreen(true, display::kInvalidDisplayId);
surface->SetFrame(SurfaceFrameType::AUTOHIDE);
surface->Commit();
shell_surface->SetGeometry(kClientBounds);
shell_surface->SetRestored();
shell_surface->SetFullscreen(false, display::kInvalidDisplayId);
surface->SetFrame(SurfaceFrameType::NORMAL);
surface->Commit();
EXPECT_TRUE(frame_view->GetFrameEnabled());
EXPECT_EQ(kNormalWindowBounds, widget->GetWindowBoundsInScreen());
EXPECT_EQ(kClientBounds,
frame_view->GetClientBoundsForWindowBounds(kNormalWindowBounds));
}
TEST_P(ClientControlledShellSurfaceTest,
ShadowRoundedCornersWithPipTransition) {
constexpr gfx::Point kOrigin(20, 20);
constexpr int kPipCornerRadius = 8;
base::test::ScopedFeatureList scoped_feature_list(
chromeos::features::kRoundedWindows);
std::unique_ptr<ClientControlledShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetOrigin(kOrigin)
.SetWindowState(chromeos::WindowStateType::kNormal)
.SetFrame(SurfaceFrameType::NORMAL)
.BuildClientControlledShellSurface();
Surface* root_surface = shell_surface->root_surface();
root_surface->Commit();
views::Widget* widget = shell_surface->GetWidget();
ASSERT_TRUE(widget);
aura::Window* window = widget->GetNativeWindow();
ui::Shadow* shadow = wm::ShadowController::GetShadowForWindow(window);
ASSERT_TRUE(shadow);
EXPECT_EQ(shadow->rounded_corner_radius_for_testing(), 0);
shell_surface->SetPip();
root_surface->Commit();
shadow = wm::ShadowController::GetShadowForWindow(window);
ASSERT_TRUE(shadow);
EXPECT_EQ(shadow->rounded_corner_radius_for_testing(), kPipCornerRadius);
shell_surface->UnsetPip();
root_surface->Commit();
ASSERT_TRUE(shadow);
EXPECT_EQ(shadow->rounded_corner_radius_for_testing(), 0);
}
namespace {
class TestEventHandler : public ui::EventHandler {
public:
TestEventHandler() = default;
TestEventHandler(const TestEventHandler&) = delete;
TestEventHandler& operator=(const TestEventHandler&) = delete;
~TestEventHandler() override = default;
// ui::EventHandler:
void OnMouseEvent(ui::MouseEvent* event) override {
mouse_events_.push_back(*event);
}
const std::vector<ui::MouseEvent>& mouse_events() const {
return mouse_events_;
}
private:
std::vector<ui::MouseEvent> mouse_events_;
};
} // namespace
TEST_P(ClientControlledShellSurfaceTest, NoSynthesizedEventOnFrameChange) {
UpdateDisplay("800x600");
auto shell_surface = exo::test::ShellSurfaceBuilder({256, 256})
.SetWindowState(chromeos::WindowStateType::kNormal)
.SetFrame(SurfaceFrameType::NORMAL)
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
// Maximized
constexpr gfx::Rect kFullscreenBounds(0, 0, 800, 600);
shell_surface->SetMaximized();
shell_surface->SetGeometry(kFullscreenBounds);
surface->Commit();
// AutoHide
test::WaitForLastFrameAck(shell_surface.get());
aura::Env* env = aura::Env::GetInstance();
constexpr gfx::Rect kCroppedFullscreenBounds(0, 0, 800, 400);
env->SetLastMouseLocation(gfx::Point(100, 30));
TestEventHandler handler;
env->AddPreTargetHandler(&handler);
surface->SetFrame(SurfaceFrameType::AUTOHIDE);
shell_surface->SetGeometry(kCroppedFullscreenBounds);
surface->Commit();
test::WaitForLastFrameAck(shell_surface.get());
EXPECT_TRUE(handler.mouse_events().empty());
env->RemovePreTargetHandler(&handler);
}
// Shell surfaces should not emit extra events on commit even if using pixel
// coordinates and a cursor is hovering over the window.
// https://crbug.com/1296315.
TEST_P(ClientControlledShellSurfaceTest,
NoSynthesizedEventsForPixelCoordinates) {
TestEventHandler event_handler;
auto shell_surface = exo::test::ShellSurfaceBuilder({400, 400})
.SetNoCommit()
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
// Pixel coordinates add a transform to the underlying layer.
shell_surface->set_client_submits_surfaces_in_pixel_coordinates(true);
display::Display primary_display =
display::Screen::GetScreen()->GetPrimaryDisplay();
constexpr gfx::Rect kInitialBounds(150, 10, 200, 200);
shell_surface->SetBounds(primary_display.id(), kInitialBounds);
// Tested condition only happens when cursor is over the window.
ui::test::EventGenerator generator(ash::Shell::GetPrimaryRootWindow());
generator.MoveMouseTo(200, 110);
shell_surface->host_window()->AddPreTargetHandler(&event_handler);
shell_surface->Activate();
// Commit an arbitrary number of frames. We expect that this will not generate
// synthetic events.
for (int i = 0; i < 5; i++) {
surface->Commit();
test::WaitForLastFrameAck(shell_surface.get());
}
// There should be 2 events. One for mouse enter and the other for move.
const auto& events = event_handler.mouse_events();
ASSERT_EQ(events.size(), 2UL);
EXPECT_EQ(events[0].type(), ui::EventType::kMouseEntered);
EXPECT_EQ(events[1].type(), ui::EventType::kMouseMoved);
shell_surface->host_window()->RemovePreTargetHandler(&event_handler);
}
TEST_P(ClientControlledShellSurfaceTest, CompositorLockInRotation) {
UpdateDisplay("800x600");
EnableTabletMode(true);
gfx::Rect maximum_bounds =
display::Screen::GetScreen()->GetPrimaryDisplay().bounds();
// Start in maximized.
auto shell_surface =
exo::test::ShellSurfaceBuilder({800, 600})
.SetWindowState(chromeos::WindowStateType::kMaximized)
.SetGeometry(maximum_bounds)
.SetNoCommit()
.BuildClientControlledShellSurface();
shell_surface->SetOrientation(Orientation::LANDSCAPE);
auto* surface = shell_surface->root_surface();
surface->Commit();
ui::Compositor* compositor =
shell_surface->GetWidget()->GetNativeWindow()->layer()->GetCompositor();
EXPECT_FALSE(compositor->IsLocked());
UpdateDisplay("800x600/r");
EXPECT_TRUE(compositor->IsLocked());
shell_surface->SetOrientation(Orientation::PORTRAIT);
surface->Commit();
test::WaitForLastFrameAck(shell_surface.get());
EXPECT_FALSE(compositor->IsLocked());
}
// If system tray is shown by click. It should be activated if user presses tab
// key while shell surface is active.
TEST_P(ClientControlledShellSurfaceTest,
KeyboardNavigationWithUnifiedSystemTray) {
auto shell_surface = exo::test::ShellSurfaceBuilder({800, 600})
.BuildClientControlledShellSurface();
EXPECT_TRUE(shell_surface->GetWidget()->IsActive());
// Show system tray by performing a gesture tap at tray.
ash::UnifiedSystemTray* system_tray = GetPrimaryUnifiedSystemTray();
GestureTapOn(system_tray);
ASSERT_TRUE(system_tray->GetWidget());
// Confirm that system tray is not active at this time.
EXPECT_TRUE(shell_surface->GetWidget()->IsActive());
EXPECT_FALSE(system_tray->IsBubbleActive());
// Send tab key event.
PressAndReleaseKey(ui::VKEY_TAB);
// Confirm that system tray is activated.
EXPECT_FALSE(shell_surface->GetWidget()->IsActive());
EXPECT_TRUE(system_tray->IsBubbleActive());
}
TEST_P(ClientControlledShellSurfaceTest, Maximize) {
auto shell_surface = exo::test::ShellSurfaceBuilder({256, 256})
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
EXPECT_FALSE(HasBackdrop());
shell_surface->SetMaximized();
EXPECT_FALSE(HasBackdrop());
surface->Commit();
EXPECT_TRUE(HasBackdrop());
EXPECT_TRUE(shell_surface->GetWidget()->IsMaximized());
// We always show backdrop because the window may be cropped.
display::Display display = display::Screen::GetScreen()->GetPrimaryDisplay();
shell_surface->SetGeometry(display.bounds());
surface->Commit();
EXPECT_TRUE(HasBackdrop());
shell_surface->SetGeometry(gfx::Rect(0, 0, 100, display.bounds().height()));
surface->Commit();
EXPECT_TRUE(HasBackdrop());
shell_surface->SetGeometry(gfx::Rect(0, 0, display.bounds().width(), 100));
surface->Commit();
EXPECT_TRUE(HasBackdrop());
// Toggle maximize.
ash::WMEvent maximize_event(ash::WM_EVENT_TOGGLE_MAXIMIZE);
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
ash::WindowState::Get(window)->OnWMEvent(&maximize_event);
EXPECT_FALSE(shell_surface->GetWidget()->IsMaximized());
EXPECT_FALSE(HasBackdrop());
ash::WindowState::Get(window)->OnWMEvent(&maximize_event);
EXPECT_TRUE(shell_surface->GetWidget()->IsMaximized());
EXPECT_TRUE(HasBackdrop());
}
TEST_P(ClientControlledShellSurfaceTest, Restore) {
auto shell_surface = exo::test::ShellSurfaceBuilder({256, 256})
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
EXPECT_FALSE(HasBackdrop());
// Note: Remove contents to avoid issues with maximize animations in tests.
shell_surface->SetMaximized();
EXPECT_FALSE(HasBackdrop());
surface->Commit();
EXPECT_TRUE(HasBackdrop());
shell_surface->SetRestored();
EXPECT_TRUE(HasBackdrop());
surface->Commit();
EXPECT_FALSE(HasBackdrop());
}
TEST_P(ClientControlledShellSurfaceTest, SetFullscreen) {
auto shell_surface =
exo::test::ShellSurfaceBuilder({256, 256})
.SetWindowState(chromeos::WindowStateType::kFullscreen)
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
EXPECT_TRUE(HasBackdrop());
// We always show backdrop becaues the window can be cropped.
display::Display display = display::Screen::GetScreen()->GetPrimaryDisplay();
shell_surface->SetGeometry(display.bounds());
surface->Commit();
EXPECT_TRUE(HasBackdrop());
shell_surface->SetGeometry(gfx::Rect(0, 0, 100, display.bounds().height()));
surface->Commit();
EXPECT_TRUE(HasBackdrop());
shell_surface->SetGeometry(gfx::Rect(0, 0, display.bounds().width(), 100));
surface->Commit();
EXPECT_TRUE(HasBackdrop());
shell_surface->SetFullscreen(false, display::kInvalidDisplayId);
surface->Commit();
EXPECT_FALSE(HasBackdrop());
EXPECT_NE(GetContext()->bounds().ToString(),
shell_surface->GetWidget()->GetWindowBoundsInScreen().ToString());
}
TEST_P(ClientControlledShellSurfaceTest, ToggleFullscreen) {
auto shell_surface = exo::test::ShellSurfaceBuilder({256, 256})
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
EXPECT_FALSE(HasBackdrop());
shell_surface->SetMaximized();
surface->Commit();
EXPECT_TRUE(HasBackdrop());
ash::WMEvent event(ash::WM_EVENT_TOGGLE_FULLSCREEN);
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
// Enter fullscreen mode.
ash::WindowState::Get(window)->OnWMEvent(&event);
EXPECT_TRUE(HasBackdrop());
// Leave fullscreen mode.
ash::WindowState::Get(window)->OnWMEvent(&event);
EXPECT_TRUE(HasBackdrop());
}
TEST_P(ClientControlledShellSurfaceTest,
DefaultDeviceScaleFactorFromDisplayManager) {
int64_t display_id = display::Screen::GetScreen()->GetPrimaryDisplay().id();
display::SetInternalDisplayIds({display_id});
constexpr gfx::Size kSize(1920, 1080);
display::DisplayManager* display_manager =
ash::Shell::Get()->display_manager();
constexpr double kScale = 1.25;
display::ManagedDisplayMode mode(kSize, 60.f, false /* overscan */,
true /*native*/, kScale);
display::ManagedDisplayInfo::ManagedDisplayModeList mode_list;
mode_list.push_back(mode);
display::ManagedDisplayInfo native_display_info(display_id, "test", false);
native_display_info.SetManagedDisplayModes(mode_list);
native_display_info.SetBounds(gfx::Rect(kSize));
native_display_info.set_device_scale_factor(kScale);
std::vector<display::ManagedDisplayInfo> display_info_list;
display_info_list.push_back(native_display_info);
display_manager->OnNativeDisplaysChanged(display_info_list);
display_manager->UpdateInternalManagedDisplayModeListForTest();
auto shell_surface = exo::test::ShellSurfaceBuilder({64, 64})
.BuildClientControlledShellSurface();
gfx::Transform transform;
transform.Scale(1.0 / kScale, 1.0 / kScale);
EXPECT_EQ(transform.ToString(),
shell_surface->root_surface()->window()->transform().ToString());
}
TEST_P(ClientControlledShellSurfaceTest, MouseAndTouchTarget) {
auto shell_surface = exo::test::ShellSurfaceBuilder({256, 256})
.SetGeometry({0, 0, 256, 256})
.BuildClientControlledShellSurface();
EXPECT_TRUE(shell_surface->CanResize());
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
aura::Window* root = window->GetRootWindow();
ui::EventTargeter* targeter =
root->GetHost()->dispatcher()->GetDefaultEventTargeter();
constexpr gfx::Point kMouseLocation(256 + 5, 150);
ui::MouseEvent mouse(ui::EventType::kMouseMoved, kMouseLocation,
kMouseLocation, ui::EventTimeForNow(), ui::EF_NONE,
ui::EF_NONE);
EXPECT_EQ(window, targeter->FindTargetForEvent(root, &mouse));
// Move 20px further away. Touch event can hit the window but
// mouse event will not.
constexpr gfx::Point kTouchLocation(256 + 25, 150);
ui::MouseEvent touch(ui::EventType::kTouchPressed, kTouchLocation,
kTouchLocation, ui::EventTimeForNow(), ui::EF_NONE,
ui::EF_NONE);
EXPECT_EQ(window, targeter->FindTargetForEvent(root, &touch));
ui::MouseEvent mouse_with_touch_loc(
ui::EventType::kMouseMoved, kTouchLocation, kTouchLocation,
ui::EventTimeForNow(), ui::EF_NONE, ui::EF_NONE);
EXPECT_FALSE(window->Contains(static_cast<aura::Window*>(
targeter->FindTargetForEvent(root, &mouse_with_touch_loc))));
// Touching further away shouldn't hit the window.
constexpr gfx::Point kNoTouchLocation(256 + 35, 150);
ui::MouseEvent no_touch(ui::EventType::kTouchPressed, kNoTouchLocation,
kNoTouchLocation, ui::EventTimeForNow(), ui::EF_NONE,
ui::EF_NONE);
EXPECT_FALSE(window->Contains(static_cast<aura::Window*>(
targeter->FindTargetForEvent(root, &no_touch))));
}
// The shell surface in SystemModal container should be unresizable.
TEST_P(ClientControlledShellSurfaceTest,
ShellSurfaceInSystemModalIsUnresizable) {
auto shell_surface = exo::test::ShellSurfaceBuilder({256, 256})
.SetUseSystemModalContainer()
.BuildClientControlledShellSurface();
EXPECT_FALSE(shell_surface->GetWidget()->widget_delegate()->CanResize());
}
// The shell surface in SystemModal container should be a target
// at the edge.
TEST_P(ClientControlledShellSurfaceTest, ShellSurfaceInSystemModalHitTest) {
display::Display display = display::Screen::GetScreen()->GetPrimaryDisplay();
auto shell_surface = exo::test::ShellSurfaceBuilder({640, 480})
.SetUseSystemModalContainer()
.SetGeometry(display.bounds())
.SetInputRegion(gfx::Rect(0, 0, 0, 0))
.BuildClientControlledShellSurface();
EXPECT_FALSE(shell_surface->GetWidget()->widget_delegate()->CanResize());
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
aura::Window* root = window->GetRootWindow();
ui::MouseEvent event(ui::EventType::kMouseMoved, gfx::Point(100, 0),
gfx::Point(100, 0), ui::EventTimeForNow(), 0, 0);
aura::WindowTargeter targeter;
aura::Window* found =
static_cast<aura::Window*>(targeter.FindTargetForEvent(root, &event));
EXPECT_TRUE(window->Contains(found));
}
// Test the snap functionalities in splitscreen in tablet mode.
TEST_P(ClientControlledShellSurfaceTest, SnapWindowInSplitViewModeTest) {
UpdateDisplay("807x607");
EnableTabletMode(true);
auto shell_surface1 =
exo::test::ShellSurfaceBuilder({800, 600})
.SetGeometry({0, 0, 800, 600})
.SetWindowState(chromeos::WindowStateType::kMaximized)
.BuildClientControlledShellSurface();
aura::Window* window1 = shell_surface1->GetWidget()->GetNativeWindow();
ash::WindowState* window_state1 = ash::WindowState::Get(window1);
ash::ClientControlledState* state1 = static_cast<ash::ClientControlledState*>(
ash::WindowState::TestApi::GetStateImpl(window_state1));
EXPECT_EQ(window_state1->GetStateType(), WindowStateType::kMaximized);
// Snap window to left.
ash::SplitViewController* split_view_controller =
ash::SplitViewController::Get(ash::Shell::GetPrimaryRootWindow());
split_view_controller->SnapWindow(window1, ash::SnapPosition::kPrimary);
state1->set_bounds_locally(true);
window1->SetBounds(split_view_controller->GetSnappedWindowBoundsInScreen(
ash::SnapPosition::kPrimary, window1, chromeos::kDefaultSnapRatio,
/*account_for_divider_width=*/true));
state1->set_bounds_locally(false);
EXPECT_EQ(window_state1->GetStateType(), WindowStateType::kPrimarySnapped);
EXPECT_EQ(
shell_surface1->GetWidget()->GetWindowBoundsInScreen(),
split_view_controller->GetSnappedWindowBoundsInScreen(
ash::SnapPosition::kPrimary,
shell_surface1->GetWidget()->GetNativeWindow(),
chromeos::kDefaultSnapRatio, /*account_for_divider_width=*/true));
EXPECT_TRUE(HasBackdrop());
split_view_controller->EndSplitView();
// Snap window to right.
split_view_controller->SnapWindow(window1, ash::SnapPosition::kSecondary);
state1->set_bounds_locally(true);
window1->SetBounds(split_view_controller->GetSnappedWindowBoundsInScreen(
ash::SnapPosition::kSecondary, window1, chromeos::kDefaultSnapRatio,
/*account_for_divider_width=*/true));
state1->set_bounds_locally(false);
EXPECT_EQ(window_state1->GetStateType(), WindowStateType::kSecondarySnapped);
EXPECT_EQ(
shell_surface1->GetWidget()->GetWindowBoundsInScreen(),
split_view_controller->GetSnappedWindowBoundsInScreen(
ash::SnapPosition::kSecondary,
shell_surface1->GetWidget()->GetNativeWindow(),
chromeos::kDefaultSnapRatio, /*account_for_divider_width=*/true));
EXPECT_TRUE(HasBackdrop());
}
// The shell surface in SystemModal container should not become target
// at the edge.
TEST_P(ClientControlledShellSurfaceTest, ClientIniatedResize) {
display::Display display = display::Screen::GetScreen()->GetPrimaryDisplay();
auto shell_surface = exo::test::ShellSurfaceBuilder({100, 100})
.SetGeometry(gfx::Rect({0, 0, 100, 100}))
.BuildClientControlledShellSurface();
EXPECT_TRUE(shell_surface->GetWidget()->widget_delegate()->CanResize());
shell_surface->StartDrag(HTTOP, gfx::PointF(0, 0));
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
// Client cannot start drag if mouse isn't pressed.
ash::WindowState* window_state = ash::WindowState::Get(window);
ASSERT_FALSE(window_state->is_dragged());
// Client can start drag only when the mouse is pressed on the widget.
ui::test::EventGenerator* event_generator = GetEventGenerator();
event_generator->MoveMouseToCenterOf(window);
event_generator->PressLeftButton();
shell_surface->StartDrag(HTTOP, gfx::PointF(0, 0));
ASSERT_TRUE(window_state->is_dragged());
event_generator->ReleaseLeftButton();
ASSERT_FALSE(window_state->is_dragged());
// Press pressed outside of the window.
event_generator->MoveMouseTo(gfx::Point(200, 50));
event_generator->PressLeftButton();
shell_surface->StartDrag(HTTOP, gfx::PointF(0, 0));
ASSERT_FALSE(window_state->is_dragged());
}
TEST_P(ClientControlledShellSurfaceTest, ResizabilityAndSizeConstraints) {
auto shell_surface = exo::test::ShellSurfaceBuilder()
.SetMinimumSize(gfx::Size(0, 0))
.SetMaximumSize(gfx::Size(0, 0))
.BuildClientControlledShellSurface();
EXPECT_FALSE(shell_surface->GetWidget()->widget_delegate()->CanResize());
shell_surface->SetMinimumSize(gfx::Size(400, 400));
shell_surface->SetMaximumSize(gfx::Size(0, 0));
auto* surface = shell_surface->root_surface();
surface->Commit();
EXPECT_TRUE(shell_surface->GetWidget()->widget_delegate()->CanResize());
shell_surface->SetMinimumSize(gfx::Size(400, 400));
shell_surface->SetMaximumSize(gfx::Size(400, 400));
surface->Commit();
EXPECT_FALSE(shell_surface->GetWidget()->widget_delegate()->CanResize());
}
namespace {
// This class is only meant to used by CloseWindowWhenDraggingTest.
// When a ClientControlledShellSurface is destroyed, its natvie window will be
// hidden first and at that time its window delegate should have been properly
// reset.
class ShellSurfaceWindowObserver : public aura::WindowObserver {
public:
explicit ShellSurfaceWindowObserver(aura::Window* window)
: window_(window),
has_delegate_(ash::WindowState::Get(window)->HasDelegate()) {
window_->AddObserver(this);
}
ShellSurfaceWindowObserver(const ShellSurfaceWindowObserver&) = delete;
ShellSurfaceWindowObserver& operator=(const ShellSurfaceWindowObserver&) =
delete;
~ShellSurfaceWindowObserver() override {
if (window_) {
window_->RemoveObserver(this);
window_ = nullptr;
}
}
bool has_delegate() const { return has_delegate_; }
// aura::WindowObserver:
void OnWindowVisibilityChanged(aura::Window* window, bool visible) override {
DCHECK_EQ(window_, window);
if (!visible) {
has_delegate_ = ash::WindowState::Get(window_)->HasDelegate();
window_->RemoveObserver(this);
window_ = nullptr;
}
}
private:
raw_ptr<aura::Window> window_;
bool has_delegate_;
};
} // namespace
// Test that when a shell surface is destroyed during its dragging, its window
// delegate should be reset properly.
TEST_P(ClientControlledShellSurfaceTest, CloseWindowWhenDraggingTest) {
auto shell_surface = exo::test::ShellSurfaceBuilder({256, 256})
.SetGeometry({0, 0, 256, 256})
.BuildClientControlledShellSurface();
// Press on the edge of the window and start dragging.
constexpr gfx::Point kTouchLocation(256, 150);
ui::test::EventGenerator* event_generator = GetEventGenerator();
event_generator->MoveTouch(kTouchLocation);
event_generator->PressTouch();
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
EXPECT_TRUE(ash::WindowState::Get(window)->is_dragged());
auto observer = std::make_unique<ShellSurfaceWindowObserver>(window);
EXPECT_TRUE(observer->has_delegate());
// Destroy the window.
shell_surface.reset();
EXPECT_FALSE(observer->has_delegate());
}
namespace {
class TestClientControlledShellSurfaceDelegate
: public test::ClientControlledShellSurfaceDelegate {
public:
explicit TestClientControlledShellSurfaceDelegate(
ClientControlledShellSurface* shell_surface)
: test::ClientControlledShellSurfaceDelegate(shell_surface) {}
~TestClientControlledShellSurfaceDelegate() override = default;
TestClientControlledShellSurfaceDelegate(
const TestClientControlledShellSurfaceDelegate&) = delete;
TestClientControlledShellSurfaceDelegate& operator=(
const TestClientControlledShellSurfaceDelegate&) = delete;
int geometry_change_count() const { return geometry_change_count_; }
std::vector<gfx::Rect> geometry_bounds() const { return geometry_bounds_; }
int bounds_change_count() const { return bounds_change_count_; }
std::vector<gfx::Rect> requested_bounds() const { return requested_bounds_; }
std::vector<int64_t> requested_display_ids() const {
return requested_display_ids_;
}
void Reset() {
geometry_change_count_ = 0;
geometry_bounds_.clear();
bounds_change_count_ = 0;
requested_bounds_.clear();
requested_display_ids_.clear();
}
static TestClientControlledShellSurfaceDelegate* SetUp(
ClientControlledShellSurface* shell_surface) {
return static_cast<TestClientControlledShellSurfaceDelegate*>(
shell_surface->set_delegate(
std::make_unique<TestClientControlledShellSurfaceDelegate>(
shell_surface)));
}
private:
// ClientControlledShellSurface::Delegate:
void OnGeometryChanged(const gfx::Rect& geometry) override {
geometry_change_count_++;
geometry_bounds_.push_back(geometry);
}
void OnBoundsChanged(chromeos::WindowStateType current_state,
chromeos::WindowStateType requested_state,
int64_t display_id,
const gfx::Rect& bounds_in_display,
bool is_resize,
int bounds_change,
bool is_adjusted_bounds) override {
bounds_change_count_++;
requested_bounds_.push_back(bounds_in_display);
requested_display_ids_.push_back(display_id);
}
int geometry_change_count_ = 0;
std::vector<gfx::Rect> geometry_bounds_;
int bounds_change_count_ = 0;
std::vector<gfx::Rect> requested_bounds_;
std::vector<int64_t> requested_display_ids_;
};
class ClientControlledShellSurfaceDisplayTest : public test::ExoTestBase {
public:
ClientControlledShellSurfaceDisplayTest() = default;
~ClientControlledShellSurfaceDisplayTest() override = default;
ClientControlledShellSurfaceDisplayTest(
const ClientControlledShellSurfaceDisplayTest&) = delete;
ClientControlledShellSurfaceDisplayTest& operator=(
const ClientControlledShellSurfaceDisplayTest&) = delete;
static ash::WindowResizer* CreateDragWindowResizer(
aura::Window* window,
const gfx::Point& point_in_parent,
int window_component) {
return ash::CreateWindowResizer(window, gfx::PointF(point_in_parent),
window_component,
::wm::WINDOW_MOVE_SOURCE_MOUSE)
.release();
}
gfx::PointF CalculateDragPoint(const ash::WindowResizer& resizer,
int delta_x,
int delta_y) {
gfx::PointF location = resizer.GetInitialLocation();
location.set_x(location.x() + delta_x);
location.set_y(location.y() + delta_y);
return location;
}
};
} // namespace
TEST_F(ClientControlledShellSurfaceDisplayTest, MoveToAnotherDisplayByDrag) {
UpdateDisplay("800x600,800x600");
aura::Window::Windows root_windows = ash::Shell::GetAllRootWindows();
auto shell_surface = exo::test::ShellSurfaceBuilder({200, 200})
.SetNoCommit()
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
display::Display primary_display =
display::Screen::GetScreen()->GetPrimaryDisplay();
constexpr gfx::Rect kInitialBounds(-100, 10, 200, 200);
shell_surface->SetBounds(primary_display.id(), kInitialBounds);
surface->Commit();
EXPECT_EQ(kInitialBounds,
shell_surface->GetWidget()->GetWindowBoundsInScreen());
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
EXPECT_EQ(root_windows[0], window->GetRootWindow());
// Prevent snapping |window|. It only distracts from the purpose of the test.
// TODO: Remove this code after adding functionality where the mouse has to
// dwell in the snap region before the dragged window can get snapped.
window->SetProperty(aura::client::kResizeBehaviorKey,
aura::client::kResizeBehaviorNone);
ASSERT_FALSE(ash::WindowState::Get(window)->CanSnap());
std::unique_ptr<ash::WindowResizer> resizer(
CreateDragWindowResizer(window, gfx::Point(), HTCAPTION));
// Drag the pointer to the right. Once it reaches the right edge of the
// primary display, it warps to the secondary.
display::Display secondary_display =
display::Screen::GetScreen()->GetDisplayNearestWindow(root_windows[1]);
// TODO(crbug.com/40638870): Unit tests should be able to simulate mouse input
// without having to call |CursorManager::SetDisplay|.
ash::Shell::Get()->cursor_manager()->SetDisplay(secondary_display);
resizer->Drag(CalculateDragPoint(*resizer, 800, 0), 0);
auto* delegate =
TestClientControlledShellSurfaceDelegate::SetUp(shell_surface.get());
resizer->CompleteDrag();
EXPECT_EQ(root_windows[1], window->GetRootWindow());
// TODO(oshima): We currently generate bounds change twice,
// first when reparented, then set bounds. Chagne wm::SetBoundsInScreen
// to simply request WM_EVENT_SET_BOUND with target display id.
ASSERT_EQ(2, delegate->bounds_change_count());
// Bounds is local to 2nd display.
EXPECT_EQ(gfx::Rect(-100, 10, 200, 200), delegate->requested_bounds()[0]);
EXPECT_EQ(gfx::Rect(-100, 10, 200, 200), delegate->requested_bounds()[1]);
EXPECT_EQ(secondary_display.id(), delegate->requested_display_ids()[0]);
EXPECT_EQ(secondary_display.id(), delegate->requested_display_ids()[1]);
}
TEST_F(ClientControlledShellSurfaceDisplayTest,
MoveToAnotherDisplayByShortcut) {
UpdateDisplay("400x600,800x600*2");
aura::Window::Windows root_windows = ash::Shell::GetAllRootWindows();
auto shell_surface = exo::test::ShellSurfaceBuilder({200, 200})
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
display::Display primary_display =
display::Screen::GetScreen()->GetPrimaryDisplay();
constexpr gfx::Rect kInitialBounds(-139, 10, 200, 200);
shell_surface->SetBounds(primary_display.id(), kInitialBounds);
surface->Commit();
shell_surface->GetWidget()->Show();
EXPECT_EQ(kInitialBounds,
shell_surface->GetWidget()->GetWindowBoundsInScreen());
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
EXPECT_EQ(root_windows[0], window->GetRootWindow());
auto* delegate =
TestClientControlledShellSurfaceDelegate::SetUp(shell_surface.get());
display::Display secondary_display =
display::Screen::GetScreen()->GetDisplayNearestWindow(root_windows[1]);
EXPECT_TRUE(
ash::window_util::MoveWindowToDisplay(window, secondary_display.id()));
ASSERT_EQ(1, delegate->bounds_change_count());
// Should be scaled by 2x in pixels on 2x-density density.
EXPECT_EQ(gfx::Rect(-278, 20, 400, 400), delegate->requested_bounds()[0]);
EXPECT_EQ(secondary_display.id(), delegate->requested_display_ids()[0]);
constexpr gfx::Rect kSecondaryPosition(700, 10, 200, 200);
shell_surface->SetScaleFactor(2.f);
shell_surface->SetBounds(secondary_display.id(), kSecondaryPosition);
surface->Commit();
// Should be scaled by half when converted from pixels to DP.
EXPECT_EQ(gfx::Rect(750, 5, 100, 100), window->GetBoundsInScreen());
delegate->Reset();
// Moving to the outside of another display.
EXPECT_TRUE(
ash::window_util::MoveWindowToDisplay(window, primary_display.id()));
ASSERT_EQ(1, delegate->bounds_change_count());
// Should fit in the primary display.
EXPECT_EQ(gfx::Rect(350, 5, 100, 100), delegate->requested_bounds()[0]);
EXPECT_EQ(primary_display.id(), delegate->requested_display_ids()[0]);
}
TEST_P(ClientControlledShellSurfaceTest, CaptionButtonModel) {
auto shell_surface = exo::test::ShellSurfaceBuilder({64, 64})
.SetGeometry(gfx::Rect(0, 0, 64, 64))
.BuildClientControlledShellSurface();
constexpr views::CaptionButtonIcon kAllButtons[] = {
views::CAPTION_BUTTON_ICON_MINIMIZE,
views::CAPTION_BUTTON_ICON_MAXIMIZE_RESTORE,
views::CAPTION_BUTTON_ICON_CLOSE,
views::CAPTION_BUTTON_ICON_BACK,
views::CAPTION_BUTTON_ICON_MENU,
views::CAPTION_BUTTON_ICON_FLOAT,
};
constexpr uint32_t kAllButtonMask =
1 << views::CAPTION_BUTTON_ICON_MINIMIZE |
1 << views::CAPTION_BUTTON_ICON_MAXIMIZE_RESTORE |
1 << views::CAPTION_BUTTON_ICON_CLOSE |
1 << views::CAPTION_BUTTON_ICON_BACK |
1 << views::CAPTION_BUTTON_ICON_MENU |
1 << views::CAPTION_BUTTON_ICON_FLOAT;
ash::NonClientFrameViewAsh* frame_view =
static_cast<ash::NonClientFrameViewAsh*>(
shell_surface->GetWidget()->non_client_view()->frame_view());
chromeos::FrameCaptionButtonContainerView* container =
static_cast<chromeos::HeaderView*>(frame_view->GetHeaderView())
->caption_button_container();
// Visible
for (auto visible : kAllButtons) {
uint32_t visible_buttons = 1 << visible;
shell_surface->SetFrameButtons(visible_buttons, 0);
const chromeos::CaptionButtonModel* model = container->model();
for (auto not_visible : kAllButtons) {
if (not_visible != visible) {
EXPECT_FALSE(model->IsVisible(not_visible));
}
}
EXPECT_TRUE(model->IsVisible(visible));
EXPECT_FALSE(model->IsEnabled(visible));
}
// Enable
for (auto enabled : kAllButtons) {
uint32_t enabled_buttons = 1 << enabled;
shell_surface->SetFrameButtons(kAllButtonMask, enabled_buttons);
const chromeos::CaptionButtonModel* model = container->model();
for (auto not_enabled : kAllButtons) {
if (not_enabled != enabled) {
EXPECT_FALSE(model->IsEnabled(not_enabled));
}
}
EXPECT_TRUE(model->IsEnabled(enabled));
EXPECT_TRUE(model->IsVisible(enabled));
}
// Zoom mode
EXPECT_FALSE(container->model()->InZoomMode());
shell_surface->SetFrameButtons(
kAllButtonMask | 1 << views::CAPTION_BUTTON_ICON_ZOOM, kAllButtonMask);
EXPECT_TRUE(container->model()->InZoomMode());
}
// Makes sure that the "extra title" is respected by the window frame. When not
// set, there should be no text in the window frame, but the window's name
// should still be set (for overview mode, accessibility, etc.). When the debug
// text is set, the window frame should paint it.
TEST_P(ClientControlledShellSurfaceTest, SetExtraTitle) {
auto shell_surface = exo::test::ShellSurfaceBuilder({640, 64})
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
const std::u16string window_title(u"title");
shell_surface->SetTitle(window_title);
const aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
EXPECT_EQ(window_title, window->GetTitle());
EXPECT_FALSE(
shell_surface->GetWidget()->widget_delegate()->ShouldShowWindowTitle());
// Paints the frame and returns whether text was drawn. Unforunately the text
// is a blob so its actual value can't be detected.
auto paint_does_draw_text = [&shell_surface]() {
TestCanvas canvas;
shell_surface->OnSetFrame(SurfaceFrameType::NORMAL);
ash::NonClientFrameViewAsh* frame_view =
static_cast<ash::NonClientFrameViewAsh*>(
shell_surface->GetWidget()->non_client_view()->frame_view());
frame_view->SetVisible(true);
// Paint to a layer so we can pass a root PaintInfo.
frame_view->GetHeaderView()->SetPaintToLayer();
constexpr gfx::Rect kBounds(100, 100);
auto list = base::MakeRefCounted<cc::DisplayItemList>();
frame_view->GetHeaderView()->Paint(views::PaintInfo::CreateRootPaintInfo(
ui::PaintContext(list.get(), 1.f, kBounds, false), kBounds.size()));
list->Finalize();
list->Raster(&canvas);
return canvas.text_was_drawn();
};
EXPECT_FALSE(paint_does_draw_text());
EXPECT_FALSE(
shell_surface->GetWidget()->widget_delegate()->ShouldShowWindowTitle());
// Setting the extra title/debug text won't change the window's title, but it
// will be drawn by the frame header.
shell_surface->SetExtraTitle(u"extra");
surface->Commit();
EXPECT_EQ(window_title, window->GetTitle());
EXPECT_TRUE(paint_does_draw_text());
EXPECT_FALSE(
shell_surface->GetWidget()->widget_delegate()->ShouldShowWindowTitle());
}
TEST_P(ClientControlledShellSurfaceTest, WideFrame) {
auto shell_surface =
exo::test::ShellSurfaceBuilder({64, 64})
.SetWindowState(chromeos::WindowStateType::kMaximized)
.SetGeometry(gfx::Rect(100, 0, 64, 64))
.SetInputRegion(gfx::Rect(0, 0, 64, 64))
.SetFrame(SurfaceFrameType::NORMAL)
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
ash::Shelf* shelf = ash::Shelf::ForWindow(window);
shelf->SetAlignment(ash::ShelfAlignment::kLeft);
const gfx::Rect work_area =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
const gfx::Rect display_bounds =
display::Screen::GetScreen()->GetPrimaryDisplay().bounds();
ASSERT_TRUE(work_area.x() != display_bounds.x());
auto* wide_frame = shell_surface->wide_frame_for_test();
ASSERT_TRUE(wide_frame);
EXPECT_FALSE(wide_frame->header_view()->in_immersive_mode());
EXPECT_EQ(work_area.x(), wide_frame->GetBoundsInScreen().x());
EXPECT_EQ(work_area.width(), wide_frame->GetBoundsInScreen().width());
auto another_window = ash::TestWidgetBuilder().BuildOwnsNativeWidget();
another_window->SetFullscreen(true);
// Make sure that the wide frame stays in maximzied size even if there is
// active fullscreen window.
EXPECT_EQ(work_area.x(), wide_frame->GetBoundsInScreen().x());
EXPECT_EQ(work_area.width(), wide_frame->GetBoundsInScreen().width());
shell_surface->Activate();
EXPECT_EQ(work_area.x(), wide_frame->GetBoundsInScreen().x());
EXPECT_EQ(work_area.width(), wide_frame->GetBoundsInScreen().width());
// If the shelf is set to auto hide by a user, use the display bounds.
ash::Shelf::ForWindow(window)->SetAutoHideBehavior(
ash::ShelfAutoHideBehavior::kAlways);
EXPECT_EQ(display_bounds.x(), wide_frame->GetBoundsInScreen().x());
EXPECT_EQ(display_bounds.width(), wide_frame->GetBoundsInScreen().width());
ash::Shelf::ForWindow(window)->SetAutoHideBehavior(
ash::ShelfAutoHideBehavior::kNever);
EXPECT_EQ(work_area.x(), wide_frame->GetBoundsInScreen().x());
EXPECT_EQ(work_area.width(), wide_frame->GetBoundsInScreen().width());
shell_surface->SetFullscreen(true, display::kInvalidDisplayId);
surface->Commit();
EXPECT_EQ(display_bounds.x(), wide_frame->GetBoundsInScreen().x());
EXPECT_EQ(display_bounds.width(), wide_frame->GetBoundsInScreen().width());
EXPECT_EQ(display_bounds,
display::Screen::GetScreen()->GetPrimaryDisplay().work_area());
// Activating maximized window should not affect the fullscreen shell
// surface's wide frame.
another_window->Activate();
another_window->SetFullscreen(false);
EXPECT_EQ(work_area,
display::Screen::GetScreen()->GetPrimaryDisplay().work_area());
EXPECT_EQ(display_bounds.x(), wide_frame->GetBoundsInScreen().x());
EXPECT_EQ(display_bounds.width(), wide_frame->GetBoundsInScreen().width());
another_window->Close();
// Check targeter is still CustomWindowTargeter.
ASSERT_TRUE(window->parent());
auto* custom_targeter = window->targeter();
constexpr gfx::Point kMouseLocation(101, 50);
auto* root = window->GetRootWindow();
aura::WindowTargeter targeter;
aura::Window* target;
{
ui::MouseEvent event(ui::EventType::kMouseMoved, kMouseLocation,
kMouseLocation, ui::EventTimeForNow(), 0, 0);
target =
static_cast<aura::Window*>(targeter.FindTargetForEvent(root, &event));
}
EXPECT_EQ(surface->window(), target);
// Disable input region and the targeter no longer find the surface.
surface->SetInputRegion(gfx::Rect(0, 0, 0, 0));
surface->Commit();
{
ui::MouseEvent event(ui::EventType::kMouseMoved, kMouseLocation,
kMouseLocation, ui::EventTimeForNow(), 0, 0);
target =
static_cast<aura::Window*>(targeter.FindTargetForEvent(root, &event));
}
EXPECT_NE(surface->window(), target);
// Test AUTOHIDE -> NORMAL
surface->SetFrame(SurfaceFrameType::AUTOHIDE);
surface->Commit();
EXPECT_TRUE(wide_frame->header_view()->in_immersive_mode());
surface->SetFrame(SurfaceFrameType::NORMAL);
surface->Commit();
EXPECT_FALSE(wide_frame->header_view()->in_immersive_mode());
EXPECT_EQ(custom_targeter, window->targeter());
// Test AUTOHIDE -> NONE
surface->SetFrame(SurfaceFrameType::AUTOHIDE);
surface->Commit();
EXPECT_TRUE(wide_frame->header_view()->in_immersive_mode());
// Switching to NONE means no frame so it should delete wide frame.
surface->SetFrame(SurfaceFrameType::NONE);
surface->Commit();
EXPECT_FALSE(shell_surface->wide_frame_for_test());
{
ui::MouseEvent event(ui::EventType::kMouseMoved, kMouseLocation,
kMouseLocation, ui::EventTimeForNow(), 0, 0);
target =
static_cast<aura::Window*>(targeter.FindTargetForEvent(root, &event));
}
EXPECT_NE(surface->window(), target);
// Unmaximize it and the frame should be normal.
shell_surface->SetRestored();
surface->Commit();
EXPECT_FALSE(shell_surface->wide_frame_for_test());
{
ui::MouseEvent event(ui::EventType::kMouseMoved, kMouseLocation,
kMouseLocation, ui::EventTimeForNow(), 0, 0);
target =
static_cast<aura::Window*>(targeter.FindTargetForEvent(root, &event));
}
EXPECT_NE(surface->window(), target);
// Re-enable input region and the targeter should find the surface again.
surface->SetInputRegion(gfx::Rect(0, 0, 64, 64));
surface->Commit();
{
ui::MouseEvent event(ui::EventType::kMouseMoved, kMouseLocation,
kMouseLocation, ui::EventTimeForNow(), 0, 0);
target =
static_cast<aura::Window*>(targeter.FindTargetForEvent(root, &event));
}
EXPECT_EQ(surface->window(), target);
}
// Tests that a WideFrameView is created for an unparented ARC task and that the
TEST_P(ClientControlledShellSurfaceTest, NoFrameOnModalContainer) {
auto shell_surface = exo::test::ShellSurfaceBuilder({64, 64})
.SetUseSystemModalContainer()
.SetGeometry(gfx::Rect(100, 0, 64, 64))
.SetFrame(SurfaceFrameType::NORMAL)
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
EXPECT_FALSE(shell_surface->frame_enabled());
surface->SetFrame(SurfaceFrameType::AUTOHIDE);
surface->Commit();
EXPECT_FALSE(shell_surface->frame_enabled());
}
TEST_P(ClientControlledShellSurfaceTest,
SetGeometryReparentsToDisplayOnFirstCommit) {
UpdateDisplay("100x200,100x200");
const auto* screen = display::Screen::GetScreen();
{
gfx::Rect geometry(16, 16, 32, 32);
auto shell_surface = exo::test::ShellSurfaceBuilder({64, 64})
.SetGeometry(geometry)
.BuildClientControlledShellSurface();
EXPECT_EQ(geometry, shell_surface->GetWidget()->GetWindowBoundsInScreen());
display::Display primary_display = screen->GetPrimaryDisplay();
display::Display display = screen->GetDisplayNearestWindow(
shell_surface->GetWidget()->GetNativeWindow());
EXPECT_EQ(primary_display.id(), display.id());
}
{
gfx::Rect geometry(116, 16, 32, 32);
auto shell_surface = exo::test::ShellSurfaceBuilder({64, 64})
.SetGeometry(geometry)
.BuildClientControlledShellSurface();
EXPECT_EQ(geometry, shell_surface->GetWidget()->GetWindowBoundsInScreen());
auto root_windows = ash::Shell::GetAllRootWindows();
display::Display secondary_display =
screen->GetDisplayNearestWindow(root_windows[1]);
display::Display display = screen->GetDisplayNearestWindow(
shell_surface->GetWidget()->GetNativeWindow());
EXPECT_EQ(secondary_display.id(), display.id());
}
}
TEST_P(ClientControlledShellSurfaceTest, SetBoundsReparentsToDisplay) {
UpdateDisplay("100x200,100x200");
const auto* screen = display::Screen::GetScreen();
gfx::Rect geometry(16, 16, 32, 32);
auto shell_surface = exo::test::ShellSurfaceBuilder({64, 64})
.SetGeometry(geometry)
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
display::Display primary_display = screen->GetPrimaryDisplay();
// Move to primary display with bounds inside display.
shell_surface->SetBounds(primary_display.id(), geometry);
surface->Commit();
EXPECT_EQ(geometry, shell_surface->GetWidget()->GetWindowBoundsInScreen());
display::Display display = screen->GetDisplayNearestWindow(
shell_surface->GetWidget()->GetNativeWindow());
EXPECT_EQ(primary_display.id(), display.id());
auto root_windows = ash::Shell::GetAllRootWindows();
display::Display secondary_display =
screen->GetDisplayNearestWindow(root_windows[1]);
// Move to secondary display with bounds inside display.
shell_surface->SetBounds(secondary_display.id(), geometry);
surface->Commit();
EXPECT_EQ(gfx::Rect(116, 16, 32, 32),
shell_surface->GetWidget()->GetWindowBoundsInScreen());
display = screen->GetDisplayNearestWindow(
shell_surface->GetWidget()->GetNativeWindow());
EXPECT_EQ(secondary_display.id(), display.id());
// Move to primary display with bounds outside display.
geometry.set_origin({-100, 0});
shell_surface->SetBounds(primary_display.id(), geometry);
surface->Commit();
EXPECT_EQ(gfx::Rect(-6, 0, 32, 32),
shell_surface->GetWidget()->GetWindowBoundsInScreen());
display = screen->GetDisplayNearestWindow(
shell_surface->GetWidget()->GetNativeWindow());
EXPECT_EQ(primary_display.id(), display.id());
// Move to secondary display with bounds outside display.
shell_surface->SetBounds(secondary_display.id(), geometry);
surface->Commit();
EXPECT_EQ(gfx::Rect(94, 0, 32, 32),
shell_surface->GetWidget()->GetWindowBoundsInScreen());
display = screen->GetDisplayNearestWindow(
shell_surface->GetWidget()->GetNativeWindow());
EXPECT_EQ(secondary_display.id(), display.id());
}
// Test if the surface bounds is correctly set when default scale cancellation
// is enabled or disabled.
TEST_P(ClientControlledShellSurfaceTest,
SetBoundsWithAndWithoutDefaultScaleCancellation) {
UpdateDisplay("800x600*2,800x600*2");
const auto primary_display_id =
display::Screen::GetScreen()->GetPrimaryDisplay().id();
constexpr gfx::Size kBufferSize(64, 64);
auto buffer = test::ExoTestHelper::CreateBuffer(kBufferSize);
constexpr double kOriginalScale = 4.f;
constexpr gfx::Rect kBoundsDp(64, 64, 128, 128);
const gfx::Rect bounds_px_for_4x =
gfx::ScaleToRoundedRect(kBoundsDp, kOriginalScale);
for (const auto default_scale_cancellation : {true, false}) {
SCOPED_TRACE(::testing::Message() << "default_scale_cancellation: "
<< default_scale_cancellation);
{
// Set display id, bounds origin, bounds size at the same time via
// SetBounds method.
auto builder = exo::test::ShellSurfaceBuilder();
if (default_scale_cancellation) {
builder.EnableDefaultScaleCancellation();
}
auto shell_surface =
builder.SetNoCommit().BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
// When display doesn't change, scale stays the same
if (!default_scale_cancellation) {
shell_surface->SetScaleFactor(kOriginalScale);
}
shell_surface->SetDisplay(primary_display_id);
shell_surface->SetBounds(primary_display_id, default_scale_cancellation
? kBoundsDp
: bounds_px_for_4x);
surface->Attach(buffer.get());
surface->Commit();
EXPECT_EQ(kBoundsDp,
shell_surface->GetWidget()->GetWindowBoundsInScreen());
}
{
// Set display id and bounds origin at the same time via SetBoundsOrigin
// method, and set bounds size separately.
const auto bounds_to_set =
default_scale_cancellation ? kBoundsDp : bounds_px_for_4x;
auto builder = exo::test::ShellSurfaceBuilder();
if (default_scale_cancellation) {
builder.EnableDefaultScaleCancellation();
}
auto shell_surface =
builder.SetNoCommit().BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
if (!default_scale_cancellation) {
shell_surface->SetScaleFactor(kOriginalScale);
}
shell_surface->SetBoundsOrigin(primary_display_id,
bounds_to_set.origin());
shell_surface->SetBoundsSize(bounds_to_set.size());
surface->Attach(buffer.get());
surface->Commit();
EXPECT_EQ(kBoundsDp,
shell_surface->GetWidget()->GetWindowBoundsInScreen());
}
}
}
// Set orientation lock to a window.
TEST_P(ClientControlledShellSurfaceTest, SetOrientationLock) {
display::test::DisplayManagerTestApi(ash::Shell::Get()->display_manager())
.SetFirstDisplayAsInternalDisplay();
EnableTabletMode(true);
ash::ScreenOrientationController* controller =
ash::Shell::Get()->screen_orientation_controller();
auto shell_surface =
exo::test::ShellSurfaceBuilder({256, 256})
.SetWindowState(chromeos::WindowStateType::kMaximized)
.BuildClientControlledShellSurface();
shell_surface->SetOrientationLock(
chromeos::OrientationType::kLandscapePrimary);
EXPECT_TRUE(controller->rotation_locked());
display::Display display(display::Screen::GetScreen()->GetPrimaryDisplay());
gfx::Size displaySize = display.size();
EXPECT_GT(displaySize.width(), displaySize.height());
shell_surface->SetOrientationLock(chromeos::OrientationType::kAny);
EXPECT_FALSE(controller->rotation_locked());
EnableTabletMode(false);
}
// Tests adjust bounds locally should also request remote client bounds update.
TEST_P(ClientControlledShellSurfaceTest, AdjustBoundsLocally) {
UpdateDisplay("800x600");
constexpr gfx::Rect kClientBounds(900, 0, 200, 300);
auto shell_surface = exo::test::ShellSurfaceBuilder({64, 64})
.SetGeometry(kClientBounds)
.SetNoCommit()
.BuildClientControlledShellSurface();
auto* delegate =
TestClientControlledShellSurfaceDelegate::SetUp(shell_surface.get());
auto* surface = shell_surface->root_surface();
surface->Commit();
views::Widget* widget = shell_surface->GetWidget();
EXPECT_EQ(gfx::Rect(739, 0, 200, 300), widget->GetWindowBoundsInScreen());
EXPECT_EQ(gfx::Rect(739, 0, 200, 300), delegate->requested_bounds().back());
// Receiving the same bounds shouldn't try to update the bounds again.
delegate->Reset();
shell_surface->SetGeometry(kClientBounds);
surface->Commit();
EXPECT_EQ(0, delegate->bounds_change_count());
}
TEST_P(ClientControlledShellSurfaceTest, SnappedInTabletMode) {
constexpr gfx::Rect kClientBounds(256, 256);
auto shell_surface = exo::test::ShellSurfaceBuilder(kClientBounds.size())
.SetGeometry(kClientBounds)
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
auto* window = shell_surface->GetWidget()->GetNativeWindow();
auto* window_state = ash::WindowState::Get(window);
EnableTabletMode(true);
ash::WindowSnapWMEvent event(ash::WM_EVENT_SNAP_PRIMARY);
window_state->OnWMEvent(&event);
EXPECT_EQ(window_state->GetStateType(), WindowStateType::kPrimarySnapped);
ash::NonClientFrameViewAsh* frame_view =
static_cast<ash::NonClientFrameViewAsh*>(
shell_surface->GetWidget()->non_client_view()->frame_view());
// Snapped window can also use auto hide.
surface->SetFrame(SurfaceFrameType::AUTOHIDE);
surface->Commit();
EXPECT_TRUE(frame_view->GetFrameEnabled());
EXPECT_TRUE(frame_view->GetHeaderView()->in_immersive_mode());
}
TEST_P(ClientControlledShellSurfaceTest, PipWindowCannotBeActivated) {
auto shell_surface = exo::test::ShellSurfaceBuilder({256, 256})
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
EXPECT_TRUE(shell_surface->GetWidget()->IsActive());
EXPECT_TRUE(shell_surface->GetWidget()->CanActivate());
// Entering PIP should unactivate the window and make the widget
// unactivatable.
shell_surface->SetPip();
surface->Commit();
EXPECT_FALSE(shell_surface->GetWidget()->IsActive());
EXPECT_FALSE(shell_surface->GetWidget()->CanActivate());
// Leaving PIP should make it activatable again.
shell_surface->SetRestored();
surface->Commit();
EXPECT_TRUE(shell_surface->GetWidget()->CanActivate());
}
TEST_F(ClientControlledShellSurfaceDisplayTest,
NoBoundsChangeEventInMinimized) {
constexpr gfx::Rect kClientBounds(100, 100);
auto shell_surface = exo::test::ShellSurfaceBuilder(kClientBounds.size())
.SetGeometry(kClientBounds)
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
auto* delegate =
TestClientControlledShellSurfaceDelegate::SetUp(shell_surface.get());
ASSERT_EQ(0, delegate->bounds_change_count());
auto* window_state =
ash::WindowState::Get(shell_surface->GetWidget()->GetNativeWindow());
int64_t display_id = window_state->GetDisplay().id();
shell_surface->OnBoundsChangeEvent(WindowStateType::kNormal,
WindowStateType::kNormal, display_id,
gfx::Rect(10, 10, 100, 100), 0,
/*is_adjusted_bounds=*/false);
ASSERT_EQ(1, delegate->bounds_change_count());
EXPECT_FALSE(shell_surface->GetWidget()->IsMinimized());
shell_surface->SetMinimized();
surface->Commit();
EXPECT_TRUE(shell_surface->GetWidget()->IsMinimized());
shell_surface->OnBoundsChangeEvent(WindowStateType::kMinimized,
WindowStateType::kMinimized, display_id,
gfx::Rect(0, 0, 100, 100), 0,
/*is_adjusted_bounds=*/false);
ASSERT_EQ(1, delegate->bounds_change_count());
// Send bounds change when exiting minmized.
shell_surface->OnBoundsChangeEvent(WindowStateType::kMinimized,
WindowStateType::kNormal, display_id,
gfx::Rect(0, 0, 100, 100), 0,
/*is_adjusted_bounds=*/false);
ASSERT_EQ(2, delegate->bounds_change_count());
// Snapped, in clamshell mode.
ash::NonClientFrameViewAsh* frame_view =
static_cast<ash::NonClientFrameViewAsh*>(
shell_surface->GetWidget()->non_client_view()->frame_view());
surface->SetFrame(SurfaceFrameType::NORMAL);
surface->Commit();
shell_surface->OnBoundsChangeEvent(WindowStateType::kMinimized,
WindowStateType::kSecondarySnapped,
display_id, gfx::Rect(0, 0, 100, 100), 0,
/*is_adjusted_bounds=*/false);
EXPECT_EQ(3, delegate->bounds_change_count());
EXPECT_EQ(
frame_view->GetClientBoundsForWindowBounds(gfx::Rect(0, 0, 100, 100)),
delegate->requested_bounds().back());
EXPECT_NE(gfx::Rect(0, 0, 100, 100), delegate->requested_bounds().back());
// Snapped, in tablet mode.
EnableTabletMode(true);
shell_surface->OnBoundsChangeEvent(WindowStateType::kMinimized,
WindowStateType::kSecondarySnapped,
display_id, gfx::Rect(0, 0, 100, 100), 0,
/*is_adjusted_bounds=*/false);
EXPECT_EQ(4, delegate->bounds_change_count());
EXPECT_EQ(gfx::Rect(0, 0, 100, 100), delegate->requested_bounds().back());
}
TEST_P(ClientControlledShellSurfaceTest, SetPipWindowBoundsAnimates) {
constexpr gfx::Rect kClientBounds(256, 256);
auto shell_surface = exo::test::ShellSurfaceBuilder(kClientBounds.size())
.SetWindowState(chromeos::WindowStateType::kPip)
.SetGeometry(kClientBounds)
.BuildClientControlledShellSurface();
shell_surface->GetWidget()->Show();
auto* surface = shell_surface->root_surface();
// Making an extra commit may set the next bounds change animation type
// wrongly.
surface->Commit();
ui::ScopedAnimationDurationScaleMode animation_scale_mode(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
EXPECT_EQ(gfx::Rect(8, 8, 256, 256), window->layer()->GetTargetBounds());
EXPECT_EQ(gfx::Rect(8, 8, 256, 256), window->layer()->bounds());
window->SetBounds(gfx::Rect(10, 10, 256, 256));
EXPECT_EQ(gfx::Rect(8, 10, 256, 256), window->layer()->GetTargetBounds());
EXPECT_EQ(gfx::Rect(8, 8, 256, 256), window->layer()->bounds());
}
TEST_P(ClientControlledShellSurfaceTest, PipWindowDragDoesNotAnimate) {
constexpr gfx::Rect kClientBounds(256, 256);
auto shell_surface = exo::test::ShellSurfaceBuilder(kClientBounds.size())
.SetWindowState(chromeos::WindowStateType::kPip)
.SetGeometry(kClientBounds)
.SetMinimumSize(gfx::Size(10, 10))
.SetMaximumSize(gfx::Size(1000, 1000))
.BuildClientControlledShellSurface();
shell_surface->SetCanMaximize(false);
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
ash::Shell::Get()->pip_controller()->SetPipWindow(window);
EXPECT_EQ(gfx::Rect(8, 8, 256, 256), window->layer()->GetTargetBounds());
EXPECT_EQ(gfx::Rect(8, 8, 256, 256), window->layer()->bounds());
ui::ScopedAnimationDurationScaleMode animation_scale_mode(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
{
// Move the window with a drag.
std::unique_ptr<ash::WindowResizer> resizer(ash::CreateWindowResizer(
window, gfx::PointF(), HTCAPTION, ::wm::WINDOW_MOVE_SOURCE_MOUSE));
resizer->Drag(gfx::PointF(10, 10), 0);
// Make sure that the animation is turned off during drag move.
EXPECT_FALSE(window->layer()->GetAnimator()->is_animating());
EXPECT_EQ(gfx::Rect(18, 18, 256, 256), window->layer()->GetTargetBounds());
EXPECT_EQ(gfx::Rect(18, 18, 256, 256), window->layer()->bounds());
// End drag and wait for the animation to end.
resizer->CompleteDrag();
ui::LayerAnimationStoppedWaiter().Wait(window->layer());
}
{
// Resize the window with a drag.
std::unique_ptr<ash::WindowResizer> resizer(ash::CreateWindowResizer(
window, gfx::PointF(), HTRIGHT, ::wm::WINDOW_MOVE_SOURCE_MOUSE));
resizer->Drag(gfx::PointF(100, 0), 0);
// Make sure that animation is turned off during drag resize.
EXPECT_FALSE(window->layer()->GetAnimator()->is_animating());
EXPECT_EQ(window->layer()->GetTargetBounds(), window->layer()->bounds());
resizer->CompleteDrag();
}
}
TEST_P(ClientControlledShellSurfaceTest,
PipWindowDragDoesNotAnimateWithExtraCommit) {
constexpr gfx::Rect kClientBounds(256, 256);
auto shell_surface = exo::test::ShellSurfaceBuilder({kClientBounds.size()})
.SetWindowState(chromeos::WindowStateType::kPip)
.SetGeometry(kClientBounds)
.SetMinimumSize(gfx::Size(10, 10))
.SetMaximumSize(gfx::Size(1000, 1000))
.BuildClientControlledShellSurface();
shell_surface->SetCanMaximize(false);
auto* surface = shell_surface->root_surface();
// Making an extra commit may set the next bounds change animation type
// wrongly.
surface->Commit();
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
ash::Shell::Get()->pip_controller()->SetPipWindow(window);
EXPECT_EQ(gfx::Rect(8, 8, 256, 256), window->layer()->GetTargetBounds());
EXPECT_EQ(gfx::Rect(8, 8, 256, 256), window->layer()->bounds());
ui::ScopedAnimationDurationScaleMode animation_scale_mode(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
std::unique_ptr<ash::WindowResizer> resizer(ash::CreateWindowResizer(
window, gfx::PointF(), HTCAPTION, ::wm::WINDOW_MOVE_SOURCE_MOUSE));
resizer->Drag(gfx::PointF(10, 10), 0);
EXPECT_EQ(gfx::Rect(18, 18, 256, 256), window->layer()->GetTargetBounds());
EXPECT_EQ(gfx::Rect(18, 18, 256, 256), window->layer()->bounds());
EXPECT_FALSE(window->layer()->GetAnimator()->is_animating());
resizer->CompleteDrag();
}
TEST_P(ClientControlledShellSurfaceTest,
ExpandingPipInTabletModeEndsSplitView) {
EnableTabletMode(true);
ash::SplitViewController* split_view_controller =
ash::SplitViewController::Get(ash::Shell::GetPrimaryRootWindow());
EXPECT_FALSE(split_view_controller->InSplitViewMode());
// Create a PIP window:
auto shell_surface = exo::test::ShellSurfaceBuilder({256, 256})
.SetWindowState(chromeos::WindowStateType::kPip)
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
auto primary_window = CreateTestWindow();
auto secondary_window = CreateTestWindow();
split_view_controller->SnapWindow(primary_window.get(),
ash::SnapPosition::kPrimary);
split_view_controller->SnapWindow(secondary_window.get(),
ash::SnapPosition::kSecondary);
EXPECT_TRUE(split_view_controller->InSplitViewMode());
// Should end split view.
shell_surface->SetRestored();
surface->Commit();
EXPECT_FALSE(split_view_controller->InSplitViewMode());
}
TEST_P(ClientControlledShellSurfaceTest,
DismissingPipInTabletModeDoesNotEndSplitView) {
EnableTabletMode(true);
ash::SplitViewController* split_view_controller =
ash::SplitViewController::Get(ash::Shell::GetPrimaryRootWindow());
EXPECT_FALSE(split_view_controller->InSplitViewMode());
// Create a PIP window:
auto shell_surface = exo::test::ShellSurfaceBuilder({256, 256})
.SetWindowState(chromeos::WindowStateType::kPip)
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
auto primary_window = CreateTestWindow();
auto secondary_window = CreateTestWindow();
split_view_controller->SnapWindow(primary_window.get(),
ash::SnapPosition::kPrimary);
split_view_controller->SnapWindow(secondary_window.get(),
ash::SnapPosition::kSecondary);
EXPECT_TRUE(split_view_controller->InSplitViewMode());
// Should not end split-view.
shell_surface->SetMinimized();
surface->Commit();
EXPECT_TRUE(split_view_controller->InSplitViewMode());
}
class StateChangeCounterDelegate
: public test::ClientControlledShellSurfaceDelegate {
public:
explicit StateChangeCounterDelegate(
ClientControlledShellSurface* shell_surface,
int expected_state_change_count)
: test::ClientControlledShellSurfaceDelegate(shell_surface),
expected_state_change_count_(expected_state_change_count) {}
~StateChangeCounterDelegate() override {
EXPECT_EQ(0, expected_state_change_count_);
}
StateChangeCounterDelegate(const StateChangeCounterDelegate&) = delete;
StateChangeCounterDelegate& operator=(const StateChangeCounterDelegate&) =
delete;
private:
int expected_state_change_count_ = 0;
// ClientControlledShellSurface::Delegate:
void OnStateChanged(chromeos::WindowStateType old_state_type,
chromeos::WindowStateType new_state_type) override {
ClientControlledShellSurfaceDelegate::OnStateChanged(old_state_type,
new_state_type);
expected_state_change_count_--;
}
};
TEST_P(ClientControlledShellSurfaceTest, DoNotReplayWindowStateRequest) {
auto shell_surface =
exo::test::ShellSurfaceBuilder({64, 64})
.SetWindowState(chromeos::WindowStateType::kMinimized)
.SetNoCommit()
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
shell_surface->set_delegate(
std::make_unique<StateChangeCounterDelegate>(shell_surface.get(), 0));
surface->Commit();
}
TEST_P(ClientControlledShellSurfaceTest, UnPinTriggersStateChangeRequest) {
// Only test in tablet mode. Because after restore from pin state, in tablet
// mode the window will still be fullscreen.
EnableTabletMode(true);
auto shell_surface = exo::test::ShellSurfaceBuilder({256, 256})
.BuildClientControlledShellSurface();
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
ash::WindowState* window_state = ash::WindowState::Get(window);
ash::WMEvent pin_event(ash::WM_EVENT_PIN);
ash::WindowState::Get(window)->OnWMEvent(&pin_event);
EXPECT_TRUE(window_state->IsPinned());
// Verify maximized->Pinned->Maximized triggers an unpin request to clients.
shell_surface->set_delegate(
std::make_unique<StateChangeCounterDelegate>(shell_surface.get(), 1));
ash::WMEvent restore_event(ash::WM_EVENT_RESTORE);
ash::WindowState::Get(window)->OnWMEvent(&restore_event);
EXPECT_FALSE(window_state->IsPinned());
}
TEST_F(ClientControlledShellSurfaceDisplayTest,
RequestBoundsChangeOnceWithStateTransition) {
constexpr gfx::Size kBufferSize(64, 64);
constexpr gfx::Rect kOriginalBounds({20, 20}, kBufferSize);
auto shell_surface = exo::test::ShellSurfaceBuilder(kBufferSize)
.SetWindowState(chromeos::WindowStateType::kNormal)
.SetGeometry(kOriginalBounds)
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
auto* delegate =
TestClientControlledShellSurfaceDelegate::SetUp(shell_surface.get());
shell_surface->SetPip();
surface->Commit();
ASSERT_EQ(1, delegate->bounds_change_count());
}
TEST_P(ClientControlledShellSurfaceTest,
DoNotSavePipBoundsAcrossMultiplePipTransition) {
// Create a PIP window:
constexpr gfx::Size kBufferSize(100, 100);
constexpr gfx::Rect kOriginalBounds({8, 10}, kBufferSize);
auto shell_surface = exo::test::ShellSurfaceBuilder(kBufferSize)
.SetWindowState(chromeos::WindowStateType::kPip)
.SetGeometry(kOriginalBounds)
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
EXPECT_EQ(gfx::Rect(8, 10, 100, 100), window->bounds());
constexpr gfx::Rect kMovedBounds(gfx::Point(8, 20), kBufferSize);
shell_surface->SetGeometry(kMovedBounds);
surface->Commit();
EXPECT_EQ(gfx::Rect(8, 20, 100, 100), window->bounds());
shell_surface->SetRestored();
surface->Commit();
shell_surface->SetGeometry(kOriginalBounds);
shell_surface->SetPip();
surface->Commit();
EXPECT_EQ(gfx::Rect(8, 10, 100, 100), window->bounds());
shell_surface->SetGeometry(kMovedBounds);
surface->Commit();
EXPECT_EQ(gfx::Rect(8, 20, 100, 100), window->bounds());
shell_surface->SetRestored();
surface->Commit();
shell_surface->SetGeometry(kMovedBounds);
shell_surface->SetPip();
surface->Commit();
EXPECT_EQ(gfx::Rect(8, 20, 100, 100), window->bounds());
}
TEST_P(ClientControlledShellSurfaceTest,
DoNotApplyCollisionDetectionWhileDraggedOrTucked) {
constexpr gfx::Size kBufferSize(256, 256);
constexpr gfx::Rect kOriginalBounds({8, 50}, kBufferSize);
auto shell_surface = exo::test::ShellSurfaceBuilder(kBufferSize)
.SetWindowState(chromeos::WindowStateType::kPip)
.SetGeometry(kOriginalBounds)
.SetMinimumSize(gfx::Size(10, 10))
.SetMaximumSize(gfx::Size(1000, 1000))
.BuildClientControlledShellSurface();
shell_surface->SetCanMaximize(false);
auto* surface = shell_surface->root_surface();
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
ash::WindowState* window_state = ash::WindowState::Get(window);
ash::Shell::Get()->pip_controller()->SetPipWindow(window);
EXPECT_EQ(gfx::Rect(8, 50, 256, 256), window->bounds());
// Ensure that the collision detection logic is not applied during drag move.
ui::test::EventGenerator* event_generator = GetEventGenerator();
event_generator->MoveMouseToCenterOf(window);
event_generator->PressLeftButton();
shell_surface->StartDrag(HTTOP, gfx::PointF(0, 0));
ASSERT_TRUE(window_state->is_dragged());
shell_surface->SetGeometry(gfx::Rect(gfx::Point(20, 50), kBufferSize));
surface->Commit();
EXPECT_EQ(gfx::Rect(20, 50, 256, 256), window->bounds());
window_state->DeleteDragDetails();
ASSERT_FALSE(window_state->is_dragged());
ash::Shell::Get()->pip_controller()->TuckWindow(/*left=*/true);
shell_surface->SetGeometry(gfx::Rect(gfx::Point(-20, 50), kBufferSize));
surface->Commit();
EXPECT_EQ(gfx::Rect(-20, 50, 256, 256), window->bounds());
}
TEST_P(ClientControlledShellSurfaceTest, EnteringPipSavesPipSnapFraction) {
constexpr gfx::Size kBufferSize(100, 100);
constexpr gfx::Rect kOriginalBounds({8, 50}, kBufferSize);
auto shell_surface = exo::test::ShellSurfaceBuilder(kBufferSize)
.SetWindowState(chromeos::WindowStateType::kPip)
.SetGeometry(kOriginalBounds)
.BuildClientControlledShellSurface();
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
ash::WindowState* window_state = ash::WindowState::Get(window);
EXPECT_EQ(gfx::Rect(8, 50, 100, 100), window->bounds());
// Ensure the correct value is saved to pip snap fraction.
EXPECT_TRUE(ash::PipPositioner::HasSnapFraction(window_state));
EXPECT_EQ(ash::PipPositioner::GetSnapFractionAppliedBounds(window_state),
window->bounds());
}
TEST_P(ClientControlledShellSurfaceTest,
ShadowBoundsChangedIsResetAfterCommit) {
auto shell_surface =
exo::test::ShellSurfaceBuilder().BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
surface->SetFrame(SurfaceFrameType::SHADOW);
shell_surface->SetShadowBounds(gfx::Rect(10, 10, 100, 100));
EXPECT_TRUE(shell_surface->get_shadow_bounds_changed_for_testing());
surface->Commit();
EXPECT_FALSE(shell_surface->get_shadow_bounds_changed_for_testing());
}
namespace {
class ClientControlledShellSurfaceScaleTest : public test::ExoTestBase {
public:
ClientControlledShellSurfaceScaleTest() = default;
~ClientControlledShellSurfaceScaleTest() override = default;
private:
ClientControlledShellSurfaceScaleTest(
const ClientControlledShellSurfaceScaleTest&) = delete;
ClientControlledShellSurfaceScaleTest& operator=(
const ClientControlledShellSurfaceScaleTest&) = delete;
};
} // namespace
TEST_F(ClientControlledShellSurfaceScaleTest, ScaleSetOnInitialCommit) {
UpdateDisplay("1200x800*2.0");
auto shell_surface = exo::test::ShellSurfaceBuilder({20, 20})
.SetNoCommit()
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
auto* delegate =
TestClientControlledShellSurfaceDelegate::SetUp(shell_surface.get());
surface->Commit();
EXPECT_EQ(2.f, 1.f / shell_surface->GetClientToDpScale());
EXPECT_EQ(0, delegate->bounds_change_count());
EXPECT_EQ(1, delegate->geometry_change_count());
}
TEST_F(ClientControlledShellSurfaceScaleTest,
ScaleFactorIsCommittedInNextCommit) {
UpdateDisplay("1200x800*2.0");
gfx::Rect initial_native_bounds(100, 100, 100, 100);
auto shell_surface = exo::test::ShellSurfaceBuilder({20, 20})
.SetWindowState(chromeos::WindowStateType::kNormal)
.SetNoCommit()
.BuildClientControlledShellSurface();
auto* delegate =
TestClientControlledShellSurfaceDelegate::SetUp(shell_surface.get());
display::Display primary_display =
display::Screen::GetScreen()->GetPrimaryDisplay();
shell_surface->SetScaleFactor(2.f);
shell_surface->SetBounds(primary_display.id(), initial_native_bounds);
auto* surface = shell_surface->root_surface();
surface->Commit();
EXPECT_EQ(2.f, 1.f / shell_surface->GetClientToDpScale());
EXPECT_EQ(0, delegate->bounds_change_count());
EXPECT_EQ(1, delegate->geometry_change_count());
EXPECT_EQ(gfx::ScaleToRoundedRect(initial_native_bounds,
shell_surface->GetClientToDpScale()),
delegate->geometry_bounds()[0]);
UpdateDisplay("2400x1600*1.0");
// The new scale factor should not be in active until the next commit.
EXPECT_EQ(2.f, 1.f / shell_surface->GetClientToDpScale());
EXPECT_EQ(0, delegate->bounds_change_count());
constexpr gfx::Size kNewBufferSize(10, 10);
auto new_buffer = test::ExoTestHelper::CreateBuffer(kNewBufferSize);
surface->Attach(new_buffer.get());
shell_surface->SetScaleFactor(1.f);
surface->Commit();
EXPECT_EQ(1.f, shell_surface->GetClientToDpScale());
EXPECT_EQ(0, delegate->bounds_change_count());
}
TEST_P(ClientControlledShellSurfaceTest, SnappedClientBounds) {
UpdateDisplay("800x600");
auto shell_surface = exo::test::ShellSurfaceBuilder({256, 256})
.SetNoCommit()
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
// Clear insets so that it won't affects the bounds.
shell_surface->SetSystemUiVisibility(true);
aura::Window* root = ash::Shell::GetPrimaryRootWindow();
ash::WorkAreaInsets::ForWindow(root)->UpdateWorkAreaInsetsForTest(
root, gfx::Rect(), gfx::Insets(), gfx::Insets());
auto* delegate =
TestClientControlledShellSurfaceDelegate::SetUp(shell_surface.get());
surface->Commit();
views::Widget* widget = shell_surface->GetWidget();
aura::Window* window = widget->GetNativeWindow();
// Normal state -> Snap.
shell_surface->SetGeometry(gfx::Rect(50, 100, 200, 300));
surface->SetFrame(SurfaceFrameType::NORMAL);
surface->Commit();
EXPECT_EQ(gfx::Rect(50, 68, 200, 332), widget->GetWindowBoundsInScreen());
ash::WindowSnapWMEvent event(ash::WM_EVENT_SNAP_PRIMARY);
ash::WindowState::Get(window)->OnWMEvent(&event);
EXPECT_EQ(gfx::Rect(0, 32, 400, 568), delegate->requested_bounds().back());
// Maximized -> Snap.
shell_surface->SetMaximized();
shell_surface->SetGeometry(gfx::Rect(0, 0, 800, 568));
surface->Commit();
EXPECT_TRUE(widget->IsMaximized());
ash::WindowState::Get(window)->OnWMEvent(&event);
EXPECT_EQ(gfx::Rect(0, 32, 400, 568), delegate->requested_bounds().back());
shell_surface->SetSnapPrimary(chromeos::kDefaultSnapRatio);
shell_surface->SetGeometry(gfx::Rect(0, 0, 400, 568));
surface->Commit();
// Clamshell mode -> tablet mode. The bounds start from top-left corner.
EnableTabletMode(true);
EXPECT_EQ(
gfx::Rect(0, 0, 400 - chromeos::wm::kSplitviewDividerShortSideLength / 2,
564),
delegate->requested_bounds().back());
shell_surface->SetGeometry(gfx::Rect(
0, 0, 400 - chromeos::wm::kSplitviewDividerShortSideLength / 2, 568));
surface->SetFrame(SurfaceFrameType::AUTOHIDE);
surface->Commit();
// Tablet mode -> clamshell mode. Top caption height should be reserved.
EnableTabletMode(false);
EXPECT_EQ(gfx::Rect(0, 32, 400, 568), delegate->requested_bounds().back());
// Clean up state.
shell_surface->SetSnapPrimary(chromeos::kDefaultSnapRatio);
surface->Commit();
}
// The shell surface with resize lock on should be unresizable.
TEST_P(ClientControlledShellSurfaceTest,
ShellSurfaceWithResizeLockOnIsUnresizable) {
auto shell_surface = exo::test::ShellSurfaceBuilder({256, 256})
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
EXPECT_TRUE(shell_surface->CanResize());
shell_surface->SetResizeLockType(
ash::ArcResizeLockType::RESIZE_DISABLED_TOGGLABLE);
surface->Commit();
EXPECT_FALSE(shell_surface->CanResize());
shell_surface->SetResizeLockType(
ash::ArcResizeLockType::RESIZE_ENABLED_TOGGLABLE);
surface->Commit();
EXPECT_TRUE(shell_surface->CanResize());
}
TEST_P(ClientControlledShellSurfaceTest, OverlayShadowBounds) {
constexpr gfx::Rect kInitialBounds(150, 10, 200, 200);
auto shell_surface = exo::test::ShellSurfaceBuilder({1, 1})
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
display::Display primary_display =
display::Screen::GetScreen()->GetPrimaryDisplay();
shell_surface->SetBounds(primary_display.id(), kInitialBounds);
shell_surface->OnSetFrame(SurfaceFrameType::NORMAL);
surface->Commit();
EXPECT_FALSE(shell_surface->HasOverlay());
ShellSurfaceBase::OverlayParams params(std::make_unique<views::View>());
params.overlaps_frame = false;
shell_surface->AddOverlay(std::move(params));
EXPECT_TRUE(shell_surface->HasOverlay());
{
gfx::Size overlay_size =
shell_surface->GetWidget()->GetWindowBoundsInScreen().size();
gfx::Size shadow_size = shell_surface->GetShadowBounds().size();
EXPECT_EQ(shadow_size, overlay_size);
}
}
// WideFrameView should be safely deleted even when the window is
// deleted directly.
TEST_P(ClientControlledShellSurfaceTest, DeleteWindowWithWideframe) {
auto shell_surface =
exo::test::ShellSurfaceBuilder({64, 64})
.SetWindowState(chromeos::WindowStateType::kMaximized)
.SetGeometry(gfx::Rect(100, 0, 64, 64))
.SetInputRegion(gfx::Rect(0, 0, 64, 64))
.SetFrame(SurfaceFrameType::NORMAL)
.BuildClientControlledShellSurface();
auto* wide_frame = shell_surface->wide_frame_for_test();
ASSERT_TRUE(wide_frame);
delete shell_surface->GetWidget()->GetNativeWindow();
}
// WideFrameView follows its respective surface when it is eventually parented.
// See crbug.com/1223135.
TEST_P(ClientControlledShellSurfaceTest, WideframeForUnparentedTasks) {
auto shell_surface = exo::test::ShellSurfaceBuilder({64, 64})
.SetGeometry(gfx::Rect(100, 0, 64, 64))
.SetInputRegion(gfx::Rect(0, 0, 64, 64))
.SetFrame(SurfaceFrameType::NORMAL)
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
auto* wide_frame = shell_surface->wide_frame_for_test();
ASSERT_FALSE(wide_frame);
// Set the |app_restore::kParentToHiddenContainerKey| for the surface and
// reparent it, simulating the Full Restore process for an unparented ARC
// task.
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
window->SetProperty(app_restore::kParentToHiddenContainerKey, true);
aura::client::ParentWindowWithContext(window,
/*context=*/window->GetRootWindow(),
window->GetBoundsInScreen(),
display::kInvalidDisplayId);
// Maximize the surface. The WideFrameView should be created and a crash
// should not occur.
shell_surface->SetMaximized();
surface->Commit();
const auto* hidden_container_parent = window->parent();
wide_frame = shell_surface->wide_frame_for_test();
EXPECT_TRUE(wide_frame);
EXPECT_EQ(hidden_container_parent,
wide_frame->GetWidget()->GetNativeWindow()->parent());
// Call the WindowRestoreController, simulating the ARC task becoming ready.
// The surface should be reparented and the WideFrameView should follow it.
ash::WindowRestoreController::Get()->OnParentWindowToValidContainer(window);
EXPECT_NE(hidden_container_parent, window->parent());
wide_frame = shell_surface->wide_frame_for_test();
EXPECT_TRUE(wide_frame);
EXPECT_EQ(window->parent(),
wide_frame->GetWidget()->GetNativeWindow()->parent());
}
TEST_P(ClientControlledShellSurfaceTest,
InitializeWindowStateGrantsPermissionToActivate) {
auto shell_surface =
exo::test::ShellSurfaceBuilder().BuildClientControlledShellSurface();
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
auto* permission = window->GetProperty(kPermissionKey);
EXPECT_TRUE(permission->Check(Permission::Capability::kActivate));
}
TEST_P(ClientControlledShellSurfaceTest, SupportsFloatedState) {
// Test disabling support.
{
auto shell_surface = exo::test::ShellSurfaceBuilder()
.DisableSupportsFloatedState()
.BuildClientControlledShellSurface();
auto* const window = shell_surface->GetWidget()->GetNativeWindow();
EXPECT_FALSE(chromeos::wm::CanFloatWindow(window));
}
// Test enabling (default) support.
{
auto shell_surface =
exo::test::ShellSurfaceBuilder().BuildClientControlledShellSurface();
auto* const window = shell_surface->GetWidget()->GetNativeWindow();
EXPECT_TRUE(chromeos::wm::CanFloatWindow(window));
}
}
// Test if the surface bounds is correctly set when the scale factor is not
// explicitly set.
TEST_P(ClientControlledShellSurfaceTest,
SetBoundsWithoutExplicitScaleFactorSet) {
UpdateDisplay("800x600*2");
aura::Window::Windows root_windows = ash::Shell::GetAllRootWindows();
const auto primary_display_id =
display::Screen::GetScreen()->GetPrimaryDisplay().id();
constexpr gfx::Size kBufferSize(64, 64);
auto buffer = test::ExoTestHelper::CreateBuffer(kBufferSize);
constexpr gfx::Rect kBoundsDp(64, 64, 128, 128);
const gfx::Rect bounds_px_for_2x = gfx::ScaleToRoundedRect(kBoundsDp, 2);
{
// Set display id, bounds origin, bounds size at the same time via
// SetBounds method.
auto shell_surface = test::ShellSurfaceBuilder()
.SetNoCommit()
.BuildClientControlledShellSurface();
auto* const surface = shell_surface->root_surface();
shell_surface->SetBounds(primary_display_id, bounds_px_for_2x);
surface->Attach(buffer.get());
surface->Commit();
const auto* window = shell_surface->GetWidget()->GetNativeWindow();
EXPECT_EQ(kBoundsDp, window->GetBoundsInRootWindow());
}
{
// Set display id and bounds origin at the same time via SetBoundsOrigin
// method, and set bounds size separately.
auto shell_surface = test::ShellSurfaceBuilder()
.SetNoCommit()
.BuildClientControlledShellSurface();
auto* const surface = shell_surface->root_surface();
shell_surface->SetBoundsOrigin(primary_display_id,
bounds_px_for_2x.origin());
shell_surface->SetBoundsSize(bounds_px_for_2x.size());
surface->Attach(buffer.get());
surface->Commit();
const auto* window = shell_surface->GetWidget()->GetNativeWindow();
EXPECT_EQ(kBoundsDp, window->GetBoundsInRootWindow());
}
}
TEST_P(ClientControlledShellSurfaceTest, FrameOverlap) {
constexpr gfx::Rect kWindowBounds(20, 50, 300, 200);
// The bounds for views::ClientView, should be kWindowBounds excluding
// caption.
constexpr gfx::Rect kClientViewBounds(20, 82, 300, 168);
auto shell_surface = exo::test::ShellSurfaceBuilder({kWindowBounds.size()})
.SetGeometry(kWindowBounds)
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
views::Widget* widget = shell_surface->GetWidget();
aura::Window* window = widget->GetNativeWindow();
ash::NonClientFrameViewAsh* frame_view =
static_cast<ash::NonClientFrameViewAsh*>(
widget->non_client_view()->frame_view());
// 1) Initial state, no frame (SurfaceFrameType is NONE). ClientView bounds
// should be the same as the window bounds.
EXPECT_FALSE(frame_view->GetHeaderView()->GetVisible());
EXPECT_FALSE(frame_view->GetFrameEnabled());
EXPECT_FALSE(frame_view->GetFrameOverlapped());
EXPECT_FALSE(wm::ShadowController::GetShadowForWindow(window));
EXPECT_EQ(kWindowBounds, widget->GetWindowBoundsInScreen());
EXPECT_EQ(kWindowBounds,
frame_view->GetWindowBoundsForClientBounds(kWindowBounds));
// 2) Set frame to OVERLAP, the frame should be visible. ClientView bounds
// should be window bounds excluding caption.
surface->SetFrame(SurfaceFrameType::OVERLAP);
surface->Commit();
EXPECT_TRUE(frame_view->GetHeaderView()->GetVisible());
EXPECT_TRUE(frame_view->GetFrameEnabled());
EXPECT_TRUE(frame_view->GetFrameOverlapped());
EXPECT_TRUE(wm::ShadowController::GetShadowForWindow(window));
EXPECT_EQ(kWindowBounds, widget->GetWindowBoundsInScreen());
EXPECT_EQ(kWindowBounds,
frame_view->GetWindowBoundsForClientBounds(kClientViewBounds));
// 3) Maximize the surface, it should be maximized properly.
shell_surface->SetMaximized();
surface->Commit();
EXPECT_TRUE(shell_surface->GetWidget()->IsMaximized());
// 4) Minimize the surface, it should be maximized properly.
shell_surface->SetMinimized();
surface->Commit();
EXPECT_TRUE(shell_surface->GetWidget()->IsMinimized());
}
TEST_P(ClientControlledShellSurfaceTest, ShowMinimizedNoActivation) {
class TestObserver : public SeatObserver {
public:
// SeatObserver:
void OnSurfaceFocused(Surface* gained_focus,
Surface* lost_focus,
bool has_focused_client) override {
focused_called_ = true;
}
bool focused_called() const { return focused_called_; }
private:
bool focused_called_ = false;
} observer;
Seat seat;
seat.AddObserver(&observer, 1);
auto shell_surface =
test::ShellSurfaceBuilder({300, 200})
.SetWindowState(chromeos::WindowStateType::kMinimized)
.BuildClientControlledShellSurface();
ASSERT_TRUE(shell_surface->GetWidget()->IsMinimized());
EXPECT_FALSE(observer.focused_called());
seat.RemoveObserver(&observer);
}
} // namespace exo