diff --git a/changelog/next.md b/changelog/next.md index a8981b9..fc6d79e 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -27,6 +27,8 @@ 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. +- Added per-corner radius support to Region. ## Other Changes @@ -38,6 +40,7 @@ set shell id. - Added `QS_DISABLE_FILE_WATCHER` environment variable to disable file watching. - Added `QS_DISABLE_CRASH_HANDLER` environment variable to disable crash handling. - Added `QS_CRASHREPORT_URL` environment variable to allow overriding the crash reporter link. +- Added `AppId` pragma and `QS_APP_ID` environment variable to allow overriding the desktop application ID. ## Bug Fixes diff --git a/src/core/instanceinfo.cpp b/src/core/instanceinfo.cpp index 1f71b8a..b9b7b44 100644 --- a/src/core/instanceinfo.cpp +++ b/src/core/instanceinfo.cpp @@ -3,14 +3,14 @@ #include QDataStream& operator<<(QDataStream& stream, const InstanceInfo& info) { - stream << info.instanceId << info.configPath << info.shellId << info.launchTime << info.pid - << info.display; + stream << info.instanceId << info.configPath << info.shellId << info.appId << info.launchTime + << info.pid << info.display; return stream; } QDataStream& operator>>(QDataStream& stream, InstanceInfo& info) { - stream >> info.instanceId >> info.configPath >> info.shellId >> info.launchTime >> info.pid - >> info.display; + stream >> info.instanceId >> info.configPath >> info.shellId >> info.appId >> info.launchTime + >> info.pid >> info.display; return stream; } diff --git a/src/core/instanceinfo.hpp b/src/core/instanceinfo.hpp index 977e4c2..a4a7e66 100644 --- a/src/core/instanceinfo.hpp +++ b/src/core/instanceinfo.hpp @@ -9,6 +9,7 @@ struct InstanceInfo { QString instanceId; QString configPath; QString shellId; + QString appId; QDateTime launchTime; pid_t pid = -1; QString display; diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 893c56e..415cf61 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -220,6 +221,7 @@ void LogManager::messageHandler( } if (display) { + auto locker = QMutexLocker(&self->stdoutMutex); LogMessage::formatMessage( self->stdoutStream, message, diff --git a/src/core/logging.hpp b/src/core/logging.hpp index bf81133..7b6a758 100644 --- a/src/core/logging.hpp +++ b/src/core/logging.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -135,6 +136,7 @@ private: QHash allFilters; QTextStream stdoutStream; + QMutex stdoutMutex; LoggingThreadProxy threadProxy; friend void initLogCategoryLevel(const char* name, QtMsgType defaultLevel); diff --git a/src/core/region.cpp b/src/core/region.cpp index 11892d6..82cc2e7 100644 --- a/src/core/region.cpp +++ b/src/core/region.cpp @@ -1,4 +1,5 @@ #include "region.hpp" +#include #include #include @@ -18,6 +19,11 @@ PendingRegion::PendingRegion(QObject* parent): QObject(parent) { QObject::connect(this, &PendingRegion::yChanged, this, &PendingRegion::changed); QObject::connect(this, &PendingRegion::widthChanged, this, &PendingRegion::changed); QObject::connect(this, &PendingRegion::heightChanged, this, &PendingRegion::changed); + QObject::connect(this, &PendingRegion::radiusChanged, this, &PendingRegion::changed); + QObject::connect(this, &PendingRegion::topLeftRadiusChanged, this, &PendingRegion::changed); + QObject::connect(this, &PendingRegion::topRightRadiusChanged, this, &PendingRegion::changed); + QObject::connect(this, &PendingRegion::bottomLeftRadiusChanged, this, &PendingRegion::changed); + QObject::connect(this, &PendingRegion::bottomRightRadiusChanged, this, &PendingRegion::changed); QObject::connect(this, &PendingRegion::childrenChanged, this, &PendingRegion::changed); } @@ -45,6 +51,79 @@ void PendingRegion::onItemDestroyed() { this->mItem = nullptr; } void PendingRegion::onChildDestroyed() { this->mRegions.removeAll(this->sender()); } +qint32 PendingRegion::radius() const { return this->mRadius; } + +void PendingRegion::setRadius(qint32 radius) { + if (radius == this->mRadius) return; + this->mRadius = radius; + emit this->radiusChanged(); + + if (!(this->mCornerOverrides & TopLeft)) emit this->topLeftRadiusChanged(); + if (!(this->mCornerOverrides & TopRight)) emit this->topRightRadiusChanged(); + if (!(this->mCornerOverrides & BottomLeft)) emit this->bottomLeftRadiusChanged(); + if (!(this->mCornerOverrides & BottomRight)) emit this->bottomRightRadiusChanged(); +} + +qint32 PendingRegion::topLeftRadius() const { + return (this->mCornerOverrides & TopLeft) ? this->mTopLeftRadius : this->mRadius; +} + +void PendingRegion::setTopLeftRadius(qint32 radius) { + this->mTopLeftRadius = radius; + this->mCornerOverrides |= TopLeft; + emit this->topLeftRadiusChanged(); +} + +void PendingRegion::resetTopLeftRadius() { + this->mCornerOverrides &= ~TopLeft; + emit this->topLeftRadiusChanged(); +} + +qint32 PendingRegion::topRightRadius() const { + return (this->mCornerOverrides & TopRight) ? this->mTopRightRadius : this->mRadius; +} + +void PendingRegion::setTopRightRadius(qint32 radius) { + this->mTopRightRadius = radius; + this->mCornerOverrides |= TopRight; + emit this->topRightRadiusChanged(); +} + +void PendingRegion::resetTopRightRadius() { + this->mCornerOverrides &= ~TopRight; + emit this->topRightRadiusChanged(); +} + +qint32 PendingRegion::bottomLeftRadius() const { + return (this->mCornerOverrides & BottomLeft) ? this->mBottomLeftRadius : this->mRadius; +} + +void PendingRegion::setBottomLeftRadius(qint32 radius) { + this->mBottomLeftRadius = radius; + this->mCornerOverrides |= BottomLeft; + emit this->bottomLeftRadiusChanged(); +} + +void PendingRegion::resetBottomLeftRadius() { + this->mCornerOverrides &= ~BottomLeft; + emit this->bottomLeftRadiusChanged(); +} + +qint32 PendingRegion::bottomRightRadius() const { + return (this->mCornerOverrides & BottomRight) ? this->mBottomRightRadius : this->mRadius; +} + +void PendingRegion::setBottomRightRadius(qint32 radius) { + this->mBottomRightRadius = radius; + this->mCornerOverrides |= BottomRight; + emit this->bottomRightRadiusChanged(); +} + +void PendingRegion::resetBottomRightRadius() { + this->mCornerOverrides &= ~BottomRight; + emit this->bottomRightRadiusChanged(); +} + QQmlListProperty PendingRegion::regions() { return QQmlListProperty( this, @@ -90,6 +169,60 @@ QRegion PendingRegion::build() const { region = QRegion(this->mX, this->mY, this->mWidth, this->mHeight, type); } + if (this->mShape == RegionShape::Rect && !region.isEmpty()) { + auto tl = std::max(this->topLeftRadius(), 0); + auto tr = std::max(this->topRightRadius(), 0); + auto bl = std::max(this->bottomLeftRadius(), 0); + auto br = std::max(this->bottomRightRadius(), 0); + + if (tl > 0 || tr > 0 || bl > 0 || br > 0) { + auto rect = region.boundingRect(); + auto x = rect.x(); + auto y = rect.y(); + auto w = rect.width(); + auto h = rect.height(); + + // Normalize so adjacent corners don't exceed their shared edge. + // Each corner is scaled by the tightest constraint of its two edges. + auto topScale = tl + tr > w ? static_cast(w) / (tl + tr) : 1.0; + auto bottomScale = bl + br > w ? static_cast(w) / (bl + br) : 1.0; + auto leftScale = tl + bl > h ? static_cast(h) / (tl + bl) : 1.0; + auto rightScale = tr + br > h ? static_cast(h) / (tr + br) : 1.0; + + tl = static_cast(tl * std::min(topScale, leftScale)); + tr = static_cast(tr * std::min(topScale, rightScale)); + bl = static_cast(bl * std::min(bottomScale, leftScale)); + br = static_cast(br * std::min(bottomScale, rightScale)); + + // Unlock each corner: subtract (cornerBox - quarterEllipse) from the + // full rect. Each corner only modifies pixels inside its own box, + // so no diagonal overlap is possible. + if (tl > 0) { + auto box = QRegion(x, y, tl, tl); + auto ellipse = QRegion(x, y, tl * 2, tl * 2, QRegion::Ellipse); + region -= box - (ellipse & box); + } + + if (tr > 0) { + auto box = QRegion(x + w - tr, y, tr, tr); + auto ellipse = QRegion(x + w - tr * 2, y, tr * 2, tr * 2, QRegion::Ellipse); + region -= box - (ellipse & box); + } + + if (bl > 0) { + auto box = QRegion(x, y + h - bl, bl, bl); + auto ellipse = QRegion(x, y + h - bl * 2, bl * 2, bl * 2, QRegion::Ellipse); + region -= box - (ellipse & box); + } + + if (br > 0) { + auto box = QRegion(x + w - br, y + h - br, br, br); + auto ellipse = QRegion(x + w - br * 2, y + h - br * 2, br * 2, br * 2, QRegion::Ellipse); + region -= box - (ellipse & box); + } + } + } + for (const auto& childRegion: this->mRegions) { region = childRegion->applyTo(region); } diff --git a/src/core/region.hpp b/src/core/region.hpp index 6637d7b..dfd1566 100644 --- a/src/core/region.hpp +++ b/src/core/region.hpp @@ -66,6 +66,29 @@ class PendingRegion: public QObject { Q_PROPERTY(qint32 width MEMBER mWidth NOTIFY widthChanged); /// Defaults to 0. Does nothing if @@item is set. Q_PROPERTY(qint32 height MEMBER mHeight NOTIFY heightChanged); + // clang-format off + /// Corner radius for rounded rectangles. Only applies when @@shape is `Rect`. Defaults to 0. + /// + /// Acts as the default for @@topLeftRadius, @@topRightRadius, @@bottomLeftRadius, + /// and @@bottomRightRadius. + Q_PROPERTY(qint32 radius READ radius WRITE setRadius NOTIFY radiusChanged); + /// Top-left corner radius. Only applies when @@shape is `Rect`. + /// + /// Defaults to @@radius, and may be reset by assigning `undefined`. + Q_PROPERTY(qint32 topLeftRadius READ topLeftRadius WRITE setTopLeftRadius RESET resetTopLeftRadius NOTIFY topLeftRadiusChanged); + /// Top-right corner radius. Only applies when @@shape is `Rect`. + /// + /// Defaults to @@radius, and may be reset by assigning `undefined`. + Q_PROPERTY(qint32 topRightRadius READ topRightRadius WRITE setTopRightRadius RESET resetTopRightRadius NOTIFY topRightRadiusChanged); + /// Bottom-left corner radius. Only applies when @@shape is `Rect`. + /// + /// Defaults to @@radius, and may be reset by assigning `undefined`. + Q_PROPERTY(qint32 bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius RESET resetBottomLeftRadius NOTIFY bottomLeftRadiusChanged); + /// Bottom-right corner radius. Only applies when @@shape is `Rect`. + /// + /// Defaults to @@radius, and may be reset by assigning `undefined`. + Q_PROPERTY(qint32 bottomRightRadius READ bottomRightRadius WRITE setBottomRightRadius RESET resetBottomRightRadius NOTIFY bottomRightRadiusChanged); + // clang-format on /// Regions to apply on top of this region. /// @@ -91,6 +114,25 @@ public: void setItem(QQuickItem* item); + [[nodiscard]] qint32 radius() const; + void setRadius(qint32 radius); + + [[nodiscard]] qint32 topLeftRadius() const; + void setTopLeftRadius(qint32 radius); + void resetTopLeftRadius(); + + [[nodiscard]] qint32 topRightRadius() const; + void setTopRightRadius(qint32 radius); + void resetTopRightRadius(); + + [[nodiscard]] qint32 bottomLeftRadius() const; + void setBottomLeftRadius(qint32 radius); + void resetBottomLeftRadius(); + + [[nodiscard]] qint32 bottomRightRadius() const; + void setBottomRightRadius(qint32 radius); + void resetBottomRightRadius(); + QQmlListProperty regions(); [[nodiscard]] bool empty() const; @@ -109,6 +151,11 @@ signals: void yChanged(); void widthChanged(); void heightChanged(); + void radiusChanged(); + void topLeftRadiusChanged(); + void topRightRadiusChanged(); + void bottomLeftRadiusChanged(); + void bottomRightRadiusChanged(); void childrenChanged(); /// Triggered when the region's geometry changes. @@ -130,12 +177,25 @@ private: static void regionsReplace(QQmlListProperty* prop, qsizetype i, PendingRegion* region); + enum CornerOverride : quint8 { + TopLeft = 0b1, + TopRight = 0b10, + BottomLeft = 0b100, + BottomRight = 0b1000, + }; + QQuickItem* mItem = nullptr; qint32 mX = 0; qint32 mY = 0; qint32 mWidth = 0; qint32 mHeight = 0; + qint32 mRadius = 0; + qint32 mTopLeftRadius = 0; + qint32 mTopRightRadius = 0; + qint32 mBottomLeftRadius = 0; + qint32 mBottomRightRadius = 0; + quint8 mCornerOverrides = 0; QList mRegions; }; diff --git a/src/crash/main.cpp b/src/crash/main.cpp index 05927f2..30cf94d 100644 --- a/src/crash/main.cpp +++ b/src/crash/main.cpp @@ -230,7 +230,9 @@ void qsCheckCrash(int argc, char** argv) { ); auto app = QApplication(argc, argv); - QApplication::setDesktopFileName("org.quickshell"); + auto desktopId = + info.instance.appId.isEmpty() ? QStringLiteral("org.quickshell") : info.instance.appId; + QApplication::setDesktopFileName(desktopId); auto crashDir = QsPaths::crashDir(info.instance.instanceId); diff --git a/src/launch/launch.cpp b/src/launch/launch.cpp index 3a9a2a5..0f5b090 100644 --- a/src/launch/launch.cpp +++ b/src/launch/launch.cpp @@ -76,6 +76,7 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio bool useSystemStyle = false; QString iconTheme = qEnvironmentVariable("QS_ICON_THEME"); QHash envOverrides; + QString appId = qEnvironmentVariable("QS_APP_ID"); QString dataDir; QString stateDir; QString cacheDir; @@ -104,6 +105,8 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio auto var = envPragma.sliced(0, splitIdx).trimmed(); auto val = envPragma.sliced(splitIdx + 1).trimmed(); pragmas.envOverrides.insert(var, val); + } else if (pragma.startsWith("AppId ")) { + pragmas.appId = pragma.sliced(6).trimmed(); } else if (pragma.startsWith("ShellId ")) { shellId = pragma.sliced(8).trimmed(); } else if (pragma.startsWith("DataDir ")) { @@ -128,10 +131,13 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio qInfo() << "Shell ID:" << shellId << "Path ID" << pathId; auto launchTime = qs::Common::LAUNCH_TIME.toSecsSinceEpoch(); + auto appId = pragmas.appId.isEmpty() ? QStringLiteral("org.quickshell") : pragmas.appId; + InstanceInfo::CURRENT = InstanceInfo { .instanceId = base36Encode(getpid()) + base36Encode(launchTime), .configPath = args.configPath, .shellId = shellId, + .appId = appId, .launchTime = qs::Common::LAUNCH_TIME, .pid = getpid(), .display = getDisplayConnection(), @@ -231,7 +237,7 @@ int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplicatio app = new QGuiApplication(qArgC, argv); } - QGuiApplication::setDesktopFileName("org.quickshell"); + QGuiApplication::setDesktopFileName(appId); if (args.debugPort != -1) { QQmlDebuggingEnabler::enableDebugging(true); diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 4a67558..cf84713 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -68,6 +68,7 @@ function (wl_proto target name dir) target_include_directories(${target} INTERFACE ${PROTO_BUILD_PATH}) target_link_libraries(${target} wl-proto-${name}-wl Qt6::WaylandClient Qt6::WaylandClientPrivate) qs_pch(${target} SET wayland-protocol) + target_compile_options(wl-proto-${name}-wl PRIVATE ${wayland_CFLAGS}) endfunction() # ----- @@ -119,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..679cb01 --- /dev/null +++ b/src/wayland/background_effect/test/manual/background_effect.qml @@ -0,0 +1,62 @@ +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 + } + + component EdgeSlider: Slider { + from: -1 + to: 1000 + value: -1 + } + + EdgeSlider { id: topLeftSlider } + EdgeSlider { id: topRightSlider } + EdgeSlider { id: bottomLeftSlider } + EdgeSlider { id: bottomRightSlider } + } + + BackgroundEffect.blurRegion: Region { + item: enableBox.checked ? root.contentItem : null + radius: radiusSlider.value == -1 ? undefined : radiusSlider.value + topLeftRadius: topLeftSlider.value == -1 ? undefined : topLeftSlider.value + topRightRadius: topRightSlider.value == -1 ? undefined : topRightSlider.value + bottomLeftRadius: bottomLeftSlider.value == -1 ? undefined : bottomLeftSlider.value + bottomRightRadius: bottomRightSlider.value == -1 ? undefined : bottomRightSlider.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", ] -----