From d3cca65d39ef1e01a7f6202837a8d032593fda05 Mon Sep 17 00:00:00 2001 From: Vlad Zahorodnii Date: Fri, 7 Aug 2020 22:01:42 +0300 Subject: [PATCH] Implement the layer-shell v1 protocol The layer-shell protocol allows wayland clients to create surfaces that can be used for building desktop environment components such as panels, notifications, etc. The support for the plasma-shell protocol will be dropped once plasma in all its entirety is ported to the layer-shell protocol. --- CMakeLists.txt | 2 + abstract_client.h | 2 +- autotests/integration/CMakeLists.txt | 5 + autotests/integration/kwin_wayland_test.h | 34 +- .../integration/layershellv1client_test.cpp | 584 ++++++++++++++++++ .../protocols/wlr-layer-shell-unstable-v1.xml | 325 ++++++++++ autotests/integration/test_helpers.cpp | 52 ++ layershellv1client.cpp | 277 +++++++++ layershellv1client.h | 68 ++ layershellv1integration.cpp | 214 +++++++ layershellv1integration.h | 37 ++ utils.cpp | 6 + utils.h | 1 + wayland_server.cpp | 9 +- waylandclient.cpp | 9 - waylandclient.h | 1 - xdgshellclient.cpp | 9 + xdgshellclient.h | 1 + 18 files changed, 1622 insertions(+), 14 deletions(-) create mode 100644 autotests/integration/layershellv1client_test.cpp create mode 100644 autotests/integration/protocols/wlr-layer-shell-unstable-v1.xml create mode 100644 layershellv1client.cpp create mode 100644 layershellv1client.h create mode 100644 layershellv1integration.cpp create mode 100644 layershellv1integration.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b898c5a69..2661bd004 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -476,6 +476,8 @@ set(kwin_SRCS keyboard_repeat.cpp killwindow.cpp layers.cpp + layershellv1client.cpp + layershellv1integration.cpp libinput/connection.cpp libinput/context.cpp libinput/device.cpp diff --git a/abstract_client.h b/abstract_client.h index 30f255f88..0e830a751 100644 --- a/abstract_client.h +++ b/abstract_client.h @@ -987,7 +987,7 @@ protected: */ void removeTransientFromList(AbstractClient* cl); - Layer belongsToLayer() const; + virtual Layer belongsToLayer() const; virtual bool belongsToDesktop() const; void invalidateLayer(); bool isActiveFullScreen() const; diff --git a/autotests/integration/CMakeLists.txt b/autotests/integration/CMakeLists.txt index 7beaf6ed2..67def6aef 100644 --- a/autotests/integration/CMakeLists.txt +++ b/autotests/integration/CMakeLists.txt @@ -13,6 +13,10 @@ ecm_add_qtwayland_client_protocol(KWinIntegrationTestFramework_SOURCES PROTOCOL ${WaylandProtocols_DATADIR}/unstable/input-method/input-method-unstable-v1.xml BASENAME input-method-unstable-v1 ) +ecm_add_qtwayland_client_protocol(KWinIntegrationTestFramework_SOURCES + PROTOCOL protocols/wlr-layer-shell-unstable-v1.xml + BASENAME wlr-layer-shell-unstable-v1 +) add_library(KWinIntegrationTestFramework STATIC ${KWinIntegrationTestFramework_SOURCES}) target_link_libraries(KWinIntegrationTestFramework kwin Qt5::Test Wayland::Client) @@ -63,6 +67,7 @@ integrationTest(WAYLAND_ONLY NAME testKeymapCreationFailure SRCS keymap_creation integrationTest(WAYLAND_ONLY NAME testShowingDesktop SRCS showing_desktop_test.cpp) integrationTest(WAYLAND_ONLY NAME testDontCrashUseractionsMenu SRCS dont_crash_useractions_menu.cpp) integrationTest(WAYLAND_ONLY NAME testKWinBindings SRCS kwinbindings_test.cpp) +integrationTest(WAYLAND_ONLY NAME testLayerShellV1Client SRCS layershellv1client_test.cpp) integrationTest(WAYLAND_ONLY NAME testVirtualDesktop SRCS virtual_desktop_test.cpp) integrationTest(WAYLAND_ONLY NAME testXdgShellClientRules SRCS xdgshellclient_rules_test.cpp) integrationTest(WAYLAND_ONLY NAME testIdleInhibition SRCS idle_inhibition_test.cpp) diff --git a/autotests/integration/kwin_wayland_test.h b/autotests/integration/kwin_wayland_test.h index 5df80e408..d8e9631c2 100644 --- a/autotests/integration/kwin_wayland_test.h +++ b/autotests/integration/kwin_wayland_test.h @@ -17,6 +17,8 @@ // KWayland #include +#include "qwayland-wlr-layer-shell-unstable-v1.h" + namespace KWayland { namespace Client @@ -83,6 +85,28 @@ namespace Test class MockInputMethod; +class LayerShellV1 : public QtWayland::zwlr_layer_shell_v1 +{ +public: + ~LayerShellV1() override; +}; + +class LayerSurfaceV1 : public QObject, public QtWayland::zwlr_layer_surface_v1 +{ + Q_OBJECT + +public: + ~LayerSurfaceV1() override; + +protected: + void zwlr_layer_surface_v1_configure(uint32_t serial, uint32_t width, uint32_t height) override; + void zwlr_layer_surface_v1_closed() override; + +Q_SIGNALS: + void closeRequested(); + void configureRequested(quint32 serial, const QSize &size); +}; + enum class AdditionalWaylandInterface { Seat = 1 << 0, Decoration = 1 << 1, @@ -95,7 +119,8 @@ enum class AdditionalWaylandInterface { XdgDecoration = 1 << 8, OutputManagement = 1 << 9, TextInputManagerV2 = 1 << 10, - InputMethodV1 = 1 << 11 + InputMethodV1 = 1 << 11, + LayerShellV1 = 1 << 12, }; Q_DECLARE_FLAGS(AdditionalWaylandInterfaces, AdditionalWaylandInterface) /** @@ -129,6 +154,7 @@ KWayland::Client::AppMenuManager *waylandAppMenuManager(); KWayland::Client::XdgDecorationManager *xdgDecorationManager(); KWayland::Client::OutputManagement *waylandOutputManagement(); KWayland::Client::TextInputManager *waylandTextInputManager(); +QVector waylandOutputs(); bool waitForWaylandPointer(); bool waitForWaylandTouch(); @@ -139,6 +165,12 @@ void flushWaylandConnection(); KWayland::Client::Surface *createSurface(QObject *parent = nullptr); KWayland::Client::SubSurface *createSubSurface(KWayland::Client::Surface *surface, KWayland::Client::Surface *parentSurface, QObject *parent = nullptr); + +LayerSurfaceV1 *createLayerSurfaceV1(KWayland::Client::Surface *surface, + const QString &scope, + KWayland::Client::Output *output = nullptr, + LayerShellV1::layer layer = LayerShellV1::layer_top); + enum class XdgShellSurfaceType { XdgShellStable }; diff --git a/autotests/integration/layershellv1client_test.cpp b/autotests/integration/layershellv1client_test.cpp new file mode 100644 index 000000000..edc8a01d0 --- /dev/null +++ b/autotests/integration/layershellv1client_test.cpp @@ -0,0 +1,584 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" +#include "abstract_client.h" +#include "abstract_output.h" +#include "main.h" +#include "platform.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include + +Q_DECLARE_METATYPE(QMargins) +Q_DECLARE_METATYPE(KWin::Layer) + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_layershellv1client-0"); + +class LayerShellV1ClientTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testOutput_data(); + void testOutput(); + void testAnchor_data(); + void testAnchor(); + void testMargins_data(); + void testMargins(); + void testLayer_data(); + void testLayer(); + void testPlacementArea_data(); + void testPlacementArea(); + void testFill_data(); + void testFill(); + void testStack(); + void testFocus(); + void testActivate_data(); + void testActivate(); + void testUnmap(); +}; + +void LayerShellV1ClientTest::initTestCase() +{ + QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); + QVERIFY(applicationStartedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + + kwinApp()->start(); + QVERIFY(applicationStartedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + waylandServer()->initWorkspace(); +} + +void LayerShellV1ClientTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::LayerShellV1)); + + screens()->setCurrent(0); + Cursors::self()->mouse()->setPos(QPoint(640, 512)); +} + +void LayerShellV1ClientTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void LayerShellV1ClientTest::testOutput_data() +{ + QTest::addColumn("screenId"); + + QTest::addRow("first output") << 0; + QTest::addRow("second output") << 1; +} + +void LayerShellV1ClientTest::testOutput() +{ + // Fetch the wl_output object. + QFETCH(int, screenId); + KWayland::Client::Output *output = Test::waylandOutputs().value(screenId); + QVERIFY(output); + + // Create a layer shell surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createLayerSurfaceV1(surface.data(), QStringLiteral("test"), output)); + + // Set the initial state of the layer surface. + shellSurface->set_size(280, 124); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.data(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), requestedSize, Qt::red); + QVERIFY(client); + + // Verify that the client is on the requested screen. + QVERIFY(output->geometry().contains(client->frameGeometry())); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void LayerShellV1ClientTest::testAnchor_data() +{ + QTest::addColumn("anchor"); + QTest::addColumn("expectedGeometry"); + + QTest::addRow("left") << int(Test::LayerSurfaceV1::anchor_left) + << QRect(0, 450, 280, 124); + + QTest::addRow("top left") << (Test::LayerSurfaceV1::anchor_top | + Test::LayerSurfaceV1::anchor_left) + << QRect(0, 0, 280, 124); + + QTest::addRow("top") << int(Test::LayerSurfaceV1::anchor_top) + << QRect(500, 0, 280, 124); + + QTest::addRow("top right") << (Test::LayerSurfaceV1::anchor_top | + Test::LayerSurfaceV1::anchor_right) + << QRect(1000, 0, 280, 124); + + QTest::addRow("right") << int(Test::LayerSurfaceV1::anchor_right) + << QRect(1000, 450, 280, 124); + + QTest::addRow("bottom right") << (Test::LayerSurfaceV1::anchor_bottom | + Test::LayerSurfaceV1::anchor_right) + << QRect(1000, 900, 280, 124); + + QTest::addRow("bottom") << int(Test::LayerSurfaceV1::anchor_bottom) + << QRect(500, 900, 280, 124); + + QTest::addRow("bottom left") << (Test::LayerSurfaceV1::anchor_bottom | + Test::LayerSurfaceV1::anchor_left) + << QRect(0, 900, 280, 124); +} + +void LayerShellV1ClientTest::testAnchor() +{ + // Create a layer shell surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createLayerSurfaceV1(surface.data(), QStringLiteral("test"))); + + // Set the initial state of the layer surface. + QFETCH(int, anchor); + shellSurface->set_anchor(anchor); + shellSurface->set_size(280, 124); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.data(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + QCOMPARE(requestedSize, QSize(280, 124)); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(280, 124), Qt::red); + QVERIFY(client); + + // Verify that the client is placed at expected location. + QTEST(client->frameGeometry(), "expectedGeometry"); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void LayerShellV1ClientTest::testMargins_data() +{ + QTest::addColumn("anchor"); + QTest::addColumn("margins"); + QTest::addColumn("expectedGeometry"); + + QTest::addRow("left") << int(Test::LayerSurfaceV1::anchor_left) + << QMargins(100, 0, 0, 0) + << QRect(100, 450, 280, 124); + + QTest::addRow("top left") << (Test::LayerSurfaceV1::anchor_top | + Test::LayerSurfaceV1::anchor_left) + << QMargins(100, 200, 0, 0) + << QRect(100, 200, 280, 124); + + QTest::addRow("top") << int(Test::LayerSurfaceV1::anchor_top) + << QMargins(0, 200, 0, 0) + << QRect(500, 200, 280, 124); + + QTest::addRow("top right") << (Test::LayerSurfaceV1::anchor_top | + Test::LayerSurfaceV1::anchor_right) + << QMargins(0, 200, 300, 0) + << QRect(700, 200, 280, 124); + + QTest::addRow("right") << int(Test::LayerSurfaceV1::anchor_right) + << QMargins(0, 0, 300, 0) + << QRect(700, 450, 280, 124); + + QTest::addRow("bottom right") << (Test::LayerSurfaceV1::anchor_bottom | + Test::LayerSurfaceV1::anchor_right) + << QMargins(0, 0, 300, 400) + << QRect(700, 500, 280, 124); + + QTest::addRow("bottom") << int(Test::LayerSurfaceV1::anchor_bottom) + << QMargins(0, 0, 0, 400) + << QRect(500, 500, 280, 124); + + QTest::addRow("bottom left") << (Test::LayerSurfaceV1::anchor_bottom | + Test::LayerSurfaceV1::anchor_left) + << QMargins(100, 0, 0, 400) + << QRect(100, 500, 280, 124); +} + +void LayerShellV1ClientTest::testMargins() +{ + // Create a layer shell surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createLayerSurfaceV1(surface.data(), QStringLiteral("test"))); + + // Set the initial state of the layer surface. + QFETCH(QMargins, margins); + QFETCH(int, anchor); + shellSurface->set_anchor(anchor); + shellSurface->set_margin(margins.top(), margins.right(), margins.bottom(), margins.left()); + shellSurface->set_size(280, 124); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.data(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), requestedSize, Qt::red); + QVERIFY(client); + + // Verify that the client is placed at expected location. + QTEST(client->frameGeometry(), "expectedGeometry"); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void LayerShellV1ClientTest::testLayer_data() +{ + QTest::addColumn("protocolLayer"); + QTest::addColumn("compositorLayer"); + + QTest::addRow("overlay") << int(Test::LayerShellV1::layer_overlay) << UnmanagedLayer; + QTest::addRow("top") << int(Test::LayerShellV1::layer_top) << AboveLayer; + QTest::addRow("bottom") << int(Test::LayerShellV1::layer_bottom) << BelowLayer; + QTest::addRow("background") << int(Test::LayerShellV1::layer_background) << DesktopLayer; +} + +void LayerShellV1ClientTest::testLayer() +{ + // Create a layer shell surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createLayerSurfaceV1(surface.data(), QStringLiteral("test"))); + + // Set the initial state of the layer surface. + QFETCH(int, protocolLayer); + shellSurface->set_layer(protocolLayer); + shellSurface->set_size(280, 124); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.data(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), requestedSize, Qt::red); + QVERIFY(client); + + // Verify that the client is placed at expected location. + QTEST(client->layer(), "compositorLayer"); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void LayerShellV1ClientTest::testPlacementArea_data() +{ + QTest::addColumn("anchor"); + QTest::addColumn("exclusiveZone"); + QTest::addColumn("placementArea"); + + QTest::addRow("left") << int(Test::LayerSurfaceV1::anchor_left) << 300 << QRect(300, 0, 980, 1024); + QTest::addRow("top") << int(Test::LayerSurfaceV1::anchor_top) << 300 << QRect(0, 300, 1280, 724); + QTest::addRow("right") << int(Test::LayerSurfaceV1::anchor_right) << 300 << QRect(0, 0, 980, 1024); + QTest::addRow("bottom") << int(Test::LayerSurfaceV1::anchor_bottom) << 300 << QRect(0, 0, 1280, 724); +} + +void LayerShellV1ClientTest::testPlacementArea() +{ + // Create a layer shell surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createLayerSurfaceV1(surface.data(), QStringLiteral("test"))); + + // Set the initial state of the layer surface. + QFETCH(int, anchor); + QFETCH(int, exclusiveZone); + shellSurface->set_anchor(anchor); + shellSurface->set_exclusive_zone(exclusiveZone); + shellSurface->set_size(280, 124); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.data(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), requestedSize, Qt::red); + QVERIFY(client); + + // Verify that the work area has been adjusted. + QTEST(workspace()->clientArea(PlacementArea, client), "placementArea"); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void LayerShellV1ClientTest::testFill_data() +{ + QTest::addColumn("anchor"); + QTest::addColumn("desiredSize"); + QTest::addColumn("expectedGeometry"); + + QTest::addRow("horizontal") << (Test::LayerSurfaceV1::anchor_left | + Test::LayerSurfaceV1::anchor_right) + << QSize(0, 124) + << QRect(0, 450, 1280, 124); + + QTest::addRow("vertical") << (Test::LayerSurfaceV1::anchor_top | + Test::LayerSurfaceV1::anchor_bottom) + << QSize(280, 0) + << QRect(500, 0, 280, 1024); + + QTest::addRow("all") << (Test::LayerSurfaceV1::anchor_left | + Test::LayerSurfaceV1::anchor_top | + Test::LayerSurfaceV1::anchor_right | + Test::LayerSurfaceV1::anchor_bottom) + << QSize(0, 0) + << QRect(0, 0, 1280, 1024); +} + +void LayerShellV1ClientTest::testFill() +{ + // Create a layer shell surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createLayerSurfaceV1(surface.data(), QStringLiteral("test"))); + + // Set the initial state of the layer surface. + QFETCH(int, anchor); + QFETCH(QSize, desiredSize); + shellSurface->set_anchor(anchor); + shellSurface->set_size(desiredSize.width(), desiredSize.height()); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.data(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), requestedSize, Qt::red); + QVERIFY(client); + + // Verify that the client is placed at expected location. + QTEST(client->frameGeometry(), "expectedGeometry"); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void LayerShellV1ClientTest::testStack() +{ + // Create a layer shell surface. + QScopedPointer surface1(Test::createSurface()); + QScopedPointer shellSurface1(Test::createLayerSurfaceV1(surface1.data(), QStringLiteral("test"))); + + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createLayerSurfaceV1(surface2.data(), QStringLiteral("test"))); + + // Set the initial state of the layer surface. + shellSurface1->set_anchor(Test::LayerSurfaceV1::anchor_left); + shellSurface1->set_size(80, 124); + shellSurface1->set_exclusive_zone(80); + surface1->commit(KWayland::Client::Surface::CommitFlag::None); + + shellSurface2->set_anchor(Test::LayerSurfaceV1::anchor_left); + shellSurface2->set_size(200, 124); + shellSurface2->set_exclusive_zone(200); + surface2->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surfaces. + QSignalSpy configureRequestedSpy1(shellSurface1.data(), &Test::LayerSurfaceV1::configureRequested); + QSignalSpy configureRequestedSpy2(shellSurface2.data(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy2.wait()); + const QSize requestedSize1 = configureRequestedSpy1.last().at(1).toSize(); + const QSize requestedSize2 = configureRequestedSpy2.last().at(1).toSize(); + + // Map the layer surface. + shellSurface1->ack_configure(configureRequestedSpy1.last().at(0).toUInt()); + AbstractClient *client1 = Test::renderAndWaitForShown(surface1.data(), requestedSize1, Qt::red); + QVERIFY(client1); + + shellSurface2->ack_configure(configureRequestedSpy2.last().at(0).toUInt()); + AbstractClient *client2 = Test::renderAndWaitForShown(surface2.data(), requestedSize2, Qt::red); + QVERIFY(client2); + + // Check that the second layer surface is placed next to the first. + QCOMPARE(client1->frameGeometry(), QRect(0, 450, 80, 124)); + QCOMPARE(client2->frameGeometry(), QRect(80, 450, 200, 124)); + + // Check that the work area has been adjusted accordingly. + QCOMPARE(workspace()->clientArea(PlacementArea, client1), QRect(280, 0, 1000, 1024)); + QCOMPARE(workspace()->clientArea(PlacementArea, client2), QRect(280, 0, 1000, 1024)); + + // Destroy the client. + shellSurface1.reset(); + QVERIFY(Test::waitForWindowDestroyed(client1)); + shellSurface2.reset(); + QVERIFY(Test::waitForWindowDestroyed(client2)); +} + +void LayerShellV1ClientTest::testFocus() +{ + // Create a layer shell surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createLayerSurfaceV1(surface.data(), QStringLiteral("test"))); + + // Set the initial state of the layer surface. + shellSurface->set_keyboard_interactivity(1); + shellSurface->set_size(280, 124); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.data(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), requestedSize, Qt::red); + QVERIFY(client); + + // The layer surface must be focused when it's mapped. + QVERIFY(client->isActive()); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void LayerShellV1ClientTest::testActivate_data() +{ + QTest::addColumn("layer"); + QTest::addColumn("active"); + + QTest::addRow("overlay") << int(Test::LayerShellV1::layer_overlay) << true; + QTest::addRow("top") << int(Test::LayerShellV1::layer_top) << true; + QTest::addRow("bottom") << int(Test::LayerShellV1::layer_bottom) << false; + QTest::addRow("background") << int(Test::LayerShellV1::layer_background) << false; +} + +void LayerShellV1ClientTest::testActivate() +{ + // Create a layer shell surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createLayerSurfaceV1(surface.data(), QStringLiteral("test"))); + + // Set the initial state of the layer surface. + QFETCH(int, layer); + shellSurface->set_layer(layer); + shellSurface->set_size(280, 124); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.data(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), requestedSize, Qt::red); + QVERIFY(client); + QVERIFY(!client->isActive()); + + // Try to activate the layer surface. + shellSurface->set_keyboard_interactivity(1); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + QSignalSpy activeChangedSpy(client, &AbstractClient::activeChanged); + QVERIFY(activeChangedSpy.isValid()); + QTEST(activeChangedSpy.wait(1000), "active"); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +void LayerShellV1ClientTest::testUnmap() +{ + // Create a layer shell surface. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createLayerSurfaceV1(surface.data(), QStringLiteral("test"))); + + // Set the initial state of the layer surface. + shellSurface->set_size(280, 124); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.data(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + QVERIFY(configureRequestedSpy.wait()); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(280, 124), Qt::red); + QVERIFY(client); + + // Unmap the layer surface. + surface->attachBuffer(KWayland::Client::Buffer::Ptr()); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(Test::waitForWindowDestroyed(client)); + + // Notify the compositor that we want to map the layer surface. + shellSurface->set_size(280, 124); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the configure event. + QVERIFY(configureRequestedSpy.wait()); + + // Map the layer surface back. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + client = Test::renderAndWaitForShown(surface.data(), QSize(280, 124), Qt::red); + QVERIFY(client); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +} // namespace KWin + +WAYLANDTEST_MAIN(KWin::LayerShellV1ClientTest) +#include "layershellv1client_test.moc" diff --git a/autotests/integration/protocols/wlr-layer-shell-unstable-v1.xml b/autotests/integration/protocols/wlr-layer-shell-unstable-v1.xml new file mode 100644 index 000000000..0736a45e2 --- /dev/null +++ b/autotests/integration/protocols/wlr-layer-shell-unstable-v1.xml @@ -0,0 +1,325 @@ + + + + Copyright © 2017 Drew DeVault + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + Clients can use this interface to assign the surface_layer role to + wl_surfaces. Such surfaces are assigned to a "layer" of the output and + rendered with a defined z-depth respective to each other. They may also be + anchored to the edges and corners of a screen and specify input handling + semantics. This interface should be suitable for the implementation of + many desktop shell components, and a broad number of other applications + that interact with the desktop. + + + + + Create a layer surface for an existing surface. This assigns the role of + layer_surface, or raises a protocol error if another role is already + assigned. + + Creating a layer surface from a wl_surface which has a buffer attached + or committed is a client error, and any attempts by a client to attach + or manipulate a buffer prior to the first layer_surface.configure call + must also be treated as errors. + + After creating a layer_surface object and setting it up, the client + must perform an initial commit without any buffer attached. + The compositor will reply with a layer_surface.configure event. + The client must acknowledge it and is then allowed to attach a buffer + to map the surface. + + You may pass NULL for output to allow the compositor to decide which + output to use. Generally this will be the one that the user most + recently interacted with. + + Clients can specify a namespace that defines the purpose of the layer + surface. + + + + + + + + + + + + + + + + + These values indicate which layers a surface can be rendered in. They + are ordered by z depth, bottom-most first. Traditional shell surfaces + will typically be rendered between the bottom and top layers. + Fullscreen shell surfaces are typically rendered at the top layer. + Multiple surfaces can share a single layer, and ordering within a + single layer is undefined. + + + + + + + + + + + + + This request indicates that the client will not use the layer_shell + object any more. Objects that have been created through this instance + are not affected. + + + + + + + An interface that may be implemented by a wl_surface, for surfaces that + are designed to be rendered as a layer of a stacked desktop-like + environment. + + Layer surface state (layer, size, anchor, exclusive zone, + margin, interactivity) is double-buffered, and will be applied at the + time wl_surface.commit of the corresponding wl_surface is called. + + Attaching a null buffer to a layer surface unmaps it. + + Unmapping a layer_surface means that the surface cannot be shown by the + compositor until it is explicitly mapped again. The layer_surface + returns to the state it had right after layer_shell.get_layer_surface. + The client can re-map the surface by performing a commit without any + buffer attached, waiting for a configure event and handling it as usual. + + + + + Sets the size of the surface in surface-local coordinates. The + compositor will display the surface centered with respect to its + anchors. + + If you pass 0 for either value, the compositor will assign it and + inform you of the assignment in the configure event. You must set your + anchor to opposite edges in the dimensions you omit; not doing so is a + protocol error. Both values are 0 by default. + + Size is double-buffered, see wl_surface.commit. + + + + + + + + Requests that the compositor anchor the surface to the specified edges + and corners. If two orthogonal edges are specified (e.g. 'top' and + 'left'), then the anchor point will be the intersection of the edges + (e.g. the top left corner of the output); otherwise the anchor point + will be centered on that edge, or in the center if none is specified. + + Anchor is double-buffered, see wl_surface.commit. + + + + + + + Requests that the compositor avoids occluding an area with other + surfaces. The compositor's use of this information is + implementation-dependent - do not assume that this region will not + actually be occluded. + + A positive value is only meaningful if the surface is anchored to one + edge or an edge and both perpendicular edges. If the surface is not + anchored, anchored to only two perpendicular edges (a corner), anchored + to only two parallel edges or anchored to all edges, a positive value + will be treated the same as zero. + + A positive zone is the distance from the edge in surface-local + coordinates to consider exclusive. + + Surfaces that do not wish to have an exclusive zone may instead specify + how they should interact with surfaces that do. If set to zero, the + surface indicates that it would like to be moved to avoid occluding + surfaces with a positive exclusive zone. If set to -1, the surface + indicates that it would not like to be moved to accommodate for other + surfaces, and the compositor should extend it all the way to the edges + it is anchored to. + + For example, a panel might set its exclusive zone to 10, so that + maximized shell surfaces are not shown on top of it. A notification + might set its exclusive zone to 0, so that it is moved to avoid + occluding the panel, but shell surfaces are shown underneath it. A + wallpaper or lock screen might set their exclusive zone to -1, so that + they stretch below or over the panel. + + The default value is 0. + + Exclusive zone is double-buffered, see wl_surface.commit. + + + + + + + Requests that the surface be placed some distance away from the anchor + point on the output, in surface-local coordinates. Setting this value + for edges you are not anchored to has no effect. + + The exclusive zone includes the margin. + + Margin is double-buffered, see wl_surface.commit. + + + + + + + + + + Set to 1 to request that the seat send keyboard events to this layer + surface. For layers below the shell surface layer, the seat will use + normal focus semantics. For layers above the shell surface layers, the + seat will always give exclusive keyboard focus to the top-most layer + which has keyboard interactivity set to true. + + Layer surfaces receive pointer, touch, and tablet events normally. If + you do not want to receive them, set the input region on your surface + to an empty region. + + Events is double-buffered, see wl_surface.commit. + + + + + + + This assigns an xdg_popup's parent to this layer_surface. This popup + should have been created via xdg_surface::get_popup with the parent set + to NULL, and this request must be invoked before committing the popup's + initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + + + + + + This request destroys the layer surface. + + + + + + The configure event asks the client to resize its surface. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + The client is free to dismiss all but the last configure event it + received. + + The width and height arguments specify the size of the window in + surface-local coordinates. + + The size is a hint, in the sense that the client is free to ignore it if + it doesn't resize, pick a smaller size (to satisfy aspect ratio or + resize in steps of NxM pixels). If the client picks a smaller size and + is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the + surface will be centered on this axis. + + If the width or height arguments are zero, it means the client should + decide its own window dimension. + + + + + + + + + The closed event is sent by the compositor when the surface will no + longer be shown. The output may have been destroyed or the user may + have asked for it to be removed. Further changes to the surface will be + ignored. The client should destroy the resource after receiving this + event, and create a new surface if they so choose. + + + + + + + + + + + + + + + + + + + + + Change the layer that the surface is rendered on. + + Layer is double-buffered, see wl_surface.commit. + + + + + diff --git a/autotests/integration/test_helpers.cpp b/autotests/integration/test_helpers.cpp index 2b56c6fd1..11d4bd43f 100644 --- a/autotests/integration/test_helpers.cpp +++ b/autotests/integration/test_helpers.cpp @@ -54,6 +54,26 @@ namespace KWin namespace Test { +LayerShellV1::~LayerShellV1() +{ + destroy(); +} + +LayerSurfaceV1::~LayerSurfaceV1() +{ + destroy(); +} + +void LayerSurfaceV1::zwlr_layer_surface_v1_configure(uint32_t serial, uint32_t width, uint32_t height) +{ + emit configureRequested(serial, QSize(width, height)); +} + +void LayerSurfaceV1::zwlr_layer_surface_v1_closed() +{ + emit closeRequested(); +} + static struct { ConnectionThread *connection = nullptr; EventQueue *queue = nullptr; @@ -78,6 +98,7 @@ static struct { QtWayland::zwp_input_panel_v1 *inputPanelV1 = nullptr; MockInputMethod *inputMethodV1 = nullptr; QtWayland::zwp_input_method_context_v1 *inputMethodContextV1 = nullptr; + LayerShellV1 *layerShellV1 = nullptr; } s_waylandConnection; class MockInputMethod : public QtWayland::zwp_input_method_v1 @@ -186,6 +207,12 @@ bool setupWaylandConnection(AdditionalWaylandInterfaces flags) s_waylandConnection.inputPanelV1 = new QtWayland::zwp_input_panel_v1(*registry, name, version); } } + if (flags & AdditionalWaylandInterface::LayerShellV1) { + if (interface == QByteArrayLiteral("zwlr_layer_shell_v1")) { + s_waylandConnection.layerShellV1 = new LayerShellV1(); + s_waylandConnection.layerShellV1->init(*registry, name, version); + } + } }); QSignalSpy allAnnounced(registry, &Registry::interfacesAnnounced); @@ -332,6 +359,8 @@ void destroyWaylandConnection() s_waylandConnection.textInputManager = nullptr; delete s_waylandConnection.inputPanelV1; s_waylandConnection.inputPanelV1 = nullptr; + delete s_waylandConnection.layerShellV1; + s_waylandConnection.layerShellV1 = nullptr; if (s_waylandConnection.thread) { QSignalSpy spy(s_waylandConnection.connection, &QObject::destroyed); s_waylandConnection.connection->deleteLater(); @@ -421,6 +450,10 @@ TextInputManager *waylandTextInputManager() return s_waylandConnection.textInputManager; } +QVector waylandOutputs() +{ + return s_waylandConnection.outputs; +} bool waitForWaylandPointer() { @@ -531,6 +564,25 @@ SubSurface *createSubSurface(Surface *surface, Surface *parentSurface, QObject * return s; } +LayerSurfaceV1 *createLayerSurfaceV1(Surface *surface, const QString &scope, Output *output, LayerShellV1::layer layer) +{ + LayerShellV1 *shell = s_waylandConnection.layerShellV1; + if (!shell) { + qWarning() << "Could not create a layer surface because the layer shell global is not bound"; + return nullptr; + } + + struct ::wl_output *nativeOutput = nullptr; + if (output) { + nativeOutput = *output; + } + + LayerSurfaceV1 *shellSurface = new LayerSurfaceV1(); + shellSurface->init(shell->get_layer_surface(*surface, nativeOutput, layer, scope)); + + return shellSurface; +} + XdgShellSurface *createXdgShellStableSurface(Surface *surface, QObject *parent, CreationSetup creationSetup) { if (!s_waylandConnection.xdgShellStable) { diff --git a/layershellv1client.cpp b/layershellv1client.cpp new file mode 100644 index 000000000..49d67a4e1 --- /dev/null +++ b/layershellv1client.cpp @@ -0,0 +1,277 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "layershellv1client.h" +#include "abstract_output.h" +#include "layershellv1integration.h" +#include "deleted.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#include + +using namespace KWaylandServer; + +namespace KWin +{ + +static NET::WindowType scopeToType(const QString &scope) +{ + static const QHash scopeToType { + { QStringLiteral("desktop"), NET::Desktop }, + { QStringLiteral("dock"), NET::Dock }, + { QStringLiteral("crititical-notification"), NET::CriticalNotification }, + { QStringLiteral("notification"), NET::Notification }, + { QStringLiteral("tooltip"), NET::Tooltip }, + { QStringLiteral("on-screen-display"), NET::OnScreenDisplay }, + { QStringLiteral("dialog"), NET::Dialog }, + { QStringLiteral("splash"), NET::Splash }, + { QStringLiteral("utility"), NET::Utility }, + }; + return scopeToType.value(scope.toLower(), NET::Normal); +} + +LayerShellV1Client::LayerShellV1Client(LayerSurfaceV1Interface *shellSurface, + AbstractOutput *output, + LayerShellV1Integration *integration) + : WaylandClient(shellSurface->surface()) + , m_output(output) + , m_integration(integration) + , m_shellSurface(shellSurface) + , m_windowType(scopeToType(shellSurface->scope())) +{ + setSkipSwitcher(!isDesktop()); + setSkipPager(true); + setSkipTaskbar(true); + setSizeSyncMode(SyncMode::Async); + setPositionSyncMode(SyncMode::Sync); + setupCompositing(); + + connect(shellSurface, &LayerSurfaceV1Interface::aboutToBeDestroyed, + this, &LayerShellV1Client::destroyClient); + connect(shellSurface->surface(), &SurfaceInterface::aboutToBeDestroyed, + this, &LayerShellV1Client::destroyClient); + + connect(output, &AbstractOutput::geometryChanged, + this, &LayerShellV1Client::scheduleRearrange); + connect(output, &AbstractOutput::destroyed, + this, &LayerShellV1Client::handleOutputDestroyed); + + connect(shellSurface->surface(), &SurfaceInterface::sizeChanged, + this, &LayerShellV1Client::handleSizeChanged); + connect(shellSurface->surface(), &SurfaceInterface::unmapped, + this, &LayerShellV1Client::handleUnmapped); + connect(shellSurface->surface(), &SurfaceInterface::committed, + this, &LayerShellV1Client::handleCommitted); + + connect(shellSurface, &LayerSurfaceV1Interface::desiredSizeChanged, + this, &LayerShellV1Client::scheduleRearrange); + connect(shellSurface, &LayerSurfaceV1Interface::layerChanged, + this, &LayerShellV1Client::scheduleRearrange); + connect(shellSurface, &LayerSurfaceV1Interface::marginsChanged, + this, &LayerShellV1Client::scheduleRearrange); + connect(shellSurface, &LayerSurfaceV1Interface::anchorChanged, + this, &LayerShellV1Client::scheduleRearrange); + connect(shellSurface, &LayerSurfaceV1Interface::exclusiveZoneChanged, + this, &LayerShellV1Client::scheduleRearrange); + connect(shellSurface, &LayerSurfaceV1Interface::acceptsFocusChanged, + this, &LayerShellV1Client::handleAcceptsFocusChanged); +} + +LayerSurfaceV1Interface *LayerShellV1Client::shellSurface() const +{ + return m_shellSurface; +} + +AbstractOutput *LayerShellV1Client::output() const +{ + return m_output; +} + +void LayerShellV1Client::scheduleRearrange() +{ + m_integration->scheduleRearrange(); +} + +NET::WindowType LayerShellV1Client::windowType(bool, int) const +{ + return m_windowType; +} + +bool LayerShellV1Client::isPlaceable() const +{ + return false; +} + +bool LayerShellV1Client::isCloseable() const +{ + return true; +} + +bool LayerShellV1Client::isMovable() const +{ + return false; +} + +bool LayerShellV1Client::isMovableAcrossScreens() const +{ + return false; +} + +bool LayerShellV1Client::isResizable() const +{ + return false; +} + +bool LayerShellV1Client::isInitialPositionSet() const +{ + return true; +} + +bool LayerShellV1Client::takeFocus() +{ + setActive(true); + return true; +} + +bool LayerShellV1Client::wantsInput() const +{ + return acceptsFocus() && readyForPainting(); +} + +StrutRect LayerShellV1Client::strutRect(StrutArea area) const +{ + switch (area) { + case StrutAreaLeft: + if (m_shellSurface->exclusiveEdge() == Qt::LeftEdge) { + return StrutRect(x(), y(), m_shellSurface->exclusiveZone(), height(), StrutAreaLeft); + } + return StrutRect(); + case StrutAreaRight: + if (m_shellSurface->exclusiveEdge() == Qt::RightEdge) { + return StrutRect(x() + width() - m_shellSurface->exclusiveZone(), y(), + m_shellSurface->exclusiveZone(), height(), StrutAreaRight); + } + return StrutRect(); + case StrutAreaTop: + if (m_shellSurface->exclusiveEdge() == Qt::TopEdge) { + return StrutRect(x(), y(), width(), m_shellSurface->exclusiveZone(), StrutAreaTop); + } + return StrutRect(); + case StrutAreaBottom: + if (m_shellSurface->exclusiveEdge() == Qt::BottomEdge) { + return StrutRect(x(), y() + height() - m_shellSurface->exclusiveZone(), + width(), m_shellSurface->exclusiveZone(), StrutAreaBottom); + } + return StrutRect(); + default: + return StrutRect(); + } +} + +bool LayerShellV1Client::hasStrut() const +{ + return m_shellSurface->exclusiveZone() > 0; +} + +void LayerShellV1Client::destroyClient() +{ + markAsZombie(); + cleanTabBox(); + Deleted *deleted = Deleted::create(this); + emit windowClosed(this, deleted); + StackingUpdatesBlocker blocker(workspace()); + cleanGrouping(); + waylandServer()->removeClient(this); + deleted->unrefWindow(); + scheduleRearrange(); + delete this; +} + +void LayerShellV1Client::closeWindow() +{ + m_shellSurface->sendClosed(); +} + +Layer LayerShellV1Client::belongsToLayer() const +{ + if (!isNormalWindow()) { + return WaylandClient::belongsToLayer(); + } + switch (m_shellSurface->layer()) { + case LayerSurfaceV1Interface::BackgroundLayer: + return DesktopLayer; + case LayerSurfaceV1Interface::BottomLayer: + return BelowLayer; + case LayerSurfaceV1Interface::TopLayer: + return AboveLayer; + case LayerSurfaceV1Interface::OverlayLayer: + return UnmanagedLayer; + default: + Q_UNREACHABLE(); + } +} + +bool LayerShellV1Client::acceptsFocus() const +{ + return m_shellSurface->acceptsFocus(); +} + +void LayerShellV1Client::addDamage(const QRegion ®ion) +{ + addRepaint(region); + WaylandClient::addDamage(region); +} + +void LayerShellV1Client::requestGeometry(const QRect &rect) +{ + WaylandClient::requestGeometry(rect); + m_shellSurface->sendConfigure(rect.size()); +} + +void LayerShellV1Client::handleSizeChanged() +{ + updateGeometry(QRect(pos(), clientSizeToFrameSize(surface()->size()))); + scheduleRearrange(); +} + +void LayerShellV1Client::handleUnmapped() +{ + m_integration->recreateClient(shellSurface()); +} + +void LayerShellV1Client::handleCommitted() +{ + if (surface()->buffer()) { + updateDepth(); + setReadyForPainting(); + } +} + +void LayerShellV1Client::handleAcceptsFocusChanged() +{ + switch (m_shellSurface->layer()) { + case LayerSurfaceV1Interface::TopLayer: + case LayerSurfaceV1Interface::OverlayLayer: + if (wantsInput()) { + workspace()->activateClient(this); + } + break; + case LayerSurfaceV1Interface::BackgroundLayer: + case LayerSurfaceV1Interface::BottomLayer: + break; + } +} + +void LayerShellV1Client::handleOutputDestroyed() +{ + closeWindow(); + destroyClient(); +} + +} // namespace KWin diff --git a/layershellv1client.h b/layershellv1client.h new file mode 100644 index 000000000..c59cc01eb --- /dev/null +++ b/layershellv1client.h @@ -0,0 +1,68 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "waylandclient.h" + +namespace KWaylandServer +{ +class LayerSurfaceV1Interface; +} + +namespace KWin +{ + +class AbstractOutput; +class LayerShellV1Integration; + +class LayerShellV1Client : public WaylandClient +{ + Q_OBJECT + +public: + explicit LayerShellV1Client(KWaylandServer::LayerSurfaceV1Interface *shellSurface, + AbstractOutput *output, + LayerShellV1Integration *integration); + + KWaylandServer::LayerSurfaceV1Interface *shellSurface() const; + AbstractOutput *output() const; + + NET::WindowType windowType(bool direct = false, int supported_types = 0) const override; + bool isPlaceable() const override; + bool isCloseable() const override; + bool isMovable() const override; + bool isMovableAcrossScreens() const override; + bool isResizable() const override; + bool isInitialPositionSet() const override; + bool takeFocus() override; + bool wantsInput() const override; + StrutRect strutRect(StrutArea area) const override; + bool hasStrut() const override; + void destroyClient() override; + void closeWindow() override; + +protected: + Layer belongsToLayer() const override; + bool acceptsFocus() const override; + void requestGeometry(const QRect &rect) override; + void addDamage(const QRegion ®ion) override; + +private: + void handleSizeChanged(); + void handleUnmapped(); + void handleCommitted(); + void handleAcceptsFocusChanged(); + void handleOutputDestroyed(); + void scheduleRearrange(); + + AbstractOutput *m_output; + LayerShellV1Integration *m_integration; + KWaylandServer::LayerSurfaceV1Interface *m_shellSurface; + NET::WindowType m_windowType; +}; + +} // namespace KWin diff --git a/layershellv1integration.cpp b/layershellv1integration.cpp new file mode 100644 index 000000000..c4c302814 --- /dev/null +++ b/layershellv1integration.cpp @@ -0,0 +1,214 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "layershellv1integration.h" +#include "abstract_wayland_output.h" +#include "layershellv1client.h" +#include "platform.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include + +#include + +using namespace KWaylandServer; + +namespace KWin +{ + +static const Qt::Edges AnchorHorizontal = Qt::LeftEdge | Qt::RightEdge; +static const Qt::Edges AnchorVertical = Qt::TopEdge | Qt::BottomEdge; + +LayerShellV1Integration::LayerShellV1Integration(QObject *parent) + : WaylandShellIntegration(parent) +{ + LayerShellV1Interface *shell = waylandServer()->display()->createLayerShellV1(this); + connect(shell, &KWaylandServer::LayerShellV1Interface::surfaceCreated, + this, &LayerShellV1Integration::createClient); + + m_rearrangeTimer = new QTimer(this); + m_rearrangeTimer->setSingleShot(true); + connect(m_rearrangeTimer, &QTimer::timeout, this, &LayerShellV1Integration::rearrange); +} + +void LayerShellV1Integration::createClient(LayerSurfaceV1Interface *shellSurface) +{ + AbstractOutput *output = waylandServer()->findOutput(shellSurface->output()); + if (!output) { + output = kwinApp()->platform()->findOutput(screens()->current()); + } + if (!output) { + qCWarning(KWIN_CORE) << "Could not find any suitable output for a layer surface"; + shellSurface->sendClosed(); + return; + } + + emit clientCreated(new LayerShellV1Client(shellSurface, output, this)); +} + +void LayerShellV1Integration::recreateClient(LayerSurfaceV1Interface *shellSurface) +{ + destroyClient(shellSurface); + createClient(shellSurface); +} + +void LayerShellV1Integration::destroyClient(LayerSurfaceV1Interface *shellSurface) +{ + const QList clients = waylandServer()->clients(); + for (AbstractClient *client : clients) { + LayerShellV1Client *layerShellClient = qobject_cast(client); + if (layerShellClient && layerShellClient->shellSurface() == shellSurface) { + layerShellClient->destroyClient(); + break; + } + } +} + +static void adjustWorkArea(const LayerSurfaceV1Interface *shellSurface, QRect *workArea) +{ + if (shellSurface->exclusiveEdge() == Qt::LeftEdge) { + workArea->adjust(shellSurface->leftMargin() + shellSurface->exclusiveZone(), 0, 0, 0); + } + if (shellSurface->exclusiveEdge() == Qt::RightEdge) { + workArea->adjust(0, 0, -shellSurface->rightMargin() - shellSurface->exclusiveZone(), 0); + } + if (shellSurface->exclusiveEdge() == Qt::TopEdge) { + workArea->adjust(0, shellSurface->topMargin() + shellSurface->exclusiveZone(), 0, 0); + } + if (shellSurface->exclusiveEdge() == Qt::BottomEdge) { + workArea->adjust(0, 0, 0, -shellSurface->bottomMargin() - shellSurface->exclusiveZone()); + } +} + +static void rearrangeLayer(const QList &clients, QRect *workArea, + LayerSurfaceV1Interface::Layer layer, bool exclusive) +{ + for (LayerShellV1Client *client : clients) { + LayerSurfaceV1Interface *shellSurface = client->shellSurface(); + + if (shellSurface->layer() != layer) { + continue; + } + if (exclusive != shellSurface->exclusiveZone() > 0) { + continue; + } + + QRect bounds; + if (shellSurface->exclusiveZone() == -1) { + bounds = workspace()->clientArea(ScreenArea, client); + } else { + bounds = *workArea; + } + + QRect geometry(QPoint(0, 0), shellSurface->desiredSize()); + + if ((shellSurface->anchor() & AnchorHorizontal) && geometry.width() == 0) { + geometry.setLeft(bounds.left()); + geometry.setWidth(bounds.width()); + } else if (shellSurface->anchor() & Qt::LeftEdge) { + geometry.moveLeft(bounds.left()); + } else if (shellSurface->anchor() & Qt::RightEdge) { + geometry.moveRight(bounds.right()); + } else { + geometry.moveLeft(bounds.left() + (bounds.width() - geometry.width()) / 2); + } + + if ((shellSurface->anchor() & AnchorVertical) && geometry.height() == 0) { + geometry.setTop(bounds.top()); + geometry.setHeight(bounds.height()); + } else if (shellSurface->anchor() & Qt::TopEdge) { + geometry.moveTop(bounds.top()); + } else if (shellSurface->anchor() & Qt::BottomEdge) { + geometry.moveBottom(bounds.bottom()); + } else { + geometry.moveTop(bounds.top() + (bounds.height() - geometry.height()) / 2); + } + + if ((shellSurface->anchor() & AnchorHorizontal) == AnchorHorizontal) { + geometry.adjust(shellSurface->leftMargin(), 0, -shellSurface->rightMargin(), 0); + } else if (shellSurface->anchor() & Qt::LeftEdge) { + geometry.translate(shellSurface->leftMargin(), 0); + } else if (shellSurface->anchor() & Qt::RightEdge) { + geometry.translate(-shellSurface->rightMargin(), 0); + } + + if ((shellSurface->anchor() & AnchorVertical) == AnchorVertical) { + geometry.adjust(0, shellSurface->topMargin(), 0, -shellSurface->bottomMargin()); + } else if (shellSurface->anchor() & Qt::TopEdge) { + geometry.translate(0, shellSurface->topMargin()); + } else if (shellSurface->anchor() & Qt::BottomEdge) { + geometry.translate(0, -shellSurface->bottomMargin()); + } + + if (geometry.isValid()) { + client->setFrameGeometry(geometry); + } else { + qCWarning(KWIN_CORE) << "Closing a layer shell client due to invalid geometry"; + client->closeWindow(); + continue; + } + + if (exclusive && shellSurface->exclusiveZone() > 0) { + adjustWorkArea(shellSurface, workArea); + } + } +} + +static QList clientsForOutput(AbstractOutput *output) +{ + QList result; + const QList clients = waylandServer()->clients(); + for (AbstractClient *client : clients) { + LayerShellV1Client *layerShellClient = qobject_cast(client); + if (!layerShellClient || layerShellClient->output() != output) { + continue; + } + if (layerShellClient->shellSurface()->isCommitted()) { + result.append(layerShellClient); + } + } + return result; +} + +static void rearrangeOutput(AbstractOutput *output) +{ + const QList clients = clientsForOutput(output); + if (!clients.isEmpty()) { + QRect workArea = output->geometry(); + + rearrangeLayer(clients, &workArea, LayerSurfaceV1Interface::OverlayLayer, true); + rearrangeLayer(clients, &workArea, LayerSurfaceV1Interface::TopLayer, true); + rearrangeLayer(clients, &workArea, LayerSurfaceV1Interface::BottomLayer, true); + rearrangeLayer(clients, &workArea, LayerSurfaceV1Interface::BackgroundLayer, true); + + rearrangeLayer(clients, &workArea, LayerSurfaceV1Interface::OverlayLayer, false); + rearrangeLayer(clients, &workArea, LayerSurfaceV1Interface::TopLayer, false); + rearrangeLayer(clients, &workArea, LayerSurfaceV1Interface::BottomLayer, false); + rearrangeLayer(clients, &workArea, LayerSurfaceV1Interface::BackgroundLayer, false); + } +} + +void LayerShellV1Integration::rearrange() +{ + m_rearrangeTimer->stop(); + + const QVector outputs = kwinApp()->platform()->outputs(); + for (AbstractOutput *output : outputs) { + rearrangeOutput(output); + } + + workspace()->updateClientArea(); +} + +void LayerShellV1Integration::scheduleRearrange() +{ + m_rearrangeTimer->start(); +} + +} // namespace KWin diff --git a/layershellv1integration.h b/layershellv1integration.h new file mode 100644 index 000000000..0ccf4bb02 --- /dev/null +++ b/layershellv1integration.h @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "waylandshellintegration.h" + +namespace KWaylandServer +{ +class LayerSurfaceV1Interface; +} + +namespace KWin +{ + +class LayerShellV1Integration : public WaylandShellIntegration +{ + Q_OBJECT + +public: + explicit LayerShellV1Integration(QObject *parent = nullptr); + + void rearrange(); + void scheduleRearrange(); + + void createClient(KWaylandServer::LayerSurfaceV1Interface *shellSurface); + void recreateClient(KWaylandServer::LayerSurfaceV1Interface *shellSurface); + void destroyClient(KWaylandServer::LayerSurfaceV1Interface *shellSurface); + +private: + QTimer *m_rearrangeTimer; +}; + +} // namespace KWin diff --git a/utils.cpp b/utils.cpp index eea1fd258..abf215b60 100644 --- a/utils.cpp +++ b/utils.cpp @@ -49,6 +49,12 @@ StrutRect::StrutRect(QRect rect, StrutArea area) { } +StrutRect::StrutRect(int x, int y, int width, int height, StrutArea area) + : QRect(x, y, width, height) + , m_area(area) +{ +} + StrutRect::StrutRect(const StrutRect& other) : QRect(other) , m_area(other.area()) diff --git a/utils.h b/utils.h index 22a780b8f..6c35a1ef4 100644 --- a/utils.h +++ b/utils.h @@ -62,6 +62,7 @@ class StrutRect : public QRect { public: explicit StrutRect(QRect rect = QRect(), StrutArea area = StrutAreaInvalid); + StrutRect(int x, int y, int width, int height, StrutArea area = StrutAreaInvalid); StrutRect(const StrutRect& other); StrutRect &operator=(const StrutRect& other); inline StrutArea area() const { diff --git a/wayland_server.cpp b/wayland_server.cpp index 4198daf28..23a043026 100644 --- a/wayland_server.cpp +++ b/wayland_server.cpp @@ -14,6 +14,7 @@ #include "idle_inhibition.h" #include "inputpanelv1client.h" #include "screens.h" +#include "layershellv1integration.h" #include "waylandxdgshellintegration.h" #include "workspace.h" #include "xdgshellclient.h" @@ -366,10 +367,14 @@ bool WaylandServer::init(const QByteArray &socketName, InitializationFlags flags m_tabletManager = m_display->createTabletManagerInterface(m_display); m_keyboardShortcutsInhibitManager = m_display->createKeyboardShortcutsInhibitManagerV1(m_display); - auto shellIntegration = new WaylandXdgShellIntegration(this); - connect(shellIntegration, &WaylandXdgShellIntegration::clientCreated, + auto xdgShellIntegration = new WaylandXdgShellIntegration(this); + connect(xdgShellIntegration, &WaylandXdgShellIntegration::clientCreated, this, &WaylandServer::registerXdgGenericClient); + auto layerShellV1Integration = new LayerShellV1Integration(this); + connect(layerShellV1Integration, &LayerShellV1Integration::clientCreated, + this, &WaylandServer::registerShellClient); + m_xdgDecorationManagerV1 = m_display->createXdgDecorationManagerV1(m_display); connect(m_xdgDecorationManagerV1, &XdgDecorationManagerV1Interface::decorationCreated, this, [this](XdgToplevelDecorationV1Interface *decoration) { diff --git a/waylandclient.cpp b/waylandclient.cpp index da599d2fd..fc4701530 100644 --- a/waylandclient.cpp +++ b/waylandclient.cpp @@ -50,8 +50,6 @@ WaylandClient::WaylandClient(SurfaceInterface *surface) connect(this, &WaylandClient::frameGeometryChanged, this, &WaylandClient::updateClientOutputs); - connect(this, &WaylandClient::frameGeometryChanged, - this, &WaylandClient::updateClientArea); connect(this, &WaylandClient::desktopFileNameChanged, this, &WaylandClient::updateIcon); connect(screens(), &Screens::changed, this, @@ -208,13 +206,6 @@ bool WaylandClient::belongsToDesktop() const ); } -void WaylandClient::updateClientArea() -{ - if (hasStrut()) { - workspace()->updateClientArea(); - } -} - void WaylandClient::updateClientOutputs() { QVector clientOutputs; diff --git a/waylandclient.h b/waylandclient.h index 6c279a655..159dee42e 100644 --- a/waylandclient.h +++ b/waylandclient.h @@ -75,7 +75,6 @@ protected: virtual void updateGeometry(const QRect &rect); private: - void updateClientArea(); void updateClientOutputs(); void updateIcon(); void updateResourceName(); diff --git a/xdgshellclient.cpp b/xdgshellclient.cpp index ce7b3dee6..a9a8bd2ee 100644 --- a/xdgshellclient.cpp +++ b/xdgshellclient.cpp @@ -1500,6 +1500,13 @@ void XdgToplevelClient::updateShowOnScreenEdge() } } +void XdgToplevelClient::updateClientArea() +{ + if (hasStrut()) { + workspace()->updateClientArea(); + } +} + void XdgToplevelClient::setupWindowManagementIntegration() { if (isLockScreen()) { @@ -1513,6 +1520,8 @@ void XdgToplevelClient::setupPlasmaShellIntegration() { connect(surface(), &SurfaceInterface::mapped, this, &XdgToplevelClient::updateShowOnScreenEdge); + connect(this, &XdgToplevelClient::frameGeometryChanged, + this, &XdgToplevelClient::updateClientArea); } void XdgToplevelClient::setFullScreen(bool set, bool user) diff --git a/xdgshellclient.h b/xdgshellclient.h index 7bb695f76..7345dc905 100644 --- a/xdgshellclient.h +++ b/xdgshellclient.h @@ -186,6 +186,7 @@ private: void updateMaximizeMode(MaximizeMode maximizeMode); void updateFullScreenMode(bool set); void updateShowOnScreenEdge(); + void updateClientArea(); void setupWindowManagementIntegration(); void setupPlasmaShellIntegration(); void sendPing(PingReason reason);