blob: b79510e1ef3b6e7e406f1701557db5bc54ce98b1 [file] [log] [blame]
// Copyright 2015 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/shell_surface.h"
#include <sstream>
#include <vector>
#include "ash/capture_mode/capture_mode_test_util.h"
#include "ash/constants/app_types.h"
#include "ash/frame/non_client_frame_view_ash.h"
#include "ash/frame_throttler/frame_throttling_controller.h"
#include "ash/frame_throttler/mock_frame_throttling_observer.h"
#include "ash/public/cpp/test/shell_test_api.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/shell.h"
#include "ash/test/test_widget_builder.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_test_util.h"
#include "ash/wm/raster_scale/raster_scale_controller.h"
#include "ash/wm/resize_shadow.h"
#include "ash/wm/resize_shadow_controller.h"
#include "ash/wm/window_state.h"
#include "ash/wm/wm_event.h"
#include "ash/wm/workspace_controller_test_api.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/test/bind.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/ui/base/window_properties.h"
#include "components/app_restore/window_properties.h"
#include "components/exo/buffer.h"
#include "components/exo/client_controlled_shell_surface.h"
#include "components/exo/permission.h"
#include "components/exo/shell_surface_util.h"
#include "components/exo/sub_surface.h"
#include "components/exo/surface.h"
#include "components/exo/surface_test_util.h"
#include "components/exo/test/exo_test_base.h"
#include "components/exo/test/exo_test_helper.h"
#include "components/exo/test/mock_security_delegate.h"
#include "components/exo/test/shell_surface_builder.h"
#include "components/exo/test/test_security_delegate.h"
#include "components/exo/window_properties.h"
#include "components/exo/wm_helper.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/client/capture_client.h"
#include "ui/aura/window.h"
#include "ui/aura/window_delegate.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/aura/window_targeter.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/hit_test.h"
#include "ui/compositor/compositor.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/compositor_extra/shadow.h"
#include "ui/display/display.h"
#include "ui/display/display_layout_builder.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/screen.h"
#include "ui/display/test/display_manager_test_api.h"
#include "ui/display/types/display_constants.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/widget/any_widget_observer.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/caption_button_layout_constants.h"
#include "ui/wm/core/shadow_controller.h"
#include "ui/wm/core/shadow_types.h"
#include "ui/wm/core/window_properties.h"
#include "ui/wm/core/window_util.h"
namespace exo {
const gfx::BufferFormat kOpaqueFormat = gfx::BufferFormat::RGBX_8888;
using ShellSurfaceTest = test::ExoTestBase;
namespace {
bool HasBackdrop() {
ash::WorkspaceController* wc = ash::ShellTestApi().workspace_controller();
return !!ash::WorkspaceControllerTestApi(wc).GetBackdropWindow();
}
uint32_t ConfigureFullscreen(
uint32_t serial,
const gfx::Rect& bounds,
chromeos::WindowStateType state_type,
bool resizing,
bool activated,
const gfx::Vector2d& origin_offset,
float raster_scale,
aura::Window::OcclusionState occlusion_state,
std::optional<chromeos::WindowStateType> restore_state_type) {
EXPECT_EQ(chromeos::WindowStateType::kFullscreen, state_type);
return serial;
}
std::unique_ptr<ShellSurface> CreatePopupShellSurface(
Surface* popup_surface,
ShellSurface* parent,
const gfx::Point& origin) {
auto popup_shell_surface = std::make_unique<ShellSurface>(popup_surface);
popup_shell_surface->DisableMovement();
popup_shell_surface->SetPopup();
popup_shell_surface->SetParent(parent);
popup_shell_surface->SetOrigin(origin);
return popup_shell_surface;
}
std::unique_ptr<ShellSurface> CreateX11TransientShellSurface(
ShellSurface* parent,
const gfx::Size& size,
const gfx::Point& origin) {
return test::ShellSurfaceBuilder(size)
.SetParent(parent)
.SetOrigin(origin)
.BuildShellSurface();
}
struct ConfigureData {
gfx::Rect suggested_bounds;
chromeos::WindowStateType state_type = chromeos::WindowStateType::kDefault;
bool is_resizing = false;
bool is_active = false;
std::optional<chromeos::WindowStateType> restore_state_type = std::nullopt;
float raster_scale = 1.0f;
aura::Window::OcclusionState occlusion_state;
size_t count = 0;
};
uint32_t Configure(
ConfigureData* config_data,
const gfx::Rect& bounds,
chromeos::WindowStateType state_type,
bool resizing,
bool activated,
const gfx::Vector2d& origin_offset,
float raster_scale,
aura::Window::OcclusionState occlusion_state,
std::optional<chromeos::WindowStateType> restore_state_type) {
config_data->suggested_bounds = bounds;
config_data->state_type = state_type;
config_data->is_resizing = resizing;
config_data->is_active = activated;
config_data->raster_scale = raster_scale;
config_data->occlusion_state = occlusion_state;
config_data->restore_state_type = restore_state_type;
config_data->count++;
return 0;
}
uint32_t ConfigureSerial(
ConfigureData* config_data,
const gfx::Rect& bounds,
chromeos::WindowStateType state_type,
bool resizing,
bool activated,
const gfx::Vector2d& origin_offset,
float raster_scale,
aura::Window::OcclusionState occlusion_state,
std::optional<chromeos::WindowStateType> restore_state_type) {
config_data->suggested_bounds = bounds;
config_data->state_type = state_type;
config_data->is_resizing = resizing;
config_data->is_active = activated;
config_data->raster_scale = raster_scale;
config_data->occlusion_state = occlusion_state;
config_data->restore_state_type = restore_state_type;
config_data->count++;
return config_data->count;
}
uint32_t ConfigureSerialVec(
std::vector<ConfigureData>* config_vec,
const gfx::Rect& bounds,
chromeos::WindowStateType state_type,
bool resizing,
bool activated,
const gfx::Vector2d& origin_offset,
float raster_scale,
aura::Window::OcclusionState occlusion_state,
std::optional<chromeos::WindowStateType> restore_state_type) {
config_vec->emplace_back(config_vec->empty() ? ConfigureData{}
: config_vec->back());
return ConfigureSerial(&config_vec->back(), bounds, state_type, resizing,
activated, origin_offset, raster_scale,
occlusion_state, restore_state_type);
}
bool IsCaptureWindow(ShellSurface* shell_surface) {
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
return WMHelper::GetInstance()->GetCaptureClient()->GetCaptureWindow() ==
window;
}
cc::Region CreateRegion(ui::Layer::ShapeRects shape_rects) {
cc::Region shape_region;
for (const gfx::Rect& rect : shape_rects) {
shape_region.Union(rect);
}
return shape_region;
}
} // namespace
TEST_F(ShellSurfaceTest, AcknowledgeConfigure) {
constexpr gfx::Size kBufferSize(32, 32);
auto shell_surface =
test::ShellSurfaceBuilder(kBufferSize).BuildShellSurface();
auto* surface = shell_surface->root_surface();
gfx::Point origin(100, 100);
shell_surface->GetWidget()->SetBounds(gfx::Rect(origin, kBufferSize));
EXPECT_EQ(origin.ToString(),
surface->window()->GetBoundsInRootWindow().origin().ToString());
const uint32_t kSerial = 1;
shell_surface->set_configure_callback(
base::BindRepeating(&ConfigureFullscreen, kSerial));
shell_surface->SetFullscreen(true, display::kInvalidDisplayId);
// Surface origin should not change until configure request is acknowledged.
EXPECT_EQ(origin.ToString(),
surface->window()->GetBoundsInRootWindow().origin().ToString());
// Compositor should be locked until configure request is acknowledged.
ui::Compositor* compositor =
shell_surface->GetWidget()->GetNativeWindow()->layer()->GetCompositor();
EXPECT_TRUE(compositor->IsLocked());
shell_surface->AcknowledgeConfigure(kSerial);
auto fullscreen_buffer =
test::ExoTestHelper::CreateBuffer(GetContext()->bounds().size());
surface->Attach(fullscreen_buffer.get());
surface->Commit();
EXPECT_EQ(gfx::Point().ToString(),
surface->window()->GetBoundsInRootWindow().origin().ToString());
EXPECT_FALSE(compositor->IsLocked());
}
TEST_F(ShellSurfaceTest, SetParent) {
constexpr gfx::Size kBufferSize(256, 256);
auto parent_shell_surface =
test::ShellSurfaceBuilder(kBufferSize).BuildShellSurface();
auto* parent_surface = parent_shell_surface->root_surface();
auto shell_surface = test::ShellSurfaceBuilder(kBufferSize)
.SetParent(parent_shell_surface.get())
.BuildShellSurface();
EXPECT_EQ(
parent_shell_surface->GetWidget()->GetNativeWindow(),
wm::GetTransientParent(shell_surface->GetWidget()->GetNativeWindow()));
// Use OnSetParent to move shell surface to 10, 10.
gfx::Point parent_origin =
parent_shell_surface->GetWidget()->GetWindowBoundsInScreen().origin();
shell_surface->OnSetParent(
parent_surface,
gfx::PointAtOffsetFromOrigin(gfx::Point(10, 10) - parent_origin));
EXPECT_EQ(gfx::Rect(10, 10, 256, 256),
shell_surface->GetWidget()->GetWindowBoundsInScreen());
EXPECT_FALSE(shell_surface->CanActivate());
}
TEST_F(ShellSurfaceTest, DeleteShellSurfaceWithTransientChildren) {
constexpr gfx::Size kBufferSize(256, 256);
auto parent_shell_surface =
test::ShellSurfaceBuilder(kBufferSize).BuildShellSurface();
auto child1_shell_surface = test::ShellSurfaceBuilder(kBufferSize)
.SetParent(parent_shell_surface.get())
.BuildShellSurface();
auto child2_shell_surface = test::ShellSurfaceBuilder(kBufferSize)
.SetParent(parent_shell_surface.get())
.BuildShellSurface();
auto child3_shell_surface = test::ShellSurfaceBuilder(kBufferSize)
.SetParent(parent_shell_surface.get())
.BuildShellSurface();
EXPECT_EQ(parent_shell_surface->GetWidget()->GetNativeWindow(),
wm::GetTransientParent(
child1_shell_surface->GetWidget()->GetNativeWindow()));
EXPECT_EQ(parent_shell_surface->GetWidget()->GetNativeWindow(),
wm::GetTransientParent(
child2_shell_surface->GetWidget()->GetNativeWindow()));
EXPECT_EQ(parent_shell_surface->GetWidget()->GetNativeWindow(),
wm::GetTransientParent(
child3_shell_surface->GetWidget()->GetNativeWindow()));
parent_shell_surface.reset();
}
TEST_F(ShellSurfaceTest, CommitAndConfigure) {
// Test that a commit produces an expected configure. A commit to a surface
// without a buffer attached should produce a configure with zero size. Then
// once a buffer is attached, a commit should produce a configure
// corresponding to the size of the buffer.
auto shell_surface =
test::ShellSurfaceBuilder().SetNoCommit().BuildShellSurface();
uint32_t serial = 0;
gfx::Rect bounds;
aura::Window::OcclusionState occlusion_state;
auto configure_callback = base::BindRepeating(
[](uint32_t* const serial_ptr, gfx::Rect* bounds_ptr,
aura::Window::OcclusionState* occlusion_state_ptr,
const gfx::Rect& bounds, chromeos::WindowStateType state_type,
bool resizing, bool activated, const gfx::Vector2d& origin_offset,
float raster_scale, aura::Window::OcclusionState occlusion_state,
std::optional<chromeos::WindowStateType>) {
*bounds_ptr = bounds;
*occlusion_state_ptr = occlusion_state;
return ++(*serial_ptr);
},
&serial, &bounds, &occlusion_state);
shell_surface->set_configure_callback(configure_callback);
// Receiving a commit without a buffer should result in an initial configure
// with bounds gfx::Rect(0, 0, 0, 0). Note that although `ShellSurface` does
// not implement xdg-shell, in xdg-shell spec sending 0x0 bounds is used to
// hint to a client that the bounds should be determined by the client (i.e.
// ShellSurface implicitly follows xdg-shell spec here).
shell_surface->root_surface()->Commit();
ASSERT_EQ(1u, serial);
EXPECT_EQ(bounds, gfx::Rect(0, 0, 0, 0));
// Attaching a buffer and committing should produce a single configure with
// the size equal to the buffer size.
constexpr gfx::Size kBufferSize(64, 64);
auto buffer = test::ExoTestHelper::CreateBuffer(kBufferSize);
shell_surface->root_surface()->Attach(buffer.get());
shell_surface->root_surface()->Commit();
ASSERT_EQ(2u, serial);
EXPECT_EQ(bounds.size(), kBufferSize);
// The occlusion state should be visible since there is no other window
// occluding the surface.
EXPECT_EQ(occlusion_state, aura::Window::OcclusionState::VISIBLE);
}
TEST_F(ShellSurfaceTest, Maximize) {
auto shell_surface =
test::ShellSurfaceBuilder({256, 256}).BuildShellSurface();
auto* surface = shell_surface->root_surface();
EXPECT_TRUE(shell_surface->IsReady());
EXPECT_FALSE(HasBackdrop());
shell_surface->Maximize();
EXPECT_FALSE(HasBackdrop());
surface->Commit();
EXPECT_FALSE(HasBackdrop());
EXPECT_EQ(GetContext()->bounds().width(),
shell_surface->GetWidget()->GetWindowBoundsInScreen().width());
EXPECT_TRUE(shell_surface->GetWidget()->IsMaximized());
// 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_FALSE(HasBackdrop());
}
TEST_F(ShellSurfaceTest, CanMaximizeResizableWindow) {
auto shell_surface =
test::ShellSurfaceBuilder({400, 300}).BuildShellSurface();
// Make sure we've created a resizable window.
EXPECT_TRUE(shell_surface->CanResize());
// Assert: Resizable windows can be maximized.
EXPECT_TRUE(shell_surface->CanMaximize());
}
TEST_F(ShellSurfaceTest, CannotMaximizeNonResizableWindow) {
constexpr gfx::Size kBufferSize(400, 300);
auto shell_surface = test::ShellSurfaceBuilder(kBufferSize)
.SetMinimumSize(kBufferSize)
.SetMaximumSize(kBufferSize)
.BuildShellSurface();
// Make sure we've created a non-resizable window.
EXPECT_FALSE(shell_surface->CanResize());
// Assert: Non-resizable windows cannot be maximized.
EXPECT_FALSE(shell_surface->CanMaximize());
}
TEST_F(ShellSurfaceTest, MaximizeFromFullscreen) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetMaximumSize(gfx::Size(10, 10))
.BuildShellSurface();
// Act: Maximize after fullscreen
shell_surface->root_surface()->Commit();
shell_surface->SetFullscreen(true, display::kInvalidDisplayId);
shell_surface->root_surface()->Commit();
shell_surface->Maximize();
shell_surface->root_surface()->Commit();
// Assert: Window should stay fullscreen.
EXPECT_TRUE(shell_surface->GetWidget()->IsFullscreen());
}
TEST_F(ShellSurfaceTest, MaximizeExitsFullscreen) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetMaximumSize(gfx::Size(10, 10))
.BuildShellSurface();
// Act: Set window property kRestoreOrMaximizeExitsFullscreen
// then maximize after fullscreen
shell_surface->root_surface()->Commit();
shell_surface->GetWidget()->GetNativeWindow()->SetProperty(
kRestoreOrMaximizeExitsFullscreen, true);
shell_surface->SetFullscreen(true, display::kInvalidDisplayId);
shell_surface->root_surface()->Commit();
shell_surface->Maximize();
shell_surface->root_surface()->Commit();
// Assert: Window should exit fullscreen and be maximized.
EXPECT_TRUE(shell_surface->GetWidget()->GetNativeWindow()->GetProperty(
kRestoreOrMaximizeExitsFullscreen));
EXPECT_TRUE(shell_surface->GetWidget()->IsMaximized());
}
TEST_F(ShellSurfaceTest, Minimize) {
constexpr gfx::Size kBufferSize(256, 256);
auto shell_surface =
test::ShellSurfaceBuilder(kBufferSize).SetNoCommit().BuildShellSurface();
EXPECT_FALSE(shell_surface->IsReady());
EXPECT_TRUE(shell_surface->CanMinimize());
// Minimizing can be performed before the surface is committed, but
// widget creation will be deferred.
shell_surface->Minimize();
EXPECT_FALSE(shell_surface->GetWidget());
EXPECT_FALSE(shell_surface->IsReady());
// Committing the buffer will create a widget with minimized state.
views::NamedWidgetShownWaiter widget_waiter(
views::test::AnyWidgetTestPasskey{}, "ExoShellSurface-0");
uint32_t serial = 0;
chromeos::WindowStateType state[1]{chromeos::WindowStateType::kNormal};
auto configure_callback = base::BindRepeating(
[](uint32_t* const serial_ptr, chromeos::WindowStateType* state_ptr,
const gfx::Rect& bounds, chromeos::WindowStateType state_type,
bool resizing, bool activated, const gfx::Vector2d& origin_offset,
float raster_scale, aura::Window::OcclusionState occlusion_state,
std::optional<chromeos::WindowStateType>) {
state_ptr[*serial_ptr] = state_type;
CHECK(*serial_ptr < 2);
return ++(*serial_ptr);
},
&serial, state);
shell_surface->set_configure_callback(configure_callback);
shell_surface->root_surface()->Commit();
EXPECT_TRUE(shell_surface->GetWidget()->IsMinimized());
// Two configures (initial configure and the state change configure) should be
// sent with the minimzied state.
ASSERT_EQ(1u, serial);
EXPECT_EQ(chromeos::WindowStateType::kMinimized, state[0]);
shell_surface->set_configure_callback(ShellSurface::ConfigureCallback());
// Minimized widget should be Shown.
widget_waiter.WaitIfNeededAndGet();
EXPECT_TRUE(shell_surface->IsReady());
shell_surface->Restore();
EXPECT_FALSE(shell_surface->GetWidget()->IsMinimized());
auto child_shell_surface =
test::ShellSurfaceBuilder(kBufferSize).SetNoCommit().BuildShellSurface();
auto* child_surface = child_shell_surface->root_surface();
// Transient shell surfaces cannot be minimized.
child_surface->SetParent(shell_surface->root_surface(), gfx::Point());
child_surface->Commit();
EXPECT_FALSE(child_shell_surface->CanMinimize());
}
TEST_F(ShellSurfaceTest, Restore) {
constexpr gfx::Size kBufferSize(256, 256);
auto shell_surface =
test::ShellSurfaceBuilder(kBufferSize).BuildShellSurface();
EXPECT_FALSE(HasBackdrop());
// Note: Remove contents to avoid issues with maximize animations in tests.
shell_surface->Maximize();
EXPECT_FALSE(HasBackdrop());
shell_surface->Restore();
EXPECT_FALSE(HasBackdrop());
EXPECT_EQ(
kBufferSize.ToString(),
shell_surface->GetWidget()->GetWindowBoundsInScreen().size().ToString());
}
TEST_F(ShellSurfaceTest, RestoreFromFullscreen) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetMaximumSize(gfx::Size(10, 10))
.BuildShellSurface();
// Act: Restore after fullscreen
shell_surface->SetFullscreen(true, display::kInvalidDisplayId);
shell_surface->root_surface()->Commit();
shell_surface->Restore();
shell_surface->root_surface()->Commit();
// Assert: Window should stay fullscreen.
EXPECT_TRUE(shell_surface->GetWidget()->IsFullscreen());
}
TEST_F(ShellSurfaceTest, RestoreExitsFullscreen) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256}).BuildShellSurface();
// Act: Set window property kRestoreOrMaximizeExitsFullscreen
// then restore after fullscreen
shell_surface->root_surface()->Commit();
shell_surface->GetWidget()->GetNativeWindow()->SetProperty(
kRestoreOrMaximizeExitsFullscreen, true);
shell_surface->SetFullscreen(true, display::kInvalidDisplayId);
shell_surface->Restore();
shell_surface->root_surface()->Commit();
// Assert: Window should exit fullscreen and be restored.
EXPECT_TRUE(shell_surface->GetWidget()->GetNativeWindow()->GetProperty(
kRestoreOrMaximizeExitsFullscreen));
EXPECT_EQ(gfx::Size(256, 256),
shell_surface->GetWidget()->GetWindowBoundsInScreen().size());
}
TEST_F(ShellSurfaceTest, HostWindowBoundsUpdatedAfterCommitWidget) {
constexpr gfx::Point kOrigin(0, 0);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetOrigin(kOrigin)
.BuildShellSurface();
shell_surface->root_surface()->SetSurfaceHierarchyContentBoundsForTest(
gfx::Rect(0, 0, 50, 50));
// Host Window Bounds size before committing.
EXPECT_EQ(gfx::Rect(0, 0, 256, 256), shell_surface->host_window()->bounds());
EXPECT_TRUE(shell_surface->OnPreWidgetCommit());
shell_surface->CommitWidget();
// CommitWidget should update the Host Window Bounds.
EXPECT_EQ(gfx::Rect(0, 0, 50, 50), shell_surface->host_window()->bounds());
}
TEST_F(ShellSurfaceTest, HostWindowBoundsUpdatedWithNegativeCoordinate) {
constexpr gfx::Point kOrigin(20, 20);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetOrigin(kOrigin)
.BuildShellSurface();
// Set content bounds to negative and larger than surface. This happens when
// subsurfaces are outside of root surface boundary.
shell_surface->root_surface()->SetSurfaceHierarchyContentBoundsForTest(
gfx::Rect(-20, -20, 300, 300));
// Host Window Bounds size before committing.
EXPECT_EQ(gfx::Rect(0, 0, 256, 256), shell_surface->host_window()->bounds());
EXPECT_TRUE(shell_surface->OnPreWidgetCommit());
shell_surface->CommitWidget();
// CommitWidget should update the Host Window Bounds.
EXPECT_EQ(gfx::Rect(-20, -20, 300, 300),
shell_surface->host_window()->bounds());
// Root surface origin must be adjusted relative to host window.
EXPECT_EQ(gfx::Point(20, 20), shell_surface->root_surface_origin_pixel());
}
TEST_F(ShellSurfaceTest, HostWindowIncludesAllSubSurfaces) {
constexpr gfx::Point kOrigin(20, 20);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetOrigin(kOrigin)
.BuildShellSurface();
constexpr gfx::Size kChildBufferSize(32, 32);
// Add child buffer at the upper-left corner of the root surface.
auto child_buffer1 = test::ExoTestHelper::CreateBuffer(kChildBufferSize);
auto child_surface1 = std::make_unique<Surface>();
child_surface1->Attach(child_buffer1.get());
auto subsurface1 = std::make_unique<SubSurface>(
child_surface1.get(), shell_surface->root_surface());
subsurface1->SetPosition(gfx::PointF(-10, -10));
child_surface1->Commit();
// Add child buffer at the bottom-right corner of the root surface.
auto child_buffer2 = test::ExoTestHelper::CreateBuffer(kChildBufferSize);
auto child_surface2 = std::make_unique<Surface>();
child_surface2->Attach(child_buffer2.get());
auto subsurface2 = std::make_unique<SubSurface>(
child_surface2.get(), shell_surface->root_surface());
subsurface2->SetPosition(gfx::PointF(250, 250));
child_surface2->Commit();
shell_surface->root_surface()->Commit();
ASSERT_TRUE(shell_surface->GetWidget());
shell_surface->SetGeometry(gfx::Rect(0, 0, 256, 256));
shell_surface->root_surface()->Commit();
// Host window must be set to include all children subsurfaces.
EXPECT_EQ(gfx::Rect(-10, -10, 292, 292),
shell_surface->host_window()->bounds());
// Root surface origin must be adjusted relative to host window.
EXPECT_EQ(gfx::Point(10, 10), shell_surface->root_surface_origin_pixel());
}
TEST_F(ShellSurfaceTest, HostWindowIncludesAllSubSurfacesWithScaleFactor) {
constexpr gfx::Point kOrigin(20, 20);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetOrigin(kOrigin)
.BuildShellSurface();
// Set scale.
constexpr float kScaleFactor = 2.0;
shell_surface->set_client_submits_surfaces_in_pixel_coordinates(true);
shell_surface->SetScaleFactor(kScaleFactor);
constexpr gfx::Size kChildBufferSize(32, 32);
// Add child buffer at the upper-left corner of the root surface.
auto child_buffer1 = test::ExoTestHelper::CreateBuffer(kChildBufferSize);
auto child_surface1 = std::make_unique<Surface>();
child_surface1->Attach(child_buffer1.get());
auto subsurface1 = std::make_unique<SubSurface>(
child_surface1.get(), shell_surface->root_surface());
subsurface1->SetPosition(gfx::PointF(-10, -10));
child_surface1->Commit();
// Add child buffer at the bottom-right corner of the root surface.
auto child_buffer2 = test::ExoTestHelper::CreateBuffer(kChildBufferSize);
auto child_surface2 = std::make_unique<Surface>();
child_surface2->Attach(child_buffer2.get());
auto subsurface2 = std::make_unique<SubSurface>(
child_surface2.get(), shell_surface->root_surface());
subsurface2->SetPosition(gfx::PointF(250, 250));
child_surface2->Commit();
shell_surface->root_surface()->Commit();
ASSERT_TRUE(shell_surface->GetWidget());
shell_surface->SetGeometry(gfx::Rect(0, 0, 256, 256));
shell_surface->root_surface()->Commit();
// Host window must be set to include all children subsurfaces.
EXPECT_EQ(gfx::Rect(-5, -5, 146, 146),
shell_surface->host_window()->bounds());
// Root surface origin must be adjusted relative to host window.
EXPECT_EQ(gfx::Point(10, 10), shell_surface->root_surface_origin_pixel());
}
TEST_F(ShellSurfaceTest, HostWindowNotIncludeAugmentedChild) {
constexpr gfx::Point kOrigin(20, 20);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetOrigin(kOrigin)
.BuildShellSurface();
constexpr gfx::Size kChildBufferSize(32, 32);
// Add child buffer at the upper-right corner of the root surface.
auto child_buffer1 = test::ExoTestHelper::CreateBuffer(kChildBufferSize);
auto child_surface1 = std::make_unique<Surface>();
child_surface1->Attach(child_buffer1.get());
child_surface1->set_is_augmented(true);
child_surface1->SetClipRect(std::make_optional(gfx::RectF(5, 5, 32, 32)));
auto subsurface1 = std::make_unique<SubSurface>(
child_surface1.get(), shell_surface->root_surface());
subsurface1->SetPosition(gfx::PointF(-10, -10));
child_surface1->Commit();
// Add child buffer at the bottom-left corner of the root surface.
auto child_buffer2 = test::ExoTestHelper::CreateBuffer(kChildBufferSize);
auto child_surface2 = std::make_unique<Surface>();
child_surface2->Attach(child_buffer2.get());
auto subsurface2 = std::make_unique<SubSurface>(
child_surface2.get(), shell_surface->root_surface());
subsurface2->SetPosition(gfx::PointF(-10, 250));
child_surface2->Commit();
shell_surface->root_surface()->Commit();
ASSERT_TRUE(shell_surface->GetWidget());
shell_surface->SetGeometry(gfx::Rect(256, 256));
shell_surface->root_surface()->Commit();
// Host window must be set to include all children subsurfaces, but not the
// clipped area.
EXPECT_EQ(gfx::Rect(-10, 0, 266, 282),
shell_surface->host_window()->bounds());
// Root surface origin must be adjusted relative to host window.
EXPECT_EQ(gfx::Point(10, 0), shell_surface->root_surface_origin_pixel());
}
TEST_F(ShellSurfaceTest, HostWindowNotIncludeAugmentedChildWithScaleFactor) {
constexpr gfx::Point kOrigin(20, 20);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetOrigin(kOrigin)
.BuildShellSurface();
// Set scale.
constexpr float kScaleFactor = 2.0;
shell_surface->set_client_submits_surfaces_in_pixel_coordinates(true);
shell_surface->SetScaleFactor(kScaleFactor);
constexpr gfx::Size kChildBufferSize(32, 32);
// Add child buffer at the upper-right corner of the root surface.
auto child_buffer1 = test::ExoTestHelper::CreateBuffer(kChildBufferSize);
auto child_surface1 = std::make_unique<Surface>();
child_surface1->Attach(child_buffer1.get());
child_surface1->set_is_augmented(true);
child_surface1->SetClipRect(std::make_optional(gfx::RectF(5, 5, 32, 32)));
auto subsurface1 = std::make_unique<SubSurface>(
child_surface1.get(), shell_surface->root_surface());
subsurface1->SetPosition(gfx::PointF(-10, -10));
child_surface1->Commit();
// Add child buffer at the bottom-left corner of the root surface.
auto child_buffer2 = test::ExoTestHelper::CreateBuffer(kChildBufferSize);
auto child_surface2 = std::make_unique<Surface>();
child_surface2->Attach(child_buffer2.get());
auto subsurface2 = std::make_unique<SubSurface>(
child_surface2.get(), shell_surface->root_surface());
subsurface2->SetPosition(gfx::PointF(-10, 250));
child_surface2->Commit();
shell_surface->root_surface()->Commit();
ASSERT_TRUE(shell_surface->GetWidget());
shell_surface->SetGeometry(gfx::Rect(0, 0, 256, 256));
shell_surface->root_surface()->Commit();
// Host window must be set to include all children subsurfaces, but not the
// clipped area.
EXPECT_EQ(gfx::Rect(-5, 0, 133, 141), shell_surface->host_window()->bounds());
// Root surface origin must be adjusted relative to host window.
EXPECT_EQ(gfx::Point(10, 0), shell_surface->root_surface_origin_pixel());
}
TEST_F(ShellSurfaceTest, LocalSurfaceIdUpdatedOnHostWindowOriginChanged) {
constexpr gfx::Point kOrigin(100, 100);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({100, 100})
.SetOrigin(kOrigin)
.SetGeometry(gfx::Rect(100, 100))
.BuildShellSurface();
auto* root_surface = shell_surface->root_surface();
auto child_buffer = test::ExoTestHelper::CreateBuffer(gfx::Size(200, 200));
auto child_surface = std::make_unique<Surface>();
child_surface->Attach(child_buffer.get());
auto subsurface =
std::make_unique<SubSurface>(child_surface.get(), root_surface);
subsurface->SetPosition(gfx::PointF(-50, -50));
child_surface->Commit();
root_surface->Commit();
EXPECT_EQ(gfx::Rect(-50, -50, 200, 200),
shell_surface->host_window()->bounds());
// Store the current local surface id.
const viz::LocalSurfaceId old_id =
shell_surface->GetSurfaceId().local_surface_id();
// If nothing is changed, no need to update local surface id.
child_surface->Commit();
root_surface->Commit();
EXPECT_EQ(shell_surface->GetSurfaceId().local_surface_id(), old_id);
// If the host window origin is updated, need to update local surface id.
subsurface->SetPosition(gfx::PointF(-25, -25));
child_surface->Commit();
root_surface->Commit();
EXPECT_EQ(gfx::Rect(-25, -25, 200, 200),
shell_surface->host_window()->bounds());
EXPECT_TRUE(
shell_surface->GetSurfaceId().local_surface_id().IsNewerThan(old_id));
EXPECT_EQ(gfx::Vector2dF(),
shell_surface->host_window()->layer()->GetSubpixelOffset());
}
TEST_F(ShellSurfaceTest,
LocalSurfaceIdUpdatedOnHostWindowOriginChangedWithScaleFactor) {
constexpr gfx::Point kOrigin(100, 100);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({100, 100})
.SetOrigin(kOrigin)
.SetGeometry(gfx::Rect(100, 100))
.BuildShellSurface();
// Set scale.
constexpr float kScaleFactor = 2.0;
shell_surface->set_client_submits_surfaces_in_pixel_coordinates(true);
shell_surface->SetScaleFactor(kScaleFactor);
auto* root_surface = shell_surface->root_surface();
auto child_buffer = test::ExoTestHelper::CreateBuffer(gfx::Size(200, 200));
auto child_surface = std::make_unique<Surface>();
child_surface->Attach(child_buffer.get());
auto subsurface =
std::make_unique<SubSurface>(child_surface.get(), root_surface);
subsurface->SetPosition(gfx::PointF(-50, -50));
child_surface->Commit();
root_surface->Commit();
EXPECT_EQ(gfx::Rect(-25, -25, 100, 100),
shell_surface->host_window()->bounds());
// Store the current local surface id.
const viz::LocalSurfaceId old_id =
shell_surface->GetSurfaceId().local_surface_id();
// If nothing is changed, no need to update local surface id.
child_surface->Commit();
root_surface->Commit();
EXPECT_EQ(shell_surface->GetSurfaceId().local_surface_id(), old_id);
// If the host window origin is updated, need to update local surface id.
subsurface->SetPosition(gfx::PointF(-25, -25));
child_surface->Commit();
root_surface->Commit();
EXPECT_EQ(gfx::Rect(-12, -12, 100, 100),
shell_surface->host_window()->bounds());
EXPECT_TRUE(
shell_surface->GetSurfaceId().local_surface_id().IsNewerThan(old_id));
EXPECT_EQ(gfx::Vector2dF(-0.5, -0.5),
shell_surface->host_window()->layer()->GetSubpixelOffset());
}
TEST_F(ShellSurfaceTest,
LocalSurfaceIdNotUpdatedOnSurfaceOutOfWindowButClipped) {
constexpr gfx::Point kOrigin(100, 100);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({100, 100})
.SetOrigin(kOrigin)
.SetGeometry(gfx::Rect(100, 100))
.BuildShellSurface();
auto* root_surface = shell_surface->root_surface();
auto child_buffer = test::ExoTestHelper::CreateBuffer(gfx::Size(200, 200));
auto child_surface = std::make_unique<Surface>();
child_surface->Attach(child_buffer.get());
child_surface->set_is_augmented(true);
child_surface->SetClipRect(std::make_optional(gfx::RectF(50, 50, 100, 100)));
auto subsurface =
std::make_unique<SubSurface>(child_surface.get(), root_surface);
subsurface->SetPosition(gfx::PointF(-50, -50));
child_surface->Commit();
root_surface->Commit();
EXPECT_EQ(gfx::Rect(100, 100), shell_surface->host_window()->bounds());
// Store the current local surface id.
const viz::LocalSurfaceId old_id =
shell_surface->GetSurfaceId().local_surface_id();
// If nothing is changed, no need to update local surface id.
child_surface->Commit();
root_surface->Commit();
EXPECT_EQ(shell_surface->GetSurfaceId().local_surface_id(), old_id);
// If the surface is moving around the out of window while it's clipped, we do
// not allocate local surface id.
child_surface->SetClipRect(std::make_optional(gfx::RectF(25, 25, 100, 100)));
subsurface->SetPosition(gfx::PointF(-25, -25));
child_surface->Commit();
root_surface->Commit();
EXPECT_EQ(gfx::Rect(100, 100), shell_surface->host_window()->bounds());
EXPECT_EQ(old_id, shell_surface->GetSurfaceId().local_surface_id());
EXPECT_EQ(gfx::Vector2dF(),
shell_surface->host_window()->layer()->GetSubpixelOffset());
}
TEST_F(ShellSurfaceTest, EventTargetWithNegativeHostWindowOrigin) {
constexpr gfx::Point kOrigin(20, 20);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetOrigin(kOrigin)
.SetGeometry(gfx::Rect(256, 256))
.SetInputRegion(gfx::Rect(256, 256))
.SetFrame(SurfaceFrameType::SHADOW)
.BuildShellSurface();
auto* root_surface = shell_surface->root_surface();
// Add child buffer at the upper-left corner of the root surface with empty
// input region.
auto* child_surface1 = test::ShellSurfaceBuilder::AddChildSurface(
root_surface, gfx::Rect(-10, -10, 32, 32));
child_surface1->SetInputRegion(cc::Region());
// Add child buffer at the bottom-right corner of the root surface with empty
// input region.
auto* child_surface2 = test::ShellSurfaceBuilder::AddChildSurface(
root_surface, gfx::Rect(250, 250, 32, 32));
child_surface2->SetInputRegion(cc::Region());
child_surface1->Commit();
child_surface2->Commit();
root_surface->Commit();
ASSERT_TRUE(shell_surface->GetWidget());
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
aura::Window* root_window = window->GetRootWindow();
ui::EventTargeter* targeter =
root_window->GetHost()->dispatcher()->GetDefaultEventTargeter();
{
// Mouse is in the middle of the root surface.
ui::MouseEvent event(ui::ET_MOUSE_MOVED, gfx::Point(120, 120),
gfx::Point(120, 120), ui::EventTimeForNow(),
ui::EF_NONE, ui::EF_NONE);
EXPECT_EQ(root_surface->window(),
targeter->FindTargetForEvent(root_window, &event));
}
{
// Mouse is on upper-left of the root surface.
ui::MouseEvent event(ui::ET_MOUSE_PRESSED, gfx::Point(21, 21),
gfx::Point(21, 21), ui::EventTimeForNow(), ui::EF_NONE,
ui::EF_NONE);
EXPECT_EQ(root_surface->window(),
targeter->FindTargetForEvent(root_window, &event));
}
{
// Mouse is on bottom-right of the root surface.
ui::MouseEvent event(ui::ET_MOUSE_PRESSED, gfx::Point(275, 275),
gfx::Point(275, 275), ui::EventTimeForNow(),
ui::EF_NONE, ui::EF_NONE);
EXPECT_EQ(root_surface->window(),
targeter->FindTargetForEvent(root_window, &event));
}
{
// Mouse is outside of the root surface and host window.
ui::MouseEvent event(ui::ET_MOUSE_PRESSED, gfx::Point(300, 300),
gfx::Point(300, 300), ui::EventTimeForNow(),
ui::EF_NONE, ui::EF_NONE);
EXPECT_FALSE(window->Contains(static_cast<aura::Window*>(
targeter->FindTargetForEvent(root_window, &event))));
}
{
// Mouse is on the left side of the root surface but inside host window.
ui::MouseEvent event(ui::ET_MOUSE_PRESSED, gfx::Point(19, 100),
gfx::Point(19, 100), ui::EventTimeForNow(),
ui::EF_NONE, ui::EF_NONE);
EXPECT_FALSE(window->Contains(static_cast<aura::Window*>(
targeter->FindTargetForEvent(root_window, &event))));
}
{
// Mouse is on the right side of the root surface but inside host window.
ui::MouseEvent event(ui::ET_MOUSE_PRESSED, gfx::Point(277, 100),
gfx::Point(277, 100), ui::EventTimeForNow(),
ui::EF_NONE, ui::EF_NONE);
EXPECT_FALSE(window->Contains(static_cast<aura::Window*>(
targeter->FindTargetForEvent(root_window, &event))));
}
{
// Mouse is above the root surface but inside host window.
ui::MouseEvent event(ui::ET_MOUSE_PRESSED, gfx::Point(100, 19),
gfx::Point(100, 19), ui::EventTimeForNow(),
ui::EF_NONE, ui::EF_NONE);
EXPECT_FALSE(window->Contains(static_cast<aura::Window*>(
targeter->FindTargetForEvent(root_window, &event))));
}
{
// Mouse is below the root surface but inside host window.
ui::MouseEvent event(ui::ET_MOUSE_PRESSED, gfx::Point(100, 277),
gfx::Point(100, 277), ui::EventTimeForNow(),
ui::EF_NONE, ui::EF_NONE);
EXPECT_FALSE(window->Contains(static_cast<aura::Window*>(
targeter->FindTargetForEvent(root_window, &event))));
}
{
// Mouse is on `child_surface1` but not on the root surface.
ui::MouseEvent event(ui::ET_MOUSE_PRESSED, gfx::Point(19, 19),
gfx::Point(19, 19), ui::EventTimeForNow(), ui::EF_NONE,
ui::EF_NONE);
EXPECT_FALSE(window->Contains(static_cast<aura::Window*>(
targeter->FindTargetForEvent(root_window, &event))));
}
{
// Mouse is on `child_surface2` but not on the root surface.
ui::MouseEvent event(ui::ET_MOUSE_PRESSED, gfx::Point(277, 277),
gfx::Point(277, 277), ui::EventTimeForNow(),
ui::EF_NONE, ui::EF_NONE);
EXPECT_FALSE(window->Contains(static_cast<aura::Window*>(
targeter->FindTargetForEvent(root_window, &event))));
}
}
TEST_F(ShellSurfaceTest,
EventTargetWithNegativeHostWindowOriginWithScaleFactor) {
constexpr gfx::Point kOrigin(20, 20);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetOrigin(kOrigin)
.SetGeometry(gfx::Rect(256, 256))
.SetInputRegion(gfx::Rect(256, 256))
.SetFrame(SurfaceFrameType::SHADOW)
.BuildShellSurface();
// Set scale.
constexpr float kScaleFactor = 2.0;
shell_surface->set_client_submits_surfaces_in_pixel_coordinates(true);
shell_surface->SetScaleFactor(kScaleFactor);
auto* root_surface = shell_surface->root_surface();
// Add child buffer at the upper-left corner of the root surface with empty
// input region.
auto* child_surface1 = test::ShellSurfaceBuilder::AddChildSurface(
root_surface, gfx::Rect(-10, -10, 32, 32));
child_surface1->SetInputRegion(cc::Region());
// Add child buffer at the bottom-right corner of the root surface with empty
// input region.
auto* child_surface2 = test::ShellSurfaceBuilder::AddChildSurface(
root_surface, gfx::Rect(250, 250, 32, 32));
child_surface2->SetInputRegion(cc::Region());
child_surface1->Commit();
child_surface2->Commit();
root_surface->Commit();
ASSERT_TRUE(shell_surface->GetWidget());
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
aura::Window* root_window = window->GetRootWindow();
ui::EventTargeter* targeter =
root_window->GetHost()->dispatcher()->GetDefaultEventTargeter();
{
// Mouse is in the middle of the root surface.
ui::MouseEvent event(ui::ET_MOUSE_MOVED, gfx::Point(80, 80),
gfx::Point(80, 80), ui::EventTimeForNow(), ui::EF_NONE,
ui::EF_NONE);
EXPECT_EQ(root_surface->window(),
targeter->FindTargetForEvent(root_window, &event));
}
{
// Mouse is on upper-left of the root surface.
ui::MouseEvent event(ui::ET_MOUSE_PRESSED, gfx::Point(21, 21),
gfx::Point(21, 21), ui::EventTimeForNow(), ui::EF_NONE,
ui::EF_NONE);
EXPECT_EQ(root_surface->window(),
targeter->FindTargetForEvent(root_window, &event));
}
{
// Mouse is on bottom-right of the root surface.
ui::MouseEvent event(ui::ET_MOUSE_PRESSED, gfx::Point(147, 147),
gfx::Point(147, 147), ui::EventTimeForNow(),
ui::EF_NONE, ui::EF_NONE);
EXPECT_EQ(root_surface->window(),
targeter->FindTargetForEvent(root_window, &event));
}
{
// Mouse is outside of the root surface and host window.
ui::MouseEvent event(ui::ET_MOUSE_PRESSED, gfx::Point(200, 200),
gfx::Point(200, 200), ui::EventTimeForNow(),
ui::EF_NONE, ui::EF_NONE);
EXPECT_FALSE(window->Contains(static_cast<aura::Window*>(
targeter->FindTargetForEvent(root_window, &event))));
}
{
// Mouse is on the left side of the root surface but inside host window.
ui::MouseEvent event(ui::ET_MOUSE_PRESSED, gfx::Point(19, 100),
gfx::Point(19, 100), ui::EventTimeForNow(),
ui::EF_NONE, ui::EF_NONE);
EXPECT_FALSE(window->Contains(static_cast<aura::Window*>(
targeter->FindTargetForEvent(root_window, &event))));
}
{
// Mouse is on the right side of the root surface but inside host window.
ui::MouseEvent event(ui::ET_MOUSE_PRESSED, gfx::Point(149, 100),
gfx::Point(149, 100), ui::EventTimeForNow(),
ui::EF_NONE, ui::EF_NONE);
EXPECT_FALSE(window->Contains(static_cast<aura::Window*>(
targeter->FindTargetForEvent(root_window, &event))));
}
{
// Mouse is above the root surface but inside host window.
ui::MouseEvent event(ui::ET_MOUSE_PRESSED, gfx::Point(100, 19),
gfx::Point(100, 19), ui::EventTimeForNow(),
ui::EF_NONE, ui::EF_NONE);
EXPECT_FALSE(window->Contains(static_cast<aura::Window*>(
targeter->FindTargetForEvent(root_window, &event))));
}
{
// Mouse is below the root surface but inside host window.
ui::MouseEvent event(ui::ET_MOUSE_PRESSED, gfx::Point(100, 149),
gfx::Point(100, 149), ui::EventTimeForNow(),
ui::EF_NONE, ui::EF_NONE);
EXPECT_FALSE(window->Contains(static_cast<aura::Window*>(
targeter->FindTargetForEvent(root_window, &event))));
}
{
// Mouse is on `child_surface1` but not on the root surface.
ui::MouseEvent event(ui::ET_MOUSE_PRESSED, gfx::Point(19, 19),
gfx::Point(19, 19), ui::EventTimeForNow(), ui::EF_NONE,
ui::EF_NONE);
EXPECT_FALSE(window->Contains(static_cast<aura::Window*>(
targeter->FindTargetForEvent(root_window, &event))));
}
{
// Mouse is on `child_surface2` but not on the root surface.
ui::MouseEvent event(ui::ET_MOUSE_PRESSED, gfx::Point(149, 149),
gfx::Point(149, 149), ui::EventTimeForNow(),
ui::EF_NONE, ui::EF_NONE);
EXPECT_FALSE(window->Contains(static_cast<aura::Window*>(
targeter->FindTargetForEvent(root_window, &event))));
}
}
TEST_F(ShellSurfaceTest, SetFullscreen) {
auto shell_surface =
test::ShellSurfaceBuilder({256, 256}).SetNoCommit().BuildShellSurface();
auto* surface = shell_surface->root_surface();
shell_surface->SetFullscreen(true, display::kInvalidDisplayId);
surface->Commit();
EXPECT_FALSE(HasBackdrop());
EXPECT_EQ(GetContext()->bounds().ToString(),
shell_surface->GetWidget()->GetWindowBoundsInScreen().ToString());
shell_surface->SetFullscreen(false, display::kInvalidDisplayId);
surface->Commit();
EXPECT_FALSE(HasBackdrop());
EXPECT_NE(GetContext()->bounds().ToString(),
shell_surface->GetWidget()->GetWindowBoundsInScreen().ToString());
}
TEST_F(ShellSurfaceTest, PreWidgetUnfullscreen) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256}).SetNoCommit().BuildShellSurface();
shell_surface->Maximize();
shell_surface->SetFullscreen(false, display::kInvalidDisplayId);
EXPECT_EQ(shell_surface->GetWidget(), nullptr);
shell_surface->root_surface()->Commit();
EXPECT_TRUE(shell_surface->GetWidget()->IsMaximized());
}
TEST_F(ShellSurfaceTest, PreWidgetMaximizeFromFullscreen) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetNoCommit()
.SetMaximumSize(gfx::Size(10, 10))
.BuildShellSurface();
// Fullscreen -> Maximize for non Lacros surfaces should stay fullscreen
shell_surface->SetFullscreen(true, display::kInvalidDisplayId);
shell_surface->Maximize();
EXPECT_EQ(shell_surface->GetWidget(), nullptr);
shell_surface->root_surface()->Commit();
EXPECT_TRUE(shell_surface->GetWidget()->IsFullscreen());
}
TEST_F(ShellSurfaceTest, SetTitle) {
auto shell_surface =
test::ShellSurfaceBuilder({256, 256}).BuildShellSurface();
shell_surface->SetTitle(std::u16string(u"test"));
// NativeWindow's title is used within the overview mode, so it should
// have the specified title.
EXPECT_EQ(u"test", shell_surface->GetWidget()->GetNativeWindow()->GetTitle());
// The titlebar shouldn't show the title.
EXPECT_FALSE(
shell_surface->GetWidget()->widget_delegate()->ShouldShowWindowTitle());
}
TEST_F(ShellSurfaceTest, SetApplicationId) {
auto shell_surface =
test::ShellSurfaceBuilder({64, 64}).SetNoCommit().BuildShellSurface();
auto* surface = shell_surface->root_surface();
EXPECT_FALSE(shell_surface->GetWidget());
shell_surface->SetApplicationId("pre-widget-id");
surface->Commit();
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
EXPECT_EQ("pre-widget-id", *GetShellApplicationId(window));
shell_surface->SetApplicationId("test");
EXPECT_EQ("test", *GetShellApplicationId(window));
EXPECT_FALSE(ash::WindowState::Get(window)->allow_set_bounds_direct());
shell_surface->SetApplicationId(nullptr);
EXPECT_EQ(nullptr, GetShellApplicationId(window));
}
TEST_F(ShellSurfaceTest, ActivationPermissionLegacy) {
auto shell_surface = test::ShellSurfaceBuilder({64, 64}).BuildShellSurface();
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
ASSERT_TRUE(window);
// No permission granted so can't activate.
EXPECT_FALSE(HasPermissionToActivate(window));
// Can grant permission.
GrantPermissionToActivate(window, base::Days(1));
exo::Permission* permission = window->GetProperty(kPermissionKey);
EXPECT_TRUE(permission->Check(Permission::Capability::kActivate));
EXPECT_TRUE(HasPermissionToActivate(window));
// Can revoke permission.
RevokePermissionToActivate(window);
EXPECT_FALSE(HasPermissionToActivate(window));
// Can grant permission again.
GrantPermissionToActivate(window, base::Days(2));
exo::Permission* permission2 = window->GetProperty(kPermissionKey);
EXPECT_TRUE(permission2->Check(Permission::Capability::kActivate));
EXPECT_TRUE(HasPermissionToActivate(window));
}
TEST_F(ShellSurfaceTest, WidgetActivationLegacy) {
constexpr gfx::Size kBufferSize(64, 64);
auto security_delegate = std::make_unique<test::TestSecurityDelegate>();
auto shell_surface1 = test::ShellSurfaceBuilder(kBufferSize)
.SetSecurityDelegate(security_delegate.get())
.BuildShellSurface();
auto* surface1 = shell_surface1->root_surface();
// The window is active.
views::Widget* widget1 = shell_surface1->GetWidget();
EXPECT_TRUE(widget1->IsActive());
// Create a second window.
auto shell_surface2 = test::ShellSurfaceBuilder(kBufferSize)
.SetSecurityDelegate(security_delegate.get())
.BuildShellSurface();
auto* surface2 = shell_surface2->root_surface();
// Now the second window is active.
views::Widget* widget2 = shell_surface2->GetWidget();
EXPECT_FALSE(widget1->IsActive());
EXPECT_TRUE(widget2->IsActive());
// Grant permission to activate the first window.
GrantPermissionToActivate(widget1->GetNativeWindow(), base::Days(1));
// The first window can activate itself.
surface1->RequestActivation();
EXPECT_TRUE(widget1->IsActive());
EXPECT_FALSE(widget2->IsActive());
// The second window cannot activate itself.
surface2->RequestActivation();
EXPECT_TRUE(widget1->IsActive());
EXPECT_FALSE(widget2->IsActive());
}
TEST_F(ShellSurfaceTest, WidgetActivation) {
test::MockSecurityDelegate security_delegate;
constexpr gfx::Size kBufferSize(64, 64);
std::unique_ptr<ShellSurface> shell_surface1 =
test::ShellSurfaceBuilder(kBufferSize)
.SetSecurityDelegate(&security_delegate)
.BuildShellSurface();
// The window is active.
views::Widget* widget1 = shell_surface1->GetWidget();
EXPECT_TRUE(widget1->IsActive());
// Create a second window.
std::unique_ptr<ShellSurface> shell_surface2 =
test::ShellSurfaceBuilder(kBufferSize)
.SetSecurityDelegate(&security_delegate)
.BuildShellSurface();
// Now the second window is active.
views::Widget* widget2 = shell_surface2->GetWidget();
EXPECT_FALSE(widget1->IsActive());
EXPECT_TRUE(widget2->IsActive());
// The first window can activate itself.
EXPECT_CALL(security_delegate, CanSelfActivate(widget1->GetNativeWindow()))
.WillOnce(testing::Return(true));
shell_surface1->surface_for_testing()->RequestActivation();
EXPECT_TRUE(widget1->IsActive());
EXPECT_FALSE(widget2->IsActive());
// The second window cannot activate itself.
EXPECT_CALL(security_delegate, CanSelfActivate(widget2->GetNativeWindow()))
.WillOnce(testing::Return(false));
shell_surface2->surface_for_testing()->RequestActivation();
EXPECT_TRUE(widget1->IsActive());
EXPECT_FALSE(widget2->IsActive());
}
TEST_F(ShellSurfaceTest, EmulateOverrideRedirect) {
constexpr gfx::Size kBufferSize(64, 64);
auto buffer = test::ExoTestHelper::CreateBuffer(kBufferSize);
std::unique_ptr<Surface> surface(new Surface);
std::unique_ptr<ShellSurface> shell_surface(new ShellSurface(surface.get()));
EXPECT_FALSE(shell_surface->GetWidget());
surface->Attach(buffer.get());
surface->Commit();
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
EXPECT_FALSE(ash::WindowState::Get(window)->allow_set_bounds_direct());
// Only surface with no app id with parent surface is considered
// override redirect.
std::unique_ptr<Surface> child_surface(new Surface);
std::unique_ptr<ShellSurface> child_shell_surface(
new ShellSurface(child_surface.get()));
child_surface->SetParent(surface.get(), gfx::Point());
child_surface->Attach(buffer.get());
child_surface->Commit();
aura::Window* child_window =
child_shell_surface->GetWidget()->GetNativeWindow();
// The window will not have a window state, thus will no be managed by window
// manager.
EXPECT_TRUE(ash::WindowState::Get(child_window)->allow_set_bounds_direct());
EXPECT_EQ(ash::kShellWindowId_ShelfBubbleContainer,
child_window->parent()->GetId());
// NONE/SHADOW frame type should work on override redirect.
child_surface->SetFrame(SurfaceFrameType::SHADOW);
child_surface->Commit();
EXPECT_EQ(wm::kShadowElevationMenuOrTooltip,
wm::GetShadowElevationConvertDefault(child_window));
child_surface->SetFrame(SurfaceFrameType::NONE);
child_surface->Commit();
EXPECT_EQ(wm::kShadowElevationNone,
wm::GetShadowElevationConvertDefault(child_window));
}
TEST_F(ShellSurfaceTest, SetStartupId) {
auto shell_surface =
test::ShellSurfaceBuilder({64, 64}).SetNoCommit().BuildShellSurface();
auto* surface = shell_surface->root_surface();
EXPECT_FALSE(shell_surface->GetWidget());
shell_surface->SetStartupId("pre-widget-id");
surface->Commit();
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
EXPECT_EQ("pre-widget-id", *GetShellStartupId(window));
shell_surface->SetStartupId("test");
EXPECT_EQ("test", *GetShellStartupId(window));
shell_surface->SetStartupId(nullptr);
EXPECT_EQ(nullptr, GetShellStartupId(window));
}
TEST_F(ShellSurfaceTest, AckRotateFocus) {
std::unique_ptr<ShellSurface> surface1 =
test::ShellSurfaceBuilder({256, 256}).BuildShellSurface();
uint32_t serial = 0;
auto dummy_cb = base::BindLambdaForTesting(
[&serial](ash::FocusCycler::Direction, bool) { return serial; });
views::View* v1 = new views::View();
v1->SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
surface1->AddChildView(v1);
surface1->set_rotate_focus_callback(dummy_cb);
std::unique_ptr<ShellSurface> surface2 =
test::ShellSurfaceBuilder({256, 256}).BuildShellSurface();
views::View* v2 = new views::View();
v2->SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
surface2->AddChildView(v2);
surface2->set_rotate_focus_callback(dummy_cb);
std::unique_ptr<ShellSurface> surface3 =
test::ShellSurfaceBuilder({256, 256}).BuildShellSurface();
views::View* v3 = new views::View();
v3->SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
surface3->AddChildView(v3);
surface3->set_rotate_focus_callback(dummy_cb);
ash::Shell::Get()->focus_cycler()->AddWidget(surface1->GetWidget());
ash::Shell::Get()->focus_cycler()->AddWidget(surface2->GetWidget());
ash::Shell::Get()->focus_cycler()->AddWidget(surface3->GetWidget());
// We will do most of our testing with surface2 because it is in the middle.
// This will allow us to easily test directional logic.
ash::Shell::Get()->focus_cycler()->FocusWidget(surface2->GetWidget());
ASSERT_TRUE(surface2->GetWidget()->IsActive());
// Test handled. This should result in no rotation.
surface2->RotatePaneFocusFromView(v2, true, false);
surface2->AckRotateFocus(serial++, true);
ASSERT_TRUE(surface2->GetWidget()->IsActive());
surface2->RotatePaneFocusFromView(v2, true, false);
surface2->AckRotateFocus(serial++, true);
ASSERT_TRUE(surface2->GetWidget()->IsActive());
// Now test unhandled in the forward direction. The next widget should be
// focused.
surface2->RotatePaneFocusFromView(v2, true, false);
surface2->AckRotateFocus(serial++, false);
ASSERT_TRUE(surface3->GetWidget()->IsActive());
// Reset
ash::Shell::Get()->focus_cycler()->FocusWidget(surface2->GetWidget());
ASSERT_TRUE(surface2->GetWidget()->IsActive());
// Now test unhandled in the forward direction. The next widget should be
// focused.
surface2->RotatePaneFocusFromView(v2, false, false);
surface2->AckRotateFocus(serial++, false);
ASSERT_TRUE(surface1->GetWidget()->IsActive());
}
TEST_F(ShellSurfaceTest, RotatePaneFocusFromView) {
using ::testing::Return;
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256}).BuildShellSurface();
base::MockRepeatingCallback<uint32_t(ash::FocusCycler::Direction, bool)> cb;
shell_surface->set_rotate_focus_callback(cb.Get());
auto serial = 0;
EXPECT_CALL(cb, Run(ash::FocusCycler::FORWARD, true))
.WillOnce(Return(serial++));
auto rotated = shell_surface->RotatePaneFocusFromView(nullptr, true, true);
// Async operations always return successful rotation immediately.
EXPECT_TRUE(rotated);
EXPECT_CALL(cb, Run(ash::FocusCycler::BACKWARD, false))
.WillOnce(Return(serial++));
rotated = shell_surface->RotatePaneFocusFromView(nullptr, false, false);
EXPECT_TRUE(rotated);
}
TEST_F(ShellSurfaceTest, RotatePaneFocusFromView_NoCallback) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256}).BuildShellSurface();
auto rotated = shell_surface->RotatePaneFocusFromView(nullptr, true, true);
// No focusable view for the shell surface. This should result in a
// non-rotation using the base rotation logic.
EXPECT_FALSE(rotated);
}
TEST_F(ShellSurfaceTest, StartMove) {
auto shell_surface = test::ShellSurfaceBuilder({64, 64}).BuildShellSurface();
ASSERT_TRUE(shell_surface->GetWidget());
aura::Env::GetInstance()->set_mouse_button_flags(ui::EF_LEFT_MOUSE_BUTTON);
// The interactive move should end when surface is destroyed.
ASSERT_TRUE(shell_surface->StartMove());
// Test that destroying the shell surface before move ends is OK.
shell_surface.reset();
}
TEST_F(ShellSurfaceTest, StartResize) {
constexpr gfx::Size kBufferSize(64, 64);
auto buffer = test::ExoTestHelper::CreateBuffer(kBufferSize);
std::unique_ptr<Surface> surface(new Surface);
std::unique_ptr<ShellSurface> shell_surface(new ShellSurface(surface.get()));
// Map shell surface.
surface->Attach(buffer.get());
surface->Commit();
ASSERT_TRUE(shell_surface->GetWidget());
aura::Env::GetInstance()->set_mouse_button_flags(ui::EF_LEFT_MOUSE_BUTTON);
// The interactive resize should end when surface is destroyed.
ASSERT_TRUE(shell_surface->StartResize(HTBOTTOMRIGHT));
// Test that destroying the surface before resize ends is OK.
surface.reset();
}
TEST_F(ShellSurfaceTest, StartResizeAndDestroyShell) {
auto shell_surface =
test::ShellSurfaceBuilder({64, 64}).SetNoCommit().BuildShellSurface();
auto* surface = shell_surface->root_surface();
uint32_t serial = 0;
auto configure_callback = base::BindRepeating(
[](uint32_t* const serial_ptr, const gfx::Rect& bounds,
chromeos::WindowStateType state_type, bool resizing, bool activated,
const gfx::Vector2d& origin_offset, float raster_scale,
aura::Window::OcclusionState occlusion_state,
std::optional<chromeos::WindowStateType>) { return ++(*serial_ptr); },
&serial);
// Map shell surface.
shell_surface->set_configure_callback(configure_callback);
surface->Commit();
ASSERT_TRUE(shell_surface->GetWidget());
aura::Env::GetInstance()->set_mouse_button_flags(ui::EF_LEFT_MOUSE_BUTTON);
// The interactive resize should end when surface is destroyed.
ASSERT_TRUE(shell_surface->StartResize(HTBOTTOMRIGHT));
// Go through configure/commit stage to update the resize component.
shell_surface->AcknowledgeConfigure(serial);
surface->Commit();
shell_surface->set_configure_callback(base::BindRepeating(
[](const gfx::Rect& bounds, chromeos::WindowStateType state_type,
bool resizing, bool activated, const gfx::Vector2d& origin_offset,
float raster_scale, aura::Window::OcclusionState occlusion_state,
std::optional<chromeos::WindowStateType>) {
ADD_FAILURE() << "Configure Should not be called";
return uint32_t{0};
}));
// Test that destroying the surface before resize ends is OK.
shell_surface.reset();
}
TEST_F(ShellSurfaceTest, SetGeometry) {
constexpr gfx::Size kBufferSize(64, 64);
gfx::Rect geometry(16, 16, 32, 32);
auto shell_surface = test::ShellSurfaceBuilder(kBufferSize)
.SetGeometry(geometry)
.BuildShellSurface();
EXPECT_EQ(
geometry.size().ToString(),
shell_surface->GetWidget()->GetWindowBoundsInScreen().size().ToString());
EXPECT_EQ(gfx::Rect(gfx::Point() - geometry.OffsetFromOrigin(), kBufferSize)
.ToString(),
shell_surface->host_window()->bounds().ToString());
}
TEST_F(ShellSurfaceTest, SetMinimumSize) {
constexpr gfx::Size kBufferSize(64, 64);
auto shell_surface = test::ShellSurfaceBuilder(kBufferSize)
.SetFrameColors(SK_ColorWHITE, SK_ColorWHITE)
.BuildShellSurface();
auto* surface = shell_surface->root_surface();
constexpr gfx::Size kSizes[] = {{50, 50}, {100, 50}};
for (const gfx::Size size : kSizes) {
SCOPED_TRACE(
base::StringPrintf("MinSize=%dx%d", size.width(), size.height()));
ConfigureData config_data;
shell_surface->set_configure_callback(
base::BindRepeating(&Configure, base::Unretained(&config_data)));
shell_surface->SetMinimumSize(size);
surface->Commit();
EXPECT_EQ(size, shell_surface->GetMinimumSize());
EXPECT_EQ(size, shell_surface->GetWidget()->GetMinimumSize());
EXPECT_EQ(size, shell_surface->GetWidget()
->GetNativeWindow()
->delegate()
->GetMinimumSize());
gfx::Size expected_size(kBufferSize);
expected_size.set_width(std::max(kBufferSize.width(), size.width()));
EXPECT_EQ(expected_size,
shell_surface->GetWidget()->GetWindowBoundsInScreen().size());
if (kBufferSize.width() > size.width()) {
EXPECT_TRUE(config_data.suggested_bounds.IsEmpty());
} else {
EXPECT_EQ(expected_size, config_data.suggested_bounds.size());
}
}
// Reset configure callback because config_data is out of scope.
shell_surface->set_configure_callback(base::NullCallback());
// With frame.
surface->SetFrame(SurfaceFrameType::NORMAL);
for (const gfx::Size size : kSizes) {
SCOPED_TRACE(base::StringPrintf("MinSize=%dx%d with frame", size.width(),
size.height()));
ConfigureData config_data;
shell_surface->set_configure_callback(
base::BindRepeating(&Configure, base::Unretained(&config_data)));
const gfx::Size size_with_frame(size.width(), size.height() + 32);
shell_surface->SetMinimumSize(size);
surface->Commit();
EXPECT_EQ(size, shell_surface->GetMinimumSize());
EXPECT_EQ(size_with_frame, shell_surface->GetWidget()->GetMinimumSize());
EXPECT_EQ(size_with_frame, shell_surface->GetWidget()
->GetNativeWindow()
->delegate()
->GetMinimumSize());
gfx::Size expected_size(kBufferSize);
expected_size.set_width(std::max(kBufferSize.width(), size.width()));
if (kBufferSize.width() > size.width()) {
EXPECT_TRUE(config_data.suggested_bounds.IsEmpty());
} else {
EXPECT_EQ(expected_size, config_data.suggested_bounds.size());
}
shell_surface->set_configure_callback(base::NullCallback());
}
}
TEST_F(ShellSurfaceTest, SetMinimumSizeTooLargeAndTranform) {
auto* screen = display::Screen::GetScreen();
auto fullscreen_bounds = screen->GetPrimaryDisplay().bounds();
auto work_area_bounds = screen->GetPrimaryDisplay().work_area();
auto shell_surface = test::ShellSurfaceBuilder({64, 64})
.SetMinimumSize(fullscreen_bounds.size())
.SetMaximumSize(fullscreen_bounds.size())
.SetBounds(work_area_bounds)
.BuildShellSurface();
auto* surface = shell_surface->root_surface();
auto* widget = shell_surface->GetWidget();
EXPECT_EQ(work_area_bounds, widget->GetWindowBoundsInScreen());
widget->GetNativeWindow()->SetTransform(
gfx::Transform::Affine(1, 1, 1, 1, 10, 10));
// Updating the buffer with expected (work area) size should not
// update the widget's bounds even when the transform is applied.
auto buffer = test::ExoTestHelper::CreateBuffer(work_area_bounds.size());
surface->Attach(buffer.get());
surface->Commit();
widget->GetNativeWindow()->SetTransform(gfx::Transform());
EXPECT_EQ(work_area_bounds, widget->GetWindowBoundsInScreen());
}
TEST_F(ShellSurfaceTest, SetMaximumSize) {
constexpr gfx::Size kBufferSize(256, 256);
auto shell_surface =
test::ShellSurfaceBuilder(kBufferSize).BuildShellSurface();
auto* surface = shell_surface->root_surface();
constexpr gfx::Size kSizes[] = {{300, 300}, {200, 300}};
for (const gfx::Size size : kSizes) {
SCOPED_TRACE(
base::StringPrintf("MaxSize=%dx%d", size.width(), size.height()));
ConfigureData config_data;
shell_surface->set_configure_callback(
base::BindRepeating(&Configure, base::Unretained(&config_data)));
shell_surface->SetMaximumSize(size);
surface->Commit();
EXPECT_EQ(size, shell_surface->GetMaximumSize());
gfx::Size expected_size(kBufferSize);
expected_size.set_width(std::min(size.width(), kBufferSize.width()));
EXPECT_EQ(expected_size,
shell_surface->GetWidget()->GetWindowBoundsInScreen().size());
if (kBufferSize.width() < size.width()) {
EXPECT_TRUE(config_data.suggested_bounds.IsEmpty());
} else {
EXPECT_EQ(expected_size, config_data.suggested_bounds.size());
}
}
}
void PreClose(int* pre_close_count, int* close_count) {
EXPECT_EQ(*pre_close_count, *close_count);
(*pre_close_count)++;
}
void Close(int* pre_close_count, int* close_count) {
(*close_count)++;
EXPECT_EQ(*pre_close_count, *close_count);
}
TEST_F(ShellSurfaceTest, CloseCallback) {
auto shell_surface =
test::ShellSurfaceBuilder({64, 64}).SetNoCommit().BuildShellSurface();
auto* surface = shell_surface->root_surface();
int pre_close_call_count = 0;
int close_call_count = 0;
shell_surface->set_pre_close_callback(
base::BindRepeating(&PreClose, base::Unretained(&pre_close_call_count),
base::Unretained(&close_call_count)));
shell_surface->set_close_callback(
base::BindRepeating(&Close, base::Unretained(&pre_close_call_count),
base::Unretained(&close_call_count)));
surface->Commit();
EXPECT_EQ(0, pre_close_call_count);
EXPECT_EQ(0, close_call_count);
shell_surface->GetWidget()->Close();
EXPECT_EQ(1, pre_close_call_count);
EXPECT_EQ(1, close_call_count);
}
void DestroyShellSurface(std::unique_ptr<ShellSurface>* shell_surface) {
shell_surface->reset();
}
TEST_F(ShellSurfaceTest, SurfaceDestroyedCallback) {
std::unique_ptr<Surface> surface(new Surface);
std::unique_ptr<ShellSurface> shell_surface(new ShellSurface(surface.get()));
shell_surface->set_surface_destroyed_callback(
base::BindOnce(&DestroyShellSurface, base::Unretained(&shell_surface)));
surface->Commit();
EXPECT_TRUE(shell_surface.get());
surface.reset();
EXPECT_FALSE(shell_surface.get());
}
TEST_F(ShellSurfaceTest, ConfigureCallbackSendsRestoreState) {
ConfigureData config_data;
auto shell_surface = test::ShellSurfaceBuilder({256, 256})
.SetMaximumSize(gfx::Size(10, 10))
.BuildShellSurface();
shell_surface->set_configure_callback(
base::BindRepeating(&Configure, base::Unretained(&config_data)));
shell_surface->root_surface()->Commit();
shell_surface->Maximize();
shell_surface->root_surface()->Commit();
shell_surface->SetFullscreen(true, display::kInvalidDisplayId);
shell_surface->root_surface()->Commit();
EXPECT_EQ(chromeos::WindowStateType::kFullscreen, config_data.state_type);
EXPECT_EQ(chromeos::WindowStateType::kMaximized,
config_data.restore_state_type.value());
}
TEST_F(ShellSurfaceTest, ConfigureCallback) {
// Must be before shell_surface so it outlives it, for shell_surface's
// destructor calls Configure() referencing these 4 variables.
ConfigureData config_data;
auto shell_surface =
test::ShellSurfaceBuilder().SetNoCommit().BuildShellSurface();
auto* surface = shell_surface->root_surface();
shell_surface->set_configure_callback(
base::BindRepeating(&Configure, base::Unretained(&config_data)));
gfx::Rect geometry(16, 16, 32, 32);
shell_surface->SetGeometry(geometry);
// Commit without contents should result in a configure callback with empty
// suggested size as a mechanisms to ask the client size itself.
surface->Commit();
ASSERT_TRUE(config_data.suggested_bounds.IsEmpty());
EXPECT_TRUE(shell_surface->GetWidget());
EXPECT_FALSE(shell_surface->GetWidget()->IsVisible());
EXPECT_EQ(geometry.size(), shell_surface->CalculatePreferredSize());
gfx::Rect maximized_bounds =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
// State change should be sent even if the content is not attached.
// See crbug.com/1138978.
shell_surface->Maximize();
shell_surface->AcknowledgeConfigure(0);
EXPECT_FALSE(config_data.suggested_bounds.IsEmpty());
EXPECT_EQ(maximized_bounds.size(), config_data.suggested_bounds.size());
EXPECT_EQ(chromeos::WindowStateType::kMaximized, config_data.state_type);
constexpr gfx::Size kBufferSize(64, 64);
auto buffer = test::ExoTestHelper::CreateBuffer(kBufferSize);
surface->Attach(buffer.get());
surface->Commit();
EXPECT_TRUE(shell_surface->GetWidget());
EXPECT_EQ(maximized_bounds.size(), config_data.suggested_bounds.size());
EXPECT_EQ(chromeos::WindowStateType::kMaximized, config_data.state_type);
shell_surface->Restore();
shell_surface->AcknowledgeConfigure(0);
// It should be restored to the original geometry size.
EXPECT_EQ(geometry.size(), shell_surface->CalculatePreferredSize());
shell_surface->SetFullscreen(true, display::kInvalidDisplayId);
shell_surface->AcknowledgeConfigure(0);
EXPECT_EQ(GetContext()->bounds().size(), config_data.suggested_bounds.size());
EXPECT_EQ(chromeos::WindowStateType::kFullscreen, config_data.state_type);
shell_surface->SetFullscreen(false, display::kInvalidDisplayId);
shell_surface->AcknowledgeConfigure(0);
EXPECT_EQ(geometry.size(), shell_surface->CalculatePreferredSize());
shell_surface->GetWidget()->Activate();
shell_surface->AcknowledgeConfigure(0);
EXPECT_TRUE(config_data.is_active);
shell_surface->GetWidget()->Deactivate();
shell_surface->AcknowledgeConfigure(0);
EXPECT_FALSE(config_data.is_active);
EXPECT_FALSE(config_data.is_resizing);
aura::Env::GetInstance()->set_mouse_button_flags(ui::EF_LEFT_MOUSE_BUTTON);
ASSERT_TRUE(shell_surface->StartResize(HTBOTTOMRIGHT));
shell_surface->AcknowledgeConfigure(0);
EXPECT_TRUE(config_data.is_resizing);
}
TEST_F(ShellSurfaceTest, CreateMinimizedWindow) {
// Must be before shell_surface so it outlives it, for shell_surface's
// destructor calls Configure() referencing these 4 variables.
ConfigureData config_data;
auto shell_surface =
test::ShellSurfaceBuilder().SetNoCommit().BuildShellSurface();
auto* surface = shell_surface->root_surface();
shell_surface->set_configure_callback(
base::BindRepeating(&Configure, base::Unretained(&config_data)));
gfx::Rect geometry(0, 0, 1, 1);
shell_surface->SetGeometry(geometry);
shell_surface->Minimize();
shell_surface->AcknowledgeConfigure(0);
// Commit without contents should result in a configure callback with empty
// suggested size as a mechanisms to ask the client size itself.
surface->Commit();
EXPECT_TRUE(shell_surface->GetWidget());
EXPECT_TRUE(shell_surface->GetWidget()->IsMinimized());
EXPECT_TRUE(config_data.suggested_bounds.IsEmpty());
EXPECT_EQ(geometry.size(), shell_surface->CalculatePreferredSize());
}
TEST_F(ShellSurfaceTest, CreateMinimizedWindow2) {
// Must be before shell_surface so it outlives it, for shell_surface's
// destructor calls Configure() referencing these 4 variables.
ConfigureData config_data;
auto shell_surface =
test::ShellSurfaceBuilder().SetNoCommit().BuildShellSurface();
auto* surface = shell_surface->root_surface();
shell_surface->set_configure_callback(
base::BindRepeating(&Configure, base::Unretained(&config_data)));
gfx::Rect geometry(0, 0, 1, 1);
shell_surface->SetGeometry(geometry);
// Commit without contents should result in a configure callback with empty
// suggested size as a mechanisms to ask the client size itself.
surface->Commit();
EXPECT_TRUE(config_data.suggested_bounds.IsEmpty());
EXPECT_TRUE(shell_surface->GetWidget());
EXPECT_FALSE(shell_surface->GetWidget()->IsVisible());
EXPECT_EQ(geometry.size(), shell_surface->CalculatePreferredSize());
shell_surface->Minimize();
shell_surface->AcknowledgeConfigure(0);
// Commit without contents should result in a configure callback with empty
// suggested size as a mechanisms to ask the client size itself.
surface->Commit();
EXPECT_TRUE(shell_surface->GetWidget());
EXPECT_TRUE(shell_surface->GetWidget()->IsMinimized());
EXPECT_EQ(geometry.size(), shell_surface->CalculatePreferredSize());
// Once the initial empty size is sent in configure,
// new configure should send the size requested.
EXPECT_EQ(geometry.size(), config_data.suggested_bounds.size());
}
TEST_F(ShellSurfaceTest,
CreateMaximizedWindowWithRestoreBoundsWithoutInitialBuffer) {
// Must be before shell_surface so it outlives it, for shell_surface's
// destructor calls Configure() referencing these 4 variables.
ConfigureData config_data;
constexpr gfx::Size kBufferSize(256, 256);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder(kBufferSize)
.SetNoRootBuffer()
.BuildShellSurface();
shell_surface->set_configure_callback(
base::BindRepeating(&Configure, base::Unretained(&config_data)));
Surface* root_surface = shell_surface->surface_for_testing();
root_surface->Commit();
shell_surface->AcknowledgeConfigure(0);
// Ash may maximize the window.
EXPECT_TRUE(shell_surface->GetWidget());
EXPECT_FALSE(shell_surface->GetWidget()->IsVisible());
EXPECT_FALSE(shell_surface->GetWidget()->IsMaximized());
shell_surface->Maximize();
shell_surface->AcknowledgeConfigure(0);
EXPECT_FALSE(shell_surface->GetWidget()->IsVisible());
EXPECT_TRUE(shell_surface->GetWidget()->IsMaximized());
auto buffer = test::ExoTestHelper::CreateBuffer(kBufferSize);
root_surface->Attach(buffer.get());
gfx::Rect geometry_full(kBufferSize);
shell_surface->SetGeometry(geometry_full);
// Commit without contents should result in a configure callback with empty
// suggested size as a mechanisms to ask the client size itself.
root_surface->Commit();
shell_surface->AcknowledgeConfigure(0);
EXPECT_TRUE(shell_surface->GetWidget()->IsVisible());
EXPECT_TRUE(shell_surface->GetWidget()->IsMaximized());
auto* window_state =
ash::WindowState::Get(shell_surface->GetWidget()->GetNativeWindow());
EXPECT_TRUE(window_state->HasRestoreBounds());
auto bounds = window_state->GetRestoreBoundsInParent();
EXPECT_EQ(geometry_full.size(), bounds.size());
}
TEST_F(ShellSurfaceTest, CreateMaximizedWindowWithRestoreBounds) {
// Must be before shell_surface so it outlives it, for shell_surface's
// destructor calls Configure() referencing these 4 variables.
ConfigureData config_data;
constexpr gfx::Size kBufferSize(256, 256);
auto buffer = test::ExoTestHelper::CreateBuffer(kBufferSize);
auto shell_surface =
test::ShellSurfaceBuilder().SetNoCommit().BuildShellSurface();
auto* surface = shell_surface->root_surface();
shell_surface->set_configure_callback(
base::BindRepeating(&Configure, base::Unretained(&config_data)));
gfx::Rect geometry(0, 0, 1, 1);
shell_surface->SetGeometry(geometry);
shell_surface->Maximize();
// Commit without contents should result in a configure callback with empty
// suggested size as a mechanisms to ask the client size itself.
surface->Attach(buffer.get());
surface->Commit();
shell_surface->AcknowledgeConfigure(0);
gfx::Rect geometry_full(0, 0, 100, 100);
shell_surface->SetGeometry(geometry_full);
surface->Commit();
shell_surface->AcknowledgeConfigure(1);
EXPECT_TRUE(shell_surface->GetWidget());
EXPECT_TRUE(shell_surface->GetWidget()->IsMaximized());
EXPECT_EQ(geometry_full.size(), shell_surface->CalculatePreferredSize());
auto* window_state =
ash::WindowState::Get(shell_surface->GetWidget()->GetNativeWindow());
EXPECT_TRUE(window_state->HasRestoreBounds());
auto bounds = window_state->GetRestoreBoundsInParent();
EXPECT_EQ(geometry.width(), bounds.width());
EXPECT_EQ(geometry.height(), bounds.height());
}
TEST_F(ShellSurfaceTest, ToggleFullscreen) {
constexpr gfx::Size kBufferSize(256, 256);
auto shell_surface =
test::ShellSurfaceBuilder(kBufferSize).BuildShellSurface();
EXPECT_FALSE(HasBackdrop());
EXPECT_EQ(kBufferSize,
shell_surface->GetWidget()->GetWindowBoundsInScreen().size());
shell_surface->Maximize();
EXPECT_FALSE(HasBackdrop());
EXPECT_EQ(GetContext()->bounds().width(),
shell_surface->GetWidget()->GetWindowBoundsInScreen().width());
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_FALSE(HasBackdrop());
EXPECT_EQ(GetContext()->bounds().ToString(),
shell_surface->GetWidget()->GetWindowBoundsInScreen().ToString());
// Leave fullscreen mode.
ash::WindowState::Get(window)->OnWMEvent(&event);
EXPECT_FALSE(HasBackdrop());
// Check that shell surface is maximized.
EXPECT_EQ(GetContext()->bounds().width(),
shell_surface->GetWidget()->GetWindowBoundsInScreen().width());
}
TEST_F(ShellSurfaceTest, FrameColors) {
auto shell_surface =
test::ShellSurfaceBuilder({64, 64}).SetNoCommit().BuildShellSurface();
auto* surface = shell_surface->root_surface();
shell_surface->OnSetFrameColors(SK_ColorRED, SK_ColorTRANSPARENT);
surface->Commit();
const ash::NonClientFrameViewAsh* frame =
static_cast<const ash::NonClientFrameViewAsh*>(
shell_surface->GetWidget()->non_client_view()->frame_view());
// Test if colors set before initial commit are set.
EXPECT_EQ(SK_ColorRED, frame->GetActiveFrameColorForTest());
// Frame should be fully opaque.
EXPECT_EQ(SK_ColorBLACK, frame->GetInactiveFrameColorForTest());
shell_surface->OnSetFrameColors(SK_ColorTRANSPARENT, SK_ColorBLUE);
EXPECT_EQ(SK_ColorBLACK, frame->GetActiveFrameColorForTest());
EXPECT_EQ(SK_ColorBLUE, frame->GetInactiveFrameColorForTest());
}
TEST_F(ShellSurfaceTest, CycleSnap) {
constexpr gfx::Size kBufferSize(256, 256);
auto shell_surface =
test::ShellSurfaceBuilder(kBufferSize).BuildShellSurface();
auto* surface = shell_surface->root_surface();
EXPECT_EQ(kBufferSize,
shell_surface->GetWidget()->GetWindowBoundsInScreen().size());
ash::WindowSnapWMEvent event(ash::WM_EVENT_CYCLE_SNAP_PRIMARY);
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
// Enter snapped mode.
ash::WindowState::Get(window)->OnWMEvent(&event);
EXPECT_EQ(GetContext()->bounds().width() / 2,
shell_surface->GetWidget()->GetWindowBoundsInScreen().width());
surface->Commit();
// Commit shouldn't change widget bounds when snapped.
EXPECT_EQ(GetContext()->bounds().width() / 2,
shell_surface->GetWidget()->GetWindowBoundsInScreen().width());
}
TEST_F(ShellSurfaceTest, ShellSurfaceMaximize) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetMaximumSize(gfx::Size(10, 10))
.BuildShellSurface();
auto* window_state =
ash::WindowState::Get(shell_surface->GetWidget()->GetNativeWindow());
// Expect: Can't resize when max_size is set.
EXPECT_FALSE(window_state->CanMaximize());
EXPECT_FALSE(window_state->CanSnap());
shell_surface->SetMaximumSize(gfx::Size(0, 0));
shell_surface->root_surface()->Commit();
// Expect: Can resize without a max_size.
EXPECT_TRUE(window_state->CanMaximize());
EXPECT_TRUE(window_state->CanSnap());
}
TEST_F(ShellSurfaceTest, ShellSurfaceMaxSizeResizabilityOnlyMaximise) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetMaximumSize(gfx::Size(10, 10))
.SetMinimumSize(gfx::Size(0, 0))
.BuildShellSurface();
shell_surface->GetWidget()->GetNativeWindow()->SetProperty(
kMaximumSizeForResizabilityOnly, true);
shell_surface->root_surface()->Commit();
auto* window_state =
ash::WindowState::Get(shell_surface->GetWidget()->GetNativeWindow());
// Expect: Can resize with max_size > min_size.
EXPECT_TRUE(window_state->CanMaximize());
EXPECT_TRUE(window_state->CanResize());
EXPECT_TRUE(window_state->CanSnap());
shell_surface->SetMaximumSize(gfx::Size(0, 0));
shell_surface->root_surface()->Commit();
// Expect: Can resize with max_size unset.
EXPECT_TRUE(window_state->CanMaximize());
EXPECT_TRUE(window_state->CanResize());
EXPECT_TRUE(window_state->CanSnap());
shell_surface->SetMaximumSize(gfx::Size(10, 10));
shell_surface->SetMinimumSize(gfx::Size(10, 10));
shell_surface->root_surface()->Commit();
// Expect: Can't resize where max_size is set and max_size == min_size.
EXPECT_FALSE(window_state->CanMaximize());
EXPECT_FALSE(window_state->CanResize());
EXPECT_FALSE(window_state->CanSnap());
}
TEST_F(ShellSurfaceTest, Transient) {
constexpr gfx::Size kBufferSize(256, 256);
auto parent_shell_surface =
test::ShellSurfaceBuilder(kBufferSize).BuildShellSurface();
auto* parent_surface = parent_shell_surface->root_surface();
auto child_shell_surface =
test::ShellSurfaceBuilder(kBufferSize).SetNoCommit().BuildShellSurface();
auto* child_surface = child_shell_surface->root_surface();
// Importantly, a transient window has an associated application.
child_surface->SetApplicationId("fake_app_id");
child_surface->SetParent(parent_surface, gfx::Point(50, 50));
child_surface->Commit();
aura::Window* parent_window =
parent_shell_surface->GetWidget()->GetNativeWindow();
aura::Window* child_window =
child_shell_surface->GetWidget()->GetNativeWindow();
ASSERT_TRUE(parent_window && child_window);
// The visibility of transient windows is controlled by the parent.
parent_window->Hide();
EXPECT_FALSE(child_window->IsVisible());
parent_window->Show();
EXPECT_TRUE(child_window->IsVisible());
}
TEST_F(ShellSurfaceTest, X11Transient) {
auto parent = test::ShellSurfaceBuilder({256, 256}).BuildShellSurface();
gfx::Point origin(50, 50);
auto transient =
CreateX11TransientShellSurface(parent.get(), gfx::Size(100, 100), origin);
EXPECT_TRUE(transient->GetWidget()->movement_disabled());
EXPECT_EQ(transient->GetWidget()->GetWindowBoundsInScreen().origin(), origin);
}
TEST_F(ShellSurfaceTest, Popup) {
constexpr gfx::Size kBufferSize(256, 256);
auto shell_surface =
test::ShellSurfaceBuilder(kBufferSize).BuildShellSurface();
auto* surface = shell_surface->root_surface();
shell_surface->GetWidget()->SetBounds(gfx::Rect(0, 0, 256, 256));
auto popup_buffer = test::ExoTestHelper::CreateBuffer(kBufferSize);
std::unique_ptr<Surface> popup_surface(new Surface);
popup_surface->Attach(popup_buffer.get());
std::unique_ptr<ShellSurface> popup_shell_surface(CreatePopupShellSurface(
popup_surface.get(), shell_surface.get(), gfx::Point(50, 50)));
popup_shell_surface->Grab();
popup_surface->Commit();
ASSERT_EQ(gfx::Rect(50, 50, 256, 256),
popup_shell_surface->GetWidget()->GetWindowBoundsInScreen());
// Verify that created shell surface is popup and has capture.
EXPECT_EQ(aura::client::WINDOW_TYPE_POPUP,
popup_shell_surface->GetWidget()->GetNativeWindow()->GetType());
EXPECT_TRUE(IsCaptureWindow(popup_shell_surface.get()));
// Setting frame type on popup should have no effect.
popup_surface->SetFrame(SurfaceFrameType::NORMAL);
EXPECT_FALSE(popup_shell_surface->frame_enabled());
// ShellSurface can capture the event even after it is created.
auto sub_popup_buffer = test::ExoTestHelper::CreateBuffer(kBufferSize);
std::unique_ptr<Surface> sub_popup_surface(new Surface);
sub_popup_surface->Attach(sub_popup_buffer.get());
std::unique_ptr<ShellSurface> sub_popup_shell_surface(CreatePopupShellSurface(
sub_popup_surface.get(), popup_shell_surface.get(), gfx::Point(100, 50)));
sub_popup_shell_surface->Grab();
sub_popup_surface->Commit();
ASSERT_EQ(gfx::Rect(100, 50, 256, 256),
sub_popup_shell_surface->GetWidget()->GetWindowBoundsInScreen());
aura::Window* target =
sub_popup_shell_surface->GetWidget()->GetNativeWindow();
EXPECT_EQ(aura::client::WINDOW_TYPE_POPUP, target->GetType());
// The capture should be on `sub_popup_shell_surface`.
EXPECT_TRUE(IsCaptureWindow(sub_popup_shell_surface.get()));
{
// Mouse is on the top most popup.
ui::MouseEvent event(ui::ET_MOUSE_MOVED, gfx::Point(0, 0),
gfx::Point(100, 50), ui::EventTimeForNow(), 0, 0);
ui::Event::DispatcherApi(&event).set_target(target);
EXPECT_EQ(sub_popup_surface.get(), GetTargetSurfaceForLocatedEvent(&event));
}
{
// Move the mouse to the parent popup.
ui::MouseEvent event(ui::ET_MOUSE_MOVED, gfx::Point(-25, 0),
gfx::Point(75, 50), ui::EventTimeForNow(), 0, 0);
ui::Event::DispatcherApi(&event).set_target(target);
EXPECT_EQ(popup_surface.get(), GetTargetSurfaceForLocatedEvent(&event));
}
{
// Move the mouse to the main window.
ui::MouseEvent event(ui::ET_MOUSE_MOVED, gfx::Point(-25, -25),
gfx::Point(75, 25), ui::EventTimeForNow(), 0, 0);
ui::Event::DispatcherApi(&event).set_target(target);
EXPECT_EQ(surface, GetTargetSurfaceForLocatedEvent(&event));
}
// Removing top most popup moves the grab to parent popup.
sub_popup_shell_surface.reset();
EXPECT_TRUE(IsCaptureWindow(popup_shell_surface.get()));
{
// Targetting should still work.
ui::MouseEvent event(ui::ET_MOUSE_MOVED, gfx::Point(0, 0),
gfx::Point(50, 50), ui::EventTimeForNow(), 0, 0);
ui::Event::DispatcherApi(&event).set_target(
popup_shell_surface->GetWidget()->GetNativeWindow());
EXPECT_EQ(popup_surface.get(), GetTargetSurfaceForLocatedEvent(&event));
}
}
TEST_F(ShellSurfaceTest, GainCaptureFromSiblingSubPopup) {
// Test that in the following setup:
//
// popup_shell_surface1 popup_shell_surface2
// (has grab, loses capture) (has grab, gains capture)
// \ /
// popup_shell_surface
//
// when popup_shell_surface2 is added, capture is correctly transferred to it
// from popup_shell_surface1.
constexpr gfx::Size kBufferSize(256, 256);
auto shell_surface =
test::ShellSurfaceBuilder(kBufferSize).BuildShellSurface();
shell_surface->GetWidget()->SetBounds(gfx::Rect(0, 0, 256, 256));
auto popup_buffer = test::ExoTestHelper::CreateBuffer(kBufferSize);
auto popup_surface = std::make_unique<Surface>();
popup_surface->Attach(popup_buffer.get());
std::unique_ptr<ShellSurface> popup_shell_surface(CreatePopupShellSurface(
popup_surface.get(), shell_surface.get(), gfx::Point(50, 50)));
popup_shell_surface->Grab();
popup_surface->Commit();
EXPECT_TRUE(IsCaptureWindow(popup_shell_surface.get()));
auto popup_buffer1 = test::ExoTestHelper::CreateBuffer(kBufferSize);
auto popup_surface1 = std::make_unique<Surface>();
popup_surface1->Attach(popup_buffer1.get());
std::unique_ptr<ShellSurface> popup_shell_surface1(CreatePopupShellSurface(
popup_surface1.get(), popup_shell_surface.get(), gfx::Point(100, 50)));
popup_shell_surface1->Grab();
popup_surface1->Commit();
EXPECT_TRUE(IsCaptureWindow(popup_shell_surface1.get()));
auto popup_buffer2 = test::ExoTestHelper::CreateBuffer(kBufferSize);
auto popup_surface2 = std::make_unique<Surface>();
popup_surface2->Attach(popup_buffer2.get());
std::unique_ptr<ShellSurface> popup_shell_surface2(CreatePopupShellSurface(
popup_surface2.get(), popup_shell_surface.get(), gfx::Point(50, 100)));
popup_shell_surface2->Grab();
popup_surface2->Commit();
EXPECT_TRUE(IsCaptureWindow(popup_shell_surface2.get()));
}
TEST_F(ShellSurfaceTest, GainCaptureFromNieceSubPopup) {
// Test that in the following setup:
//
// popup_shell_surface3
// (no grab)
// |
// popup_shell_surface2
// (has grab; loses capture)
// |
// popup_shell_surface1 popup_shell_surface4
// (no grab) (has grab; gains capture)
// \ /
// popup_shell_surface
//
// when popup_shell_surface4 is added, capture is correctly transferred to
// it from popup_shell_surface2.
constexpr gfx::Size kBufferSize(256, 256);
auto shell_surface =
test::ShellSurfaceBuilder(kBufferSize).BuildShellSurface();
shell_surface->GetWidget()->SetBounds(gfx::Rect(0, 0, 256, 256));
auto popup_buffer = test::ExoTestHelper::CreateBuffer(kBufferSize);
auto popup_surface = std::make_unique<Surface>();
popup_surface->Attach(popup_buffer.get());
std::unique_ptr<ShellSurface> popup_shell_surface(CreatePopupShellSurface(
popup_surface.get(), shell_surface.get(), gfx::Point(50, 50)));
popup_shell_surface->Grab();
popup_surface->Commit();
EXPECT_TRUE(IsCaptureWindow(popup_shell_surface.get()));
auto popup_buffer1 = test::ExoTestHelper::CreateBuffer(kBufferSize);
auto popup_surface1 = std::make_unique<Surface>();
popup_surface1->Attach(popup_buffer1.get());
std::unique_ptr<ShellSurface> popup_shell_surface1(CreatePopupShellSurface(
popup_surface1.get(), popup_shell_surface.get(), gfx::Point(100, 50)));
popup_surface1->Commit();
// Doesn't change capture.
EXPECT_TRUE(IsCaptureWindow(popup_shell_surface.get()));
auto popup_buffer2 = test::ExoTestHelper::CreateBuffer(kBufferSize);
auto popup_surface2 = std::make_unique<Surface>();
popup_surface2->Attach(popup_buffer2.get());
std::unique_ptr<ShellSurface> popup_shell_surface2(CreatePopupShellSurface(
popup_surface2.get(), popup_shell_surface1.get(), gfx::Point(150, 50)));
popup_shell_surface2->Grab();
popup_surface2->Commit();
EXPECT_TRUE(IsCaptureWindow(popup_shell_surface2.get()));
auto popup_buffer3 = test::ExoTestHelper::CreateBuffer(kBufferSize);
auto popup_surface3 = std::make_unique<Surface>();
popup_surface3->Attach(popup_buffer3.get());
std::unique_ptr<ShellSurface> popup_shell_surface3(CreatePopupShellSurface(
popup_surface3.get(), popup_shell_surface2.get(), gfx::Point(200, 50)));
popup_surface3->Commit();
// Doesn't change capture.
EXPECT_TRUE(IsCaptureWindow(popup_shell_surface2.get()));
auto popup_buffer4 = test::ExoTestHelper::CreateBuffer(kBufferSize);
auto popup_surface4 = std::make_unique<Surface>();
popup_surface4->Attach(popup_buffer4.get());
std::unique_ptr<ShellSurface> popup_shell_surface4(CreatePopupShellSurface(
popup_surface4.get(), popup_shell_surface.get(), gfx::Point(50, 100)));
popup_shell_surface4->Grab();
popup_surface4->Commit();
EXPECT_TRUE(IsCaptureWindow(popup_shell_surface4.get()));
}
TEST_F(ShellSurfaceTest, GainCaptureFromDescendantSubPopup) {
// Test that in the following setup:
//
// popup_shell_surface3
// (has grab; loses capture)
// |
// popup_shell_surface2
// (no grab)
// |
// popup_shell_surface1
// (has grab; gains capture)
// |
// popup_shell_surface
//
// when popup_shell_surface3 is closed, capture is correctly transferred to
// popup_shell_surface1.
constexpr gfx::Size kBufferSize(256, 256);
auto shell_surface =
test::ShellSurfaceBuilder(kBufferSize).BuildShellSurface();
shell_surface->GetWidget()->SetBounds(gfx::Rect(0, 0, 256, 256));
auto popup_buffer = test::ExoTestHelper::CreateBuffer(kBufferSize);
auto popup_surface = std::make_unique<Surface>();
popup_surface->Attach(popup_buffer.get());
std::unique_ptr<ShellSurface> popup_shell_surface(CreatePopupShellSurface(
popup_surface.get(), shell_surface.get(), gfx::Point(50, 50)));
popup_shell_surface->Grab();
popup_surface->Commit();
EXPECT_TRUE(IsCaptureWindow(popup_shell_surface.get()));
auto popup_buffer1 = test::ExoTestHelper::CreateBuffer(kBufferSize);
auto popup_surface1 = std::make_unique<Surface>();
popup_surface1->Attach(popup_buffer1.get());
std::unique_ptr<ShellSurface> popup_shell_surface1(CreatePopupShellSurface(
popup_surface1.get(), popup_shell_surface.get(), gfx::Point(100, 50)));
popup_shell_surface1->Grab();
popup_surface1->Commit();
EXPECT_TRUE(IsCaptureWindow(popup_shell_surface1.get()));
auto popup_buffer2 = test::ExoTestHelper::CreateBuffer(kBufferSize);
auto popup_surface2 = std::make_unique<Surface>();
popup_surface2->Attach(popup_buffer2.get());
std::unique_ptr<ShellSurface> popup_shell_surface2(CreatePopupShellSurface(
popup_surface2.get(), popup_shell_surface1.get(), gfx::Point(150, 50)));
popup_surface2->Commit();
// Doesn't change capture.
EXPECT_TRUE(IsCaptureWindow(popup_shell_surface1.get()));
auto popup_buffer3 = test::ExoTestHelper::CreateBuffer(kBufferSize);
auto popup_surface3 = std::make_unique<Surface>();
popup_surface3->Attach(popup_buffer3.get());
std::unique_ptr<ShellSurface> popup_shell_surface3(CreatePopupShellSurface(
popup_surface3.get(), popup_shell_surface2.get(), gfx::Point(200, 50)));
popup_shell_surface3->Grab();
popup_surface3->Commit();
EXPECT_TRUE(IsCaptureWindow(popup_shell_surface3.get()));
popup_shell_surface3.reset();
EXPECT_TRUE(IsCaptureWindow(popup_shell_surface1.get()));
}
TEST_F(ShellSurfaceTest, Menu) {
std::unique_ptr<ShellSurface> root_shell_surface =
test::ShellSurfaceBuilder({256, 256}).BuildShellSurface();
EXPECT_TRUE(root_shell_surface->GetWidget()->IsVisible());
std::unique_ptr<ShellSurface> menu_shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetAsMenu()
.SetParent(root_shell_surface.get())
.BuildShellSurface();
EXPECT_TRUE(menu_shell_surface->GetWidget()->IsVisible());
EXPECT_EQ(aura::client::WINDOW_TYPE_MENU,
menu_shell_surface->GetWidget()->GetNativeWindow()->GetType());
}
TEST_F(ShellSurfaceTest, MenuOnPopup) {
std::unique_ptr<ShellSurface> root_shell_surface =
test::ShellSurfaceBuilder({256, 256}).BuildShellSurface();
EXPECT_TRUE(root_shell_surface->GetWidget()->IsVisible());
std::unique_ptr<ShellSurface> popup_shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetAsPopup()
.SetParent(root_shell_surface.get())
.BuildShellSurface();
EXPECT_TRUE(popup_shell_surface->GetWidget()->IsVisible());
EXPECT_EQ(aura::client::WINDOW_TYPE_POPUP,
popup_shell_surface->GetWidget()->GetNativeWindow()->GetType());
std::unique_ptr<ShellSurface> menu_shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetAsMenu()
.SetParent(popup_shell_surface.get())
.BuildShellSurface();
EXPECT_TRUE(menu_shell_surface->GetWidget()->IsVisible());
EXPECT_EQ(aura::client::WINDOW_TYPE_MENU,
menu_shell_surface->GetWidget()->GetNativeWindow()->GetType());
}
TEST_F(ShellSurfaceTest, PopupOnMenu) {
std::unique_ptr<ShellSurface> root_shell_surface =
test::ShellSurfaceBuilder({256, 256}).BuildShellSurface();
EXPECT_TRUE(root_shell_surface->GetWidget()->IsVisible());
std::unique_ptr<ShellSurface> menu_shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetAsMenu()
.SetParent(root_shell_surface.get())
.BuildShellSurface();
EXPECT_TRUE(menu_shell_surface->GetWidget()->IsVisible());
EXPECT_EQ(aura::client::WINDOW_TYPE_MENU,
menu_shell_surface->GetWidget()->GetNativeWindow()->GetType());
std::unique_ptr<ShellSurface> popup_shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetAsPopup()
.SetParent(menu_shell_surface.get())
.BuildShellSurface();
EXPECT_TRUE(popup_shell_surface->GetWidget()->IsVisible());
EXPECT_EQ(aura::client::WINDOW_TYPE_POPUP,
popup_shell_surface->GetWidget()->GetNativeWindow()->GetType());
}
TEST_F(ShellSurfaceTest, PopupOnPopup) {
std::unique_ptr<ShellSurface> root_shell_surface =
test::ShellSurfaceBuilder({256, 256}).BuildShellSurface();
EXPECT_TRUE(root_shell_surface->GetWidget()->IsVisible());
std::unique_ptr<ShellSurface> popup_shell_surface_1 =
test::ShellSurfaceBuilder({256, 256})
.SetAsPopup()
.SetParent(root_shell_surface.get())
.BuildShellSurface();
EXPECT_TRUE(popup_shell_surface_1->GetWidget()->IsVisible());
EXPECT_EQ(aura::client::WINDOW_TYPE_POPUP,
popup_shell_surface_1->GetWidget()->GetNativeWindow()->GetType());
std::unique_ptr<ShellSurface> popup_shell_surface_2 =
test::ShellSurfaceBuilder({256, 256})
.SetAsPopup()
.SetParent(popup_shell_surface_1.get())
.BuildShellSurface();
EXPECT_TRUE(popup_shell_surface_2->GetWidget()->IsVisible());
EXPECT_EQ(aura::client::WINDOW_TYPE_POPUP,
popup_shell_surface_2->GetWidget()->GetNativeWindow()->GetType());
}
TEST_F(ShellSurfaceTest, MenuOnMenu) {
std::unique_ptr<ShellSurface> root_shell_surface =
test::ShellSurfaceBuilder({256, 256}).BuildShellSurface();
EXPECT_TRUE(root_shell_surface->GetWidget()->IsVisible());
std::unique_ptr<ShellSurface> menu_shell_surface_1 =
test::ShellSurfaceBuilder({256, 256})
.SetAsMenu()
.SetParent(root_shell_surface.get())
.BuildShellSurface();
EXPECT_TRUE(menu_shell_surface_1->GetWidget()->IsVisible());
EXPECT_EQ(aura::client::WINDOW_TYPE_MENU,
menu_shell_surface_1->GetWidget()->GetNativeWindow()->GetType());
std::unique_ptr<ShellSurface> menu_shell_surface_2 =
test::ShellSurfaceBuilder({256, 256})
.SetAsMenu()
.SetParent(menu_shell_surface_1.get())
.BuildShellSurface();
EXPECT_TRUE(menu_shell_surface_2->GetWidget()->IsVisible());
EXPECT_EQ(aura::client::WINDOW_TYPE_MENU,
menu_shell_surface_2->GetWidget()->GetNativeWindow()->GetType());
}
TEST_F(ShellSurfaceTest, PopupWithInputRegion) {
constexpr gfx::Size kBufferSize(256, 256);
auto shell_surface = test::ShellSurfaceBuilder(kBufferSize)
.SetInputRegion(cc::Region())
.BuildShellSurface();
auto* surface = shell_surface->root_surface();
auto child_buffer = test::ExoTestHelper::CreateBuffer(kBufferSize);
std::unique_ptr<Surface> child_surface(new Surface);
child_surface->Attach(child_buffer.get());
auto subsurface = std::make_unique<SubSurface>(child_surface.get(), surface);
subsurface->SetPosition(gfx::PointF(10, 10));
child_surface->SetInputRegion(cc::Region(gfx::Rect(0, 0, 256, 2560)));
child_surface->Commit();
surface->Commit();
shell_surface->GetWidget()->SetBounds(gfx::Rect(0, 0, 256, 256));
auto popup_buffer = test::ExoTestHelper::CreateBuffer(kBufferSize);
std::unique_ptr<Surface> popup_surface(new Surface);
popup_surface->Attach(popup_buffer.get());
std::unique_ptr<ShellSurface> popup_shell_surface(CreatePopupShellSurface(
popup_surface.get(), shell_surface.get(), gfx::Point(50, 50)));
popup_shell_surface->Grab();
popup_surface->Commit();
ASSERT_EQ(gfx::Rect(50, 50, 256, 256),
popup_shell_surface->GetWidget()->GetWindowBoundsInScreen());
// Verify that created shell surface is popup and has capture.
EXPECT_EQ(aura::client::WINDOW_TYPE_POPUP,
popup_shell_surface->GetWidget()->GetNativeWindow()->GetType());
EXPECT_EQ(WMHelper::GetInstance()->GetCaptureClient()->GetCaptureWindow(),
popup_shell_surface->GetWidget()->GetNativeWindow());
// Setting frame type on popup should have no effect.
popup_surface->SetFrame(SurfaceFrameType::NORMAL);
EXPECT_FALSE(popup_shell_surface->frame_enabled());
aura::Window* target = popup_shell_surface->GetWidget()->GetNativeWindow();
{
// Mouse is on the popup.
ui::MouseEvent event(ui::ET_MOUSE_MOVED, gfx::Point(0, 0),
gfx::Point(50, 50), ui::EventTimeForNow(), 0, 0);
ui::Event::DispatcherApi(&event).set_target(target);
EXPECT_EQ(popup_surface.get(), GetTargetSurfaceForLocatedEvent(&event));
}
{
// If it matches the parent's sub surface, use it.
ui::MouseEvent event(ui::ET_MOUSE_MOVED, gfx::Point(-25, 0),
gfx::Point(25, 50), ui::EventTimeForNow(), 0, 0);
ui::Event::DispatcherApi(&event).set_target(target);
EXPECT_EQ(child_surface.get(), GetTargetSurfaceForLocatedEvent(&event));
}
{
// If it didnt't match any surface in the parent, fallback to
// the popup's surface.
ui::MouseEvent event(ui::ET_MOUSE_MOVED, gfx::Point(-50, 0),
gfx::Point(0, 50), ui::EventTimeForNow(), 0, 0);
ui::Event::DispatcherApi(&event).set_target(target);
EXPECT_EQ(popup_surface.get(), GetTargetSurfaceForLocatedEvent(&event));
}
popup_surface.reset();
EXPECT_EQ(WMHelper::GetInstance()->GetCaptureClient()->GetCaptureWindow(),
nullptr);
auto window = std::make_unique<aura::Window>(nullptr);
window->Init(ui::LAYER_TEXTURED);
window->SetBounds(gfx::Rect(0, 0, 256, 256));
shell_surface->GetWidget()->GetNativeWindow()->AddChild(window.get());
window->Show();
{
// If non surface window covers the window,
/// GetTargetSurfaceForLocatedEvent should return nullptr.
ui::MouseEvent event(ui::ET_MOUSE_MOVED, gfx::Point(0, 0),
gfx::Point(50, 50), ui::EventTimeForNow(), 0, 0);
ui::Event::DispatcherApi(&event).set_target(window.get());
EXPECT_EQ(nullptr, GetTargetSurfaceForLocatedEvent(&event));
}
}
// Test that popup does not close when trying to take a screenshot.
TEST_F(ShellSurfaceTest, PopupWithCaptureMode) {
// Setup popup_shell_surface.
constexpr gfx::Size kBufferSize(256, 256);
auto shell_surface =
test::ShellSurfaceBuilder(kBufferSize).BuildShellSurface();
auto popup_buffer = test::ExoTestHelper::CreateBuffer(kBufferSize);
auto popup_surface = std::make_unique<Surface>();
popup_surface->Attach(popup_buffer.get());
std::unique_ptr<ShellSurface> popup_shell_surface(CreatePopupShellSurface(
popup_surface.get(), shell_surface.get(), gfx::Point(50, 50)));
popup_shell_surface->Grab();
popup_surface->Commit();
bool closed = false;
auto callback =
base::BindRepeating([](bool* closed) { *closed = true; }, &closed);
popup_shell_surface->set_close_callback(callback);
// This simulates enabling (screenshot) capture mode.
ash::GetTestDelegate()->OnSessionStateChanged(true);
popup_shell_surface->OnCaptureChanged(
popup_shell_surface->GetWidget()->GetNativeWindow(), nullptr);
// With (screenshot) capture mode on, losing capture should not close the
// shell surface.
EXPECT_FALSE(closed);
// This simulates ending (screenshot) capture mode.
ash::GetTestDelegate()->OnSessionStateChanged(false);
popup_shell_surface->OnCaptureChanged(
popup_shell_surface->GetWidget()->GetNativeWindow(), nullptr);
// With (screenshot) capture mode off, losing capture should close the shell
// surface.
EXPECT_TRUE(closed);
}
TEST_F(ShellSurfaceTest, PopupWithInvisibleParent) {
// Invisible main window.
std::unique_ptr<ShellSurface> root_shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetNoRootBuffer()
.BuildShellSurface();
EXPECT_FALSE(root_shell_surface->GetWidget()->IsVisible());
std::unique_ptr<ShellSurface> popup_shell_surface_1 =
test::ShellSurfaceBuilder({256, 256})
.SetNoRootBuffer()
.SetAsPopup()
.SetDisableMovement()
.SetParent(root_shell_surface.get())
.BuildShellSurface();
EXPECT_EQ(root_shell_surface->GetWidget()->GetNativeWindow(),
wm::GetTransientParent(
popup_shell_surface_1->GetWidget()->GetNativeWindow()));
EXPECT_FALSE(popup_shell_surface_1->GetWidget()->IsVisible());
// Create visible popup.
std::unique_ptr<ShellSurface> popup_shell_surface_2 =
test::ShellSurfaceBuilder({256, 256})
.SetAsPopup()
.SetDisableMovement()
.SetParent(popup_shell_surface_1.get())
.BuildShellSurface();
EXPECT_TRUE(popup_shell_surface_2->GetWidget()->IsVisible());
EXPECT_EQ(popup_shell_surface_1->GetWidget()->GetNativeWindow(),
wm::GetTransientParent(
popup_shell_surface_2->GetWidget()->GetNativeWindow()));
}
TEST_F(ShellSurfaceTest, Caption) {
auto shell_surface =
test::ShellSurfaceBuilder({256, 256}).SetNoCommit().BuildShellSurface();
auto* surface = shell_surface->root_surface();
shell_surface->OnSetFrame(SurfaceFrameType::NORMAL);
surface->Commit();
shell_surface->GetWidget()->SetBounds(gfx::Rect(0, 0, 256, 256));
aura::Window* target = shell_surface->GetWidget()->GetNativeWindow();
target->SetCapture();
EXPECT_EQ(WMHelper::GetInstance()->GetCaptureClient()->GetCaptureWindow(),
shell_surface->GetWidget()->GetNativeWindow());
{
// Move the mouse at the caption of the captured window.
ui::MouseEvent event(ui::ET_MOUSE_MOVED, gfx::Point(5, 5), gfx::Point(5, 5),
ui::EventTimeForNow(), 0, 0);
ui::Event::DispatcherApi(&event).set_target(target);
EXPECT_EQ(nullptr, GetTargetSurfaceForLocatedEvent(&event));
}
{
// Move the mouse at the center of the captured window.
gfx::Rect bounds = shell_surface->GetWidget()->GetWindowBoundsInScreen();
gfx::Point center = bounds.CenterPoint();
ui::MouseEvent event(ui::ET_MOUSE_MOVED, center - bounds.OffsetFromOrigin(),
center, ui::EventTimeForNow(), 0, 0);
ui::Event::DispatcherApi(&event).set_target(target);
EXPECT_EQ(surface, GetTargetSurfaceForLocatedEvent(&event));
}
}
TEST_F(ShellSurfaceTest, DragMaximizedWindow) {
auto shell_surface =
test::ShellSurfaceBuilder({256, 256}).SetNoCommit().BuildShellSurface();
auto* surface = shell_surface->root_surface();
shell_surface->OnSetFrame(SurfaceFrameType::NORMAL);
surface->Commit();
shell_surface->GetWidget()->SetBounds(gfx::Rect(0, 0, 256, 256));
// Maximize the window.
shell_surface->Maximize();
chromeos::WindowStateType configured_state =
chromeos::WindowStateType::kDefault;
shell_surface->set_configure_callback(base::BindLambdaForTesting(
[&](const gfx::Rect& bounds, chromeos::WindowStateType state,
bool resizing, bool activated, const gfx::Vector2d& origin_offset,
float raster_scale, aura::Window::OcclusionState occlusion_state,
std::optional<chromeos::WindowStateType>) {
configured_state = state;
return uint32_t{0};
}));
// Initiate caption bar dragging for a window.
gfx::Size size = shell_surface->GetWidget()->GetWindowBoundsInScreen().size();
ui::test::EventGenerator* event_generator = GetEventGenerator();
// Move mouse pointer to caption bar.
event_generator->MoveMouseTo(size.width() / 2, 2);
constexpr int kDragAmount = 10;
// Start dragging a window.
event_generator->DragMouseBy(kDragAmount, kDragAmount);
EXPECT_FALSE(shell_surface->GetWidget()->IsMaximized());
// `WindowStateType` after dragging should be `kNormal`.
EXPECT_EQ(chromeos::WindowStateType::kNormal, configured_state);
}
TEST_F(ShellSurfaceTest, CaptionWithPopup) {
constexpr gfx::Size kBufferSize(256, 256);
auto shell_surface = test::ShellSurfaceBuilder(kBufferSize)
.SetRootBufferFormat(kOpaqueFormat)
.SetFrame(SurfaceFrameType::NORMAL)
.BuildShellSurface();
auto* surface = shell_surface->root_surface();
shell_surface->GetWidget()->SetBounds(gfx::Rect(0, 0, 256, 288));
auto popup_buffer =
test::ExoTestHelper::CreateBuffer(kBufferSize, kOpaqueFormat);
auto popup_surface = std::make_unique<Surface>();
popup_surface->Attach(popup_buffer.get());
std::unique_ptr<ShellSurface> popup_shell_surface(CreatePopupShellSurface(
popup_surface.get(), shell_surface.get(), gfx::Point(50, 50)));
popup_shell_surface->Grab();
popup_surface->Commit();
aura::Window* target = popup_shell_surface->GetWidget()->GetNativeWindow();
EXPECT_EQ(WMHelper::GetInstance()->GetCaptureClient()->GetCaptureWindow(),
target);
{
// Move the mouse at the popup window.
ui::MouseEvent event(ui::ET_MOUSE_MOVED, gfx::Point(5, 5),
gfx::Point(55, 55), ui::EventTimeForNow(), 0, 0);
ui::Event::DispatcherApi(&event).set_target(target);
EXPECT_EQ(popup_surface.get(), GetTargetSurfaceForLocatedEvent(&event));
}
{
// Move the mouse at the caption of the main window.
ui::MouseEvent event(ui::ET_MOUSE_MOVED, gfx::Point(-45, -45),
gfx::Point(5, 5), ui::EventTimeForNow(), 0, 0);
ui::Event::DispatcherApi(&event).set_target(target);
EXPECT_EQ(nullptr, GetTargetSurfaceForLocatedEvent(&event));
}
{
// Move the mouse in the main window.
ui::MouseEvent event(ui::ET_MOUSE_MOVED, gfx::Point(-25, 0),
gfx::Point(25, 50), ui::EventTimeForNow(), 0, 0);
ui::Event::DispatcherApi(&event).set_target(target);
EXPECT_EQ(surface, GetTargetSurfaceForLocatedEvent(&event));
}
}
TEST_F(ShellSurfaceTest, SkipImeProcessingPropagateToSurface) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetFrame(SurfaceFrameType::NORMAL)
.BuildShellSurface();
shell_surface->GetWidget()->SetBounds(gfx::Rect(0, 0, 256, 256));
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
ASSERT_FALSE(window->GetProperty(aura::client::kSkipImeProcessing));
ASSERT_FALSE(shell_surface->root_surface()->window()->GetProperty(
aura::client::kSkipImeProcessing));
window->SetProperty(aura::client::kSkipImeProcessing, true);
EXPECT_TRUE(window->GetProperty(aura::client::kSkipImeProcessing));
EXPECT_TRUE(shell_surface->root_surface()->window()->GetProperty(
aura::client::kSkipImeProcessing));
}
TEST_F(ShellSurfaceTest, NotifyLeaveEnter) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256}).SetNoCommit().BuildShellSurface();
auto func = [](int64_t* old_display_id, int64_t* new_display_id,
int64_t old_id, int64_t new_id) {
DCHECK_EQ(0, *old_display_id);
DCHECK_EQ(0, *new_display_id);
*old_display_id = old_id;
*new_display_id = new_id;
return true;
};
int64_t old_display_id = 0, new_display_id = 0;
shell_surface->root_surface()->set_leave_enter_callback(
base::BindRepeating(func, &old_display_id, &new_display_id));
// Creating a new shell surface should notify on which display
// it is created.
shell_surface->root_surface()->Commit();
EXPECT_EQ(display::kInvalidDisplayId, old_display_id);
EXPECT_EQ(display::Screen::GetScreen()->GetPrimaryDisplay().id(),
new_display_id);
// Attaching a 2nd display should not change where the surface
// is located.
old_display_id = 0;
new_display_id = 0;
UpdateDisplay("800x600, 800x600");
EXPECT_EQ(0, old_display_id);
EXPECT_EQ(0, new_display_id);
// Move the window to 2nd display.
shell_surface->GetWidget()->SetBounds(gfx::Rect(1000, 0, 256, 256));
int64_t secondary_id =
display::test::DisplayManagerTestApi(ash::Shell::Get()->display_manager())
.GetSecondaryDisplay()
.id();
EXPECT_EQ(display::Screen::GetScreen()->GetPrimaryDisplay().id(),
old_display_id);
EXPECT_EQ(secondary_id, new_display_id);
// Disconnect the display the surface is currently on.
old_display_id = 0;
new_display_id = 0;
UpdateDisplay("800x600");
EXPECT_EQ(display::Screen::GetScreen()->GetPrimaryDisplay().id(),
new_display_id);
EXPECT_EQ(secondary_id, old_display_id);
}
// Make sure that the server side triggers resize when the
// set_server_start_resize is called, and the resize shadow is created for the
// window.
TEST_F(ShellSurfaceTest, ServerStartResize) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({64, 64}).SetNoCommit().BuildShellSurface();
shell_surface->OnSetServerStartResize();
shell_surface->root_surface()->Commit();
ASSERT_TRUE(shell_surface->GetWidget());
auto* widget = shell_surface->GetWidget();
gfx::Size size = widget->GetWindowBoundsInScreen().size();
widget->SetBounds(gfx::Rect(size));
ui::test::EventGenerator* event_generator = GetEventGenerator();
event_generator->MoveMouseTo(size.width() + 2, size.height() / 2);
// Ash creates resize shadow for resizable exo window when the
// server_start_resize is set.
ash::ResizeShadow* resize_shadow =
ash::Shell::Get()->resize_shadow_controller()->GetShadowForWindowForTest(
widget->GetNativeWindow());
ASSERT_TRUE(resize_shadow);
constexpr int kDragAmount = 10;
event_generator->PressLeftButton();
event_generator->MoveMouseBy(kDragAmount, 0);
event_generator->ReleaseLeftButton();
EXPECT_EQ(widget->GetWindowBoundsInScreen().size().width(),
size.width() + kDragAmount);
}
TEST_F(ShellSurfaceTest, LacrosToggleAxisMaximize) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({64, 64})
.SetOrigin({10, 10})
.BuildShellSurface();
shell_surface->OnSetServerStartResize();
auto* widget = shell_surface->GetWidget();
gfx::Size size = widget->GetWindowBoundsInScreen().size();
gfx::Rect restored_bounds = shell_surface->GetBoundsInScreen();
ui::test::EventGenerator* event_generator = GetEventGenerator();
// Move mouse to top middle and double click to vertically maximize.
event_generator->MoveMouseTo(10 + size.width() / 2, 10);
event_generator->DoubleClickLeftButton();
gfx::Rect work_area =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
gfx::Rect bounds_in_screen = shell_surface->GetBoundsInScreen();
EXPECT_EQ(restored_bounds.x(), bounds_in_screen.x());
EXPECT_EQ(restored_bounds.width(), bounds_in_screen.width());
EXPECT_EQ(work_area.y(), bounds_in_screen.y());
EXPECT_EQ(work_area.height(), bounds_in_screen.height());
// Move mouse to top middle and double click to vertically Restore.
event_generator->MoveMouseTo(10 + size.width() / 2, 0);
event_generator->DoubleClickLeftButton();
bounds_in_screen = shell_surface->GetBoundsInScreen();
EXPECT_EQ(restored_bounds, bounds_in_screen);
}
TEST_F(ShellSurfaceTest, ServerStartResizeComponent) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({64, 64}).SetNoCommit().BuildShellSurface();
shell_surface->OnSetServerStartResize();
shell_surface->root_surface()->Commit();
ASSERT_TRUE(shell_surface->GetWidget());
auto* widget = shell_surface->GetWidget();
gfx::Size size = widget->GetWindowBoundsInScreen().size();
widget->SetBounds(gfx::Rect(size));
uint32_t serial = 0;
auto configure_callback = base::BindRepeating(
[](uint32_t* const serial_ptr, const gfx::Rect& bounds,
chromeos::WindowStateType state_type, bool resizing, bool activated,
const gfx::Vector2d& origin_offset, float raster_scale,
aura::Window::OcclusionState occlusion_state,
std::optional<chromeos::WindowStateType>) { return ++(*serial_ptr); },
&serial);
ui::test::EventGenerator* event_generator = GetEventGenerator();
event_generator->MoveMouseTo(size.width() + 2, size.height() / 2);
EXPECT_EQ(shell_surface->resize_component_for_test(), HTCAPTION);
constexpr int kDragAmount = 10;
event_generator->PressLeftButton();
event_generator->MoveMouseBy(kDragAmount, 0);
EXPECT_EQ(shell_surface->resize_component_for_test(), HTCAPTION);
shell_surface->AcknowledgeConfigure(serial);
shell_surface->root_surface()->Commit();
EXPECT_EQ(shell_surface->resize_component_for_test(), HTRIGHT);
event_generator->ReleaseLeftButton();
shell_surface->AcknowledgeConfigure(serial);
shell_surface->root_surface()->Commit();
EXPECT_EQ(shell_surface->resize_component_for_test(), HTCAPTION);
}
// Make sure that dragging to another display will update the origin to
// correct value.
TEST_F(ShellSurfaceTest, UpdateBoundsWhenDraggedToAnotherDisplay) {
exo::test::TestSecurityDelegate securityDelegate;
securityDelegate.SetCanSetBounds(
SecurityDelegate::SetBoundsPolicy::DCHECK_IF_DECORATED);
UpdateDisplay("800x600, 800x600");
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({64, 64})
.SetSecurityDelegate(&securityDelegate)
.BuildShellSurface();
ui::test::EventGenerator* event_generator = GetEventGenerator();
shell_surface->SetWindowBounds({0, 0, 64, 64});
gfx::Point last_origin;
auto origin_change = [&](const gfx::Point& p) { last_origin = p; };
shell_surface->set_origin_change_callback(
base::BindLambdaForTesting(origin_change));
event_generator->MoveMouseTo(1, 1);
event_generator->PressLeftButton();
ASSERT_TRUE(shell_surface->StartMove());
event_generator->MoveMouseTo(801, 1);
event_generator->ReleaseLeftButton();
EXPECT_EQ(last_origin, gfx::Point(800, 0));
}
// Make sure that commit during window drag should not move the
// window to another display.
TEST_F(ShellSurfaceTest, CommitShouldNotMoveDisplay) {
UpdateDisplay("800x600, 800x600");
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({64, 64})
.SetOrigin({750, 0})
.BuildShellSurface();
auto* screen = display::Screen::GetScreen();
auto* root_surface = shell_surface->root_surface();
EXPECT_EQ(screen->GetPrimaryDisplay().id(),
screen
->GetDisplayNearestWindow(
shell_surface->GetWidget()->GetNativeWindow())
.id());
aura::Env::GetInstance()->set_mouse_button_flags(ui::EF_LEFT_MOUSE_BUTTON);
ASSERT_TRUE(shell_surface->StartMove());
constexpr gfx::Size kBufferSize(256, 256);
auto new_buffer = test::ExoTestHelper::CreateBuffer(kBufferSize);
root_surface->Attach(new_buffer.get());
root_surface->Commit();
EXPECT_EQ(screen->GetPrimaryDisplay().id(),
screen
->GetDisplayNearestWindow(
shell_surface->GetWidget()->GetNativeWindow())
.id());
GetEventGenerator()->ReleaseLeftButton();
// shell_surface->EndDrag();
// Ending drag will not move the window unless the mouse cursor enters
// another display.
EXPECT_EQ(screen->GetPrimaryDisplay().id(),
screen
->GetDisplayNearestWindow(
shell_surface->GetWidget()->GetNativeWindow())
.id());
}
TEST_F(ShellSurfaceTest, ShadowBoundsWithNegativeCoordinate) {
constexpr gfx::Point kOrigin(20, 20);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetOrigin(kOrigin)
.BuildShellSurface();
shell_surface->root_surface()->Commit();
ASSERT_TRUE(shell_surface->GetWidget());
// Create subsurface outside of root surface.
constexpr gfx::Size kChildBufferSize(32, 32);
auto child_buffer = test::ExoTestHelper::CreateBuffer(kChildBufferSize);
auto child_surface = std::make_unique<Surface>();
child_surface->Attach(child_buffer.get());
auto subsurface = std::make_unique<SubSurface>(child_surface.get(),
shell_surface->root_surface());
// Set subsurface to left and upper of the window.
subsurface->SetPosition(gfx::PointF(-10, -10));
child_surface->Commit();
shell_surface->root_surface()->Commit();
ASSERT_TRUE(shell_surface->GetWidget());
auto* widget = shell_surface->GetWidget();
// Geometry is relative to the root surface.
shell_surface->SetGeometry(gfx::Rect(0, 0, 256, 256));
shell_surface->root_surface()->SetFrame(SurfaceFrameType::SHADOW);
shell_surface->root_surface()->Commit();
EXPECT_FALSE(widget->GetNativeWindow()->GetProperty(
aura::client::kUseWindowBoundsForShadow));
// Host window should be a rectangle including root surface and subsurface.
EXPECT_EQ(gfx::Rect(-10, -10, 266, 266),
shell_surface->host_window()->bounds());
// Shadow content bounds is relative to ExoShellSurface and should use window
// bounds.
ui::Shadow* shadow =
wm::ShadowController::GetShadowForWindow(widget->GetNativeWindow());
ASSERT_TRUE(shadow);
EXPECT_EQ(gfx::Rect(0, 0, 256, 256), shadow->content_bounds());
}
TEST_F(ShellSurfaceTest, ShadowBoundsWithScaleFactor) {
constexpr gfx::Point kOrigin(20, 20);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetOrigin(kOrigin)
.BuildShellSurface();
shell_surface->root_surface()->Commit();
ASSERT_TRUE(shell_surface->GetWidget());
// Set scale.
constexpr float kScaleFactor = 2.0;
shell_surface->set_client_submits_surfaces_in_pixel_coordinates(true);
shell_surface->SetScaleFactor(kScaleFactor);
// Create subsurface outside of root surface.
constexpr gfx::Size kChildBufferSize(32, 32);
auto child_buffer = test::ExoTestHelper::CreateBuffer(kChildBufferSize);
auto child_surface = std::make_unique<Surface>();
child_surface->Attach(child_buffer.get());
auto subsurface = std::make_unique<SubSurface>(child_surface.get(),
shell_surface->root_surface());
// Set subsurface to left and upper of the window.
subsurface->SetPosition(gfx::PointF(-10, -10));
child_surface->Commit();
shell_surface->root_surface()->Commit();
ASSERT_TRUE(shell_surface->GetWidget());
auto* widget = shell_surface->GetWidget();
// Geometry is relative to the root surface.
shell_surface->SetGeometry(gfx::Rect(0, 0, 256, 256));
shell_surface->root_surface()->SetFrame(SurfaceFrameType::SHADOW);
shell_surface->root_surface()->Commit();
EXPECT_FALSE(widget->GetNativeWindow()->GetProperty(
aura::client::kUseWindowBoundsForShadow));
// Host window should be a rectangle including root surface and subsurface.
EXPECT_EQ(gfx::Rect(-5, -5, 133, 133),
shell_surface->host_window()->bounds());
// Shadow content bounds is relative to ExoShellSurface and should use window
// bounds.
ui::Shadow* shadow =
wm::ShadowController::GetShadowForWindow(widget->GetNativeWindow());
ASSERT_TRUE(shadow);
EXPECT_EQ(gfx::Rect(0, 0, 256, 256), shadow->content_bounds());
}
TEST_F(ShellSurfaceTest, ShadowRoundedCorners) {
constexpr gfx::Point kOrigin(20, 20);
constexpr int kWindowCornerRadius = 12;
base::test::ScopedFeatureList scoped_feature_list(
chromeos::features::kRoundedWindows);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetOrigin(kOrigin)
.SetWindowState(chromeos::WindowStateType::kNormal)
.SetFrame(SurfaceFrameType::NORMAL)
.BuildShellSurface();
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);
// Window shadow radius needs to match the window radius.
EXPECT_EQ(shadow->rounded_corner_radius_for_testing(), 0);
// Have a window with radius of 12dp.
shell_surface->SetWindowCornersRadii(
gfx::RoundedCornersF(kWindowCornerRadius));
root_surface->Commit();
shadow = wm::ShadowController::GetShadowForWindow(window);
ASSERT_TRUE(shadow);
EXPECT_EQ(shadow->rounded_corner_radius_for_testing(), kWindowCornerRadius);
// Have a window with radius of 0dp.
shell_surface->SetWindowCornersRadii(gfx::RoundedCornersF());
root_surface->Commit();
shadow = wm::ShadowController::GetShadowForWindow(window);
ASSERT_TRUE(shadow);
EXPECT_EQ(shadow->rounded_corner_radius_for_testing(), 0);
}
// Make sure that resize shadow does not update until commit when the window
// property |aura::client::kUseWindowBoundsForShadow| is false.
TEST_F(ShellSurfaceTest, ResizeShadowIndependentBounds) {
constexpr gfx::Point kOrigin(10, 10);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({64, 64})
.SetOrigin(kOrigin)
.BuildShellSurface();
shell_surface->OnSetServerStartResize();
shell_surface->root_surface()->Commit();
ASSERT_TRUE(shell_surface->GetWidget());
auto* widget = shell_surface->GetWidget();
gfx::Size size = widget->GetWindowBoundsInScreen().size();
widget->SetBounds(gfx::Rect(size));
// Starts mouse event to make sure resize shadow is created.
ui::test::EventGenerator* event_generator = GetEventGenerator();
event_generator->MoveMouseTo(size.width() + 2, size.height() / 2);
// Creates resize shadow and normal shadow for resizable exo window.
ash::ResizeShadow* resize_shadow =
ash::Shell::Get()->resize_shadow_controller()->GetShadowForWindowForTest(
widget->GetNativeWindow());
ASSERT_TRUE(resize_shadow);
shell_surface->root_surface()->SetFrame(SurfaceFrameType::SHADOW);
shell_surface->root_surface()->Commit();
EXPECT_FALSE(widget->GetNativeWindow()->GetProperty(
aura::client::kUseWindowBoundsForShadow));
ui::Shadow* normal_shadow =
wm::ShadowController::GetShadowForWindow(widget->GetNativeWindow());
ASSERT_TRUE(normal_shadow);
// ash::ResizeShadow::InitParams set the default |thickness| to 8.
constexpr int kResizeShadowThickness = 8;
EXPECT_EQ(gfx::Size(size.width() + kResizeShadowThickness, size.height()),
resize_shadow->GetLayerForTest()->bounds().size());
EXPECT_EQ(size, normal_shadow->content_bounds().size());
constexpr gfx::Rect kNewBounds(kOrigin, {100, 100});
uint32_t serial = 0;
auto configure_callback = base::BindRepeating(
[](uint32_t* const serial_ptr, const gfx::Rect& bounds,
chromeos::WindowStateType state_type, bool resizing, bool activated,
const gfx::Vector2d& origin_offset, float raster_scale,
aura::Window::OcclusionState occlusion_state,
std::optional<chromeos::WindowStateType>) { return ++(*serial_ptr); },
&serial);
shell_surface->set_configure_callback(configure_callback);
// Resize the widget and set geometry.
aura::Env::GetInstance()->set_mouse_button_flags(ui::EF_LEFT_MOUSE_BUTTON);
ASSERT_TRUE(shell_surface->StartResize(HTBOTTOMRIGHT));
shell_surface->SetWidgetBounds(kNewBounds, /*adjusted by server=*/false);
shell_surface->SetGeometry(gfx::Rect(kNewBounds.size()));
// Client acknowledge configure for resizing. Shadow sizes should not be
// updated yet until commit.
shell_surface->AcknowledgeConfigure(serial);
EXPECT_EQ(gfx::Size(size.width() + kResizeShadowThickness, size.height()),
resize_shadow->GetLayerForTest()->bounds().size());
EXPECT_EQ(size, normal_shadow->content_bounds().size());
// Normal and resize shadow sizes are updated after commit.
shell_surface->root_surface()->Commit();
EXPECT_EQ(gfx::Size(kNewBounds.width() + kResizeShadowThickness,
kNewBounds.height()),
resize_shadow->GetLayerForTest()->bounds().size());
EXPECT_EQ(kNewBounds.size(), normal_shadow->content_bounds().size());
// Explicitly ends the drag here.
ash::Shell::Get()->toplevel_window_event_handler()->CompleteDragForTesting(
ash::ToplevelWindowEventHandler::DragResult::SUCCESS);
// Hide Shadow
event_generator->MoveMouseTo({0, 0});
EXPECT_FALSE(resize_shadow->visible());
// Move
UpdateDisplay("800x600,1200x1000");
shell_surface->GetWidget()->SetBounds({{1000, 100}, kNewBounds.size()});
shell_surface->AcknowledgeConfigure(serial);
shell_surface->root_surface()->Commit();
auto* screen = display::Screen::GetScreen();
int64_t secondary_id =
display::test::DisplayManagerTestApi(ash::Shell::Get()->display_manager())
.GetSecondaryDisplay()
.id();
ASSERT_EQ(secondary_id, screen
->GetDisplayNearestWindow(
shell_surface->GetWidget()->GetNativeWindow())
.id());
// Use outside to start drag because exo consumes events inside.
event_generator->MoveMouseTo({999, 99});
gfx::Rect bounds = shell_surface->GetWidget()->GetNativeWindow()->bounds();
EXPECT_TRUE(resize_shadow->visible());
gfx::Rect expected_shadow_on_secondary(
bounds.x() - kResizeShadowThickness, bounds.y() - kResizeShadowThickness,
bounds.width() + kResizeShadowThickness,
bounds.height() + kResizeShadowThickness);
EXPECT_EQ(expected_shadow_on_secondary,
resize_shadow->GetLayerForTest()->bounds());
aura::Env::GetInstance()->set_mouse_button_flags(ui::EF_LEFT_MOUSE_BUTTON);
constexpr gfx::Rect kResizedBoundsOn2nd{950, 50, 150, 150};
ASSERT_TRUE(shell_surface->StartResize(HTTOPLEFT));
shell_surface->GetWidget()->SetBounds(kResizedBoundsOn2nd);
shell_surface->SetGeometry(gfx::Rect(kResizedBoundsOn2nd.size()));
shell_surface->AcknowledgeConfigure(serial);
EXPECT_EQ(expected_shadow_on_secondary,
resize_shadow->GetLayerForTest()->bounds());
shell_surface->root_surface()->Commit();
constexpr gfx::Rect kExpectedShadowBoundsOn2nd(
150 - kResizeShadowThickness, 50 - kResizeShadowThickness,
kResizedBoundsOn2nd.width() + kResizeShadowThickness,
kResizedBoundsOn2nd.height() + kResizeShadowThickness);
EXPECT_EQ(kExpectedShadowBoundsOn2nd,
resize_shadow->GetLayerForTest()->bounds());
}
// Make sure that resize shadow updates as soon as widget bounds change when
// the window property |aura::client::kUseWindowBoundsForShadow| is false.
TEST_F(ShellSurfaceTest, ResizeShadowDependentBounds) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({64, 64}).BuildShellSurface();
shell_surface->OnSetServerStartResize();
shell_surface->root_surface()->Commit();
ASSERT_TRUE(shell_surface->GetWidget());
auto* widget = shell_surface->GetWidget();
gfx::Size size = widget->GetWindowBoundsInScreen().size();
widget->SetBounds(gfx::Rect(size));
// Starts mouse event to make sure resize shadow is created.
ui::test::EventGenerator* event_generator = GetEventGenerator();
event_generator->MoveMouseTo(size.width() + 2, size.height() / 2);
// Creates resize shadow and normal shadow for resizable exo window.
ash::ResizeShadow* resize_shadow =
ash::Shell::Get()->resize_shadow_controller()->GetShadowForWindowForTest(
widget->GetNativeWindow());
ASSERT_TRUE(resize_shadow);
shell_surface->root_surface()->SetFrame(SurfaceFrameType::SHADOW);
shell_surface->root_surface()->Commit();
EXPECT_FALSE(widget->GetNativeWindow()->GetProperty(
aura::client::kUseWindowBoundsForShadow));
// Override the property to update the shadow bounds immediately.
widget->GetNativeWindow()->SetProperty(
aura::client::kUseWindowBoundsForShadow, true);
ui::Shadow* normal_shadow =
wm::ShadowController::GetShadowForWindow(widget->GetNativeWindow());
ASSERT_TRUE(normal_shadow);
// ash::ResizeShadow::InitParams set the default |thickness| to 8.
const int kResizeShadowThickness = 8;
EXPECT_EQ(gfx::Size(size.width() + kResizeShadowThickness, size.height()),
resize_shadow->GetLayerForTest()->bounds().size());
EXPECT_EQ(size, normal_shadow->content_bounds().size());
gfx::Size new_size(100, 100);
gfx::Rect new_bounds(new_size);
aura::Env::GetInstance()->set_mouse_button_flags(ui::EF_LEFT_MOUSE_BUTTON);
// Resize the widget and set geometry.
ASSERT_TRUE(shell_surface->StartResize(HTBOTTOMRIGHT));
shell_surface->SetWidgetBounds(new_bounds, /*adjusted_by_server=*/false);
shell_surface->SetGeometry(new_bounds);
// Shadow bounds are updated as soon as the widget bounds change.
EXPECT_EQ(
gfx::Size(new_size.width() + kResizeShadowThickness, new_size.height()),
resize_shadow->GetLayerForTest()->bounds().size());
EXPECT_EQ(new_size, normal_shadow->content_bounds().size());
}
TEST_F(ShellSurfaceTest, PropertyResolverTest) {
class TestPropertyResolver : public exo::WMHelper::AppPropertyResolver {
public:
TestPropertyResolver() = default;
~TestPropertyResolver() override = default;
void PopulateProperties(
const Params& params,
ui::PropertyHandler& out_properties_container) override {
if (expected_app_id == params.app_id) {
out_properties_container.AcquireAllPropertiesFrom(
std::move(params.for_creation ? properties_for_creation
: properties_after_creation));
}
}
std::string expected_app_id;
ui::PropertyHandler properties_for_creation;
ui::PropertyHandler properties_after_creation;
};
std::unique_ptr<TestPropertyResolver> resolver_holder =
std::make_unique<TestPropertyResolver>();
auto* resolver = resolver_holder.get();
WMHelper::GetInstance()->RegisterAppPropertyResolver(
std::move(resolver_holder));
resolver->properties_for_creation.SetProperty(ash::kShelfItemTypeKey, 1);
resolver->properties_after_creation.SetProperty(ash::kShelfItemTypeKey, 2);
resolver->expected_app_id = "test";
// Make sure that properties are properly populated for both
// "before widget creation", and "after widget creation".
{
auto shell_surface =
test::ShellSurfaceBuilder({256, 256}).SetNoCommit().BuildShellSurface();
auto* surface = shell_surface->root_surface();
surface->SetApplicationId("test");
surface->Commit();
EXPECT_EQ(1, shell_surface->GetWidget()->GetNativeWindow()->GetProperty(
ash::kShelfItemTypeKey));
surface->SetApplicationId("test");
EXPECT_EQ(2, shell_surface->GetWidget()->GetNativeWindow()->GetProperty(
ash::kShelfItemTypeKey));
}
// Make sure that properties are will not be popluated when the app ids
// mismatch "before" and "after" widget is created.
resolver->properties_for_creation.SetProperty(ash::kShelfItemTypeKey, 1);
resolver->properties_after_creation.SetProperty(ash::kShelfItemTypeKey, 2);
{
auto shell_surface =
test::ShellSurfaceBuilder({256, 256}).SetNoCommit().BuildShellSurface();
auto* surface = shell_surface->root_surface();
surface->SetApplicationId("testx");
surface->Commit();
EXPECT_NE(1, shell_surface->GetWidget()->GetNativeWindow()->GetProperty(
ash::kShelfItemTypeKey));
surface->SetApplicationId("testy");
surface->Commit();
EXPECT_NE(1, shell_surface->GetWidget()->GetNativeWindow()->GetProperty(
ash::kShelfItemTypeKey));
EXPECT_NE(2, shell_surface->GetWidget()->GetNativeWindow()->GetProperty(
ash::kShelfItemTypeKey));
// Updating to the matching |app_id| should set the window property.
surface->SetApplicationId("test");
EXPECT_EQ(2, shell_surface->GetWidget()->GetNativeWindow()->GetProperty(
ash::kShelfItemTypeKey));
}
}
TEST_F(ShellSurfaceTest, Overlay) {
auto shell_surface =
test::ShellSurfaceBuilder({100, 100}).BuildShellSurface();
shell_surface->GetWidget()->GetNativeWindow()->SetProperty(
aura::client::kSkipImeProcessing, true);
EXPECT_FALSE(shell_surface->HasOverlay());
auto textfield = std::make_unique<views::Textfield>();
auto* textfield_ptr = textfield.get();
ShellSurfaceBase::OverlayParams params(std::move(textfield));
shell_surface->AddOverlay(std::move(params));
EXPECT_TRUE(shell_surface->HasOverlay());
EXPECT_NE(shell_surface->GetWidget()->GetFocusManager()->GetFocusedView(),
textfield_ptr);
EXPECT_TRUE(shell_surface->GetWidget()->widget_delegate()->CanResize());
EXPECT_EQ(gfx::Size(100, 100), shell_surface->overlay_widget_for_testing()
->GetWindowBoundsInScreen()
.size());
PressAndReleaseKey(ui::VKEY_X);
EXPECT_EQ(textfield_ptr->GetText(), u"");
ui::test::EventGenerator* generator = GetEventGenerator();
generator->MoveMouseToCenterOf(shell_surface->GetWidget()->GetNativeWindow());
generator->ClickLeftButton();
// Test normal key input, which should go through IME.
EXPECT_EQ(shell_surface->GetWidget()->GetFocusManager()->GetFocusedView(),
textfield_ptr);
PressAndReleaseKey(ui::VKEY_X);
EXPECT_EQ(textfield_ptr->GetText(), u"x");
EXPECT_TRUE(textfield_ptr->GetSelectedText().empty());
// Controls (Select all) should work.
PressAndReleaseKey(ui::VKEY_A, ui::EF_CONTROL_DOWN);
EXPECT_EQ(textfield_ptr->GetText(), u"x");
EXPECT_EQ(textfield_ptr->GetSelectedText(), u"x");
auto* widget = ash::TestWidgetBuilder()
.SetBounds(gfx::Rect(200, 200))
.BuildOwnedByNativeWidget();
ASSERT_TRUE(widget->IsActive());
PressAndReleaseKey(ui::VKEY_Y);
EXPECT_EQ(textfield_ptr->GetText(), u"x");
EXPECT_EQ(textfield_ptr->GetSelectedText(), u"x");
// Re-activate the surface and make sure that the overlay can still handle
// keys.
shell_surface->GetWidget()->Activate();
// The current text will be replaced with new character because
// the text is selected.
PressAndReleaseKey(ui::VKEY_Y);
EXPECT_EQ(textfield_ptr->GetText(), u"y");
EXPECT_TRUE(textfield_ptr->GetSelectedText().empty());
}
TEST_F(ShellSurfaceTest, OverlayOverlapsFrame) {
auto shell_surface = test::ShellSurfaceBuilder({100, 100})
.SetFrame(SurfaceFrameType::NORMAL)
.BuildShellSurface();
shell_surface->GetWidget()->GetNativeWindow()->SetProperty(
aura::client::kSkipImeProcessing, true);
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();
overlay_size.set_height(
overlay_size.height() -
views::GetCaptionButtonLayoutSize(
views::CaptionButtonLayoutSize::kNonBrowserCaption)
.height());
EXPECT_EQ(overlay_size, shell_surface->overlay_widget_for_testing()
->GetWindowBoundsInScreen()
.size());
}
shell_surface->OnSetFrame(SurfaceFrameType::NONE);
{
gfx::Size overlay_size =
shell_surface->GetWidget()->GetWindowBoundsInScreen().size();
EXPECT_EQ(overlay_size, shell_surface->overlay_widget_for_testing()
->GetWindowBoundsInScreen()
.size());
}
}
TEST_F(ShellSurfaceTest, OverlayCanResize) {
auto shell_surface = test::ShellSurfaceBuilder({100, 100})
.SetFrame(SurfaceFrameType::NORMAL)
.BuildShellSurface();
shell_surface->GetWidget()->GetNativeWindow()->SetProperty(
aura::client::kSkipImeProcessing, true);
EXPECT_FALSE(shell_surface->HasOverlay());
{
ShellSurfaceBase::OverlayParams params(std::make_unique<views::View>());
params.can_resize = false;
shell_surface->AddOverlay(std::move(params));
}
EXPECT_FALSE(shell_surface->GetWidget()->widget_delegate()->CanResize());
shell_surface->RemoveOverlay();
{
ShellSurfaceBase::OverlayParams params(std::make_unique<views::View>());
params.can_resize = true;
shell_surface->AddOverlay(std::move(params));
}
EXPECT_TRUE(shell_surface->GetWidget()->widget_delegate()->CanResize());
}
class TestWindowObserver : public WMHelper::ExoWindowObserver {
public:
TestWindowObserver() {}
TestWindowObserver(const TestWindowObserver&) = delete;
TestWindowObserver& operator=(const TestWindowObserver&) = delete;
// WMHelper::ExoWindowObserver overrides
void OnExoWindowCreated(aura::Window* window) override {
windows_.push_back(window);
}
const std::vector<raw_ptr<aura::Window, VectorExperimental>>&
observed_windows() {
return windows_;
}
private:
std::vector<raw_ptr<aura::Window, VectorExperimental>> windows_;
};
TEST_F(ShellSurfaceTest, NotifyOnWindowCreation) {
auto shell_surface =
test::ShellSurfaceBuilder({100, 100}).SetNoCommit().BuildShellSurface();
TestWindowObserver observer;
WMHelper::GetInstance()->AddExoWindowObserver(&observer);
// Committing a surface triggers window creation if it isn't already attached
// to the root.
shell_surface->surface_for_testing()->Commit();
EXPECT_EQ(1u, observer.observed_windows().size());
}
TEST_F(ShellSurfaceTest, Reparent) {
auto shell_surface1 = test::ShellSurfaceBuilder({20, 20}).BuildShellSurface();
views::Widget* widget1 = shell_surface1->GetWidget();
// Create a second window.
auto shell_surface2 = test::ShellSurfaceBuilder({20, 20}).BuildShellSurface();
views::Widget* widget2 = shell_surface2->GetWidget();
auto child_shell_surface =
test::ShellSurfaceBuilder({20, 20}).BuildShellSurface();
child_shell_surface->SetParent(shell_surface1.get());
views::Widget* child_widget = child_shell_surface->GetWidget();
// By default, a child widget is not activatable. Explicitly make it
// activatable so that calling child_surface->RequestActivation() is
// possible.
child_widget->widget_delegate()->SetCanActivate(true);
GrantPermissionToActivateIndefinitely(widget1->GetNativeWindow());
GrantPermissionToActivateIndefinitely(widget2->GetNativeWindow());
GrantPermissionToActivateIndefinitely(child_widget->GetNativeWindow());
shell_surface2->Activate();
EXPECT_FALSE(child_widget->ShouldPaintAsActive());
EXPECT_FALSE(widget1->ShouldPaintAsActive());
EXPECT_TRUE(widget2->ShouldPaintAsActive());
child_shell_surface->Activate();
// A widget should have the same paint-as-active state with its parent.
EXPECT_TRUE(child_widget->ShouldPaintAsActive());
EXPECT_TRUE(widget1->ShouldPaintAsActive());
EXPECT_FALSE(widget2->ShouldPaintAsActive());
// Reparent child to widget2.
child_shell_surface->SetParent(shell_surface2.get());
EXPECT_TRUE(child_widget->ShouldPaintAsActive());
EXPECT_TRUE(widget2->ShouldPaintAsActive());
EXPECT_FALSE(widget1->ShouldPaintAsActive());
shell_surface1->Activate();
EXPECT_FALSE(child_widget->ShouldPaintAsActive());
EXPECT_FALSE(widget2->ShouldPaintAsActive());
EXPECT_TRUE(widget1->ShouldPaintAsActive());
// Delete widget1 (i.e. the non-parent widget) shouldn't crash.
widget1->Close();
shell_surface1.reset();
child_shell_surface->Activate();
EXPECT_TRUE(child_widget->ShouldPaintAsActive());
EXPECT_TRUE(widget2->ShouldPaintAsActive());
}
TEST_F(ShellSurfaceTest, ThrottleFrameRate) {
auto shell_surface = test::ShellSurfaceBuilder({20, 20}).BuildShellSurface();
SurfaceObserverForTest observer(
shell_surface->root_surface()->window()->GetOcclusionState());
shell_surface->root_surface()->AddSurfaceObserver(&observer);
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
EXPECT_CALL(observer, ThrottleFrameRate(true));
window->SetProperty(ash::kFrameRateThrottleKey, true);
EXPECT_CALL(observer, ThrottleFrameRate(false));
window->SetProperty(ash::kFrameRateThrottleKey, false);
shell_surface->root_surface()->RemoveSurfaceObserver(&observer);
}
// Tests raster scale changes happen before occlusion changes on entering
// overview mode, and raster scale changes happen after occlusion changes on
// exiting overview mode. This to ensure windows that become visible do not get
// rastered twice.
TEST_F(ShellSurfaceTest, RasterScaleChangeVsOcclusionChangeOrder) {
ash::Shell::Get()
->raster_scale_controller()
->set_raster_scale_slop_proportion_for_testing(0.0f);
ash::Shell::Get()->overview_controller()->set_windows_have_snapshot_for_test(
false);
auto* overview_controller = ash::Shell::Get()->overview_controller();
overview_controller->set_occlusion_pause_duration_for_end_for_test(
base::Milliseconds(1));
std::vector<ConfigureData> config_vec;
auto shell_surface =
test::ShellSurfaceBuilder({100, 100})
.SetConfigureCallback(base::BindRepeating(
&ConfigureSerialVec, base::Unretained(&config_vec)))
.BuildShellSurface();
auto* surface = shell_surface->root_surface();
aura::Window* root_window = surface->window();
aura::Window* widget_window = shell_surface->GetWidget()->GetNativeWindow();
shell_surface->Minimize();
// Initial configure and configure for minimize.
EXPECT_EQ(2u, config_vec.size());
ui::ScopedAnimationDurationScaleMode test_duration_mode(
ui::ScopedAnimationDurationScaleMode::NON_ZERO_DURATION);
EXPECT_EQ(aura::Window::OcclusionState::HIDDEN,
root_window->GetOcclusionState());
EXPECT_EQ(1.0, widget_window->GetProperty(aura::client::kRasterScale));
overview_controller->StartOverview(ash::OverviewStartAction::kTests);
ash::WaitForOverviewEnterAnimation();
EXPECT_EQ(aura::Window::OcclusionState::VISIBLE,
root_window->GetOcclusionState());
EXPECT_NE(1.0, widget_window->GetProperty(aura::client::kRasterScale));
// Make sure raster scale was changed first.
ASSERT_EQ(4u, config_vec.size());
EXPECT_NE(1.0, config_vec[2].raster_scale);
EXPECT_EQ(aura::Window::OcclusionState::HIDDEN,
config_vec[2].occlusion_state);
EXPECT_NE(1.0, config_vec[3].raster_scale);
EXPECT_EQ(aura::Window::OcclusionState::VISIBLE,
config_vec[3].occlusion_state);
ash::WaitForOverviewEnterAnimation();
// No extra configure should occur.
EXPECT_EQ(4u, config_vec.size());
overview_controller->EndOverview(ash::OverviewEndAction::kTests);
EXPECT_EQ(aura::Window::OcclusionState::VISIBLE,
root_window->GetOcclusionState());
EXPECT_NE(1.0, widget_window->GetProperty(aura::client::kRasterScale));
// No extra configure should occur.
EXPECT_EQ(4u, config_vec.size());
ash::WaitForOverviewExitAnimation();
// Overview mode on exiting pauses the occlusion tracker for a while.
ash::WaitForOcclusionStateChange(root_window,
aura::Window::OcclusionState::HIDDEN);
EXPECT_EQ(1.0, widget_window->GetProperty(aura::client::kRasterScale));
// Make sure occlusion is updated before raster scale.
ASSERT_EQ(6u, config_vec.size());
EXPECT_NE(1.0, config_vec[4].raster_scale);
EXPECT_EQ(aura::Window::OcclusionState::HIDDEN,
config_vec[4].occlusion_state);
EXPECT_EQ(1.0, config_vec[5].raster_scale);
EXPECT_EQ(aura::Window::OcclusionState::HIDDEN,
config_vec[5].occlusion_state);
}
TEST_F(ShellSurfaceTest, ThrottleFrameRateViaController) {
ash::FrameThrottlingController* frame_throttling_controller =
ash::Shell::Get()->frame_throttling_controller();
for (auto app_type : {ash::AppType::LACROS, ash::AppType::BROWSER,
ash::AppType::CROSTINI_APP}) {
auto shell_surface = test::ShellSurfaceBuilder({20, 20})
.SetAppType(app_type)
.BuildShellSurface();
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
frame_throttling_controller->StartThrottling({window});
// Crostini should not be throttled currently.
const auto should_throttle_set =
app_type != ash::AppType::CROSTINI_APP
? testing::UnorderedElementsAreArray(
{shell_surface->GetSurfaceId().frame_sink_id()})
: testing::UnorderedElementsAreArray<viz::FrameSinkId>({});
EXPECT_THAT(frame_throttling_controller->GetFrameSinkIdsToThrottle(),
should_throttle_set);
// ash::kFrameRateThrottleKey is only set for lacros.
const bool should_set_property = app_type == ash::AppType::LACROS;
EXPECT_EQ(should_set_property,
window->GetProperty(ash::kFrameRateThrottleKey));
}
}
TEST_F(ShellSurfaceTest, ThrottleFrameRateViaControllerArc) {
ash::MockFrameThrottlingObserver observer;
ash::FrameThrottlingController* frame_throttling_controller =
ash::Shell::Get()->frame_throttling_controller();
frame_throttling_controller->AddArcObserver(&observer);
auto shell_surface = test::ShellSurfaceBuilder({20, 20})
.SetAppType(ash::AppType::ARC_APP)
.BuildShellSurface();
aura::Window* window = shell_surface->GetWidget()->GetNativeWindow();
EXPECT_CALL(observer,
OnThrottlingStarted(
testing::UnorderedElementsAreArray({window}),
frame_throttling_controller->GetCurrentThrottledFrameRate()));
frame_throttling_controller->StartThrottling({window});
frame_throttling_controller->RemoveArcObserver(&observer);
}
namespace {
struct ShellSurfaceCallbacks {
struct ConfigureState {
gfx::Rect bounds;
chromeos::WindowStateType state_type;
bool resizing;
bool activated;
};
uint32_t OnConfigure(
const gfx::Rect& bounds,
chromeos::WindowStateType state_type,
bool resizing,
bool activated,
const gfx::Vector2d& origin_offset,
float raster_scale,
aura::Window::OcclusionState occlusion_state,
std::optional<chromeos::WindowStateType> restore_state_type) {
configure_state.emplace();
*configure_state = {bounds, state_type, resizing, activated};
return ++serial;
}
void OnOriginChange(const gfx::Point& origin_) { origin = origin_; }
void Reset() {
configure_state.reset();
origin.reset();
}
std::optional<ConfigureState> configure_state;
std::optional<gfx::Point> origin;
int32_t serial = 0;
};
} // namespace
TEST_F(ShellSurfaceTest, DragWithHTCLIENT) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({64, 64}).BuildShellSurface();
ShellSurfaceCallbacks callbacks;
shell_surface->set_configure_callback(base::BindRepeating(
&ShellSurfaceCallbacks::OnConfigure, base::Unretained(&callbacks)));
ash::WindowState::Get(shell_surface->GetWidget()->GetNativeWindow())
->CreateDragDetails(gfx::PointF(0, 0), HTCLIENT,
::wm::WINDOW_MOVE_SOURCE_TOUCH);
shell_surface->GetWidget()->SetBounds(gfx::Rect(0, 0, 80, 80));
shell_surface->AcknowledgeConfigure(callbacks.serial);
shell_surface->root_surface()->Commit();
}
TEST_F(ShellSurfaceTest, ScreenCoordinates) {
exo::test::TestSecurityDelegate securityDelegate;
securityDelegate.SetCanSetBounds(
SecurityDelegate::SetBoundsPolicy::DCHECK_IF_DECORATED);
auto shell_surface = test::ShellSurfaceBuilder({20, 20})
.SetSecurityDelegate(&securityDelegate)
.BuildShellSurface();
ShellSurfaceCallbacks callbacks;
shell_surface->set_configure_callback(base::BindRepeating(
&ShellSurfaceCallbacks::OnConfigure, base::Unretained(&callbacks)));
shell_surface->set_origin_change_callback(base::BindRepeating(
&ShellSurfaceCallbacks::OnOriginChange, base::Unretained(&callbacks)));
shell_surface->SetWindowBounds(gfx::Rect(10, 10, 300, 300));
ASSERT_TRUE(!!callbacks.configure_state);
ASSERT_TRUE(!callbacks.origin);
EXPECT_EQ(callbacks.configure_state->bounds, gfx::Rect(10, 10, 300, 300));
callbacks.Reset();
shell_surface->SetWindowBounds(gfx::Rect(100, 100, 300, 300));
ASSERT_TRUE(!callbacks.configure_state);
ASSERT_TRUE(!!callbacks.origin);
EXPECT_EQ(*callbacks.origin, gfx::Point(100, 100));
callbacks.Reset();
shell_surface->SetWindowBounds(gfx::Rect(0, 0, 300000, 300000));
ASSERT_TRUE(!!callbacks.configure_state);
EXPECT_EQ(callbacks.configure_state->bounds,
display::Screen::GetScreen()->GetPrimaryDisplay().work_area());
}
TEST_F(ShellSurfaceTest, InitialBounds) {
{
gfx::Size size(20, 30);
auto shell_surface =
test::ShellSurfaceBuilder(size).SetNoCommit().BuildShellSurface();
EXPECT_FALSE(shell_surface->GetWidget());
gfx::Rect bounds(gfx::Point(35, 135), size);
shell_surface->SetWindowBounds(bounds);
shell_surface->root_surface()->Commit();
ASSERT_TRUE(shell_surface->GetWidget());
EXPECT_EQ(bounds, shell_surface->GetWidget()->GetWindowBoundsInScreen());
}
{
// Requesting larger than display work area bounds should not be allowed.
gfx::Size size(2000, 3000);
auto shell_surface =
test::ShellSurfaceBuilder(size).SetNoCommit().BuildShellSurface();
EXPECT_FALSE(shell_surface->GetWidget());
gfx::Rect bounds(gfx::Point(35, 135), size);
shell_surface->SetWindowBounds(bounds);
shell_surface->root_surface()->Commit();
ASSERT_TRUE(shell_surface->GetWidget());
EXPECT_EQ(display::Screen::GetScreen()->GetPrimaryDisplay().work_area(),
shell_surface->GetWidget()->GetWindowBoundsInScreen());
}
}
// Make sure that the centering logic can use the correct size
// even if there is a pending configure.
TEST_F(ShellSurfaceTest, InitialCenteredBoundsWithConfigure) {
auto shell_surface = test::ShellSurfaceBuilder(gfx::Size(0, 0))
.SetNoRootBuffer()
.SetNoCommit()
.BuildShellSurface();
EXPECT_FALSE(shell_surface->IsReady());
ShellSurfaceCallbacks callbacks;
shell_surface->set_configure_callback(base::BindRepeating(
&ShellSurfaceCallbacks::OnConfigure, base::Unretained(&callbacks)));
shell_surface->root_surface()->Commit();
EXPECT_FALSE(shell_surface->GetWidget()->IsVisible());
EXPECT_FALSE(shell_surface->IsReady());
gfx::Size size(256, 256);
auto new_buffer = test::ExoTestHelper::CreateBuffer(size);
shell_surface->root_surface()->Attach(new_buffer.get());
shell_surface->root_surface()->Commit();
EXPECT_TRUE(shell_surface->GetWidget()->IsVisible());
EXPECT_TRUE(shell_surface->IsReady());
gfx::Rect expected =
display::Screen::GetScreen()->GetPrimaryDisplay().work_area();
expected.ClampToCenteredSize(size);
EXPECT_EQ(expected, shell_surface->GetWidget()->GetWindowBoundsInScreen());
}
// Test that restore info is set correctly.
TEST_F(ShellSurfaceTest, SetRestoreInfo) {
int32_t restore_session_id = 200;
int32_t restore_window_id = 100;
gfx::Size size(20, 30);
auto shell_surface =
test::ShellSurfaceBuilder(size).SetNoCommit().BuildShellSurface();
shell_surface->SetRestoreInfo(restore_session_id, restore_window_id);
shell_surface->Restore();
shell_surface->root_surface()->Commit();
EXPECT_TRUE(shell_surface->GetWidget()->IsVisible());
EXPECT_EQ(restore_session_id,
shell_surface->GetWidget()->GetNativeWindow()->GetProperty(
app_restore::kWindowIdKey));
EXPECT_EQ(restore_window_id,
shell_surface->GetWidget()->GetNativeWindow()->GetProperty(
app_restore::kRestoreWindowIdKey));
}
TEST_F(ShellSurfaceTest, SetNotPersistable) {
auto shell_surface = test::ShellSurfaceBuilder(gfx::Size(20, 30))
.SetNoCommit()
.BuildShellSurface();
shell_surface->SetPersistable(/*persistable=*/false);
shell_surface->root_surface()->Commit();
EXPECT_TRUE(shell_surface->GetWidget()->IsVisible());
EXPECT_FALSE(shell_surface->GetWidget()->GetNativeWindow()->GetProperty(
wm::kPersistableKey));
}
// Test that restore id is set correctly.
TEST_F(ShellSurfaceTest, SetRestoreInfoWithWindowIdSource) {
int32_t restore_session_id = 200;
const std::string app_id = "app_id";
gfx::Size size(20, 30);
auto shell_surface =
test::ShellSurfaceBuilder(size).SetNoCommit().BuildShellSurface();
shell_surface->SetRestoreInfoWithWindowIdSource(restore_session_id, app_id);
shell_surface->Restore();
shell_surface->root_surface()->Commit();
EXPECT_TRUE(shell_surface->GetWidget()->IsVisible());
// FetchRestoreWindowId will return 0, because no app with id "app_id" is
// installed.
EXPECT_EQ(0, shell_surface->GetWidget()->GetNativeWindow()->GetProperty(
app_restore::kRestoreWindowIdKey));
EXPECT_EQ(app_id, *shell_surface->GetWidget()->GetNativeWindow()->GetProperty(
app_restore::kAppIdKey));
}
// Surfaces without non-client view should not crash.
TEST_F(ShellSurfaceTest, NoNonClientViewWithConfigure) {
exo::test::TestSecurityDelegate securityDelegate;
securityDelegate.SetCanSetBounds(
SecurityDelegate::SetBoundsPolicy::DCHECK_IF_DECORATED);
// Popup windows don't have a non-client view.
auto shell_surface = test::ShellSurfaceBuilder({20, 20})
.SetAsPopup()
.SetSecurityDelegate(&securityDelegate)
.BuildShellSurface();
ShellSurfaceCallbacks callbacks;
// Having a configure callback leads to a call to GetClientBoundsInScreen().
shell_surface->set_configure_callback(base::BindRepeating(
&ShellSurfaceCallbacks::OnConfigure, base::Unretained(&callbacks)));
shell_surface->SetWindowBounds(gfx::Rect(10, 10, 300, 300));
EXPECT_TRUE(callbacks.configure_state);
EXPECT_EQ(callbacks.configure_state->bounds, gfx::Rect(10, 10, 300, 300));
}
TEST_F(ShellSurfaceTest, WindowIsResizableWithEmptySizeConstraints) {
auto shell_surface = test::ShellSurfaceBuilder({20, 20})
.SetMinimumSize(gfx::Size(0, 0))
.SetMaximumSize(gfx::Size(0, 0))
.BuildShellSurface();
EXPECT_TRUE(shell_surface->GetWidget()->widget_delegate()->CanResize());
}
TEST_F(ShellSurfaceTest, SetSystemModal) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetUseSystemModalContainer()
.SetNoCommit()
.BuildShellSurface();
shell_surface->SetSystemModal(true);
shell_surface->root_surface()->Commit();
EXPECT_EQ(ui::MODAL_TYPE_SYSTEM, shell_surface->GetModalType());
EXPECT_FALSE(shell_surface->frame_enabled());
}
TEST_F(ShellSurfaceTest, PipInitialPosition) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetUseSystemModalContainer()
.SetNoCommit()
.BuildShellSurface();
shell_surface->SetWindowBounds(gfx::Rect(20, 20, 256, 256));
shell_surface->SetPip();
shell_surface->root_surface()->Commit();
// PIP positioner place the PIP window to the edge that is closer to the given
// position
EXPECT_EQ(gfx::Rect(8, 20, 256, 256),
shell_surface->GetWidget()->GetWindowBoundsInScreen());
}
TEST_F(ShellSurfaceTest, PostWindowChangeCallback) {
chromeos::WindowStateType state_type = chromeos::WindowStateType::kDefault;
auto test_callback = base::BindRepeating(
[](chromeos::WindowStateType* state_type, const gfx::Rect&,
chromeos::WindowStateType new_type, bool, bool, const gfx::Vector2d&,
float, aura::Window::OcclusionState,
std::optional<chromeos::WindowStateType>) -> uint32_t {
*state_type = new_type;
return 0;
},
&state_type);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256}).BuildShellSurface();
shell_surface->set_configure_callback(test_callback);
auto* state = ash::WindowState::Get(
shell_surface->GetWidget()->GetNativeWindow()->GetToplevelWindow());
// Make sure we are in a non-snapped state before testing state change.
ASSERT_FALSE(state->IsSnapped());
auto snap_event =
std::make_unique<ash::WindowSnapWMEvent>(ash::WM_EVENT_SNAP_PRIMARY);
// Trigger a snap event, this should cause a configure event.
state->OnWMEvent(snap_event.get());
EXPECT_EQ(state_type, chromeos::WindowStateType::kPrimarySnapped);
}
// A single configuration event should be sent when both the bounds and the
// window state change.
TEST_F(ShellSurfaceTest, ConfigureOnlySentOnceForBoundsAndWindowStateChange) {
int times_configured = 0;
auto test_callback = base::BindRepeating(
[](int* times_configured, const gfx::Rect&,
chromeos::WindowStateType new_type, bool, bool, const gfx::Vector2d&,
float, aura::Window::OcclusionState,
std::optional<chromeos::WindowStateType>) -> uint32_t {
++(*times_configured);
return 0;
},
&times_configured);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({1, 1}).BuildShellSurface();
shell_surface->set_configure_callback(test_callback);
auto* state = ash::WindowState::Get(
shell_surface->GetWidget()->GetNativeWindow()->GetToplevelWindow());
// Make sure we are in normal mode. Maximizing from this state should result
// in BOTH the bounds and state changing.
ASSERT_TRUE(state->IsNormalStateType());
auto maximize_event = std::make_unique<ash::WMEvent>(ash::WM_EVENT_MAXIMIZE);
// Trigger a snap event, this should cause a configure event.
state->OnWMEvent(maximize_event.get());
// The bounds change event should have been suppressed because the window
// state is changing.
EXPECT_EQ(times_configured, 1);
}
TEST_F(ShellSurfaceTest, SetImmersiveModeTriggersConfigure) {
int times_configured = 0;
auto test_callback = base::BindRepeating(
[](int* times_configured, const gfx::Rect&,
chromeos::WindowStateType new_type, bool, bool, const gfx::Vector2d&,
float, aura::Window::OcclusionState,
std::optional<chromeos::WindowStateType>) -> uint32_t {
++(*times_configured);
return 0;
},
&times_configured);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({1, 1}).BuildShellSurface();
shell_surface->set_configure_callback(test_callback);
shell_surface->SetUseImmersiveForFullscreen(true);
EXPECT_EQ(times_configured, 1);
}
TEST_F(ShellSurfaceTest,
SetRasterScaleWindowPropertyConfiguresRasterScaleAndWaitsForAck) {
ConfigureData config_data;
constexpr gfx::Size kBufferSize(256, 256);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder(kBufferSize).BuildShellSurface();
shell_surface->set_configure_callback(
base::BindRepeating(&Configure, base::Unretained(&config_data)));
auto* window = shell_surface->GetWidget()->GetNativeWindow();
window->SetProperty(aura::client::kRasterScale, 0.1f);
shell_surface->AcknowledgeConfigure(0);
EXPECT_EQ(0.1f, config_data.raster_scale);
window->SetProperty(aura::client::kRasterScale, 1.0f);
shell_surface->AcknowledgeConfigure(0);
EXPECT_EQ(1.0f, config_data.raster_scale);
}
TEST_F(ShellSurfaceTest, MoveParentWithoutWidget) {
UpdateDisplay("800x600, 800x600");
constexpr gfx::Size kSize{256, 256};
std::unique_ptr<ShellSurface> parent_surface =
test::ShellSurfaceBuilder(kSize).BuildShellSurface();
std::unique_ptr<ShellSurface> child_surface =
test::ShellSurfaceBuilder(kSize).SetNoCommit().BuildShellSurface();
child_surface->SetParent(parent_surface.get());
auto* parent_widget = parent_surface->GetWidget();
auto* root_before = parent_widget->GetNativeWindow()->GetRootWindow();
parent_widget->SetBounds({{1000, 0}, kSize});
// Crash (crbug.com/1395433) happens when a transient parent moved
// to another root window before widget is created. Make sure that
// happened.
EXPECT_NE(root_before, parent_widget->GetNativeWindow()->GetRootWindow());
}
// Assert SetShape() applies the shape to the host window's layer on commit.
TEST_F(ShellSurfaceTest, SetShapeAppliedAfterSurfaceCommit) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({64, 64}).BuildShellSurface();
shell_surface->OnSetServerStartResize();
shell_surface->OnSetFrame(SurfaceFrameType::SHADOW);
shell_surface->root_surface()->Commit();
const views::Widget* widget = shell_surface->GetWidget();
ASSERT_TRUE(widget);
// Windows shadows should be applied with resizing enabled.
EXPECT_NE(wm::kShadowElevationNone,
wm::GetShadowElevationConvertDefault(widget->GetNativeWindow()));
EXPECT_TRUE(shell_surface->server_side_resize());
// Create a window shape from two unique rects.
const cc::Region shape_region =
CreateRegion({{10, 10, 32, 32}, {20, 20, 32, 32}});
// Apply the shape to the surface. This should not yet be reflected on the
// host window's layer.
shell_surface->SetShape(shape_region);
const ui::Layer::ShapeRects* layer_shape_rects =
widget->GetNativeWindow()->layer()->alpha_shape();
EXPECT_FALSE(layer_shape_rects);
// After surface commit the shape should have been applied to the layer.
shell_surface->root_surface()->Commit();
layer_shape_rects = widget->GetNativeWindow()->layer()->alpha_shape();
EXPECT_TRUE(layer_shape_rects);
EXPECT_EQ(shape_region, CreateRegion(*layer_shape_rects));
// Window shadows and resizing should be disabled when window shapes are set.
EXPECT_EQ(wm::kShadowElevationNone,
wm::GetShadowElevationConvertDefault(widget->GetNativeWindow()));
EXPECT_FALSE(shell_surface->server_side_resize());
// Ensure the window targeter correctly passes through events to areas of the
// window not covered by the shape.
gfx::Rect target_bounds = widget->GetWindowBoundsInScreen();
ui::test::EventGenerator* event_generator = GetEventGenerator();
{
// Send an event to the point just outside of the region, it should not
// target the root surface.
gfx::Point location = target_bounds.origin() + gfx::Vector2d(9, 9);
ui::MouseEvent event(ui::ET_MOUSE_MOVED, location, location,
ui::EventTimeForNow(), 0, 0);
event_generator->Dispatch(&event);
EXPECT_NE(shell_surface->root_surface(),
GetTargetSurfaceForLocatedEvent(&event));
}
{
// Send an event to the point just within of the region, it should target
// the root surface.
gfx::Point location = target_bounds.origin() + gfx::Vector2d(11, 11);
ui::MouseEvent event(ui::ET_MOUSE_MOVED, location, location,
ui::EventTimeForNow(), 0, 0);
event_generator->Dispatch(&event);
EXPECT_EQ(shell_surface->root_surface(),
GetTargetSurfaceForLocatedEvent(&event));
}
}
// Assert SetShape() updates the host window's layer with the most recent shape
// when the surface commits.
TEST_F(ShellSurfaceTest, SetShapeUpdatesAndUnsetsCorrectlyAfterCommit) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({64, 64})
.SetFrame(SurfaceFrameType::SHADOW)
.BuildShellSurface();
shell_surface->OnSetServerStartResize();
shell_surface->root_surface()->Commit();
const views::Widget* widget = shell_surface->GetWidget();
ASSERT_TRUE(widget);
// Create several unique window shapes.
const cc::Region shape_region_1 =
CreateRegion({{5, 5, 32, 32}, {10, 10, 32, 32}});
const cc::Region shape_region_2 =
CreateRegion({{15, 15, 32, 32}, {20, 20, 32, 32}});
const cc::Region shape_region_3 =
CreateRegion({{25, 25, 32, 32}, {30, 40, 32, 32}});
// Apply two shapes to the surface without committing. Neither should be
// applied to the host window's layer.
shell_surface->SetShape(shape_region_1);
shell_surface->SetShape(shape_region_2);
const ui::Layer::ShapeRects* layer_shape_rects =
widget->GetNativeWindow()->layer()->alpha_shape();
EXPECT_FALSE(layer_shape_rects);
// After surface commit only the most recent shape should have been applied.
shell_surface->root_surface()->Commit();
layer_shape_rects = widget->GetNativeWindow()->layer()->alpha_shape();
EXPECT_TRUE(layer_shape_rects);
EXPECT_EQ(shape_region_2, CreateRegion(*layer_shape_rects));
// Apply another shape to the surface. The layer shape should not change.
shell_surface->SetShape(shape_region_3);
layer_shape_rects = widget->GetNativeWindow()->layer()->alpha_shape();
EXPECT_TRUE(layer_shape_rects);
EXPECT_EQ(shape_region_2, CreateRegion(*layer_shape_rects));
// The new shape should have been applied after the surface is committed.
shell_surface->root_surface()->Commit();
layer_shape_rects = widget->GetNativeWindow()->layer()->alpha_shape();
EXPECT_TRUE(layer_shape_rects);
EXPECT_EQ(shape_region_3, CreateRegion(*layer_shape_rects));
// Setting a null shape should unset the host window's layer shape.
shell_surface->SetShape(std::nullopt);
shell_surface->root_surface()->Commit();
layer_shape_rects = widget->GetNativeWindow()->layer()->alpha_shape();
EXPECT_FALSE(layer_shape_rects);
}
// SetShape() is not supported for windows with the frame enabled.
TEST_F(ShellSurfaceTest, SetShapeWithFrameNotSupported) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({64, 64})
.SetFrame(SurfaceFrameType::NORMAL)
.BuildShellSurface();
shell_surface->OnSetServerStartResize();
shell_surface->root_surface()->Commit();
const views::Widget* widget = shell_surface->GetWidget();
ASSERT_TRUE(widget);
// Create a window shape from two unique rects.
const cc::Region shape_region =
CreateRegion({{10, 10, 32, 32}, {20, 20, 32, 32}});
// Try to apply the shape to the surface and commit, this should have no
// effect.
shell_surface->SetShape(shape_region);
const ui::Layer::ShapeRects* layer_shape_rects =
widget->GetNativeWindow()->layer()->alpha_shape();
shell_surface->root_surface()->Commit();
layer_shape_rects = widget->GetNativeWindow()->layer()->alpha_shape();
EXPECT_FALSE(layer_shape_rects);
}
TEST_F(ShellSurfaceTest, MaximizedOrFullscreenInitialState) {
UpdateDisplay("800x600, 800x600");
constexpr gfx::Size kEmptySize{0, 0};
// on secondary display.
constexpr gfx::Rect kInitialBounds{800, 0, 100, 100};
const auto primary_display = GetPrimaryDisplay();
const auto secondary_display = GetSecondaryDisplay();
for (auto initial_state : {chromeos::WindowStateType::kMaximized,
chromeos::WindowStateType::kFullscreen}) {
std::stringstream ss;
ss << initial_state;
SCOPED_TRACE(ss.str());
gfx::Rect primary_bounds =
initial_state == chromeos::WindowStateType::kMaximized
? primary_display.work_area()
: primary_display.bounds();
gfx::Rect secondary_bounds =
initial_state == chromeos::WindowStateType::kMaximized
? secondary_display.work_area()
: secondary_display.bounds();
// While it is possible to start in fullscreen, SessionRestore restores the
// originally fullscreen window to maximized, so the fullscreen window won't
// have restore bounds.
bool verify_restore_bounds =
initial_state == chromeos::WindowStateType::kMaximized;
{
ConfigureData config_data;
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder(kEmptySize)
.SetConfigureCallback(base::BindRepeating(
&Configure, base::Unretained(&config_data)))
.SetWindowState(initial_state)
.BuildShellSurface();
EXPECT_EQ(1u, config_data.count);
EXPECT_EQ(initial_state, config_data.state_type);
EXPECT_EQ(primary_bounds, config_data.suggested_bounds);
}
{
ConfigureData config_data;
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder(kEmptySize)
.SetConfigureCallback(base::BindRepeating(
&Configure, base::Unretained(&config_data)))
.SetWindowState(initial_state)
.SetDisplayId(secondary_display.id())
.BuildShellSurface();
EXPECT_EQ(1u, config_data.count);
EXPECT_EQ(initial_state, config_data.state_type);
EXPECT_EQ(secondary_bounds, config_data.suggested_bounds);
}
{
ConfigureData config_data;
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder(kEmptySize)
.SetConfigureCallback(base::BindRepeating(
&Configure, base::Unretained(&config_data)))
.SetWindowState(initial_state)
.SetBounds(kInitialBounds)
.BuildShellSurface();
EXPECT_EQ(1u, config_data.count);
EXPECT_EQ(initial_state, config_data.state_type);
EXPECT_EQ(secondary_bounds, config_data.suggested_bounds);
EXPECT_EQ(secondary_bounds,
shell_surface->GetWidget()->GetWindowBoundsInScreen());
if (verify_restore_bounds) {
EXPECT_EQ(kInitialBounds,
shell_surface->GetWidget()->GetRestoredBounds());
}
}
{
ConfigureData config_data;
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder(kEmptySize)
.SetConfigureCallback(base::BindRepeating(
&Configure, base::Unretained(&config_data)))
.SetWindowState(initial_state)
.SetBounds(kInitialBounds)
.BuildShellSurface();
EXPECT_EQ(1u, config_data.count);
EXPECT_EQ(initial_state, config_data.state_type);
EXPECT_EQ(secondary_bounds, config_data.suggested_bounds);
if (verify_restore_bounds) {
EXPECT_EQ(kInitialBounds,
shell_surface->GetWidget()->GetRestoredBounds());
}
}
{
// The display id has higher priority.
ConfigureData config_data;
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder(kEmptySize)
.SetConfigureCallback(base::BindRepeating(
&Configure, base::Unretained(&config_data)))
.SetWindowState(initial_state)
.SetBounds(kInitialBounds)
.SetDisplayId(primary_display.id())
.BuildShellSurface();
EXPECT_EQ(1u, config_data.count);
EXPECT_EQ(initial_state, config_data.state_type);
EXPECT_EQ(primary_bounds, config_data.suggested_bounds);
if (verify_restore_bounds) {
EXPECT_EQ(kInitialBounds,
shell_surface->GetWidget()->GetRestoredBounds());
}
}
}
}
TEST_F(ShellSurfaceTest, MinimizedInitialState) {
constexpr gfx::Rect kInitialBounds(100, 50, 400, 300);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder()
.SetBounds(kInitialBounds)
.SetGeometry(gfx::Rect(kInitialBounds.size()))
.SetWindowState(chromeos::WindowStateType::kMinimized)
.BuildShellSurface();
EXPECT_TRUE(shell_surface->GetWidget()->IsMinimized());
EXPECT_EQ(kInitialBounds, shell_surface->GetWidget()->GetRestoredBounds());
// The buffer hasn't been attached yet.
ASSERT_FALSE(shell_surface->root_surface()->GetBuffer());
// Initial buffer arrives and the window should stay in minimized.
auto new_buffer = test::ExoTestHelper::CreateBuffer(kInitialBounds.size());
shell_surface->root_surface()->Attach(new_buffer.get());
shell_surface->root_surface()->Commit();
EXPECT_TRUE(shell_surface->GetWidget()->IsMinimized());
shell_surface->GetWidget()->Activate();
EXPECT_FALSE(shell_surface->GetWidget()->IsMinimized());
EXPECT_EQ(kInitialBounds,
shell_surface->GetWidget()->GetWindowBoundsInScreen());
}
TEST_F(ShellSurfaceTest, NoGeometryWidgetBoundsUpdate) {
constexpr gfx::Size kInitialSize(100, 100);
constexpr gfx::Size kLargerSize(256, 256);
constexpr gfx::Size kSmallerSize(50, 50);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder(kInitialSize).BuildShellSurface();
EXPECT_EQ(kInitialSize,
shell_surface->GetWidget()->GetWindowBoundsInScreen().size());
auto larger_buffer = test::ExoTestHelper::CreateBuffer(kLargerSize);
shell_surface->root_surface()->Attach(larger_buffer.get());
shell_surface->root_surface()->Commit();
EXPECT_EQ(kLargerSize,
shell_surface->GetWidget()->GetWindowBoundsInScreen().size());
auto smaller_buffer = test::ExoTestHelper::CreateBuffer(kSmallerSize);
shell_surface->root_surface()->Attach(smaller_buffer.get());
shell_surface->root_surface()->Commit();
EXPECT_EQ(kSmallerSize,
shell_surface->GetWidget()->GetWindowBoundsInScreen().size());
}
TEST_F(ShellSurfaceTest, SubpixelPositionOffset) {
UpdateDisplay("1200x800*1.6");
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetOrigin({20, 20})
.SetFrame(SurfaceFrameType::NORMAL)
.BuildShellSurface();
// Enabling a normal frame makes `GetClientViewBounds().origin()` return
// (0, 32), which makes the host window not align with any pixel boundary.
shell_surface->root_surface()->SetSurfaceHierarchyContentBoundsForTest(
gfx::Rect(-20, -20, 256, 256));
EXPECT_TRUE(shell_surface->OnPreWidgetCommit());
shell_surface->CommitWidget();
EXPECT_EQ(gfx::Point(20, 20), shell_surface->root_surface_origin_pixel());
EXPECT_EQ(gfx::Rect(-12, 20, 256, 256),
shell_surface->host_window()->bounds());
// Verify that 'root_surface_origin()' is exactly preservered in pixels with
// subpixel offset.
// (0, -0.125) is caused by the frame like above, and (-0.5, -0.5) is for
// 'root_surface_origin()'.
EXPECT_EQ(gfx::Vector2dF(-0.5, -0.625),
shell_surface->host_window()->layer()->GetSubpixelOffset());
}
// Make sure the shell surface with capture can be safely deleted
// even if the widget is not visible.
TEST_F(ShellSurfaceTest, DeleteWithGrab) {
auto shell_surface =
test::ShellSurfaceBuilder({200, 200}).BuildShellSurface();
auto popup_shell_surface = test::ShellSurfaceBuilder({100, 100})
.SetAsPopup()
.SetParent(shell_surface.get())
.SetGrab()
.BuildShellSurface();
popup_shell_surface->GetWidget()->GetNativeWindow()->layer()->SetVisible(
false);
popup_shell_surface.reset();
// Close with grab.
popup_shell_surface = test::ShellSurfaceBuilder({100, 100})
.SetAsPopup()
.SetParent(shell_surface.get())
.SetGrab()
.BuildShellSurface();
popup_shell_surface->GetWidget()->Close();
// Close is async.
base::RunLoop().RunUntilIdle();
popup_shell_surface.reset();
// CloseNow with grab.
popup_shell_surface = test::ShellSurfaceBuilder({100, 100})
.SetAsPopup()
.SetParent(shell_surface.get())
.SetGrab()
.BuildShellSurface();
popup_shell_surface->GetWidget()->CloseNow();
popup_shell_surface.reset();
}
TEST_F(ShellSurfaceTest, WindowPropertyChangedNotificationWithoutRootSurface) {
// Test OnWindowPropertyChanged() notification on a ShellSurface, whose root
// surface has gone.
auto* overview_controller = ash::Shell::Get()->overview_controller();
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetAppType(ash::AppType::LACROS)
.BuildShellSurface();
std::unique_ptr<ShellSurface> shell_surface1 =
test::ShellSurfaceBuilder({256, 256})
.SetAppType(ash::AppType::LACROS)
.BuildShellSurface();
overview_controller->StartOverview(ash::OverviewStartAction::kTests);
ash::WaitForOverviewEnterAnimation();
test::ShellSurfaceBuilder::DestroyRootSurface(shell_surface1.get());
// Destroying `shell_surface` will close its aura window, causing update of
// frame throttling in the overview mode for the remaining Lacros window(s).
// In this case, the remaining window is associated with `shell_surface1`. It
// receives OnWindowPropertyChanged() notification with
// ash::kFrameRateThrottleKey key. The root surface of `shell_surface1` has
// gone at this point. Verify that it doesn't cause crash.
shell_surface.reset();
overview_controller->EndOverview(ash::OverviewEndAction::kTests);
}
TEST_F(ShellSurfaceTest, SurfaceSyncWithShellSurfaceCreatedOnDisplayWithScale) {
ash::Shell::GetRootWindowForNewWindows()->layer()->OnDeviceScaleFactorChanged(
2.f);
// Empty means no initial size.
constexpr gfx::Size kEmptySize(0, 0);
ConfigureData config_data;
auto shell_surface =
test::ShellSurfaceBuilder(kEmptySize)
.SetNoCommit()
.SetConfigureCallback(base::BindRepeating(
&ConfigureSerial, base::Unretained(&config_data)))
.BuildShellSurface();
auto* surface = shell_surface->root_surface();
// Creates the shell widget, and add the surface_tree_host's window as child.
surface->Commit();
EXPECT_EQ(config_data.count, 1u);
EXPECT_EQ(config_data.suggested_bounds, gfx::Rect());
EXPECT_EQ(shell_surface->GetCommitTargetLayer(),
shell_surface->host_window()->layer());
EXPECT_EQ(shell_surface->GetSurfaceId(),
shell_surface->host_window()->GetSurfaceId());
}
TEST_F(ShellSurfaceTest,
DisplayScaleChangeTakesCompositorLockForMaximizedWindow) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256})
.SetWindowState(chromeos::WindowStateType::kMaximized)
.BuildShellSurface();
auto* surface = shell_surface->root_surface();
uint32_t serial = 0;
auto configure_callback = base::BindRepeating(
[](uint32_t* const serial_ptr, const gfx::Rect& bounds,
chromeos::WindowStateType state_type, bool resizing, bool activated,
const gfx::Vector2d& origin_offset, float raster_scale,
aura::Window::OcclusionState occlusion_state,
std::optional<chromeos::WindowStateType>) { return ++(*serial_ptr); },
&serial);
shell_surface->set_configure_callback(configure_callback);
ui::Compositor* compositor =
shell_surface->GetWidget()->GetNativeWindow()->layer()->GetCompositor();
EXPECT_FALSE(compositor->IsLocked());
auto* display_manager = ash::Shell::Get()->display_manager();
const auto display_id = display_manager->GetDisplayAt(0).id();
display_manager->ZoomDisplay(display_id, /*up=*/true);
// Compositor locked until maximized window updates.
EXPECT_TRUE(compositor->IsLocked());
shell_surface->AcknowledgeConfigure(serial);
EXPECT_TRUE(compositor->IsLocked());
surface->Commit();
EXPECT_FALSE(compositor->IsLocked());
display_manager->ZoomDisplay(display_id, /*up=*/false);
// Compositor locked until maximized window updates.
EXPECT_TRUE(compositor->IsLocked());
shell_surface->AcknowledgeConfigure(serial);
EXPECT_TRUE(compositor->IsLocked());
surface->Commit();
EXPECT_FALSE(compositor->IsLocked());
}
TEST_F(ShellSurfaceTest,
DisplayScaleChangeDoesNotTakeCompositorLockForFreeformWindow) {
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256}).BuildShellSurface();
ui::Compositor* compositor =
shell_surface->GetWidget()->GetNativeWindow()->layer()->GetCompositor();
EXPECT_FALSE(compositor->IsLocked());
auto* display_manager = ash::Shell::Get()->display_manager();
const auto display_id = display_manager->GetDisplayAt(0).id();
display_manager->ZoomDisplay(display_id, /*up=*/true);
// Should not take compositor lock.
EXPECT_FALSE(compositor->IsLocked());
display_manager->ZoomDisplay(display_id, /*up=*/false);
// Should not take compositor lock.
EXPECT_FALSE(compositor->IsLocked());
}
// Tests that updates to the display layout configuration update the origin on
// relevant hosted surfaces.
TEST_F(ShellSurfaceTest, DisplayLayoutConfigurationUpdatesSurfaceOrigin) {
// Start with a single display configuration.
UpdateDisplay("800x600");
// Create the surface.
constexpr gfx::Size kBufferSize(256, 256);
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder(kBufferSize).SetNoCommit().BuildShellSurface();
// Set origin and leave/enter callbacks.
gfx::Point client_origin;
int64_t old_display_id = display::kInvalidDisplayId;
int64_t new_display_id = display::kInvalidDisplayId;
shell_surface->set_origin_change_callback(base::BindLambdaForTesting(
[&](const gfx::Point& origin) { client_origin = origin; }));
shell_surface->root_surface()->set_leave_enter_callback(
base::BindLambdaForTesting([&](int64_t old_id, int64_t new_id) {
old_display_id = old_id;
new_display_id = new_id;
return true;
}));
// Creating a new shell surface should notify on which display it is created.
constexpr gfx::Point kInitialOrigin = {200, 200};
shell_surface->root_surface()->Commit();
shell_surface->GetWidget()->SetBounds({kInitialOrigin, kBufferSize});
EXPECT_EQ(display::kInvalidDisplayId, old_display_id);
EXPECT_EQ(GetPrimaryDisplay().id(), new_display_id);
EXPECT_EQ(kInitialOrigin, client_origin);
// Attaching a second display should not change where the surface is located.
UpdateDisplay("800x600,800x600");
EXPECT_EQ(kInitialOrigin, client_origin);
// Move the window to second display.
constexpr gfx::Point kNewOrigin = {1000, 200};
shell_surface->GetWidget()->SetBounds({kNewOrigin, kBufferSize});
EXPECT_EQ(GetPrimaryDisplay().id(), old_display_id);
EXPECT_EQ(GetSecondaryDisplay().id(), new_display_id);
EXPECT_EQ(kNewOrigin, client_origin);
// Reposition the second display, the surface should receive an origin change
// event representing the updated bounds in screen coordinates.
constexpr int kVerticalOffset = 200;
display::DisplayLayoutBuilder builder(GetPrimaryDisplay().id());
builder.SetSecondaryPlacement(GetSecondaryDisplay().id(),
display::DisplayPlacement::RIGHT,
kVerticalOffset);
ash::Shell::Get()->display_manager()->SetLayoutForCurrentDisplays(
builder.Build());
EXPECT_EQ(kNewOrigin + gfx::Vector2d(0, kVerticalOffset), client_origin);
}
// Tests the unnecessary occlusion events are fired when opaque buffer and no
// frame are used.
TEST_F(ShellSurfaceTest, DisplayScaleChangeDoesNotSendOcclusionUpdates) {
std::unique_ptr<ShellSurface> shell_surface1 =
test::ShellSurfaceBuilder({256, 256})
.SetRootBufferFormat(kOpaqueFormat)
.BuildShellSurface();
std::unique_ptr<ShellSurface> shell_surface2 =
test::ShellSurfaceBuilder({256, 256})
.SetRootBufferFormat(kOpaqueFormat)
.BuildShellSurface();
auto* surface1 = shell_surface1->root_surface();
auto* surface2 = shell_surface2->root_surface();
auto* window1 = shell_surface1->GetWidget()->GetNativeWindow();
auto* window2 = shell_surface2->GetWidget()->GetNativeWindow();
// xdg-shell without a frame type will use NOT_DRAWN layer type and
// should control the opacity by themselves.
EXPECT_EQ(ui::LAYER_NOT_DRAWN, window1->layer()->type());
EXPECT_FALSE(window1->GetProperty(chromeos::kWindowManagerManagesOpacityKey));
EXPECT_EQ(ui::LAYER_NOT_DRAWN, window2->layer()->type());
EXPECT_FALSE(window2->GetProperty(chromeos::kWindowManagerManagesOpacityKey));
const std::vector<gfx::Rect> kNormalOpaqueRegion{gfx::Rect(256, 256)};
// Normal window should have custom opacity regions.
EXPECT_TRUE(window1->GetTransparent());
EXPECT_TRUE(window2->GetTransparent());
EXPECT_EQ(kNormalOpaqueRegion, window1->opaque_regions_for_occlusion());
EXPECT_EQ(kNormalOpaqueRegion, window2->opaque_regions_for_occlusion());
// Test Maximzied State
shell_surface1->Maximize();
shell_surface2->Maximize();
EXPECT_FALSE(window1->GetTransparent());
EXPECT_FALSE(window2->GetTransparent());
const std::vector<gfx::Rect> kMaximizedOpaqueRegion{gfx::Rect(800, 552)};
EXPECT_EQ(kMaximizedOpaqueRegion, window1->opaque_regions_for_occlusion());
EXPECT_EQ(kMaximizedOpaqueRegion, window2->opaque_regions_for_occlusion());
// Update root surfaces (this happens asynchronously normally) and set
// occlusion tracking.
surface1->SetOcclusionTracking(true);
auto surface1_buffer =
test::ExoTestHelper::CreateBuffer(shell_surface1.get(), kOpaqueFormat);
surface1->Attach(surface1_buffer.get());
surface1->Commit();
surface2->SetOcclusionTracking(true);
auto surface2_buffer =
test::ExoTestHelper::CreateBuffer(shell_surface2.get(), kOpaqueFormat);
surface2->Attach(surface2_buffer.get());
surface2->Commit();
SurfaceObserverForTest observer1(surface1->window()->GetOcclusionState());
surface1->AddSurfaceObserver(&observer1);
SurfaceObserverForTest observer2(surface2->window()->GetOcclusionState());
surface2->AddSurfaceObserver(&observer2);
EXPECT_EQ(aura::Window::OcclusionState::OCCLUDED,
surface1->window()->GetOcclusionState());
EXPECT_EQ(aura::Window::OcclusionState::VISIBLE,
surface2->window()->GetOcclusionState());
auto* display_manager = ash::Shell::Get()->display_manager();
const auto display_id = display_manager->GetDisplayAt(0).id();
EXPECT_EQ(0, observer1.num_occlusion_state_changes());
EXPECT_EQ(0, observer2.num_occlusion_state_changes());
display_manager->ZoomDisplay(display_id, /*up=*/true);
// Update root surfaces (this happens asynchronously normally).
{
auto surface1_buffer_zoom =
test::ExoTestHelper::CreateBuffer(shell_surface1.get(), kOpaqueFormat);
surface1->Attach(surface1_buffer_zoom.get());
surface1->Commit();
auto surface2_buffer_zoom =
test::ExoTestHelper::CreateBuffer(shell_surface2.get(), kOpaqueFormat);
surface2->Attach(surface2_buffer_zoom.get());
surface2->Commit();
}
EXPECT_EQ(0, observer1.num_occlusion_state_changes());
EXPECT_EQ(0, observer2.num_occlusion_state_changes());
display_manager->ZoomDisplay(display_id, /*up=*/false);
{
auto surface1_buffer_zoom =
test::ExoTestHelper::CreateBuffer(shell_surface1.get(), kOpaqueFormat);
surface1->Attach(surface1_buffer_zoom.get());
surface1->Commit();
auto surface2_buffer_zoom =
test::ExoTestHelper::CreateBuffer(shell_surface2.get(), kOpaqueFormat);
surface2->Attach(surface2_buffer_zoom.get());
surface2->Commit();
}
// Should not get any occlusion changes - requires occlusion tracking clip
// to the root window and that the shelf occlude what is below it, too.
EXPECT_EQ(0, observer1.num_occlusion_state_changes());
EXPECT_EQ(0, observer2.num_occlusion_state_changes());
// Test Snapped State
ash::WindowSnapWMEvent snap_event(ash::WM_EVENT_SNAP_PRIMARY);
ash::WindowState* window_state1 = ash::WindowState::Get(window1);
ash::WindowState* window_state2 = ash::WindowState::Get(window2);
window_state1->OnWMEvent(&snap_event);
window_state2->OnWMEvent(&snap_event);
EXPECT_EQ(0, observer1.num_occlusion_state_changes());
EXPECT_EQ(0, observer2.num_occlusion_state_changes());
EXPECT_TRUE(window1->GetTransparent());
EXPECT_TRUE(window2->GetTransparent());
const std::vector<gfx::Rect> kSnappedOpaqueRegion{gfx::Rect(400, 552)};
EXPECT_EQ(kSnappedOpaqueRegion, window1->opaque_regions_for_occlusion());
EXPECT_EQ(kSnappedOpaqueRegion, window2->opaque_regions_for_occlusion());
{
auto snapped_buffer1 =
test::ExoTestHelper::CreateBuffer(shell_surface1.get(), kOpaqueFormat);
surface1->Attach(snapped_buffer1.get());
surface1->Commit();
auto snapped_buffer2 =
test::ExoTestHelper::CreateBuffer(shell_surface2.get(), kOpaqueFormat);
surface2->Attach(snapped_buffer2.get());
surface2->Commit();
}
EXPECT_EQ(0, observer1.num_occlusion_state_changes());
EXPECT_EQ(0, observer2.num_occlusion_state_changes());
display_manager->ZoomDisplay(display_id, /*up=*/true);
{
auto snapped_buffer1 =
test::ExoTestHelper::CreateBuffer(shell_surface1.get(), kOpaqueFormat);
surface1->Attach(snapped_buffer1.get());
surface1->Commit();
auto snapped_buffer2 =
test::ExoTestHelper::CreateBuffer(shell_surface2.get(), kOpaqueFormat);
surface2->Attach(snapped_buffer2.get());
surface2->Commit();
}
EXPECT_EQ(0, observer1.num_occlusion_state_changes());
EXPECT_EQ(0, observer2.num_occlusion_state_changes());
display_manager->ZoomDisplay(display_id, /*up=*/false);
{
auto snapped_buffer1 =
test::ExoTestHelper::CreateBuffer(shell_surface1.get(), kOpaqueFormat);
surface1->Attach(snapped_buffer1.get());
surface1->Commit();
auto snapped_buffer2 =
test::ExoTestHelper::CreateBuffer(shell_surface2.get(), kOpaqueFormat);
surface2->Attach(snapped_buffer2.get());
surface2->Commit();
}
EXPECT_EQ(0, observer1.num_occlusion_state_changes());
EXPECT_EQ(0, observer2.num_occlusion_state_changes());
// Make sure the occlusion tracking is working.
surface2->RemoveSurfaceObserver(&observer2);
shell_surface2.reset();
EXPECT_EQ(1, observer1.num_occlusion_state_changes());
surface1->RemoveSurfaceObserver(&observer1);
}
TEST_F(ShellSurfaceTest, GetWidgetHitTestMask) {
auto shell_surface = test::ShellSurfaceBuilder({256, 256})
.SetOrigin({100, 100})
.BuildShellSurface();
EXPECT_TRUE(shell_surface->WidgetHasHitTestMask());
SkPath mask;
shell_surface->GetWidgetHitTestMask(&mask);
// Returned HitMask should be in the widget local coordinates.
EXPECT_EQ(SkRect::MakeLTRB(0, 0, 256, 256), mask.getBounds());
}
TEST_F(ShellSurfaceTest, InitiallyMaximizedWindowIsOpaque) {
auto shell_surface = test::ShellSurfaceBuilder({256, 256})
.SetFrame(SurfaceFrameType::SHADOW)
.SetOrigin({100, 100})
.SetNoCommit()
.BuildShellSurface();
shell_surface->Maximize();
shell_surface->root_surface()->Commit();
EXPECT_FALSE(shell_surface->GetWidget()->GetNativeWindow()->GetTransparent());
}
TEST_F(ShellSurfaceTest, ConfigureOcclusionSentAfterShellSurfaceIsReady) {
std::vector<ConfigureData> config_vec;
auto shell_surface =
test::ShellSurfaceBuilder({100, 100})
.SetConfigureCallback(base::BindRepeating(
&ConfigureSerialVec, base::Unretained(&config_vec)))
.SetWindowState(chromeos::WindowStateType::kMinimized)
.SetNoRootBuffer()
.BuildShellSurface();
Surface* root_surface = shell_surface->root_surface();
root_surface->Commit();
// Expect initial configure with no content (buffer for root surface).
EXPECT_EQ(1u, config_vec.size());
// TODO(crbug.com/328172097): We use VISIBLE window state for minimized
// windows on initial configure for now.
EXPECT_EQ(aura::Window::OcclusionState::VISIBLE,
config_vec[0].occlusion_state);
constexpr gfx::Size kBufferSize(256, 256);
auto buffer = test::ExoTestHelper::CreateBuffer(kBufferSize);
root_surface->Attach(buffer.get());
root_surface->Commit();
// Once we provide a buffer for the root surface, expect that the occlusion
// state is updated to HIDDEN, since it is a minimized window.
EXPECT_EQ(2u, config_vec.size());
EXPECT_EQ(aura::Window::OcclusionState::HIDDEN,
config_vec[1].occlusion_state);
}
// Regression test for crbug.com/322388171. Ensure shell surfaces with no
// backing widget handle display changes without crashing.
TEST_F(ShellSurfaceTest, HandlesDisplayChangeNoWidget) {
// Start with a single display configuration.
UpdateDisplay("800x600");
// Create the surface.
std::unique_ptr<ShellSurface> shell_surface =
test::ShellSurfaceBuilder({256, 256}).BuildShellSurface();
EXPECT_TRUE(shell_surface->GetWidget());
// Keep the shell surface alive but close the widget.
shell_surface->GetWidget()->CloseNow();
EXPECT_FALSE(shell_surface->GetWidget());
// Trigger an update to the display configuration, the shell surface should
// handle this without crashing.
UpdateDisplay("1200x800");
}
} // namespace exo