diff --git a/CMakeLists.txt b/CMakeLists.txt index 5c8fb483..a4919952 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,9 +55,10 @@ boption(HYPRLAND " Hyprland" ON REQUIRES WAYLAND) boption(HYPRLAND_IPC " Hyprland IPC" ON REQUIRES HYPRLAND) boption(HYPRLAND_GLOBAL_SHORTCUTS " Hyprland Global Shortcuts" ON REQUIRES HYPRLAND) boption(HYPRLAND_FOCUS_GRAB " Hyprland Focus Grabbing" ON REQUIRES HYPRLAND) -boption(I3 " I3/Sway" ON) -boption(I3_IPC " I3/Sway IPC" ON REQUIRES I3) +boption(HYPRLAND_SURFACE_EXTENSIONS " Hyprland Surface Extensions" ON REQUIRES HYPRLAND) boption(X11 "X11" ON) +boption(I3 "I3/Sway" ON) +boption(I3_IPC " I3/Sway IPC" ON REQUIRES I3) boption(SERVICE_STATUS_NOTIFIER "System Tray" ON) boption(SERVICE_PIPEWIRE "PipeWire" ON) boption(SERVICE_MPRIS "Mpris" ON) diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 8005a833..db03cf16 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -77,6 +77,7 @@ qt_add_library(quickshell-wayland STATIC platformmenu.cpp popupanchor.cpp xdgshell.cpp + util.cpp ) # required to make sure the constructor is linked diff --git a/src/wayland/hyprland/CMakeLists.txt b/src/wayland/hyprland/CMakeLists.txt index cb375358..570cbe54 100644 --- a/src/wayland/hyprland/CMakeLists.txt +++ b/src/wayland/hyprland/CMakeLists.txt @@ -19,6 +19,11 @@ if (HYPRLAND_GLOBAL_SHORTCUTS) list(APPEND HYPRLAND_MODULES Quickshell.Hyprland._GlobalShortcuts) endif() +if (HYPRLAND_SURFACE_EXTENSIONS) + add_subdirectory(surface) + list(APPEND HYPRLAND_MODULES Quickshell.Hyprland._SurfaceExtensions) +endif() + qt_add_qml_module(quickshell-hyprland URI Quickshell.Hyprland VERSION 0.1 diff --git a/src/wayland/hyprland/module.md b/src/wayland/hyprland/module.md index 6c2de249..0bdb6a7f 100644 --- a/src/wayland/hyprland/module.md +++ b/src/wayland/hyprland/module.md @@ -7,5 +7,6 @@ headers = [ "ipc/qml.hpp", "focus_grab/qml.hpp", "global_shortcuts/qml.hpp", + "surface/qml.hpp", ] ----- diff --git a/src/wayland/hyprland/surface/CMakeLists.txt b/src/wayland/hyprland/surface/CMakeLists.txt new file mode 100644 index 00000000..04fa5c58 --- /dev/null +++ b/src/wayland/hyprland/surface/CMakeLists.txt @@ -0,0 +1,26 @@ +qt_add_library(quickshell-hyprland-surface-extensions STATIC + qml.cpp + manager.cpp + surface.cpp +) + +qt_add_qml_module(quickshell-hyprland-surface-extensions + URI Quickshell.Hyprland._SurfaceExtensions + VERSION 0.1 + DEPENDENCIES QtQml +) + +install_qml_module(quickshell-hyprland-surface-extensions) + +wl_proto(quickshell-hyprland-surface-extensions + hyprland-surface-v1 + "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-surface-v1.xml" +) + +target_link_libraries(quickshell-hyprland-surface-extensions PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client +) + +qs_module_pch(quickshell-hyprland-surface-extensions) + +target_link_libraries(quickshell PRIVATE quickshell-hyprland-surface-extensionsplugin) diff --git a/src/wayland/hyprland/surface/hyprland-surface-v1.xml b/src/wayland/hyprland/surface/hyprland-surface-v1.xml new file mode 100644 index 00000000..2f683365 --- /dev/null +++ b/src/wayland/hyprland/surface/hyprland-surface-v1.xml @@ -0,0 +1,100 @@ +<?xml version="1.0" encoding="UTF-8"?> +<protocol name="hyprland_surface_v1"> + <copyright> + Copyright © 2025 outfoxxed + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + </copyright> + + <description summary="hyprland-specific wl_surface extensions"> + This protocol exposes hyprland-specific wl_surface properties. + </description> + + <interface name="hyprland_surface_manager_v1" version="1"> + <description summary="manager for hyprland surface objects"> + This interface allows a client to create hyprland surface objects. + </description> + + <request name="get_hyprland_surface"> + <description summary="create a hyprland surface object"> + Create a hyprland surface object for the given wayland surface. + + If the wl_surface already has an associated hyprland_surface_v1 object, + even from a different manager, creation is a protocol error. + </description> + + <arg name="id" type="new_id" interface="hyprland_surface_v1"/> + <arg name="surface" type="object" interface="wl_surface"/> + </request> + + <request name="destroy" type="destructor"> + <description summary="destroy the hyprland surface manager"> + Destroy the surface manager. + This does not destroy existing surface objects. + </description> + </request> + + <enum name="error"> + <entry name="already_constructed" value="0" summary="wl_surface already has a hyprland surface object"/> + </enum> + </interface> + + <interface name="hyprland_surface_v1" version="1"> + <description summary="hyprland-specific wl_surface properties"> + This interface allows access to hyprland-specific properties of a wl_surface. + + Once the wl_surface has been destroyed, the hyprland surface object must be + destroyed as well. All other operations are a protocol error. + </description> + + <request name="set_opacity"> + <description summary="set the overall opacity of the surface"> + Sets a multiplier for the overall opacity of the surface. + This multiplier applies to visual effects such as blur behind the surface + in addition to the surface's content. + + The default value is 1.0. + Setting a value outside of the range 0.0 - 1.0 (inclusive) is a protocol error. + Does not take effect until wl_surface.commit is called. + </description> + + <arg name="opacity" type="fixed"/> + </request> + + <request name="destroy" type="destructor"> + <description summary="destroy the hyprland surface interface"> + Destroy the hyprland surface object, resetting properties provided + by this interface to their default values on the next wl_surface.commit. + </description> + </request> + + <enum name="error"> + <entry name="no_surface" value="0" summary="wl_surface was destroyed"/> + <entry name="out_of_range" value="1" summary="given opacity was not in the range 0.0 - 1.0 (inclusive)"/> + </enum> + </interface> +</protocol> diff --git a/src/wayland/hyprland/surface/manager.cpp b/src/wayland/hyprland/surface/manager.cpp new file mode 100644 index 00000000..31829bb6 --- /dev/null +++ b/src/wayland/hyprland/surface/manager.cpp @@ -0,0 +1,24 @@ +#include "manager.hpp" + +#include <private/qwaylandwindow_p.h> +#include <qwaylandclientextension.h> + +#include "surface.hpp" + +namespace qs::hyprland::surface::impl { + +HyprlandSurfaceManager::HyprlandSurfaceManager(): QWaylandClientExtensionTemplate(1) { + this->initialize(); +} + +HyprlandSurface* +HyprlandSurfaceManager::createHyprlandExtension(QtWaylandClient::QWaylandWindow* surface) { + return new HyprlandSurface(this->get_hyprland_surface(surface->surface())); +} + +HyprlandSurfaceManager* HyprlandSurfaceManager::instance() { + static auto* instance = new HyprlandSurfaceManager(); + return instance; +} + +} // namespace qs::hyprland::surface::impl diff --git a/src/wayland/hyprland/surface/manager.hpp b/src/wayland/hyprland/surface/manager.hpp new file mode 100644 index 00000000..8332241f --- /dev/null +++ b/src/wayland/hyprland/surface/manager.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include <private/qwaylandwindow_p.h> +#include <qwayland-hyprland-surface-v1.h> +#include <qwaylandclientextension.h> + +#include "surface.hpp" + +namespace qs::hyprland::surface::impl { + +class HyprlandSurfaceManager + : public QWaylandClientExtensionTemplate<HyprlandSurfaceManager> + , public QtWayland::hyprland_surface_manager_v1 { +public: + explicit HyprlandSurfaceManager(); + + HyprlandSurface* createHyprlandExtension(QtWaylandClient::QWaylandWindow* surface); + + static HyprlandSurfaceManager* instance(); +}; + +} // namespace qs::hyprland::surface::impl diff --git a/src/wayland/hyprland/surface/qml.cpp b/src/wayland/hyprland/surface/qml.cpp new file mode 100644 index 00000000..8477de55 --- /dev/null +++ b/src/wayland/hyprland/surface/qml.cpp @@ -0,0 +1,154 @@ +#include "qml.hpp" +#include <memory> + +#include <private/qwaylandwindow_p.h> +#include <qlogging.h> +#include <qobject.h> +#include <qqmlinfo.h> +#include <qtmetamacros.h> +#include <qtypes.h> +#include <qwindow.h> + +#include "../../../window/proxywindow.hpp" +#include "../../../window/windowinterface.hpp" +#include "../../util.hpp" +#include "manager.hpp" +#include "surface.hpp" + +using QtWaylandClient::QWaylandWindow; + +namespace qs::hyprland::surface { + +HyprlandWindow* HyprlandWindow::qmlAttachedProperties(QObject* object) { + auto* proxyWindow = qobject_cast<ProxyWindowBase*>(object); + + if (!proxyWindow) { + if (auto* iface = qobject_cast<WindowInterface*>(object)) { + proxyWindow = iface->proxyWindow(); + } + } + + qDebug() << "hlwindow for" << proxyWindow; + if (!proxyWindow) return nullptr; + return new HyprlandWindow(proxyWindow); +} + +HyprlandWindow::HyprlandWindow(ProxyWindowBase* window): QObject(nullptr), proxyWindow(window) { + QObject::connect( + window, + &ProxyWindowBase::windowConnected, + this, + &HyprlandWindow::onWindowConnected + ); + + QObject::connect(window, &QObject::destroyed, this, &HyprlandWindow::onProxyWindowDestroyed); + + if (window->backingWindow()) { + this->onWindowConnected(); + } +} + +qreal HyprlandWindow::opacity() const { return this->mOpacity; } + +void HyprlandWindow::setOpacity(qreal opacity) { + if (opacity == this->mOpacity) return; + + if (opacity < 0.0 || opacity > 1.0) { + qmlWarning(this + ) << "Cannot set HyprlandWindow.opacity to a value larger than 1.0 or smaller than 0.0"; + return; + } + + this->mOpacity = opacity; + + if (this->surface) { + this->surface->setOpacity(opacity); + qs::wayland::util::scheduleCommit(this->mWaylandWindow); + } + + emit this->opacityChanged(); +} + +void HyprlandWindow::onWindowConnected() { + this->mWindow = this->proxyWindow->backingWindow(); + // disconnected by destructor + QObject::connect( + this->mWindow, + &QWindow::visibleChanged, + this, + &HyprlandWindow::onWindowVisibleChanged + ); + + this->onWindowVisibleChanged(); +} + +void HyprlandWindow::onWindowVisibleChanged() { + if (this->mWindow->isVisible()) { + if (!this->mWindow->handle()) { + this->mWindow->create(); + } + + this->mWaylandWindow = dynamic_cast<QWaylandWindow*>(this->mWindow->handle()); + + if (this->mWaylandWindow) { + // disconnected by destructor + + QObject::connect( + this->mWaylandWindow, + &QWaylandWindow::surfaceCreated, + this, + &HyprlandWindow::onWaylandSurfaceCreated + ); + + QObject::connect( + this->mWaylandWindow, + &QWaylandWindow::surfaceDestroyed, + this, + &HyprlandWindow::onWaylandSurfaceDestroyed + ); + + if (this->mWaylandWindow->surface()) { + this->onWaylandSurfaceCreated(); + } + } + } +} + +void HyprlandWindow::onWaylandSurfaceCreated() { + auto* manager = impl::HyprlandSurfaceManager::instance(); + + if (!manager->isActive()) { + qWarning() << "The active compositor does not support the hyprland_surface_v1 protocol. " + "HyprlandWindow will not work."; + return; + } + + auto* ext = manager->createHyprlandExtension(this->mWaylandWindow); + this->surface = std::unique_ptr<impl::HyprlandSurface>(ext); + + if (this->mOpacity != 1.0) { + this->surface->setOpacity(this->mOpacity); + qs::wayland::util::scheduleCommit(this->mWaylandWindow); + } +} + +void HyprlandWindow::onWaylandSurfaceDestroyed() { + this->surface = nullptr; + + if (!this->proxyWindow) { + this->deleteLater(); + } +} + +void HyprlandWindow::onProxyWindowDestroyed() { + // Don't delete the HyprlandWindow, and therefore the impl::HyprlandSurface until the wl_surface is destroyed. + // Deleting it when the proxy window is deleted will cause a full opacity frame between the destruction of the + // hyprland_surface_v1 and wl_surface objects. + + if (this->surface == nullptr) { + this->proxyWindow = nullptr; + this->deleteLater(); + } +} + +} // namespace qs::hyprland::surface diff --git a/src/wayland/hyprland/surface/qml.hpp b/src/wayland/hyprland/surface/qml.hpp new file mode 100644 index 00000000..ce32a967 --- /dev/null +++ b/src/wayland/hyprland/surface/qml.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include <memory> + +#include <private/qwaylandwindow_p.h> +#include <qobject.h> +#include <qqmlintegration.h> +#include <qtmetamacros.h> +#include <qtypes.h> +#include <qwindow.h> + +#include "../../../window/proxywindow.hpp" +#include "surface.hpp" + +namespace qs::hyprland::surface { + +///! Hyprland specific QsWindow properties. +/// Allows setting hyprland specific window properties on a @@Quickshell.QsWindow or subclass, +/// as an attached object. +/// +/// #### Example +/// ```qml +/// @@Quickshell.PopupWindow { +/// // ... +/// HyprlandWindow.opacity: 0.6 // any number or binding +/// } +/// ``` +/// +/// > [!NOTE] Requires at least hyprland 0.47.0, or [hyprland-surface-v1] support. +/// +/// [hyprland-surface-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-surface-v1.xml +class HyprlandWindow: public QObject { + Q_OBJECT; + /// A multiplier for the window's overall opacity, ranging from 1.0 to 0.0. Overall opacity includes the opacity of + /// both the window content *and* visual effects such as blur that apply to it. + /// + /// Default: 1.0 + Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity NOTIFY opacityChanged); + QML_ELEMENT; + QML_UNCREATABLE("HyprlandWindow can only be used as an attached object."); + QML_ATTACHED(HyprlandWindow); + +public: + explicit HyprlandWindow(ProxyWindowBase* window); + + [[nodiscard]] static bool available(); + + [[nodiscard]] qreal opacity() const; + void setOpacity(qreal opacity); + + static HyprlandWindow* qmlAttachedProperties(QObject* object); + +signals: + void opacityChanged(); + +private slots: + void onWindowConnected(); + void onWindowVisibleChanged(); + void onWaylandSurfaceCreated(); + void onWaylandSurfaceDestroyed(); + void onProxyWindowDestroyed(); + +private: + void disconnectWaylandWindow(); + + ProxyWindowBase* proxyWindow = nullptr; + QWindow* mWindow = nullptr; + QtWaylandClient::QWaylandWindow* mWaylandWindow = nullptr; + + qreal mOpacity = 1.0; + std::unique_ptr<impl::HyprlandSurface> surface; +}; + +} // namespace qs::hyprland::surface diff --git a/src/wayland/hyprland/surface/surface.cpp b/src/wayland/hyprland/surface/surface.cpp new file mode 100644 index 00000000..d1aa24fb --- /dev/null +++ b/src/wayland/hyprland/surface/surface.cpp @@ -0,0 +1,19 @@ +#include "surface.hpp" + +#include <qtypes.h> +#include <qwayland-hyprland-surface-v1.h> +#include <wayland-hyprland-surface-v1-client-protocol.h> +#include <wayland-util.h> + +namespace qs::hyprland::surface::impl { + +HyprlandSurface::HyprlandSurface(::hyprland_surface_v1* surface) + : QtWayland::hyprland_surface_v1(surface) {} + +HyprlandSurface::~HyprlandSurface() { this->destroy(); } + +void HyprlandSurface::setOpacity(qreal opacity) { + this->set_opacity(wl_fixed_from_double(opacity)); +} + +} // namespace qs::hyprland::surface::impl diff --git a/src/wayland/hyprland/surface/surface.hpp b/src/wayland/hyprland/surface/surface.hpp new file mode 100644 index 00000000..a27e50e3 --- /dev/null +++ b/src/wayland/hyprland/surface/surface.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include <qobject.h> +#include <qtclasshelpermacros.h> +#include <qtmetamacros.h> +#include <qtypes.h> +#include <qwayland-hyprland-surface-v1.h> +#include <wayland-hyprland-surface-v1-client-protocol.h> + +namespace qs::hyprland::surface::impl { + +class HyprlandSurface: public QtWayland::hyprland_surface_v1 { +public: + explicit HyprlandSurface(::hyprland_surface_v1* surface); + ~HyprlandSurface() override; + Q_DISABLE_COPY_MOVE(HyprlandSurface); + + void setOpacity(qreal opacity); +}; + +} // namespace qs::hyprland::surface::impl diff --git a/src/wayland/util.cpp b/src/wayland/util.cpp new file mode 100644 index 00000000..6bce2621 --- /dev/null +++ b/src/wayland/util.cpp @@ -0,0 +1,17 @@ +#include "util.hpp" + +#include <private/qwaylandwindow_p.h> +#include <qpa/qwindowsysteminterface.h> + +namespace qs::wayland::util { + +void scheduleCommit(QtWaylandClient::QWaylandWindow* window) { + // This seems to be one of the less offensive ways to force Qt to send a wl_surface.commit on its own terms. + // Ideally we would trigger the commit more directly. + QWindowSystemInterface::handleExposeEvent( + window->window(), + QRect(QPoint(), window->geometry().size()) + ); +} + +} // namespace qs::wayland::util diff --git a/src/wayland/util.hpp b/src/wayland/util.hpp new file mode 100644 index 00000000..7967fadc --- /dev/null +++ b/src/wayland/util.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include <private/qwaylandwindow_p.h> + +namespace qs::wayland::util { + +void scheduleCommit(QtWaylandClient::QWaylandWindow* window); + +}