diff --git a/changelog/next.md b/changelog/next.md index 3f059b1..c5d93e2 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -27,6 +27,7 @@ set shell id. - Added a way to detect if an icon is from the system icon theme or not. - Added vulkan support to screencopy. - Added generic WindowManager interface implementing ext-workspace. +- Added ext-background-effect window blur support. ## Other Changes diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 196f1e0..cf84713 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -120,6 +120,9 @@ if (HYPRLAND) add_subdirectory(hyprland) endif() +add_subdirectory(background_effect) +list(APPEND WAYLAND_MODULES Quickshell.Wayland._BackgroundEffect) + add_subdirectory(idle_inhibit) list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleInhibitor) diff --git a/src/wayland/background_effect/CMakeLists.txt b/src/wayland/background_effect/CMakeLists.txt new file mode 100644 index 0000000..f45f94d --- /dev/null +++ b/src/wayland/background_effect/CMakeLists.txt @@ -0,0 +1,24 @@ +qt_add_library(quickshell-wayland-background-effect STATIC + manager.cpp + surface.cpp + qml.cpp +) + +qt_add_qml_module(quickshell-wayland-background-effect + URI Quickshell.Wayland._BackgroundEffect + VERSION 0.1 + DEPENDENCIES QtQml +) + +install_qml_module(quickshell-wayland-background-effect) + +wl_proto(wlp-background-effect ext-background-effect-v1 "${WAYLAND_PROTOCOLS}/staging/ext-background-effect") + +target_link_libraries(quickshell-wayland-background-effect PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + wlp-background-effect +) + +qs_module_pch(quickshell-wayland-background-effect) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-background-effectplugin) diff --git a/src/wayland/background_effect/manager.cpp b/src/wayland/background_effect/manager.cpp new file mode 100644 index 0000000..4cb06f1 --- /dev/null +++ b/src/wayland/background_effect/manager.cpp @@ -0,0 +1,38 @@ +#include "manager.hpp" +#include + +#include +#include +#include +#include + +#include "surface.hpp" + +namespace qs::wayland::background_effect::impl { + +BackgroundEffectManager::BackgroundEffectManager(): QWaylandClientExtensionTemplate(1) { + this->initialize(); +} + +BackgroundEffectSurface* +BackgroundEffectManager::createEffectSurface(QtWaylandClient::QWaylandWindow* window) { + return new BackgroundEffectSurface(this->get_background_effect(window->surface())); +} + +bool BackgroundEffectManager::blurAvailable() const { + return this->isActive() && this->mBlurAvailable; +} + +void BackgroundEffectManager::ext_background_effect_manager_v1_capabilities(uint32_t flags) { + auto available = static_cast(flags & capability_blur); + if (available == this->mBlurAvailable) return; + this->mBlurAvailable = available; + emit this->blurAvailableChanged(); +} + +BackgroundEffectManager* BackgroundEffectManager::instance() { + static auto* instance = new BackgroundEffectManager(); // NOLINT + return instance->isInitialized() ? instance : nullptr; +} + +} // namespace qs::wayland::background_effect::impl diff --git a/src/wayland/background_effect/manager.hpp b/src/wayland/background_effect/manager.hpp new file mode 100644 index 0000000..6c2e981 --- /dev/null +++ b/src/wayland/background_effect/manager.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "surface.hpp" + +namespace qs::wayland::background_effect::impl { + +class BackgroundEffectManager + : public QWaylandClientExtensionTemplate + , public QtWayland::ext_background_effect_manager_v1 { + Q_OBJECT; + +public: + explicit BackgroundEffectManager(); + + BackgroundEffectSurface* createEffectSurface(QtWaylandClient::QWaylandWindow* window); + + [[nodiscard]] bool blurAvailable() const; + + static BackgroundEffectManager* instance(); + +signals: + void blurAvailableChanged(); + +protected: + void ext_background_effect_manager_v1_capabilities(uint32_t flags) override; + +private: + bool mBlurAvailable = false; +}; + +} // namespace qs::wayland::background_effect::impl diff --git a/src/wayland/background_effect/qml.cpp b/src/wayland/background_effect/qml.cpp new file mode 100644 index 0000000..b54a847 --- /dev/null +++ b/src/wayland/background_effect/qml.cpp @@ -0,0 +1,246 @@ +#include "qml.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/region.hpp" +#include "../../window/proxywindow.hpp" +#include "../../window/windowinterface.hpp" +#include "manager.hpp" +#include "surface.hpp" + +using QtWaylandClient::QWaylandWindow; + +namespace qs::wayland::background_effect { + +BackgroundEffect* BackgroundEffect::qmlAttachedProperties(QObject* object) { + auto* proxyWindow = qobject_cast(object); + + if (!proxyWindow) { + if (auto* iface = qobject_cast(object)) { + proxyWindow = iface->proxyWindow(); + } + } + + if (!proxyWindow) return nullptr; + return new BackgroundEffect(proxyWindow); +} + +BackgroundEffect::BackgroundEffect(ProxyWindowBase* window): QObject(nullptr), proxyWindow(window) { + QObject::connect( + window, + &ProxyWindowBase::windowConnected, + this, + &BackgroundEffect::onWindowConnected + ); + + QObject::connect(window, &ProxyWindowBase::polished, this, &BackgroundEffect::onWindowPolished); + + QObject::connect( + window, + &ProxyWindowBase::devicePixelRatioChanged, + this, + &BackgroundEffect::updateBlurRegion + ); + + QObject::connect(window, &QObject::destroyed, this, &BackgroundEffect::onProxyWindowDestroyed); + + if (window->backingWindow()) { + this->onWindowConnected(); + } +} + +PendingRegion* BackgroundEffect::blurRegion() const { return this->mBlurRegion; } + +void BackgroundEffect::setBlurRegion(PendingRegion* region) { + if (region == this->mBlurRegion) return; + + if (this->mBlurRegion) { + QObject::disconnect(this->mBlurRegion, nullptr, this, nullptr); + } + + this->mBlurRegion = region; + + if (region) { + QObject::connect(region, &QObject::destroyed, this, &BackgroundEffect::onBlurRegionDestroyed); + QObject::connect(region, &PendingRegion::changed, this, &BackgroundEffect::updateBlurRegion); + } + + this->updateBlurRegion(); + emit this->blurRegionChanged(); +} + +void BackgroundEffect::onBlurRegionDestroyed() { + this->mBlurRegion = nullptr; + this->updateBlurRegion(); + emit this->blurRegionChanged(); +} + +void BackgroundEffect::updateBlurRegion() { + if (!this->surface || !this->proxyWindow) return; + + this->pendingBlurRegion = true; + this->proxyWindow->schedulePolish(); +} + +void BackgroundEffect::onWindowPolished() { + if (!this->surface || !this->pendingBlurRegion) return; + if (!this->mWaylandWindow || !this->mWaylandWindow->surface()) { + this->pendingBlurRegion = false; + return; + } + + QRegion region; + if (this->mBlurRegion) { + region = + this->mBlurRegion->applyTo(QRect(0, 0, this->mWindow->width(), this->mWindow->height())); + + auto scale = QHighDpiScaling::factor(this->mWindow); + if (!qFuzzyCompare(scale, 1.0)) { + region = QHighDpi::scale(region, scale); + } + + auto margins = this->mWaylandWindow->clientSideMargins(); + region.translate(margins.left(), margins.top()); + } + + this->surface->setBlurRegion(region); + this->pendingBlurRegion = false; +} + +bool BackgroundEffect::eventFilter(QObject* object, QEvent* event) { + if (event->type() == QEvent::PlatformSurface) { + auto* surfaceEvent = dynamic_cast(event); + if (surfaceEvent->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed) { + this->surface = nullptr; + this->pendingBlurRegion = false; + } + } + + return this->QObject::eventFilter(object, event); +} + +void BackgroundEffect::onWindowConnected() { + this->mWindow = this->proxyWindow->backingWindow(); + this->mWindow->installEventFilter(this); + + QObject::connect( + this->mWindow, + &QWindow::visibleChanged, + this, + &BackgroundEffect::onWindowVisibleChanged + ); + + this->onWindowVisibleChanged(); +} + +void BackgroundEffect::onWindowVisibleChanged() { + if (this->mWindow->isVisible()) { + if (!this->mWindow->handle()) { + this->mWindow->create(); + } + } + + auto* window = dynamic_cast(this->mWindow->handle()); + if (window == this->mWaylandWindow) return; + + if (this->mWaylandWindow) { + QObject::disconnect(this->mWaylandWindow, nullptr, this, nullptr); + } + + this->mWaylandWindow = window; + if (!window) return; + + QObject::connect( + this->mWaylandWindow, + &QObject::destroyed, + this, + &BackgroundEffect::onWaylandWindowDestroyed + ); + + QObject::connect( + this->mWaylandWindow, + &QWaylandWindow::surfaceCreated, + this, + &BackgroundEffect::onWaylandSurfaceCreated + ); + + QObject::connect( + this->mWaylandWindow, + &QWaylandWindow::surfaceDestroyed, + this, + &BackgroundEffect::onWaylandSurfaceDestroyed + ); + + if (this->mWaylandWindow->surface()) { + this->onWaylandSurfaceCreated(); + } +} + +void BackgroundEffect::onWaylandWindowDestroyed() { this->mWaylandWindow = nullptr; } + +void BackgroundEffect::onWaylandSurfaceCreated() { + auto* manager = impl::BackgroundEffectManager::instance(); + + if (!manager) { + qWarning() << "Cannot enable background effect as ext-background-effect-v1 is not supported " + "by the current compositor."; + return; + } + + // Steal protocol surface from previous BackgroundEffect to avoid duplicate-attachment on reload. + auto v = this->mWaylandWindow->property("qs_background_effect"); + if (v.canConvert()) { + auto* prev = v.value(); + if (prev != this && prev->surface) { + this->surface.swap(prev->surface); + } + } + + if (!this->surface) { + this->surface = std::unique_ptr( + manager->createEffectSurface(this->mWaylandWindow) + ); + } + + this->mWaylandWindow->setProperty("qs_background_effect", QVariant::fromValue(this)); + + this->pendingBlurRegion = this->mBlurRegion != nullptr; + if (this->pendingBlurRegion) { + this->proxyWindow->schedulePolish(); + } +} + +void BackgroundEffect::onWaylandSurfaceDestroyed() { + this->surface = nullptr; + this->pendingBlurRegion = false; + + if (!this->proxyWindow) { + this->deleteLater(); + } +} + +void BackgroundEffect::onProxyWindowDestroyed() { + // Don't delete the BackgroundEffect, and therefore the impl::BackgroundEffectSurface + // until the wl_surface is destroyed. Deleting it when the proxy window is deleted would + // cause a frame without blur between the destruction of the ext_background_effect_surface_v1 + // and wl_surface objects. + + this->proxyWindow = nullptr; + + if (this->surface == nullptr) { + this->deleteLater(); + } +} + +} // namespace qs::wayland::background_effect diff --git a/src/wayland/background_effect/qml.hpp b/src/wayland/background_effect/qml.hpp new file mode 100644 index 0000000..dd93aec --- /dev/null +++ b/src/wayland/background_effect/qml.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "../../core/region.hpp" +#include "../../window/proxywindow.hpp" +#include "surface.hpp" + +namespace qs::wayland::background_effect { + +///! Background blur effect for Wayland surfaces. +/// Applies background blur behind a @@Quickshell.QsWindow or subclass, +/// as an attached object, using the [ext-background-effect-v1] Wayland protocol. +/// +/// > [!NOTE] Using a background effect requires the compositor support the +/// > [ext-background-effect-v1] protocol. +/// +/// [ext-background-effect-v1]: https://wayland.app/protocols/ext-background-effect-v1 +/// +/// #### Example +/// ```qml +/// @@Quickshell.PanelWindow { +/// id: root +/// color: "#80000000" +/// +/// BackgroundEffect.blurRegion: Region { item: root.contentItem } +/// } +/// ``` +class BackgroundEffect: public QObject { + Q_OBJECT; + // clang-format off + /// Region to blur behind the surface. Set to null to remove blur. + Q_PROPERTY(PendingRegion* blurRegion READ blurRegion WRITE setBlurRegion NOTIFY blurRegionChanged); + // clang-format on + QML_ELEMENT; + QML_UNCREATABLE("BackgroundEffect can only be used as an attached object."); + QML_ATTACHED(BackgroundEffect); + +public: + explicit BackgroundEffect(ProxyWindowBase* window); + + [[nodiscard]] PendingRegion* blurRegion() const; + void setBlurRegion(PendingRegion* region); + + static BackgroundEffect* qmlAttachedProperties(QObject* object); + + bool eventFilter(QObject* object, QEvent* event) override; + +signals: + void blurRegionChanged(); + +private slots: + void onWindowConnected(); + void onWindowVisibleChanged(); + void onWaylandWindowDestroyed(); + void onWaylandSurfaceCreated(); + void onWaylandSurfaceDestroyed(); + void onProxyWindowDestroyed(); + void onBlurRegionDestroyed(); + void onWindowPolished(); + void updateBlurRegion(); + +private: + ProxyWindowBase* proxyWindow = nullptr; + QWindow* mWindow = nullptr; + QtWaylandClient::QWaylandWindow* mWaylandWindow = nullptr; + + bool pendingBlurRegion = false; + PendingRegion* mBlurRegion = nullptr; + std::unique_ptr surface; +}; + +} // namespace qs::wayland::background_effect diff --git a/src/wayland/background_effect/surface.cpp b/src/wayland/background_effect/surface.cpp new file mode 100644 index 0000000..648361d --- /dev/null +++ b/src/wayland/background_effect/surface.cpp @@ -0,0 +1,37 @@ +#include "surface.hpp" + +#include +#include +#include +#include +#include + +namespace qs::wayland::background_effect::impl { + +BackgroundEffectSurface::BackgroundEffectSurface( + ::ext_background_effect_surface_v1* surface // NOLINT(misc-include-cleaner) +) + : QtWayland::ext_background_effect_surface_v1(surface) {} + +BackgroundEffectSurface::~BackgroundEffectSurface() { + if (!this->isInitialized()) return; + this->destroy(); +} + +void BackgroundEffectSurface::setBlurRegion(const QRegion& region) { + if (!this->isInitialized()) return; + + if (region.isEmpty()) { + this->set_blur_region(nullptr); + return; + } + + static const auto* waylandIntegration = QtWaylandClient::QWaylandIntegration::instance(); + auto* display = waylandIntegration->display(); + + auto* wlRegion = display->createRegion(region); + this->set_blur_region(wlRegion); + wl_region_destroy(wlRegion); // NOLINT(misc-include-cleaner) +} + +} // namespace qs::wayland::background_effect::impl diff --git a/src/wayland/background_effect/surface.hpp b/src/wayland/background_effect/surface.hpp new file mode 100644 index 0000000..65b0bc8 --- /dev/null +++ b/src/wayland/background_effect/surface.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include + +namespace qs::wayland::background_effect::impl { + +class BackgroundEffectSurface: public QtWayland::ext_background_effect_surface_v1 { +public: + explicit BackgroundEffectSurface(::ext_background_effect_surface_v1* surface); + ~BackgroundEffectSurface() override; + Q_DISABLE_COPY_MOVE(BackgroundEffectSurface); + + void setBlurRegion(const QRegion& region); +}; + +} // namespace qs::wayland::background_effect::impl diff --git a/src/wayland/background_effect/test/manual/background_effect.qml b/src/wayland/background_effect/test/manual/background_effect.qml new file mode 100644 index 0000000..8cb4e12 --- /dev/null +++ b/src/wayland/background_effect/test/manual/background_effect.qml @@ -0,0 +1,47 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland + +FloatingWindow { + id: root + color: "transparent" + contentItem.palette.windowText: "white" + + ColumnLayout { + anchors.centerIn: parent + + CheckBox { + id: enableBox + checked: true + text: "Enable Blur" + } + + Button { + text: "Hide->Show" + onClicked: { + root.visible = false + showTimer.start() + } + } + + Timer { + id: showTimer + interval: 200 + onTriggered: root.visible = true + } + + Slider { + id: radiusSlider + from: 0 + to: 1000 + value: 100 + } + } + + BackgroundEffect.blurRegion: Region { + item: enableBox.checked ? root.contentItem : null + radius: radiusSlider.value == -1 ? undefined : radiusSlider.value + } +} diff --git a/src/wayland/module.md b/src/wayland/module.md index 9ad15ba..964fa76 100644 --- a/src/wayland/module.md +++ b/src/wayland/module.md @@ -8,5 +8,6 @@ headers = [ "idle_inhibit/inhibitor.hpp", "idle_notify/monitor.hpp", "shortcuts_inhibit/inhibitor.hpp", + "background_effect/qml.hpp", ] -----