wayland/background-effect: add ext-background-effect-v1 support
This commit is contained in:
parent
77c04a9447
commit
d745184823
11 changed files with 532 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
24
src/wayland/background_effect/CMakeLists.txt
Normal file
24
src/wayland/background_effect/CMakeLists.txt
Normal file
|
|
@ -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)
|
||||
38
src/wayland/background_effect/manager.cpp
Normal file
38
src/wayland/background_effect/manager.cpp
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#include "manager.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
#include <private/qwaylandwindow_p.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qwayland-ext-background-effect-v1.h>
|
||||
#include <qwaylandclientextension.h>
|
||||
|
||||
#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<bool>(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
|
||||
37
src/wayland/background_effect/manager.hpp
Normal file
37
src/wayland/background_effect/manager.hpp
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#pragma once
|
||||
|
||||
#include <private/qwaylandwindow_p.h>
|
||||
#include <qobject.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qwayland-ext-background-effect-v1.h>
|
||||
#include <qwaylandclientextension.h>
|
||||
|
||||
#include "surface.hpp"
|
||||
|
||||
namespace qs::wayland::background_effect::impl {
|
||||
|
||||
class BackgroundEffectManager
|
||||
: public QWaylandClientExtensionTemplate<BackgroundEffectManager>
|
||||
, 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
|
||||
246
src/wayland/background_effect/qml.cpp
Normal file
246
src/wayland/background_effect/qml.cpp
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
#include "qml.hpp"
|
||||
#include <memory>
|
||||
|
||||
#include <private/qhighdpiscaling_p.h>
|
||||
#include <private/qwaylandwindow_p.h>
|
||||
#include <qcoreevent.h>
|
||||
#include <qevent.h>
|
||||
#include <qlogging.h>
|
||||
#include <qnumeric.h>
|
||||
#include <qobject.h>
|
||||
#include <qregion.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qvariant.h>
|
||||
#include <qwindow.h>
|
||||
|
||||
#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<ProxyWindowBase*>(object);
|
||||
|
||||
if (!proxyWindow) {
|
||||
if (auto* iface = qobject_cast<WindowInterface*>(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<QPlatformSurfaceEvent*>(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<QWaylandWindow*>(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<BackgroundEffect*>()) {
|
||||
auto* prev = v.value<BackgroundEffect*>();
|
||||
if (prev != this && prev->surface) {
|
||||
this->surface.swap(prev->surface);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->surface) {
|
||||
this->surface = std::unique_ptr<impl::BackgroundEffectSurface>(
|
||||
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
|
||||
80
src/wayland/background_effect/qml.hpp
Normal file
80
src/wayland/background_effect/qml.hpp
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <private/qwaylandwindow_p.h>
|
||||
#include <qcoreevent.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qwindow.h>
|
||||
|
||||
#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<impl::BackgroundEffectSurface> surface;
|
||||
};
|
||||
|
||||
} // namespace qs::wayland::background_effect
|
||||
37
src/wayland/background_effect/surface.cpp
Normal file
37
src/wayland/background_effect/surface.cpp
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#include "surface.hpp"
|
||||
|
||||
#include <private/qwaylanddisplay_p.h>
|
||||
#include <private/qwaylandintegration_p.h>
|
||||
#include <qregion.h>
|
||||
#include <qwayland-ext-background-effect-v1.h>
|
||||
#include <wayland-client-protocol.h>
|
||||
|
||||
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
|
||||
18
src/wayland/background_effect/surface.hpp
Normal file
18
src/wayland/background_effect/surface.hpp
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#pragma once
|
||||
|
||||
#include <qregion.h>
|
||||
#include <qtclasshelpermacros.h>
|
||||
#include <qwayland-ext-background-effect-v1.h>
|
||||
|
||||
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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -8,5 +8,6 @@ headers = [
|
|||
"idle_inhibit/inhibitor.hpp",
|
||||
"idle_notify/monitor.hpp",
|
||||
"shortcuts_inhibit/inhibitor.hpp",
|
||||
"background_effect/qml.hpp",
|
||||
]
|
||||
-----
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue