/* KWin - the KDE window manager This file is part of the KDE project. SPDX-FileCopyrightText: 2019 David Edmundson SPDX-FileCopyrightText: 2019 Vlad Zahorodnii SPDX-License-Identifier: GPL-2.0-or-later */ #include "kwin_wayland_test.h" #include "cursor.h" #include "output.h" #include "platform.h" #include "screens.h" #include "wayland_server.h" #include "window.h" #include "workspace.h" #include #include #include #include using namespace KWin; static const QString s_socketName = QStringLiteral("wayland_test_kwin_placement-0"); struct PlaceWindowResult { QSize initiallyConfiguredSize; Test::XdgToplevel::States initiallyConfiguredStates; QRect finalGeometry; }; class TestPlacement : public QObject { Q_OBJECT private Q_SLOTS: void init(); void cleanup(); void initTestCase(); void testPlaceSmart(); void testPlaceZeroCornered(); void testPlaceMaximized(); void testPlaceMaximizedLeavesFullscreen(); void testPlaceCentered(); void testPlaceUnderMouse(); void testPlaceCascaded(); void testPlaceRandom(); void testFullscreen(); private: void setPlacementPolicy(Placement::Policy policy); /* * Create a window with the lifespan of parent and return relevant results for testing * defaultSize is the buffer size to use if the compositor returns an empty size in the first configure * event. */ PlaceWindowResult createAndPlaceWindow(const QSize &defaultSize, QObject *parent); }; void TestPlacement::init() { QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::PlasmaShell)); workspace()->setActiveOutput(QPoint(640, 512)); KWin::Cursors::self()->mouse()->setPos(QPoint(640, 512)); } void TestPlacement::cleanup() { Test::destroyWaylandConnection(); } void TestPlacement::initTestCase() { qRegisterMetaType(); QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); QVERIFY(applicationStartedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName)); QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); kwinApp()->start(); QVERIFY(applicationStartedSpy.wait()); const auto outputs = kwinApp()->platform()->enabledOutputs(); QCOMPARE(outputs.count(), 2); QCOMPARE(outputs[0]->geometry(), QRect(0, 0, 1280, 1024)); QCOMPARE(outputs[1]->geometry(), QRect(1280, 0, 1280, 1024)); Test::initWaylandWorkspace(); } void TestPlacement::setPlacementPolicy(Placement::Policy policy) { auto group = kwinApp()->config()->group("Windows"); group.writeEntry("Placement", Placement::policyToString(policy)); group.sync(); Workspace::self()->slotReconfigure(); } PlaceWindowResult TestPlacement::createAndPlaceWindow(const QSize &defaultSize, QObject *parent) { PlaceWindowResult rc; // create a new window auto surface = Test::createSurface(parent); auto shellSurface = Test::createXdgToplevelSurface(surface, Test::CreationSetup::CreateOnly, surface); QSignalSpy toplevelConfigureRequestedSpy(shellSurface, &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); surface->commit(KWayland::Client::Surface::CommitFlag::None); surfaceConfigureRequestedSpy.wait(); rc.initiallyConfiguredSize = toplevelConfigureRequestedSpy[0][0].toSize(); rc.initiallyConfiguredStates = toplevelConfigureRequestedSpy[0][1].value(); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy[0][0].toUInt()); QSize size = rc.initiallyConfiguredSize; if (size.isEmpty()) { size = defaultSize; } auto c = Test::renderAndWaitForShown(surface, size, Qt::red); rc.finalGeometry = c->frameGeometry(); return rc; } void TestPlacement::testPlaceSmart() { setPlacementPolicy(Placement::Smart); QScopedPointer testParent(new QObject); // dumb QObject just for scoping surfaces to the test QRegion usedArea; for (int i = 0; i < 4; i++) { PlaceWindowResult windowPlacement = createAndPlaceWindow(QSize(600, 500), testParent.data()); // smart placement shouldn't define a size on clients QCOMPARE(windowPlacement.initiallyConfiguredSize, QSize(0, 0)); QCOMPARE(windowPlacement.finalGeometry.size(), QSize(600, 500)); // exact placement isn't a defined concept that should be tested // but the goal of smart placement is to make sure windows don't overlap until they need to // 4 windows of 600, 500 should fit without overlap QVERIFY(!usedArea.intersects(windowPlacement.finalGeometry)); usedArea += windowPlacement.finalGeometry; } } void TestPlacement::testPlaceZeroCornered() { setPlacementPolicy(Placement::ZeroCornered); QScopedPointer testParent(new QObject); for (int i = 0; i < 4; i++) { PlaceWindowResult windowPlacement = createAndPlaceWindow(QSize(600, 500), testParent.data()); // smart placement shouldn't define a size on clients QCOMPARE(windowPlacement.initiallyConfiguredSize, QSize(0, 0)); // size should match our buffer QCOMPARE(windowPlacement.finalGeometry.size(), QSize(600, 500)); // and it should be in the corner QCOMPARE(windowPlacement.finalGeometry.topLeft(), QPoint(0, 0)); } } void TestPlacement::testPlaceMaximized() { setPlacementPolicy(Placement::Maximizing); // add a top panel QScopedPointer panelSurface(Test::createSurface()); QScopedPointer panelShellSurface(Test::createXdgToplevelSurface(panelSurface.data())); QScopedPointer plasmaSurface(Test::waylandPlasmaShell()->createSurface(panelSurface.data())); plasmaSurface->setRole(KWayland::Client::PlasmaShellSurface::Role::Panel); plasmaSurface->setPosition(QPoint(0, 0)); Test::renderAndWaitForShown(panelSurface.data(), QSize(1280, 20), Qt::blue); QScopedPointer testParent(new QObject); // all windows should be initially maximized with an initial configure size sent for (int i = 0; i < 4; i++) { PlaceWindowResult windowPlacement = createAndPlaceWindow(QSize(600, 500), testParent.data()); QVERIFY(windowPlacement.initiallyConfiguredStates & Test::XdgToplevel::State::Maximized); QCOMPARE(windowPlacement.initiallyConfiguredSize, QSize(1280, 1024 - 20)); QCOMPARE(windowPlacement.finalGeometry, QRect(0, 20, 1280, 1024 - 20)); // under the panel } } void TestPlacement::testPlaceMaximizedLeavesFullscreen() { setPlacementPolicy(Placement::Maximizing); // add a top panel QScopedPointer panelSurface(Test::createSurface()); QScopedPointer panelShellSurface(Test::createXdgToplevelSurface(panelSurface.data())); QScopedPointer plasmaSurface(Test::waylandPlasmaShell()->createSurface(panelSurface.data())); plasmaSurface->setRole(KWayland::Client::PlasmaShellSurface::Role::Panel); plasmaSurface->setPosition(QPoint(0, 0)); Test::renderAndWaitForShown(panelSurface.data(), QSize(1280, 20), Qt::blue); QScopedPointer testParent(new QObject); // all windows should be initially fullscreen with an initial configure size sent, despite the policy for (int i = 0; i < 4; i++) { auto surface = Test::createSurface(testParent.data()); auto shellSurface = Test::createXdgToplevelSurface(surface, Test::CreationSetup::CreateOnly, surface); shellSurface->set_fullscreen(nullptr); QSignalSpy toplevelConfigureRequestedSpy(shellSurface, &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(surfaceConfigureRequestedSpy.wait()); auto initiallyConfiguredSize = toplevelConfigureRequestedSpy[0][0].toSize(); auto initiallyConfiguredStates = toplevelConfigureRequestedSpy[0][1].value(); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy[0][0].toUInt()); auto c = Test::renderAndWaitForShown(surface, initiallyConfiguredSize, Qt::red); QVERIFY(initiallyConfiguredStates & Test::XdgToplevel::State::Fullscreen); QCOMPARE(initiallyConfiguredSize, QSize(1280, 1024)); QCOMPARE(c->frameGeometry(), QRect(0, 0, 1280, 1024)); } } void TestPlacement::testPlaceCentered() { // This test verifies that Centered placement policy works. KConfigGroup group = kwinApp()->config()->group("Windows"); group.writeEntry("Placement", Placement::policyToString(Placement::Centered)); group.sync(); workspace()->slotReconfigure(); QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); Window *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::red); QVERIFY(client); QCOMPARE(client->frameGeometry(), QRect(590, 487, 100, 50)); shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestPlacement::testPlaceUnderMouse() { // This test verifies that Under Mouse placement policy works. KConfigGroup group = kwinApp()->config()->group("Windows"); group.writeEntry("Placement", Placement::policyToString(Placement::UnderMouse)); group.sync(); workspace()->slotReconfigure(); KWin::Cursors::self()->mouse()->setPos(QPoint(200, 300)); QCOMPARE(KWin::Cursors::self()->mouse()->pos(), QPoint(200, 300)); QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); Window *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::red); QVERIFY(client); QCOMPARE(client->frameGeometry(), QRect(151, 276, 100, 50)); shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestPlacement::testPlaceCascaded() { // This test verifies that Cascaded placement policy works. KConfigGroup group = kwinApp()->config()->group("Windows"); group.writeEntry("Placement", Placement::policyToString(Placement::Cascade)); group.sync(); workspace()->slotReconfigure(); QScopedPointer surface1(Test::createSurface()); QScopedPointer shellSurface1(Test::createXdgToplevelSurface(surface1.data())); Window *client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::red); QVERIFY(client1); QCOMPARE(client1->pos(), QPoint(0, 0)); QCOMPARE(client1->size(), QSize(100, 50)); QScopedPointer surface2(Test::createSurface()); QScopedPointer shellSurface2(Test::createXdgToplevelSurface(surface2.data())); Window *client2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); QVERIFY(client2); QCOMPARE(client2->pos(), client1->pos() + workspace()->cascadeOffset(client2)); QCOMPARE(client2->size(), QSize(100, 50)); QScopedPointer surface3(Test::createSurface()); QScopedPointer shellSurface3(Test::createXdgToplevelSurface(surface3.data())); Window *client3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::green); QVERIFY(client3); QCOMPARE(client3->pos(), client2->pos() + workspace()->cascadeOffset(client3)); QCOMPARE(client3->size(), QSize(100, 50)); shellSurface3.reset(); QVERIFY(Test::waitForWindowDestroyed(client3)); shellSurface2.reset(); QVERIFY(Test::waitForWindowDestroyed(client2)); shellSurface1.reset(); QVERIFY(Test::waitForWindowDestroyed(client1)); } void TestPlacement::testPlaceRandom() { // This test verifies that Random placement policy works. KConfigGroup group = kwinApp()->config()->group("Windows"); group.writeEntry("Placement", Placement::policyToString(Placement::Random)); group.sync(); workspace()->slotReconfigure(); QScopedPointer surface1(Test::createSurface()); QScopedPointer shellSurface1(Test::createXdgToplevelSurface(surface1.data())); Window *client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::red); QVERIFY(client1); QCOMPARE(client1->size(), QSize(100, 50)); QScopedPointer surface2(Test::createSurface()); QScopedPointer shellSurface2(Test::createXdgToplevelSurface(surface2.data())); Window *client2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); QVERIFY(client2); QVERIFY(client2->pos() != client1->pos()); QCOMPARE(client2->size(), QSize(100, 50)); QScopedPointer surface3(Test::createSurface()); QScopedPointer shellSurface3(Test::createXdgToplevelSurface(surface3.data())); Window *client3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::green); QVERIFY(client3); QVERIFY(client3->pos() != client1->pos()); QVERIFY(client3->pos() != client2->pos()); QCOMPARE(client3->size(), QSize(100, 50)); shellSurface3.reset(); QVERIFY(Test::waitForWindowDestroyed(client3)); shellSurface2.reset(); QVERIFY(Test::waitForWindowDestroyed(client2)); shellSurface1.reset(); QVERIFY(Test::waitForWindowDestroyed(client1)); } void TestPlacement::testFullscreen() { const QVector outputs = kwinApp()->platform()->enabledOutputs(); setPlacementPolicy(Placement::Smart); QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); Window *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::red); QVERIFY(client); client->sendToOutput(outputs[0]); // Wait for the configure event with the activated state. QSignalSpy toplevelConfigureRequestedSpy(shellSurface.data(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); QVERIFY(surfaceConfigureRequestedSpy.wait()); client->setFullScreen(true); QSignalSpy geometryChangedSpy(client, &Window::frameGeometryChanged); QVERIFY(surfaceConfigureRequestedSpy.wait()); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Test::render(surface.data(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::red); QVERIFY(geometryChangedSpy.wait()); QCOMPARE(client->frameGeometry(), screens()->geometry(0)); // this doesn't require a round trip, so should be immediate client->sendToOutput(outputs[1]); QCOMPARE(client->frameGeometry(), screens()->geometry(1)); QCOMPARE(geometryChangedSpy.count(), 2); } WAYLANDTEST_MAIN(TestPlacement) #include "placement_test.moc"