From 3cf65af49f22843386ac421f3889762e6f43a425 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 17 Mar 2026 03:49:20 -0700 Subject: [PATCH 01/15] docs: ask users not to submit v1 crash reports --- .github/ISSUE_TEMPLATE/crash.yml | 91 +++++------------------------ .github/ISSUE_TEMPLATE/crash2.yml | 8 +-- src/wayland/wl_proxy_safe_deref.cpp | 1 - 3 files changed, 17 insertions(+), 83 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml index 80fa827..958c884 100644 --- a/.github/ISSUE_TEMPLATE/crash.yml +++ b/.github/ISSUE_TEMPLATE/crash.yml @@ -1,82 +1,17 @@ name: Crash Report (v1) -description: Quickshell has crashed -labels: ["bug", "crash"] +description: Quickshell has crashed (old) +labels: ["unactionable"] body: - - type: textarea - id: crashinfo + - type: markdown attributes: - label: General crash information - description: | - Paste the contents of the `info.txt` file in your crash folder here. - value: "
General information - - - ``` - - - - ``` - - -
" - validations: - required: true - - type: textarea - id: userinfo + value: | + Thank you for taking the time to click the report button. + At this point most of the worst issues in 0.2.1 and before have been fixed and we are + preparing for a new release. Please do not submit this report. + - type: checkboxes + id: donotcheck attributes: - label: What caused the crash - description: | - Any information likely to help debug the crash. What were you doing when the crash occurred, - what changes did you make, can you get it to happen again? - - type: textarea - id: dump - attributes: - label: Minidump - description: | - Attach `minidump.dmp.log` here. If it is too big to upload, compress it. - - You may skip this step if quickshell crashed while processing a password - or other sensitive information. If you skipped it write why instead. - validations: - required: true - - type: textarea - id: logs - attributes: - label: Log file - description: | - Attach `log.qslog.log` here. If it is too big to upload, compress it. - - You can preview the log if you'd like using `quickshell read-log `. - validations: - required: true - - type: textarea - id: config - attributes: - label: Configuration - description: | - Attach your configuration here, preferrably in full (not just one file). - Compress it into a zip, tar, etc. - - This will help us reproduce the crash ourselves. - - type: textarea - id: bt - attributes: - label: Backtrace - description: | - If you have gdb installed and use systemd, or otherwise know how to get a backtrace, - we would appreciate one. (You may have gdb installed without knowing it) - - 1. Run `coredumpctl debug ` where `pid` is the number shown after "Crashed process ID" - in the crash reporter. - 2. Once it loads, type `bt -full` (then enter) - 3. Copy the output and attach it as a file or in a spoiler. - - type: textarea - id: exe - attributes: - label: Executable - description: | - If the crash folder contains a executable.txt file, upload it here. If not you can ignore this field. - If it is too big to upload, compress it. - - Note: executable.txt is the quickshell binary. It has a .txt extension due to github's limitations on - filetypes. + label: Read the text above. Do not submit the report. + options: + - label: Yes I want this report to be deleted. + required: true diff --git a/.github/ISSUE_TEMPLATE/crash2.yml b/.github/ISSUE_TEMPLATE/crash2.yml index 84beef8..86f490c 100644 --- a/.github/ISSUE_TEMPLATE/crash2.yml +++ b/.github/ISSUE_TEMPLATE/crash2.yml @@ -9,21 +9,21 @@ body: description: | Any information likely to help debug the crash. What were you doing when the crash occurred, what changes did you make, can you get it to happen again? - - type: textarea + - type: upload id: report attributes: label: Report file description: Attach `report.txt` here. validations: required: true - - type: textarea + - type: upload id: logs attributes: label: Log file description: | Attach `log.qslog.log` here. If it is too big to upload, compress it. - You can preview the log if you'd like using `quickshell read-log `. + You can preview the log if you'd like using `qs log -r '*=true'`. validations: required: true - type: textarea @@ -31,7 +31,7 @@ body: attributes: label: Configuration description: | - Attach your configuration here, preferrably in full (not just one file). + Attach or link your configuration here, preferrably in full (not just one file). Compress it into a zip, tar, etc. This will help us reproduce the crash ourselves. diff --git a/src/wayland/wl_proxy_safe_deref.cpp b/src/wayland/wl_proxy_safe_deref.cpp index 0ebc258..2664a99 100644 --- a/src/wayland/wl_proxy_safe_deref.cpp +++ b/src/wayland/wl_proxy_safe_deref.cpp @@ -1,4 +1,3 @@ - #include #include #include From 3520c85d77ccf6cbfc158057447f44657a0bc9d4 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 17 Mar 2026 19:42:47 -0700 Subject: [PATCH 02/15] wayland: remove --require-defined linker argument Not supported by lld --- src/wayland/CMakeLists.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 13e648a..4a67558 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -83,10 +83,7 @@ qt_add_library(quickshell-wayland STATIC # required for wl_proxy_safe_deref target_link_libraries(quickshell-wayland PRIVATE ${CMAKE_DL_LIBS}) -target_link_options(quickshell PRIVATE - "LINKER:--export-dynamic-symbol=wl_proxy_get_listener" - "LINKER:--require-defined=wl_proxy_get_listener" -) +target_link_options(quickshell PRIVATE "LINKER:--export-dynamic-symbol=wl_proxy_get_listener") # required to make sure the constructor is linked add_library(quickshell-wayland-init OBJECT init.cpp) From 0cb62920a7ab0b199754c941046ae86e3a1c368d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 18 Mar 2026 02:34:06 -0700 Subject: [PATCH 03/15] hyprland/focus_grab: handle destruction of tracked windows --- changelog/next.md | 1 + src/core/platformmenu.cpp | 5 +- src/core/popupanchor.cpp | 5 +- src/wayland/hyprland/focus_grab/qml.cpp | 99 +++++++++++---------- src/wayland/hyprland/focus_grab/qml.hpp | 4 +- src/wayland/hyprland/surface/qml.cpp | 9 +- src/wayland/idle_inhibit/inhibitor.cpp | 19 +--- src/wayland/shortcuts_inhibit/inhibitor.cpp | 10 +-- src/wayland/toplevel_management/qml.cpp | 9 +- src/window/proxywindow.cpp | 6 ++ src/window/proxywindow.hpp | 2 + 11 files changed, 67 insertions(+), 102 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index cceb79e..a8981b9 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -60,6 +60,7 @@ set shell id. - Desktop action order is now preserved. - Fixed partial socket reads in greetd and hyprland on slow machines. - Worked around Qt bug causing crashes when plugging and unplugging monitors. +- Fixed HyprlandFocusGrab crashing if windows were destroyed after being passed to it. ## Packaging Changes diff --git a/src/core/platformmenu.cpp b/src/core/platformmenu.cpp index 427dde0..d8901e2 100644 --- a/src/core/platformmenu.cpp +++ b/src/core/platformmenu.cpp @@ -18,7 +18,6 @@ #include #include "../window/proxywindow.hpp" -#include "../window/windowinterface.hpp" #include "iconprovider.hpp" #include "model.hpp" #include "platformmenu_p.hpp" @@ -91,10 +90,8 @@ bool PlatformMenuEntry::display(QObject* parentWindow, int relativeX, int relati } else if (parentWindow == nullptr) { qCritical() << "Cannot display PlatformMenuEntry with null parent window."; return false; - } else if (auto* proxy = qobject_cast(parentWindow)) { + } else if (auto* proxy = ProxyWindowBase::forObject(parentWindow)) { window = proxy->backingWindow(); - } else if (auto* interface = qobject_cast(parentWindow)) { - window = interface->proxyWindow()->backingWindow(); } else { qCritical() << "PlatformMenuEntry.display() must be called with a window."; return false; diff --git a/src/core/popupanchor.cpp b/src/core/popupanchor.cpp index 151dd5d..ca817c9 100644 --- a/src/core/popupanchor.cpp +++ b/src/core/popupanchor.cpp @@ -11,7 +11,6 @@ #include #include "../window/proxywindow.hpp" -#include "../window/windowinterface.hpp" #include "types.hpp" bool PopupAnchorState::operator==(const PopupAnchorState& other) const { @@ -40,10 +39,8 @@ void PopupAnchor::setWindowInternal(QObject* window) { } if (window) { - if (auto* proxy = qobject_cast(window)) { + if (auto* proxy = ProxyWindowBase::forObject(window)) { this->bProxyWindow = proxy; - } else if (auto* interface = qobject_cast(window)) { - this->bProxyWindow = interface->proxyWindow(); } else { qWarning() << "Tried to set popup anchor window to" << window << "which is not a quickshell window."; diff --git a/src/wayland/hyprland/focus_grab/qml.cpp b/src/wayland/hyprland/focus_grab/qml.cpp index e26a75a..cf1ac24 100644 --- a/src/wayland/hyprland/focus_grab/qml.cpp +++ b/src/wayland/hyprland/focus_grab/qml.cpp @@ -9,7 +9,6 @@ #include #include "../../../window/proxywindow.hpp" -#include "../../../window/windowinterface.hpp" #include "grab.hpp" #include "manager.hpp" @@ -38,8 +37,51 @@ QObjectList HyprlandFocusGrab::windows() const { return this->windowObjects; } void HyprlandFocusGrab::setWindows(QObjectList windows) { if (windows == this->windowObjects) return; + if (this->grab) this->grab->startTransaction(); + + for (auto* obj: this->windowObjects) { + if (windows.contains(obj)) continue; + QObject::disconnect(obj, nullptr, this, nullptr); + + auto* proxy = ProxyWindowBase::forObject(obj); + if (!proxy) continue; + + QObject::disconnect(proxy, nullptr, this, nullptr); + + if (this->grab && proxy->backingWindow()) { + this->grab->removeWindow(proxy->backingWindow()); + } + } + + for (auto it = windows.begin(); it != windows.end();) { + auto* proxy = ProxyWindowBase::forObject(*it); + if (!proxy) { + it = windows.erase(it); + continue; + } + + if (this->windowObjects.contains(*it)) { + ++it; + continue; + } + + QObject::connect(*it, &QObject::destroyed, this, &HyprlandFocusGrab::onObjectDestroyed); + QObject::connect( + proxy, + &ProxyWindowBase::windowConnected, + this, + &HyprlandFocusGrab::onProxyConnected + ); + + if (this->grab && proxy->backingWindow()) { + this->grab->addWindow(proxy->backingWindow()); + } + + ++it; + } + + if (this->grab) this->grab->completeTransaction(); this->windowObjects = std::move(windows); - this->syncWindows(); emit this->windowsChanged(); } @@ -75,59 +117,18 @@ void HyprlandFocusGrab::tryActivate() { QObject::connect(this->grab, &FocusGrab::cleared, this, &HyprlandFocusGrab::onGrabCleared); this->grab->startTransaction(); - for (auto* proxy: this->trackedProxies) { - if (proxy->backingWindow() != nullptr) { + for (auto* obj: this->windowObjects) { + auto* proxy = ProxyWindowBase::forObject(obj); + if (proxy && proxy->backingWindow()) { this->grab->addWindow(proxy->backingWindow()); } } this->grab->completeTransaction(); } -void HyprlandFocusGrab::syncWindows() { - auto newProxy = QList(); - for (auto* windowObject: this->windowObjects) { - auto* proxyWindow = qobject_cast(windowObject); - - if (proxyWindow == nullptr) { - if (auto* iface = qobject_cast(windowObject)) { - proxyWindow = iface->proxyWindow(); - } - } - - if (proxyWindow != nullptr) { - newProxy.push_back(proxyWindow); - } - } - - if (this->grab) this->grab->startTransaction(); - - for (auto* oldWindow: this->trackedProxies) { - if (!newProxy.contains(oldWindow)) { - QObject::disconnect(oldWindow, nullptr, this, nullptr); - - if (this->grab != nullptr && oldWindow->backingWindow() != nullptr) { - this->grab->removeWindow(oldWindow->backingWindow()); - } - } - } - - for (auto* newProxy: newProxy) { - if (!this->trackedProxies.contains(newProxy)) { - QObject::connect( - newProxy, - &ProxyWindowBase::windowConnected, - this, - &HyprlandFocusGrab::onProxyConnected - ); - - if (this->grab != nullptr && newProxy->backingWindow() != nullptr) { - this->grab->addWindow(newProxy->backingWindow()); - } - } - } - - this->trackedProxies = newProxy; - if (this->grab) this->grab->completeTransaction(); +void HyprlandFocusGrab::onObjectDestroyed(QObject* object) { + this->windowObjects.removeOne(object); + emit this->windowsChanged(); } } // namespace qs::hyprland diff --git a/src/wayland/hyprland/focus_grab/qml.hpp b/src/wayland/hyprland/focus_grab/qml.hpp index 705b0d3..97a10de 100644 --- a/src/wayland/hyprland/focus_grab/qml.hpp +++ b/src/wayland/hyprland/focus_grab/qml.hpp @@ -96,15 +96,13 @@ private slots: void onGrabActivated(); void onGrabCleared(); void onProxyConnected(); + void onObjectDestroyed(QObject* object); private: void tryActivate(); - void syncWindows(); bool targetActive = false; QObjectList windowObjects; - QList trackedProxies; - QList trackedWindows; focus_grab::FocusGrab* grab = nullptr; }; diff --git a/src/wayland/hyprland/surface/qml.cpp b/src/wayland/hyprland/surface/qml.cpp index c4f7d67..4575842 100644 --- a/src/wayland/hyprland/surface/qml.cpp +++ b/src/wayland/hyprland/surface/qml.cpp @@ -14,7 +14,6 @@ #include "../../../core/region.hpp" #include "../../../window/proxywindow.hpp" -#include "../../../window/windowinterface.hpp" #include "manager.hpp" #include "surface.hpp" @@ -23,13 +22,7 @@ using QtWaylandClient::QWaylandWindow; namespace qs::hyprland::surface { HyprlandWindow* HyprlandWindow::qmlAttachedProperties(QObject* object) { - auto* proxyWindow = qobject_cast(object); - - if (!proxyWindow) { - if (auto* iface = qobject_cast(object)) { - proxyWindow = iface->proxyWindow(); - } - } + auto* proxyWindow = ProxyWindowBase::forObject(object); if (!proxyWindow) return nullptr; return new HyprlandWindow(proxyWindow); diff --git a/src/wayland/idle_inhibit/inhibitor.cpp b/src/wayland/idle_inhibit/inhibitor.cpp index efeeae1..bfea7a0 100644 --- a/src/wayland/idle_inhibit/inhibitor.cpp +++ b/src/wayland/idle_inhibit/inhibitor.cpp @@ -6,7 +6,6 @@ #include #include "../../window/proxywindow.hpp" -#include "../../window/windowinterface.hpp" #include "proto.hpp" namespace qs::wayland::idle_inhibit { @@ -25,27 +24,13 @@ QObject* IdleInhibitor::window() const { return this->bWindowObject; } void IdleInhibitor::setWindow(QObject* window) { if (window == this->bWindowObject) return; - auto* proxyWindow = qobject_cast(window); - - if (proxyWindow == nullptr) { - if (auto* iface = qobject_cast(window)) { - proxyWindow = iface->proxyWindow(); - } - } - + auto* proxyWindow = ProxyWindowBase::forObject(window); this->bWindowObject = proxyWindow ? window : nullptr; } void IdleInhibitor::boundWindowChanged() { auto* window = this->bBoundWindow.value(); - auto* proxyWindow = qobject_cast(window); - - if (proxyWindow == nullptr) { - if (auto* iface = qobject_cast(window)) { - proxyWindow = iface->proxyWindow(); - } - } - + auto* proxyWindow = ProxyWindowBase::forObject(window); if (proxyWindow == this->proxyWindow) return; if (this->mWaylandWindow) { diff --git a/src/wayland/shortcuts_inhibit/inhibitor.cpp b/src/wayland/shortcuts_inhibit/inhibitor.cpp index 2fca9bc..a91d5e2 100644 --- a/src/wayland/shortcuts_inhibit/inhibitor.cpp +++ b/src/wayland/shortcuts_inhibit/inhibitor.cpp @@ -9,7 +9,6 @@ #include #include "../../window/proxywindow.hpp" -#include "../../window/windowinterface.hpp" #include "proto.hpp" namespace qs::wayland::shortcuts_inhibit { @@ -48,14 +47,7 @@ ShortcutInhibitor::~ShortcutInhibitor() { void ShortcutInhibitor::onBoundWindowChanged() { auto* window = this->bBoundWindow.value(); - auto* proxyWindow = qobject_cast(window); - - if (!proxyWindow) { - if (auto* iface = qobject_cast(window)) { - proxyWindow = iface->proxyWindow(); - } - } - + auto* proxyWindow = ProxyWindowBase::forObject(window); if (proxyWindow == this->proxyWindow) return; if (this->proxyWindow) { diff --git a/src/wayland/toplevel_management/qml.cpp b/src/wayland/toplevel_management/qml.cpp index 6a1d96b..cb53381 100644 --- a/src/wayland/toplevel_management/qml.cpp +++ b/src/wayland/toplevel_management/qml.cpp @@ -9,7 +9,6 @@ #include "../../core/qmlscreen.hpp" #include "../../core/util.hpp" #include "../../window/proxywindow.hpp" -#include "../../window/windowinterface.hpp" #include "../output_tracking.hpp" #include "handle.hpp" #include "manager.hpp" @@ -73,13 +72,7 @@ void Toplevel::fullscreenOn(QuickshellScreenInfo* screen) { } void Toplevel::setRectangle(QObject* window, QRect rect) { - auto* proxyWindow = qobject_cast(window); - - if (proxyWindow == nullptr) { - if (auto* iface = qobject_cast(window)) { - proxyWindow = iface->proxyWindow(); - } - } + auto* proxyWindow = ProxyWindowBase::forObject(window); if (proxyWindow != this->rectWindow) { if (this->rectWindow != nullptr) { diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index 62126bd..8a20dfa 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -57,6 +57,12 @@ ProxyWindowBase::ProxyWindowBase(QObject* parent) ProxyWindowBase::~ProxyWindowBase() { this->deleteWindow(true); } +ProxyWindowBase* ProxyWindowBase::forObject(QObject* obj) { + if (auto* proxy = qobject_cast(obj)) return proxy; + if (auto* iface = qobject_cast(obj)) return iface->proxyWindow(); + return nullptr; +} + void ProxyWindowBase::onReload(QObject* oldInstance) { if (this->mVisible) this->window = this->retrieveWindow(oldInstance); auto wasVisible = this->window != nullptr && this->window->isVisible(); diff --git a/src/window/proxywindow.hpp b/src/window/proxywindow.hpp index aec821e..9ff66c4 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -66,6 +66,8 @@ public: explicit ProxyWindowBase(QObject* parent = nullptr); ~ProxyWindowBase() override; + static ProxyWindowBase* forObject(QObject* obj); + ProxyWindowBase(ProxyWindowBase&) = delete; ProxyWindowBase(ProxyWindowBase&&) = delete; void operator=(ProxyWindowBase&) = delete; From 7511545ee20664e3b8b8d3322c0ffe7567c56f7a Mon Sep 17 00:00:00 2001 From: Mia Herkt Date: Wed, 18 Mar 2026 20:37:17 +0100 Subject: [PATCH 04/15] build: add missing wayland-client CFLAGS Fixes #276 --- src/wayland/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 4a67558..196f1e0 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() # ----- From eb6eaf59c79408f1778248a3360c7a6d8ff89a47 Mon Sep 17 00:00:00 2001 From: Dan Aloni Date: Fri, 3 Oct 2025 17:11:03 +0300 Subject: [PATCH 05/15] core/log: add a mutex to protect stdoutStream QTextStream is not thread safe. --- src/core/logging.cpp | 2 ++ src/core/logging.hpp | 2 ++ 2 files changed, 4 insertions(+) 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); From 77c04a9447918bf7d052b4203b01ac2947ab9b35 Mon Sep 17 00:00:00 2001 From: bbedward Date: Thu, 19 Mar 2026 09:40:36 -0400 Subject: [PATCH 06/15] launch: add ability to override AppId via pragma or QS_APP_ID --- changelog/next.md | 1 + src/core/instanceinfo.cpp | 8 ++++---- src/core/instanceinfo.hpp | 1 + src/crash/main.cpp | 4 +++- src/launch/launch.cpp | 8 +++++++- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index a8981b9..3f059b1 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -38,6 +38,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/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); From d7451848238f7f7ade38f00fe7ef91da1cc719a6 Mon Sep 17 00:00:00 2001 From: bbedward Date: Thu, 19 Mar 2026 23:39:21 -0700 Subject: [PATCH 07/15] wayland/background-effect: add ext-background-effect-v1 support --- changelog/next.md | 1 + src/wayland/CMakeLists.txt | 3 + src/wayland/background_effect/CMakeLists.txt | 24 ++ src/wayland/background_effect/manager.cpp | 38 +++ src/wayland/background_effect/manager.hpp | 37 +++ src/wayland/background_effect/qml.cpp | 246 ++++++++++++++++++ src/wayland/background_effect/qml.hpp | 80 ++++++ src/wayland/background_effect/surface.cpp | 37 +++ src/wayland/background_effect/surface.hpp | 18 ++ .../test/manual/background_effect.qml | 47 ++++ src/wayland/module.md | 1 + 11 files changed, 532 insertions(+) create mode 100644 src/wayland/background_effect/CMakeLists.txt create mode 100644 src/wayland/background_effect/manager.cpp create mode 100644 src/wayland/background_effect/manager.hpp create mode 100644 src/wayland/background_effect/qml.cpp create mode 100644 src/wayland/background_effect/qml.hpp create mode 100644 src/wayland/background_effect/surface.cpp create mode 100644 src/wayland/background_effect/surface.hpp create mode 100644 src/wayland/background_effect/test/manual/background_effect.qml 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", ] ----- From 6a244c3c560b45f3b860ed6c0fc54d0291ab6f57 Mon Sep 17 00:00:00 2001 From: bbedward Date: Thu, 19 Mar 2026 23:42:29 -0700 Subject: [PATCH 08/15] core/region: add per-corner radius support --- changelog/next.md | 1 + src/core/region.cpp | 133 ++++++++++++++++++ src/core/region.hpp | 60 ++++++++ .../test/manual/background_effect.qml | 15 ++ 4 files changed, 209 insertions(+) diff --git a/changelog/next.md b/changelog/next.md index c5d93e2..fc6d79e 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -28,6 +28,7 @@ set shell id. - 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 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/wayland/background_effect/test/manual/background_effect.qml b/src/wayland/background_effect/test/manual/background_effect.qml index 8cb4e12..679cb01 100644 --- a/src/wayland/background_effect/test/manual/background_effect.qml +++ b/src/wayland/background_effect/test/manual/background_effect.qml @@ -38,10 +38,25 @@ FloatingWindow { 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 } } From 08058326f04e9b5e55c903b3702405a8d3556ac6 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 25 Mar 2026 00:16:36 -0700 Subject: [PATCH 09/15] core: reuse global pragma parsing js engine during scanning QJSEngine cleanup is not fast or clean and results in speed degradation over time if too many are destroyed. --- src/core/scan.cpp | 15 +++++++++++---- src/core/scan.hpp | 3 +++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/core/scan.cpp b/src/core/scan.cpp index 58da38c..3605c52 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -145,10 +145,7 @@ bool QmlScanner::scanQmlFile(const QString& path, bool& singleton, bool& interna QString overrideText; bool isOverridden = false; - auto pragmaEngine = QJSEngine(); - pragmaEngine.globalObject().setPrototype( - pragmaEngine.newQObject(new qs::scan::env::PreprocEnv()) - ); + auto& pragmaEngine = *QmlScanner::preprocEngine(); auto postError = [&, this](QString error) { this->scanErrors.append({.file = path, .message = std::move(error), .line = lineNum}); @@ -370,3 +367,13 @@ QPair QmlScanner::jsonToQml(const QJsonValue& value, int inden return qMakePair(QStringLiteral("var"), "null"); } } + +QJSEngine* QmlScanner::preprocEngine() { + static auto* engine = [] { + auto* engine = new QJSEngine(); + engine->globalObject().setPrototype(engine->newQObject(new qs::scan::env::PreprocEnv())); + return engine; + }(); + + return engine; +} diff --git a/src/core/scan.hpp b/src/core/scan.hpp index 26034e1..7d807e1 100644 --- a/src/core/scan.hpp +++ b/src/core/scan.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -42,4 +43,6 @@ private: bool scanQmlFile(const QString& path, bool& singleton, bool& internal); bool scanQmlJson(const QString& path); [[nodiscard]] static QPair jsonToQml(const QJsonValue& value, int indent = 0); + + static QJSEngine* preprocEngine(); }; From 308f1e249b178c394509341ba7ab49fc98b9c824 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 28 Mar 2026 20:14:58 -0700 Subject: [PATCH 10/15] crash: unmask signals before reexec Signals were previously left masked before reexec, causing UB if a child were to crash again, instead of triggering the reporter. This might've been responsible for a number of unexplainable bugs. --- src/crash/handler.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp index 8f37085..045a148 100644 --- a/src/crash/handler.cpp +++ b/src/crash/handler.cpp @@ -58,6 +58,12 @@ void signalHandler( siginfo_t* /*info*/, // NOLINT (misc-include-cleaner) void* /*context*/ ) { + // NOLINTBEGIN (misc-include-cleaner) + sigset_t set; + sigfillset(&set); + sigprocmask(SIG_UNBLOCK, &set, nullptr); + // NOLINTEND + if (CrashInfo::INSTANCE.traceFd != -1) { auto traceBuffer = std::array(); auto frameCount = cpptrace::safe_generate_raw_trace(traceBuffer.data(), traceBuffer.size(), 1); @@ -79,13 +85,9 @@ void signalHandler( fail:; } + // TODO: coredump fork and crash reporter remain as zombies, fix auto coredumpPid = fork(); if (coredumpPid == 0) { - // NOLINTBEGIN (misc-include-cleaner) - sigset_t set; - sigfillset(&set); - sigprocmask(SIG_UNBLOCK, &set, nullptr); - // NOLINTEND raise(sig); _exit(-1); } @@ -131,7 +133,6 @@ void signalHandler( perror("Failed to fork and launch crash reporter.\n"); _exit(-1); } else if (pid == 0) { - // dup to remove CLOEXEC auto dumpFdStr = std::array(); auto logFdStr = std::array(); From 6ef86dd5aa3dec6fe7dbc8f51e08ad6d1b5c8cc0 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 28 Mar 2026 20:17:07 -0700 Subject: [PATCH 11/15] crash: run platform compat hooks in crash reporter init For some reason, QtWayland crashes we work around trigger in this path. This was previously masked when the crash reporter didn't unmask signals, as long as the original process crashed with SIGSEGV. --- src/core/plugin.cpp | 16 ++++++++++++++++ src/core/plugin.hpp | 2 ++ src/crash/main.cpp | 4 ++++ src/wayland/init.cpp | 3 ++- 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/core/plugin.cpp b/src/core/plugin.cpp index 0eb9a06..e6cd1bb 100644 --- a/src/core/plugin.cpp +++ b/src/core/plugin.cpp @@ -9,6 +9,18 @@ static QVector plugins; // NOLINT void QsEnginePlugin::registerPlugin(QsEnginePlugin& plugin) { plugins.push_back(&plugin); } +void QsEnginePlugin::preinitPluginsOnly() { + plugins.removeIf([](QsEnginePlugin* plugin) { return !plugin->applies(); }); + + std::ranges::sort(plugins, [](QsEnginePlugin* a, QsEnginePlugin* b) { + return b->dependencies().contains(a->name()); + }); + + for (QsEnginePlugin* plugin: plugins) { + plugin->preinit(); + } +} + void QsEnginePlugin::initPlugins() { plugins.removeIf([](QsEnginePlugin* plugin) { return !plugin->applies(); }); @@ -16,6 +28,10 @@ void QsEnginePlugin::initPlugins() { return b->dependencies().contains(a->name()); }); + for (QsEnginePlugin* plugin: plugins) { + plugin->preinit(); + } + for (QsEnginePlugin* plugin: plugins) { plugin->init(); } diff --git a/src/core/plugin.hpp b/src/core/plugin.hpp index f0c14dc..f692e91 100644 --- a/src/core/plugin.hpp +++ b/src/core/plugin.hpp @@ -18,12 +18,14 @@ public: virtual QString name() { return QString(); } virtual QList dependencies() { return {}; } virtual bool applies() { return true; } + virtual void preinit() {} virtual void init() {} virtual void registerTypes() {} virtual void constructGeneration(EngineGeneration& /*unused*/) {} // NOLINT virtual void onReload() {} static void registerPlugin(QsEnginePlugin& plugin); + static void preinitPluginsOnly(); static void initPlugins(); static void runConstructGeneration(EngineGeneration& generation); static void runOnReload(); diff --git a/src/crash/main.cpp b/src/crash/main.cpp index 30cf94d..6533b43 100644 --- a/src/crash/main.cpp +++ b/src/crash/main.cpp @@ -25,6 +25,7 @@ #include "../core/logging.hpp" #include "../core/logging_p.hpp" #include "../core/paths.hpp" +#include "../core/plugin.hpp" #include "../core/ringbuf.hpp" #include "interface.hpp" @@ -238,6 +239,9 @@ void qsCheckCrash(int argc, char** argv) { qCInfo(logCrashReporter) << "Starting crash reporter..."; + // Required platform compatibility hooks + QsEnginePlugin::preinitPluginsOnly(); + recordCrashInfo(crashDir, info.instance); auto gui = CrashReporterGui(crashDir.path(), crashProc); diff --git a/src/wayland/init.cpp b/src/wayland/init.cpp index 790cebb..579e42a 100644 --- a/src/wayland/init.cpp +++ b/src/wayland/init.cpp @@ -33,8 +33,9 @@ class WaylandPlugin: public QsEnginePlugin { return isWayland; } + void preinit() override { installWlProxySafeDeref(); } + void init() override { - installWlProxySafeDeref(); installPlatformMenuHook(); installPopupPositioner(); } From 313f4e47f6f3d7204586721b2fbd0a54d542a84c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 28 Mar 2026 20:25:58 -0700 Subject: [PATCH 12/15] core: track XDG_CURRENT_DESKTOP in debuginfo --- src/core/debuginfo.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/debuginfo.cpp b/src/core/debuginfo.cpp index ae227f8..abc467d 100644 --- a/src/core/debuginfo.cpp +++ b/src/core/debuginfo.cpp @@ -129,12 +129,13 @@ QString envInfo() { auto stream = QTextStream(&info); for (auto** envp = environ; *envp != nullptr; ++envp) { // NOLINT - auto prefixes = std::array { + auto prefixes = std::array { "QS_", "QT_", "QML_", "QML2_", "QSG_", + "XDG_CURRENT_DESKTOP=", }; for (const auto& prefix: prefixes) { From 9bf752ac33b2181356d33251c3b1b4dedde0bbc6 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 28 Mar 2026 23:07:37 -0700 Subject: [PATCH 13/15] crash: add std::terminate handler --- src/crash/handler.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp index 045a148..33506a6 100644 --- a/src/crash/handler.cpp +++ b/src/crash/handler.cpp @@ -5,9 +5,11 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -156,6 +158,21 @@ void signalHandler( } } +void handleCppTerminate() { + if (auto ptr = std::current_exception()) { + try { + std::rethrow_exception(ptr); + } catch (std::exception& e) { + qFatal().nospace() << "Terminate called with C++ exception (" + << cpptrace::demangle(typeid(e).name()).data() << "): " << e.what(); + } catch (...) { + qFatal() << "Terminate called with non exception object"; + } + } + + qFatal() << "Terminate called without active C++ exception"; +} + } // namespace void CrashHandler::init() { @@ -204,6 +221,8 @@ void CrashHandler::init() { // NOLINTEND (misc-include-cleaner) + std::set_terminate(&handleCppTerminate); + qCInfo(logCrashHandler) << "Crash handler initialized."; } From ee1100eb98d5033d8d4b76bf9fb0e720fec4c191 Mon Sep 17 00:00:00 2001 From: Mia Herkt Date: Sun, 29 Mar 2026 09:31:28 +0200 Subject: [PATCH 14/15] wayland/buffer: drop unused GLESv3 include MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit That one is often in a separate Mesa package and contrary to GLESv2 doesn’t come with a pkg-config file. Mildly annoying… --- src/wayland/buffer/dmabuf.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wayland/buffer/dmabuf.cpp b/src/wayland/buffer/dmabuf.cpp index ed9dbeb..47462fb 100644 --- a/src/wayland/buffer/dmabuf.cpp +++ b/src/wayland/buffer/dmabuf.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include From b83c39c8afd58c86af1c49a7c0e081b30c86d823 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 30 Mar 2026 21:41:10 -0700 Subject: [PATCH 15/15] services/pipewire: add -fno-strict-overflow to fix PCH with pipewire Unclear how this should be handled long term. --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1226342..4ed8374 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,8 +87,9 @@ include(cmake/util.cmake) add_compile_options(-Wall -Wextra -Wno-vla-cxx-extension) -# pipewire defines this, breaking PCH +# pipewire defines these, breaking PCH add_compile_definitions(_REENTRANT) +add_compile_options(-fno-strict-overflow) if (FRAME_POINTERS) add_compile_options(-fno-omit-frame-pointer)