// Copyright 2021 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/wayland/zcr_remote_shell_impl.h"
#include <wayland-server-core.h>
#include <wayland-server.h>
#include <memory>
#include <vector>
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h"
#include "ash/wm/window_state.h"
#include "ash/wm/wm_event.h"
#include "base/bit_cast.h"
#include "base/functional/bind.h"
#include "base/posix/unix_domain_socket.h"
#include "components/exo/display.h"
#include "components/exo/shell_surface.h"
#include "components/exo/test/exo_test_base.h"
#include "components/exo/test/shell_surface_builder.h"
#include "components/exo/wayland/server_util.h"
#include "ui/aura/window_delegate.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/display/test/display_manager_test_api.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/views/widget/widget.h"
namespace exo {
namespace wayland {
namespace {
const int kDefaultWindowLength = 100;
enum class RemoteShellEventType { kSendBoundsChanged, kSendWorkspaceInfo };
struct WlDisplayDeleter {
void operator()(wl_display* ptr) const { wl_display_destroy(ptr); }
};
using ScopedWlDisplay = std::unique_ptr<wl_display, WlDisplayDeleter>;
struct WlClientDeleter {
void operator()(wl_client* ptr) const { wl_client_destroy(ptr); }
};
using ScopedWlClient = std::unique_ptr<wl_client, WlClientDeleter>;
struct WlResourceDeleter {
void operator()(wl_resource* ptr) const { wl_resource_destroy(ptr); }
};
using ScopedWlResource = std::unique_ptr<wl_resource, WlResourceDeleter>;
} // namespace
class WaylandRemoteShellTest : public test::ExoTestBase {
public:
WaylandRemoteShellTest()
: test::ExoTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
WaylandRemoteShellTest(const WaylandRemoteShellTest&) = delete;
WaylandRemoteShellTest& operator=(const WaylandRemoteShellTest&) = delete;
// test::ExoTestBase:
void SetUp() override {
test::ExoTestBase::SetUp();
ResetEventRecords();
UpdateDisplay("800x600");
base::CreateSocketPair(&reader_, &writer_);
wl_display_.reset(wl_display_create());
wl_client_.reset(wl_client_create(wl_display_.get(), reader_.release()));
// Use 0 as the id here to avoid the id conflict (i.e. let wayland library
// choose the id from available ids.) Otherwise that will cause memory leak.
wl_shell_resource_.reset(wl_resource_create(wl_client_.get(),
&zcr_remote_shell_v2_interface,
/*version=*/1, /*id=*/0));
wl_remote_surface_resource_.reset(
wl_resource_create(wl_client(), &zcr_remote_surface_v2_interface,
/*version=*/1, /*id=*/0));
display_ = std::make_unique<Display>();
shell_ = std::make_unique<WaylandRemoteShell>(
display_.get(), wl_shell_resource_.get(),
base::BindRepeating(
[](int64_t) { return static_cast<wl_resource*>(nullptr); }),
test_event_mapping_,
/*use_default_scale_cancellation_default=*/true);
}
void TearDown() override {
shell_.reset();
display_.reset();
wl_remote_surface_resource_.reset();
test::ExoTestBase::TearDown();
}
std::unique_ptr<ClientControlledShellSurface::Delegate> CreateDelegate() {
return shell()->CreateShellSurfaceDelegate(
wl_remote_surface_resource_.get());
}
void ResetEventRecords() {
remote_shell_event_sequence_.clear();
remote_shell_requested_bounds_changes_.clear();
}
WaylandRemoteShell* shell() { return shell_.get(); }
wl_client* wl_client() { return wl_client_.get(); }
wl_resource* wl_remote_surface() { return wl_remote_surface_resource_.get(); }
static std::vector<RemoteShellEventType> remote_shell_event_sequence() {
return remote_shell_event_sequence_;
}
static std::vector<WaylandRemoteShell::BoundsChangeData>
remote_shell_requested_bounds_changes() {
return remote_shell_requested_bounds_changes_;
}
static int last_desktop_focus_state() { return last_desktop_focus_state_; }
private:
std::unique_ptr<Display> display_;
base::ScopedFD reader_, writer_;
ScopedWlDisplay wl_display_;
ScopedWlClient wl_client_;
ScopedWlResource wl_shell_resource_;
ScopedWlResource wl_remote_surface_resource_;
std::unique_ptr<WaylandRemoteShell> shell_;
static std::vector<RemoteShellEventType> remote_shell_event_sequence_;
static std::vector<WaylandRemoteShell::BoundsChangeData>
remote_shell_requested_bounds_changes_;
static uint32_t last_desktop_focus_state_;
const WaylandRemoteShellEventMapping test_event_mapping_ = {
/*send_window_geometry_changed=*/+[](struct wl_resource*,
int32_t,
int32_t,
int32_t,
int32_t) {},
/*send_change_zoom_level=*/+[](struct wl_resource*, int32_t) {},
/*send_state_type_changed=*/+[](struct wl_resource*, uint32_t) {},
/*send_bounds_changed_in_output=*/
+[](struct wl_resource*,
struct wl_resource*,
int32_t,
int32_t,
int32_t,
int32_t,
uint32_t) {},
/*send_bounds_changed=*/
+[](struct wl_resource*,
uint32_t display_id_hi,
uint32_t display_id_lo,
int32_t x,
int32_t y,
int32_t width,
int32_t height,
uint32_t reason) {
remote_shell_event_sequence_.push_back(
RemoteShellEventType::kSendBoundsChanged);
remote_shell_requested_bounds_changes_.emplace_back(
(((int64_t)display_id_hi << 32) | display_id_lo),
gfx::Rect(x, y, width, height),
static_cast<zcr_remote_surface_v1_bounds_change_reason>(reason));
},
/*send_activated=*/
+[](struct wl_resource*, struct wl_resource*, struct wl_resource*) {},
/*send_desktop_focus_state_changed=*/
+[](struct wl_resource*, uint32_t state) {
last_desktop_focus_state_ = state;
},
/*send_workspace_info=*/
+[](struct wl_resource*,
uint32_t,
uint32_t,
int32_t,
int32_t,
int32_t,
int32_t,
int32_t,
int32_t,
int32_t,
int32_t,
int32_t,
int32_t,
int32_t,
int32_t,
int32_t,
int32_t,
uint32_t,
struct wl_array*) {
remote_shell_event_sequence_.push_back(
RemoteShellEventType::kSendWorkspaceInfo);
},
/*send_drag_finished=*/
+[](struct wl_resource*, int32_t, int32_t, int32_t) {},
/*send_drag_started=*/+[](struct wl_resource*, uint32_t) {},
/*send_layout_mode=*/+[](struct wl_resource*, uint32_t) {},
/*send_default_device_scale_factor=*/+[](struct wl_resource*, int32_t) {},
/*send_configure=*/+[](struct wl_resource*, uint32_t) {},
/*bounds_changed_in_output_since_version=*/0,
/*desktop_focus_state_changed_since_version=*/0,
/*layout_mode_since_version=*/0,
/*default_device_scale_factor_since_version=*/0,
/*change_zoom_level_since_version=*/0,
/*send_workspace_info_since_version=*/0,
/*set_use_default_scale_cancellation_since_version=*/0,
/*has_bounds_change_reason_float=*/true,
};
};
std::vector<RemoteShellEventType>
WaylandRemoteShellTest::remote_shell_event_sequence_;
std::vector<WaylandRemoteShell::BoundsChangeData>
WaylandRemoteShellTest::remote_shell_requested_bounds_changes_;
uint32_t WaylandRemoteShellTest::last_desktop_focus_state_ = 0;
// Test that all bounds change requests are deferred while the tablet transition
// is happening until it's finished.
TEST_F(WaylandRemoteShellTest, TabletTransition) {
auto shell_surface = exo::test::ShellSurfaceBuilder({256, 256})
.SetDelegate(CreateDelegate())
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
auto* const widget = shell_surface->GetWidget();
auto* const window = widget->GetNativeWindow();
// Snap window.
ash::WindowSnapWMEvent event(ash::WM_EVENT_SNAP_PRIMARY);
ash::WindowState::Get(window)->OnWMEvent(&event);
shell_surface->SetSnapPrimary(chromeos::kDefaultSnapRatio);
shell_surface->SetGeometry(gfx::Rect(0, 0, 400, 520));
surface->Commit();
// Enable tablet mode.
ResetEventRecords();
ash::TabletModeControllerTestApi().EnterTabletMode();
task_environment()->FastForwardBy(base::Seconds(1));
task_environment()->RunUntilIdle();
const auto expected_sequence = std::vector<RemoteShellEventType>{
RemoteShellEventType::kSendWorkspaceInfo,
RemoteShellEventType::kSendBoundsChanged};
EXPECT_EQ(expected_sequence, remote_shell_event_sequence());
// TODO(b/236432849): Add a reasonable bounds check.
}
// Verifies bounds change events and workspace info events are triggered with
// proper values and in proper order when display zoom happens. A bounds change
// event must be triggered only for PIP.
TEST_F(WaylandRemoteShellTest, DisplayZoom) {
// Test a restored window first.
auto shell_surface =
exo::test::ShellSurfaceBuilder({256, 256})
.SetDelegate(CreateDelegate())
.SetWindowState(chromeos::WindowStateType::kNormal)
.SetGeometry({100, 100, kDefaultWindowLength, kDefaultWindowLength})
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
auto* window = shell_surface->GetWidget()->GetNativeWindow();
const display::Display& display =
display::Screen::GetScreen()->GetDisplayNearestWindow(window);
ResetEventRecords();
ash::Shell::Get()->display_manager()->ZoomDisplay(display.id(), /*up=*/true);
task_environment()->RunUntilIdle();
const auto expected_sequence_for_restored = std::vector<RemoteShellEventType>{
RemoteShellEventType::kSendWorkspaceInfo};
EXPECT_EQ(expected_sequence_for_restored, remote_shell_event_sequence());
// Test a maximized window.
shell_surface->SetMaximized();
surface->Commit();
ResetEventRecords();
ash::Shell::Get()->display_manager()->ZoomDisplay(display.id(), /*up=*/true);
task_environment()->RunUntilIdle();
const auto expected_sequence_for_maximized =
std::vector<RemoteShellEventType>{
RemoteShellEventType::kSendWorkspaceInfo};
EXPECT_EQ(expected_sequence_for_maximized, remote_shell_event_sequence());
// Test a PIP window.
shell_surface->SetPip();
// Place PIP at the bottom-right corner so the position will be adjusted with
// display size change. This means no bounds change event is triggered if PIP
// is at the top-left corner, but this is fine as the position doesn't need
// to be adjusted on the client side.
shell_surface->SetGeometry(
gfx::Rect(display.bounds().right() - kDefaultWindowLength,
display.bounds().bottom() - kDefaultWindowLength,
kDefaultWindowLength, kDefaultWindowLength));
surface->Commit();
ResetEventRecords();
ash::Shell::Get()->display_manager()->ZoomDisplay(display.id(), /*up=*/true);
task_environment()->RunUntilIdle();
const auto expected_sequence_for_pip = std::vector<RemoteShellEventType>{
RemoteShellEventType::kSendWorkspaceInfo,
RemoteShellEventType::kSendBoundsChanged};
EXPECT_EQ(remote_shell_event_sequence(), expected_sequence_for_pip);
ASSERT_EQ(1UL, remote_shell_requested_bounds_changes().size());
const auto bounds_change = remote_shell_requested_bounds_changes()[0];
EXPECT_EQ(display.id(), bounds_change.display_id);
// Verify that the new bounds is scaled larger in pixels.
EXPECT_GT(kDefaultWindowLength, bounds_change.bounds_in_display.width());
EXPECT_GT(kDefaultWindowLength, bounds_change.bounds_in_display.height());
EXPECT_EQ(ZCR_REMOTE_SURFACE_V1_BOUNDS_CHANGE_REASON_PIP,
bounds_change.reason);
}
// Verifies bounds change events and workspace info events are triggered with
// proper values and in proper order when display rotation happens. A bounds
// change event must be triggered only for PIP.
TEST_F(WaylandRemoteShellTest, DisplayRotation) {
// Test a restored window first.
auto shell_surface =
exo::test::ShellSurfaceBuilder({256, 256})
.SetDelegate(CreateDelegate())
.SetWindowState(chromeos::WindowStateType::kNormal)
.SetGeometry({100, 100, kDefaultWindowLength, kDefaultWindowLength})
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
auto* window = shell_surface->GetWidget()->GetNativeWindow();
const display::Display& display =
display::Screen::GetScreen()->GetDisplayNearestWindow(window);
ResetEventRecords();
ash::Shell::Get()->display_manager()->SetDisplayRotation(
display.id(), display::Display::ROTATE_90,
display::Display::RotationSource::ACCELEROMETER);
task_environment()->RunUntilIdle();
const auto expected_sequence_for_restored = std::vector<RemoteShellEventType>{
RemoteShellEventType::kSendWorkspaceInfo};
EXPECT_EQ(expected_sequence_for_restored, remote_shell_event_sequence());
// Test a maximized window.
shell_surface->SetMaximized();
surface->Commit();
ResetEventRecords();
ash::Shell::Get()->display_manager()->SetDisplayRotation(
display.id(), display::Display::ROTATE_180,
display::Display::RotationSource::ACCELEROMETER);
task_environment()->RunUntilIdle();
const auto expected_sequence_for_maximized =
std::vector<RemoteShellEventType>{
RemoteShellEventType::kSendWorkspaceInfo,
RemoteShellEventType::kSendBoundsChanged};
EXPECT_EQ(expected_sequence_for_maximized, remote_shell_event_sequence());
// Test a PIP window.
shell_surface->SetPip();
// Place PIP at the bottom-right corner so the position will be adjusted with
// display rotation.
shell_surface->SetGeometry(
gfx::Rect(display.bounds().right(), display.bounds().bottom(),
kDefaultWindowLength, kDefaultWindowLength));
surface->Commit();
const gfx::Rect bounds = window->GetBoundsInScreen();
const int right_inset = display.bounds().right() - bounds.right();
const int bottom_inset = display.bounds().bottom() - bounds.bottom();
ResetEventRecords();
ash::Shell::Get()->display_manager()->SetDisplayRotation(
display.id(), display::Display::ROTATE_270,
display::Display::RotationSource::ACCELEROMETER);
task_environment()->RunUntilIdle();
const auto expected_sequence_for_pip = std::vector<RemoteShellEventType>{
RemoteShellEventType::kSendWorkspaceInfo,
RemoteShellEventType::kSendBoundsChanged};
EXPECT_EQ(expected_sequence_for_pip, remote_shell_event_sequence());
ASSERT_EQ(1UL, remote_shell_requested_bounds_changes().size());
const auto bounds_change = remote_shell_requested_bounds_changes()[0];
EXPECT_EQ(display.id(), bounds_change.display_id);
const display::Display& rotated_display =
display::Screen::GetScreen()->GetDisplayNearestWindow(window);
const int expected_x =
rotated_display.bounds().right() - right_inset - kDefaultWindowLength;
const int expected_y =
rotated_display.bounds().bottom() - bottom_inset - kDefaultWindowLength;
const gfx::Rect expected_bounds = gfx::Rect(
expected_x, expected_y, kDefaultWindowLength, kDefaultWindowLength);
EXPECT_EQ(expected_bounds, bounds_change.bounds_in_display);
EXPECT_EQ(ZCR_REMOTE_SURFACE_V1_BOUNDS_CHANGE_REASON_PIP,
bounds_change.reason);
}
// Test that bounds changes are properly handled when the display is rotated in
// tablet mode.
TEST_F(WaylandRemoteShellTest, DisplayRotationInTabletMode) {
UpdateDisplay("800x600");
display::test::DisplayManagerTestApi(ash::Shell::Get()->display_manager())
.SetFirstDisplayAsInternalDisplay();
// Enable tablet mode.
ash::TabletModeControllerTestApi().EnterTabletMode();
task_environment()->RunUntilIdle();
auto shell_surface = exo::test::ShellSurfaceBuilder({256, 256})
.SetDelegate(CreateDelegate())
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
auto* const widget = shell_surface->GetWidget();
auto* const window = widget->GetNativeWindow();
const display::Display& display =
display::Screen::GetScreen()->GetDisplayNearestWindow(window);
// Snap window.
ash::WindowSnapWMEvent event(ash::WM_EVENT_SNAP_SECONDARY);
ash::WindowState::Get(window)->OnWMEvent(&event);
shell_surface->SetSnapSecondary(chromeos::kDefaultSnapRatio);
shell_surface->SetGeometry(gfx::Rect(400, 0, 400, 520));
surface->Commit();
// Rotate the display.
ResetEventRecords();
ash::Shell::Get()->display_manager()->SetDisplayRotation(
display.id(), display::Display::ROTATE_90,
display::Display::RotationSource::ACCELEROMETER);
// Any bounds change due to display rotation is deferred until the next event
// loop.
EXPECT_TRUE(remote_shell_event_sequence().empty());
// When the bounds set by the client requires the "adjustment" on the new
// display configuration, do not adjust it.
shell_surface->SetBounds(display.id(), gfx::Rect(600, 0, 400, 520));
surface->Commit();
task_environment()->RunUntilIdle();
EXPECT_EQ(1UL, remote_shell_requested_bounds_changes().size());
EXPECT_EQ(
ash::SplitViewController::Get(window->GetRootWindow())
->GetSnappedWindowBoundsInScreen(ash::SnapPosition::kSecondary,
window, chromeos::kDefaultSnapRatio,
/*account_for_divider_width=*/true),
remote_shell_requested_bounds_changes()[0].bounds_in_display);
}
// Removing secandary display and re-reconnect it restores the bounds of
// windows on secandary display. This test verifies bounds change events
// and workspace info events are triggered with proper values and in
// proper order.
TEST_F(WaylandRemoteShellTest, DisplayRemovalAddition) {
auto shell_surface = exo::test::ShellSurfaceBuilder(
{kDefaultWindowLength, kDefaultWindowLength})
.SetDelegate(CreateDelegate())
.BuildClientControlledShellSurface();
auto* surface = shell_surface->root_surface();
// Add secondary display with a different scale factor.
UpdateDisplay("800x600,800x600*2");
auto* display_manager = ash::Shell::Get()->display_manager();
const int64_t primary_display_id = display_manager->GetDisplayAt(0).id();
const int64_t secondary_display_id = display_manager->GetDisplayAt(1).id();
display::ManagedDisplayInfo primary_display_info =
display_manager->GetDisplayInfo(primary_display_id);
display::ManagedDisplayInfo secondary_display_info =
display_manager->GetDisplayInfo(secondary_display_id);
// Move the window to the secandary display.
const int initial_x = 100;
const int initial_y = 100;
shell_surface->SetScaleFactor(2.f);
shell_surface->SetBounds(secondary_display_id,
gfx::Rect(initial_x, initial_y, kDefaultWindowLength,
kDefaultWindowLength));
surface->Commit();
// Disconnect secondary display.
ResetEventRecords();
std::vector<display::ManagedDisplayInfo> display_info_list;
display_info_list.push_back(primary_display_info);
display_manager->OnNativeDisplaysChanged(display_info_list);
task_environment()->RunUntilIdle();
const auto event_sequence_disconnect = std::vector<RemoteShellEventType>{
RemoteShellEventType::kSendWorkspaceInfo,
RemoteShellEventType::kSendBoundsChanged};
EXPECT_EQ(remote_shell_event_sequence(), event_sequence_disconnect);
ASSERT_EQ(1UL, remote_shell_requested_bounds_changes().size());
const auto bounds_change = remote_shell_requested_bounds_changes()[0];
EXPECT_EQ(bounds_change.display_id, primary_display_id);
// Verify the new bounds is scaled in pixles with the scale factor of the
// primary display.
const gfx::Rect expected_bounds_after_disconnection = gfx::Rect(
initial_x, initial_y, kDefaultWindowLength / 2, kDefaultWindowLength / 2);
EXPECT_EQ(expected_bounds_after_disconnection,
bounds_change.bounds_in_display);
EXPECT_EQ(ZCR_REMOTE_SURFACE_V1_BOUNDS_CHANGE_REASON_MOVE,
bounds_change.reason);
// Reconnects the previously connected secondary display.
ResetEventRecords();
display_info_list.push_back(secondary_display_info);
display_manager->OnNativeDisplaysChanged(display_info_list);
task_environment()->RunUntilIdle();
// Reconnecting the secondary display seems to cause two workspace info
// events: One for a display metrics change for the primary display, and the
// other for a display addition event of the secondary display.
const auto event_sequence_reconnect = std::vector<RemoteShellEventType>{
RemoteShellEventType::kSendWorkspaceInfo,
RemoteShellEventType::kSendWorkspaceInfo,
RemoteShellEventType::kSendBoundsChanged};
EXPECT_EQ(event_sequence_reconnect, remote_shell_event_sequence());
ASSERT_EQ(1UL, remote_shell_requested_bounds_changes().size());
const auto bounds_change_to_secondary =
remote_shell_requested_bounds_changes()[0];
EXPECT_EQ(secondary_display_id, bounds_change_to_secondary.display_id);
const gfx::Rect expected_bounds_after_reconnection = gfx::Rect(
initial_x, initial_y, kDefaultWindowLength, kDefaultWindowLength);
EXPECT_EQ(expected_bounds_after_reconnection,
bounds_change_to_secondary.bounds_in_display);
EXPECT_EQ(ZCR_REMOTE_SURFACE_V1_BOUNDS_CHANGE_REASON_MOVE,
bounds_change_to_secondary.reason);
}
// Test that the desktop focus state event is called with the proper value in
// response to window focus change.
// Note that some clients such as ARC T+ rely on the behavior that the desktop
// focus change event is invoked immediately once focus switches in ash, which
// means, for example, we must not call `RunLoop::RunUntilIdle()` to wait for
// the event in this test.
TEST_F(WaylandRemoteShellTest, DesktopFocusState) {
auto client_controlled_shell_surface =
exo::test::ShellSurfaceBuilder(
{kDefaultWindowLength, kDefaultWindowLength})
.SetDelegate(CreateDelegate())
.SetNoCommit()
.BuildClientControlledShellSurface();
auto* surface = client_controlled_shell_surface->root_surface();
SetSurfaceResource(surface, wl_remote_surface());
surface->Commit();
EXPECT_EQ(last_desktop_focus_state(), 2);
client_controlled_shell_surface->SetMinimized();
surface->Commit();
EXPECT_EQ(last_desktop_focus_state(), 1);
auto shell_surface = exo::test::ShellSurfaceBuilder(
{kDefaultWindowLength, kDefaultWindowLength})
.BuildShellSurface();
auto* other_client_window = shell_surface->GetWidget()->GetNativeWindow();
other_client_window->Show();
other_client_window->Focus();
EXPECT_EQ(last_desktop_focus_state(), 3);
}
// Test that the float procedure works.
TEST_F(WaylandRemoteShellTest, FloatSurface) {
auto shell_surface =
exo::test::ShellSurfaceBuilder(
{kDefaultWindowLength, kDefaultWindowLength})
.SetDelegate(CreateDelegate())
.SetGeometry({100, 100, kDefaultWindowLength, kDefaultWindowLength})
.BuildClientControlledShellSurface();
auto* const surface = shell_surface->root_surface();
auto* const window_state =
ash::WindowState::Get(shell_surface->GetWidget()->GetNativeWindow());
SetImplementation(wl_remote_surface(), /*implementation=*/nullptr,
std::move(shell_surface));
// Emitting float event.
const ash::WindowFloatWMEvent float_event(
chromeos::FloatStartLocation::kBottomRight);
window_state->OnWMEvent(&float_event);
ASSERT_EQ(1UL, remote_shell_requested_bounds_changes().size());
ASSERT_EQ(remote_shell_requested_bounds_changes()[0].reason,
ZCR_REMOTE_SURFACE_V2_BOUNDS_CHANGE_REASON_FLOAT);
// Set float state from clients.
zcr_remote_shell::remote_surface_set_float(wl_client(), wl_remote_surface());
surface->Commit();
EXPECT_TRUE(window_state->IsFloated());
}
// Move the window across displays with the different scale factors.
TEST_F(WaylandRemoteShellTest, MoveAcrossDisplaysWithDifferentScaleFactors) {
UpdateDisplay("800x600,800x600*2");
auto shell_surface =
exo::test::ShellSurfaceBuilder(
{kDefaultWindowLength, kDefaultWindowLength})
.SetDelegate(CreateDelegate())
.SetGeometry({100, 100, kDefaultWindowLength, kDefaultWindowLength})
// Disable maximize for verifying the max size.
.SetCanMaximize(false)
.BuildClientControlledShellSurface();
const auto* window = shell_surface->GetWidget()->GetNativeWindow();
auto* const shell_surface_ptr = shell_surface.get();
auto* const surface = shell_surface->root_surface();
SetImplementation(wl_remote_surface(), /*implementation=*/nullptr,
std::move(shell_surface));
const auto* display_manager = ash::Shell::Get()->display_manager();
// Parameters in dp.
constexpr gfx::Rect bounds_in_dp(10, 20, kDefaultWindowLength,
kDefaultWindowLength);
constexpr gfx::Size min_size_in_dp = bounds_in_dp.size();
const gfx::Size max_size_in_dp =
gfx::ScaleToRoundedSize(bounds_in_dp.size(), 2);
// Move the window inside the primary display, and then move it to the
// secondary display.
for (int displayIndex = 0; displayIndex < 2; displayIndex++) {
const int64_t display_id = display_manager->GetDisplayAt(displayIndex).id();
const auto device_scale_factor =
display_manager->GetDisplayInfo(display_id).device_scale_factor();
// Parameters in pixels.
const auto bounds_in_px =
gfx::ScaleToRoundedRect(bounds_in_dp, device_scale_factor);
const auto min_size_in_px =
gfx::ScaleToRoundedSize(min_size_in_dp, device_scale_factor);
const auto max_size_in_px =
gfx::ScaleToRoundedSize(max_size_in_dp, device_scale_factor);
const uint scale_factor_value =
base::bit_cast<const uint>(device_scale_factor);
zcr_remote_shell::remote_surface_set_scale_factor(
wl_client(), wl_remote_surface(), scale_factor_value);
// Set bounds, min size, max size, and then commit.
shell_surface_ptr->SetBounds(display_id, bounds_in_px);
zcr_remote_shell::remote_surface_set_min_size(
wl_client(), wl_remote_surface(), min_size_in_px.width(),
min_size_in_px.height());
zcr_remote_shell::remote_surface_set_max_size(
wl_client(), wl_remote_surface(), max_size_in_px.width(),
max_size_in_px.height());
surface->Commit();
EXPECT_EQ(window->GetBoundsInRootWindow(), bounds_in_dp);
EXPECT_EQ(window->delegate()->GetMinimumSize(), min_size_in_dp);
EXPECT_EQ(window->delegate()->GetMaximumSize(), max_size_in_dp);
}
}
// Change the display's device scale factor.
TEST_F(WaylandRemoteShellTest, DeviceScaleFactorChange) {
UpdateDisplay("800x600");
auto shell_surface =
exo::test::ShellSurfaceBuilder(
{kDefaultWindowLength, kDefaultWindowLength})
.SetDelegate(CreateDelegate())
.SetGeometry({100, 100, kDefaultWindowLength, kDefaultWindowLength})
// Disable maximize for verifying the max size.
.SetCanMaximize(false)
.BuildClientControlledShellSurface();
const auto* window = shell_surface->GetWidget()->GetNativeWindow();
auto* const shell_surface_ptr = shell_surface.get();
auto* const surface = shell_surface->root_surface();
SetImplementation(wl_remote_surface(), /*implementation=*/nullptr,
std::move(shell_surface));
// Change the display's device scale factor.
UpdateDisplay("800x600*2");
const auto* display_manager = ash::Shell::Get()->display_manager();
const int64_t display_id = display_manager->GetDisplayAt(0).id();
const auto device_scale_factor =
display_manager->GetDisplayInfo(display_id).device_scale_factor();
// Parameters in dp.
constexpr gfx::Rect bounds_in_dp(10, 20, kDefaultWindowLength,
kDefaultWindowLength);
constexpr gfx::Size min_size_in_dp = bounds_in_dp.size();
const gfx::Size max_size_in_dp =
gfx::ScaleToRoundedSize(bounds_in_dp.size(), 2);
// Parameters in pixels.
const auto bounds_in_px =
gfx::ScaleToRoundedRect(bounds_in_dp, device_scale_factor);
const auto min_size_in_px =
gfx::ScaleToRoundedSize(min_size_in_dp, device_scale_factor);
const auto max_size_in_px =
gfx::ScaleToRoundedSize(max_size_in_dp, device_scale_factor);
// Set bounds, min size, max size, and then commit.
shell_surface_ptr->SetBounds(display_id, bounds_in_px);
zcr_remote_shell::remote_surface_set_min_size(
wl_client(), wl_remote_surface(), min_size_in_px.width(),
min_size_in_px.height());
zcr_remote_shell::remote_surface_set_max_size(
wl_client(), wl_remote_surface(), max_size_in_px.width(),
max_size_in_px.height());
surface->Commit();
EXPECT_EQ(window->GetBoundsInRootWindow(), bounds_in_dp);
EXPECT_EQ(window->delegate()->GetMinimumSize(), min_size_in_dp);
EXPECT_EQ(window->delegate()->GetMaximumSize(), max_size_in_dp);
}
} // namespace wayland
} // namespace exo