diff --git a/Modules/ServiceLoader.qml b/Modules/ServiceLoader.qml new file mode 100644 index 0000000..01a2092 --- /dev/null +++ b/Modules/ServiceLoader.qml @@ -0,0 +1,9 @@ +import Quickshell +import QtQuick +import qs.Modules.Wallpaper + +Scope { + Component.onCompleted: { + Wallpaper; + } +} diff --git a/Modules/Settings/Common/DialogRowButton.qml b/Modules/Settings/Common/DialogRowButton.qml index ee9d4cb..fc59f88 100644 --- a/Modules/Settings/Common/DialogRowButton.qml +++ b/Modules/Settings/Common/DialogRowButton.qml @@ -139,12 +139,13 @@ Item { id: dialogBg anchors.fill: parent - bottomLeftRadius: Tokens.rounding.largeIncreased - bottomRightRadius: Tokens.rounding.largeIncreased + bottomLeftRadius: root.last ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall + bottomRightRadius: root.last ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall deformScale: 0 group: blobGroup opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5) - radius: Tokens.rounding.extraSmall + topLeftRadius: root.first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall + topRightRadius: root.first ? Tokens.rounding.largeIncreased : Tokens.rounding.extraSmall } RowButton { @@ -153,9 +154,11 @@ Item { anchors.left: parent.left anchors.right: parent.right color: "transparent" - height: Math.min(implicitHeight, parent.height) // Clamp to parent height due to overshoot anim + first: root.first + height: Math.min(implicitHeight, parent.height) icon: root.icon - last: true + last: root.last + subtext: root.subtext text: root.label transform: Matrix4x4 { diff --git a/Modules/Wallpaper/WallBackground.qml b/Modules/Wallpaper/WallBackground.qml deleted file mode 100644 index 76e44dd..0000000 --- a/Modules/Wallpaper/WallBackground.qml +++ /dev/null @@ -1,51 +0,0 @@ -pragma ComponentBehavior: Bound - -import Quickshell -import Quickshell.Hyprland -import QtQuick -import qs.Components -import qs.Helpers -import ZShell.Internal - -Item { - id: root - - readonly property var displayData: Wallpapers.getCrop(screen.name) - required property ShellScreen screen - property size screenResolution: Qt.size(screen.width * screenScale, screen.height * screenScale) - property real screenScale: Hyprland.monitorFor(screen).scale - - anchors.fill: parent - - Component.onCompleted: Hyprland.refreshMonitors() - - WallpaperImage { - id: img - - anchors.fill: parent - cropHeight: root.displayData?.height ?? 1.0 - cropWidth: root.displayData?.width ?? 1.0 - cropX: root.displayData?.x ?? 0.0 - cropY: root.displayData?.y ?? 0.0 - fadeDuration: 150 - screenResolution: root.screenResolution - source: Wallpapers.current - - Behavior on cropHeight { - Anim { - } - } - Behavior on cropWidth { - Anim { - } - } - Behavior on cropX { - Anim { - } - } - Behavior on cropY { - Anim { - } - } - } -} diff --git a/Modules/Wallpaper/Wallpaper.qml b/Modules/Wallpaper/Wallpaper.qml index 02fe54b..563e4e8 100644 --- a/Modules/Wallpaper/Wallpaper.qml +++ b/Modules/Wallpaper/Wallpaper.qml @@ -1,46 +1,49 @@ +pragma Singleton + import Quickshell import QtQuick -import Quickshell.Wayland -import ZShell.Config -import qs.Modules.DesktopIcons +import ZShell.Internal +import qs.Components +import qs.Helpers -Loader { - active: Config.background.enabled - asynchronous: false +Singleton { + id: root - sourceComponent: Variants { + WallpaperService { + id: wallpaper + + path: Wallpapers.current + } + + Variants { model: Quickshell.screens - PanelWindow { - id: root - + WallpaperObject { + readonly property var crops: Wallpapers.getCrop(modelData.name) required property var modelData - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.layer: WlrLayer.Background - WlrLayershell.namespace: "ZShell-Wallpaper" - color: "transparent" - screen: modelData - surfaceFormat.opaque: true + cropHeight: crops?.height ?? 1.0 + cropWidth: crops?.width ?? 1.0 + cropX: crops?.x ?? 0.0 + cropY: crops?.y ?? 0.0 + screen: modelData.name + service: wallpaper + zoom: crops?.zoom ?? 1.0 - anchors { - bottom: true - left: true - right: true - top: true + Behavior on cropHeight { + Anim { + } } - - WallBackground { - screen: root.screen + Behavior on cropWidth { + Anim { + } } - - Loader { - id: loader - - active: Config.general.desktopIcons - anchors.fill: parent - - sourceComponent: DesktopIcons { + Behavior on cropX { + Anim { + } + } + Behavior on cropY { + Anim { } } } diff --git a/Plugins/CMakeLists.txt b/Plugins/CMakeLists.txt index 5c302ae..bca6758 100644 --- a/Plugins/CMakeLists.txt +++ b/Plugins/CMakeLists.txt @@ -1,5 +1,24 @@ -find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick QuickPrivate Concurrent Sql Network DBus CanvasPainter) +find_package(Qt6 REQUIRED COMPONENTS + ShaderTools + Core + Qml + Gui + GuiPrivate + Quick + QuickPrivate + Concurrent + Sql + Network + DBus + CanvasPainter + WaylandClient + WaylandClientPrivate +) find_package(PkgConfig REQUIRED) +pkg_check_modules(WAYLAND_CLIENT REQUIRED IMPORTED_TARGET wayland-client) +pkg_check_modules(EGL REQUIRED IMPORTED_TARGET egl) +pkg_check_modules(WAYLAND_EGL REQUIRED IMPORTED_TARGET wayland-egl) +pkg_check_modules(GL REQUIRED IMPORTED_TARGET gl) pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED) @@ -17,6 +36,7 @@ qt_standard_project_setup(REQUIRES 6.9) include(cmake/pch.cmake) include(cmake/qml-module.cmake) +include(cmake/wayland-protocol.cmake) add_library(zshell-util INTERFACE) target_include_directories(zshell-util INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") diff --git a/Plugins/ZShell/Internal/CMakeLists.txt b/Plugins/ZShell/Internal/CMakeLists.txt index f789bba..62911fd 100644 --- a/Plugins/ZShell/Internal/CMakeLists.txt +++ b/Plugins/ZShell/Internal/CMakeLists.txt @@ -1,3 +1,9 @@ +wl_proto(wlp-layer-shell wlr-layer-shell-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}/protocols") +wl_proto(wlp-viewporter viewporter "${WAYLAND_PROTOCOLS}/stable/viewporter") +wl_proto(wlp-fractional-scale fractional-scale-v1 "${WAYLAND_PROTOCOLS}/staging/fractional-scale") + +wl_proto(wlp-xdg-shell xdg-shell "${WAYLAND_PROTOCOLS}/stable/xdg-shell") + qml_module(ZShell-internal URI ZShell.Internal INCLUDE_PREFIX internal @@ -9,6 +15,9 @@ qml_module(ZShell-internal circularbuffer.hpp circularbuffer.cpp arcgauge.hpp arcgauge.cpp wallpaperimage.hpp wallpaperimage.cpp + wallpaperservice.hpp wallpaperservice.cpp + wallpapersurface.hpp wallpapersurface.cpp + wallpaperobject.hpp wallpaperobject.cpp lidwatcher.hpp lidwatcher.cpp visualizerbars.hpp visualizerbars.cpp linearindicatormanager.hpp linearindicatormanager.cpp @@ -23,4 +32,12 @@ qml_module(ZShell-internal Qt::Network Qt::DBus Qt::CanvasPainter + Qt::GuiPrivate + wlp-layer-shell + wlp-xdg-shell + wlp-viewporter + wlp-fractional-scale + PkgConfig::EGL + PkgConfig::WAYLAND_EGL + PkgConfig::GL ) diff --git a/Plugins/ZShell/Internal/protocols/wlr-layer-shell-unstable-v1.xml b/Plugins/ZShell/Internal/protocols/wlr-layer-shell-unstable-v1.xml new file mode 100644 index 0000000..283f391 --- /dev/null +++ b/Plugins/ZShell/Internal/protocols/wlr-layer-shell-unstable-v1.xml @@ -0,0 +1,407 @@ + + + + Copyright © 2017 Drew DeVault + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + Clients can use this interface to assign the surface_layer role to + wl_surfaces. Such surfaces are assigned to a "layer" of the output and + rendered with a defined z-depth respective to each other. They may also be + anchored to the edges and corners of a screen and specify input handling + semantics. This interface should be suitable for the implementation of + many desktop shell components, and a broad number of other applications + that interact with the desktop. + + + + + Create a layer surface for an existing surface. This assigns the role of + layer_surface, or raises a protocol error if another role is already + assigned. + + Creating a layer surface from a wl_surface which has a buffer attached + or committed is a client error, and any attempts by a client to attach + or manipulate a buffer prior to the first layer_surface.configure call + must also be treated as errors. + + After creating a layer_surface object and setting it up, the client + must perform an initial commit without any buffer attached. + The compositor will reply with a layer_surface.configure event. + The client must acknowledge it and is then allowed to attach a buffer + to map the surface. + + You may pass NULL for output to allow the compositor to decide which + output to use. Generally this will be the one that the user most + recently interacted with. + + Clients can specify a namespace that defines the purpose of the layer + surface. + + + + + + + + + + + + + + + + + These values indicate which layers a surface can be rendered in. They + are ordered by z depth, bottom-most first. Traditional shell surfaces + will typically be rendered between the bottom and top layers. + Fullscreen shell surfaces are typically rendered at the top layer. + Multiple surfaces can share a single layer, and ordering within a + single layer is undefined. + + + + + + + + + + + + + This request indicates that the client will not use the layer_shell + object any more. Objects that have been created through this instance + are not affected. + + + + + + + An interface that may be implemented by a wl_surface, for surfaces that + are designed to be rendered as a layer of a stacked desktop-like + environment. + + Layer surface state (layer, size, anchor, exclusive zone, + margin, interactivity) is double-buffered, and will be applied at the + time wl_surface.commit of the corresponding wl_surface is called. + + Attaching a null buffer to a layer surface unmaps it. + + Unmapping a layer_surface means that the surface cannot be shown by the + compositor until it is explicitly mapped again. The layer_surface + returns to the state it had right after layer_shell.get_layer_surface. + The client can re-map the surface by performing a commit without any + buffer attached, waiting for a configure event and handling it as usual. + + + + + Sets the size of the surface in surface-local coordinates. The + compositor will display the surface centered with respect to its + anchors. + + If you pass 0 for either value, the compositor will assign it and + inform you of the assignment in the configure event. You must set your + anchor to opposite edges in the dimensions you omit; not doing so is a + protocol error. Both values are 0 by default. + + Size is double-buffered, see wl_surface.commit. + + + + + + + + Requests that the compositor anchor the surface to the specified edges + and corners. If two orthogonal edges are specified (e.g. 'top' and + 'left'), then the anchor point will be the intersection of the edges + (e.g. the top left corner of the output); otherwise the anchor point + will be centered on that edge, or in the center if none is specified. + + Anchor is double-buffered, see wl_surface.commit. + + + + + + + Requests that the compositor avoids occluding an area with other + surfaces. The compositor's use of this information is + implementation-dependent - do not assume that this region will not + actually be occluded. + + A positive value is only meaningful if the surface is anchored to one + edge or an edge and both perpendicular edges. If the surface is not + anchored, anchored to only two perpendicular edges (a corner), anchored + to only two parallel edges or anchored to all edges, a positive value + will be treated the same as zero. + + A positive zone is the distance from the edge in surface-local + coordinates to consider exclusive. + + Surfaces that do not wish to have an exclusive zone may instead specify + how they should interact with surfaces that do. If set to zero, the + surface indicates that it would like to be moved to avoid occluding + surfaces with a positive exclusive zone. If set to -1, the surface + indicates that it would not like to be moved to accommodate for other + surfaces, and the compositor should extend it all the way to the edges + it is anchored to. + + For example, a panel might set its exclusive zone to 10, so that + maximized shell surfaces are not shown on top of it. A notification + might set its exclusive zone to 0, so that it is moved to avoid + occluding the panel, but shell surfaces are shown underneath it. A + wallpaper or lock screen might set their exclusive zone to -1, so that + they stretch below or over the panel. + + The default value is 0. + + Exclusive zone is double-buffered, see wl_surface.commit. + + + + + + + Requests that the surface be placed some distance away from the anchor + point on the output, in surface-local coordinates. Setting this value + for edges you are not anchored to has no effect. + + The exclusive zone includes the margin. + + Margin is double-buffered, see wl_surface.commit. + + + + + + + + + + Types of keyboard interaction possible for layer shell surfaces. The + rationale for this is twofold: (1) some applications are not interested + in keyboard events and not allowing them to be focused can improve the + desktop experience; (2) some applications will want to take exclusive + keyboard focus. + + + + + This value indicates that this surface is not interested in keyboard + events and the compositor should never assign it the keyboard focus. + + This is the default value, set for newly created layer shell surfaces. + + This is useful for e.g. desktop widgets that display information or + only have interaction with non-keyboard input devices. + + + + + Request exclusive keyboard focus if this surface is above the shell surface layer. + + For the top and overlay layers, the seat will always give + exclusive keyboard focus to the top-most layer which has keyboard + interactivity set to exclusive. If this layer contains multiple + surfaces with keyboard interactivity set to exclusive, the compositor + determines the one receiving keyboard events in an implementation- + defined manner. In this case, no guarantee is made when this surface + will receive keyboard focus (if ever). + + For the bottom and background layers, the compositor is allowed to use + normal focus semantics. + + This setting is mainly intended for applications that need to ensure + they receive all keyboard events, such as a lock screen or a password + prompt. + + + + + This requests the compositor to allow this surface to be focused and + unfocused by the user in an implementation-defined manner. The user + should be able to unfocus this surface even regardless of the layer + it is on. + + Typically, the compositor will want to use its normal mechanism to + manage keyboard focus between layer shell surfaces with this setting + and regular toplevels on the desktop layer (e.g. click to focus). + Nevertheless, it is possible for a compositor to require a special + interaction to focus or unfocus layer shell surfaces (e.g. requiring + a click even if focus follows the mouse normally, or providing a + keybinding to switch focus between layers). + + This setting is mainly intended for desktop shell components (e.g. + panels) that allow keyboard interaction. Using this option can allow + implementing a desktop shell that can be fully usable without the + mouse. + + + + + + + Set how keyboard events are delivered to this surface. By default, + layer shell surfaces do not receive keyboard events; this request can + be used to change this. + + This setting is inherited by child surfaces set by the get_popup + request. + + Layer surfaces receive pointer, touch, and tablet events normally. If + you do not want to receive them, set the input region on your surface + to an empty region. + + Keyboard interactivity is double-buffered, see wl_surface.commit. + + + + + + + This assigns an xdg_popup's parent to this layer_surface. This popup + should have been created via xdg_surface::get_popup with the parent set + to NULL, and this request must be invoked before committing the popup's + initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + + + + + + This request destroys the layer surface. + + + + + + The configure event asks the client to resize its surface. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + The client is free to dismiss all but the last configure event it + received. + + The width and height arguments specify the size of the window in + surface-local coordinates. + + The size is a hint, in the sense that the client is free to ignore it if + it doesn't resize, pick a smaller size (to satisfy aspect ratio or + resize in steps of NxM pixels). If the client picks a smaller size and + is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the + surface will be centered on this axis. + + If the width or height arguments are zero, it means the client should + decide its own window dimension. + + + + + + + + + The closed event is sent by the compositor when the surface will no + longer be shown. The output may have been destroyed or the user may + have asked for it to be removed. Further changes to the surface will be + ignored. The client should destroy the resource after receiving this + event, and create a new surface if they so choose. + + + + + + + + + + + + + + + + + + + + + + + Change the layer that the surface is rendered on. + + Layer is double-buffered, see wl_surface.commit. + + + + + + + + + Requests an edge for the exclusive zone to apply. The exclusive + edge will be automatically deduced from anchor points when possible, + but when the surface is anchored to a corner, it will be necessary + to set it explicitly to disambiguate, as it is not possible to deduce + which one of the two corner edges should be used. + + The edge must be one the surface is anchored to, otherwise the + invalid_exclusive_edge protocol error will be raised. + + + + + diff --git a/Plugins/ZShell/Internal/wallpaperobject.cpp b/Plugins/ZShell/Internal/wallpaperobject.cpp new file mode 100644 index 0000000..b11057e --- /dev/null +++ b/Plugins/ZShell/Internal/wallpaperobject.cpp @@ -0,0 +1,83 @@ +#include "wallpaperobject.hpp" + +namespace ZShell::internal { + +WallpaperObject::WallpaperObject(QObject* parent) : QObject(parent) {} + +WallpaperObject::~WallpaperObject() { + if (m_service && !m_screen.isEmpty()) m_service->removeScreen(m_screen); +} + +void WallpaperObject::registerScreen() { + if (!m_service || m_screen.isEmpty()) return; + + m_service->addScreen(m_screen); +} + +void WallpaperObject::setService(WallpaperService* service) { + if (m_service == service) return; + m_service = service; + emit serviceChanged(); + + registerScreen(); + push(); +} + +void WallpaperObject::setScreen(const QString& screen) { + if (m_screen == screen) return; + m_screen = screen; + emit screenChanged(); + + registerScreen(); + push(); +} + +void WallpaperObject::setCropX(qreal x) { + if (qFuzzyCompare(m_cropX, x)) return; + m_cropX = x; + emit cropXChanged(); + push(); +} + +void WallpaperObject::setCropY(qreal y) { + if (qFuzzyCompare(m_cropY, y)) return; + m_cropY = y; + emit cropYChanged(); + push(); +} + +void WallpaperObject::setCropWidth(qreal w) { + if (qFuzzyCompare(m_cropWidth, w)) return; + m_cropWidth = w; + emit cropWidthChanged(); + push(); +} + +void WallpaperObject::setCropHeight(qreal h) { + if (qFuzzyCompare(m_cropHeight, h)) return; + m_cropHeight = h; + emit cropHeightChanged(); + push(); +} + +void WallpaperObject::setZoom(qreal zoom) { + if (qFuzzyCompare(m_zoom, zoom)) return; + m_zoom = zoom; + emit zoomChanged(); + push(); +} + +void WallpaperObject::push() { + if (!m_service || m_screen.isEmpty()) return; + + QVariantMap crop; + crop.insert("x", m_cropX); + crop.insert("y", m_cropY); + crop.insert("width", m_cropWidth); + crop.insert("height", m_cropHeight); + crop.insert("zoom", m_zoom); + + m_service->setCrop(m_screen, crop); +} + +} // namespace ZShell::internal diff --git a/Plugins/ZShell/Internal/wallpaperobject.hpp b/Plugins/ZShell/Internal/wallpaperobject.hpp new file mode 100644 index 0000000..eb3d8a4 --- /dev/null +++ b/Plugins/ZShell/Internal/wallpaperobject.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "wallpaperservice.hpp" + +namespace ZShell::internal { + +class WallpaperObject : public QObject { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY( + WallpaperService* service READ service WRITE setService NOTIFY + serviceChanged) + Q_PROPERTY(QString screen READ screen WRITE setScreen NOTIFY screenChanged) + + Q_PROPERTY(qreal cropX READ cropX WRITE setCropX NOTIFY cropXChanged) + Q_PROPERTY(qreal cropY READ cropY WRITE setCropY NOTIFY cropYChanged) + Q_PROPERTY( + qreal cropWidth READ cropWidth WRITE setCropWidth NOTIFY + cropWidthChanged) + Q_PROPERTY( + qreal cropHeight READ cropHeight WRITE setCropHeight NOTIFY + cropHeightChanged) + Q_PROPERTY(qreal zoom READ zoom WRITE setZoom NOTIFY zoomChanged) + + public: + explicit WallpaperObject(QObject* parent = nullptr); + ~WallpaperObject() override; + + WallpaperService* service() const { return m_service; } + void setService(WallpaperService* service); + + QString screen() const { return m_screen; } + void setScreen(const QString& screen); + + qreal cropX() const { return m_cropX; } + void setCropX(qreal x); + + qreal cropY() const { return m_cropY; } + void setCropY(qreal y); + + qreal cropWidth() const { return m_cropWidth; } + void setCropWidth(qreal w); + + qreal cropHeight() const { return m_cropHeight; } + void setCropHeight(qreal h); + + qreal zoom() const { return m_zoom; } + void setZoom(qreal zoom); + + signals: + void serviceChanged(); + void screenChanged(); + void cropXChanged(); + void cropYChanged(); + void cropWidthChanged(); + void cropHeightChanged(); + void zoomChanged(); + + private: + void registerScreen(); + + void push(); + + QPointer m_service; + QString m_screen; + + qreal m_cropX = 0.0; + qreal m_cropY = 0.0; + qreal m_cropWidth = 1.0; + qreal m_cropHeight = 1.0; + qreal m_zoom = 1.0; +}; + +} // namespace ZShell::internal diff --git a/Plugins/ZShell/Internal/wallpaperservice.cpp b/Plugins/ZShell/Internal/wallpaperservice.cpp new file mode 100644 index 0000000..9933218 --- /dev/null +++ b/Plugins/ZShell/Internal/wallpaperservice.cpp @@ -0,0 +1,749 @@ +#define EGL_EGLEXT_PROTOTYPES +#define GL_GLEXT_PROTOTYPES + +#include "wallpaperservice.hpp" + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include + +#include + +namespace ZShell::internal { + +namespace { + +const char* kVertexShader = R"( +#version 300 es +void main() { + // The corners are (-1,-1), (3,-1) and (-1,3), which cover the whole buffer. + vec2 corner = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2)); + gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0); +} +)"; + +const char* kFragmentShader = R"( +#version 300 es +precision highp float; +uniform sampler2D source; +uniform vec2 size; +uniform vec4 crop; +out vec4 outColor; +void main() { + // The pixels are stored top-down, so the fragment coordinate is flipped. + vec2 p = vec2(gl_FragCoord.x, size.y - gl_FragCoord.y) / size; + outColor = texture(source, crop.xy + p * crop.zw); +} +)"; + +constexpr int kWaitTimeoutMs = 20; + +void logShader(uint32_t object, bool program) { + int ok = 0; + if (program) + glGetProgramiv(object, GL_LINK_STATUS, &ok); + else + glGetShaderiv(object, GL_COMPILE_STATUS, &ok); + + if (ok) return; + + char info[512] = {}; + if (program) + glGetProgramInfoLog(object, int(sizeof(info)), nullptr, info); + else + glGetShaderInfoLog(object, int(sizeof(info)), nullptr, info); + + qWarning() << "WallpaperService:" << (program ? "linking" : "compiling") + << "failed:" << info; +} + +QVariantMap cropFor(const QVariantMap& crops, const QString& screen) { + QVariantMap crop = crops.value(screen).toMap(); + + if (crop.isEmpty()) { + crop.insert("x", 0.0); + crop.insert("y", 0.0); + crop.insert("width", 1.0); + crop.insert("height", 1.0); + crop.insert("zoom", 1.0); + } + + return crop; +} + +} // namespace + +WallpaperService::WallpaperService(QObject* parent) : QObject(parent) {} + +WallpaperService::~WallpaperService() { + m_stop = true; + wake(); + + if (m_thread.joinable()) m_thread.join(); + + for (auto* surface : m_surfaces) + delete surface; + m_surfaces.clear(); + + if (m_eglDisplay != EGL_NO_DISPLAY && m_context != EGL_NO_CONTEXT) { + if (eglMakeCurrent( + m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, m_context)) { + if (m_texture) glDeleteTextures(1, &m_texture); + if (m_program) glDeleteProgram(m_program); + } + + eglDestroyContext(m_eglDisplay, m_context); + } + + if (m_queue) wl_event_queue_destroy(m_queue); + + if (m_wake[0] >= 0) { + close(m_wake[0]); + close(m_wake[1]); + } + + if (m_layerShell) m_layerShell->destroy(); + if (m_viewporter) m_viewporter->destroy(); + if (m_scaleManager) m_scaleManager->destroy(); + if (m_registry) wl_registry_destroy(m_registry); +} + +QString WallpaperService::path() const { + std::lock_guard lock(m_mutex); + return m_path; +} + +void WallpaperService::setPath(const QString& path) { + if (m_path == path) return; + + { + std::lock_guard lock(m_mutex); + m_path = path; + + m_forceDecode = true; + m_imageChanged = true; + } + + emit pathChanged(); + + start(); + queueAll(); +} + +void WallpaperService::addScreen(const QString& screen) { + { + std::lock_guard lock(m_mutex); + + if (m_screens.contains(screen)) return; + + m_screens.append(screen); + } + + start(); + + queueAll(); +} + +void WallpaperService::removeScreen(const QString& screen) { + { + std::lock_guard lock(m_mutex); + + if (!m_screens.removeOne(screen)) return; + } + + queueAll(); +} + +const QVariantMap& WallpaperService::crops() const { + std::lock_guard lock(m_mutex); + return m_crops; +} + +void WallpaperService::setCrops(const QVariantMap& crops) { + { + std::lock_guard lock(m_mutex); + + if (m_crops == crops) return; + + m_crops = crops; + } + + emit cropsChanged(); + queueAll(); +} + +void WallpaperService::setCrop(const QString& screen, const QVariantMap& crop) { + WallpaperSurface* surface = nullptr; + + { + std::lock_guard lock(m_mutex); + + if (m_crops.value(screen) == crop) return; + + m_crops.insert(screen, crop); + + for (auto* candidate : m_surfaces) { + if (candidate->screen() == screen) { + surface = candidate; + break; + } + } + } + + emit cropsChanged(); + + if (surface) queue(surface); +} + +void WallpaperService::redraw() { + queueAll(); +} + +void WallpaperService::start() { + { + std::lock_guard lock(m_mutex); + + if (started || m_path.isEmpty() || m_screens.isEmpty()) return; + + started = true; + } + + auto* interface = + qApp->nativeInterface(); + if (!interface) { + qWarning() << "WallpaperService: this is not a Wayland session"; + return; + } + + m_display = interface->display(); + m_compositor = interface->compositor(); + if (!m_display || !m_compositor) return; + + m_queue = wl_display_create_queue(m_display); + if (!m_queue) return; + + if (pipe2(m_wake, O_CLOEXEC) < 0) { + qWarning() << "WallpaperService: could not make the pipe to wake on"; + return; + } + + bindGlobals(); + + if (!setupGl()) return; + + m_drawing = true; + m_thread = std::thread([this] { loop(); }); + + wake(); +} + +void WallpaperService::wake() { + if (m_wake[1] < 0) return; + + char byte = 0; + + if (::write(m_wake[1], &byte, 1) < 0) + qWarning() << "WallpaperService: could not wake the drawing thread"; +} + +void WallpaperService::queue(WallpaperSurface* surface) { + { + std::lock_guard lock(m_mutex); + m_pending.push_back(surface); + } + + wake(); +} + +void WallpaperService::queueAll() { + { + std::lock_guard lock(m_mutex); + + for (auto* surface : m_surfaces) + m_pending.push_back(surface); + } + + wake(); +} + +void WallpaperService::loop() { + while (!m_stop) { + present(); + + if (wl_display_prepare_read(m_display) == 0) { + wl_display_flush(m_display); + + struct pollfd fds[2] = { + {wl_display_get_fd(m_display), POLLIN, 0}, + {m_wake[0], POLLIN, 0}, + }; + + int ready = poll(fds, 2, kWaitTimeoutMs); + + if (ready > 0 && fds[0].revents) { + wl_display_read_events(m_display); + continue; + } + + if (ready > 0 && fds[1].revents) { + char byte = 0; + ::read(m_wake[0], &byte, 1); + } + + wl_display_cancel_read(m_display); + continue; + } + + struct pollfd fds[1] = {{m_wake[0], POLLIN, 0}}; + + if (poll(fds, 1, kWaitTimeoutMs) <= 0) continue; + + char byte = 0; + ::read(m_wake[0], &byte, 1); + } +} + +void WallpaperService::present() { + wl_display_dispatch_queue_pending(m_display, m_queue); + + addSurfaces(); + + ensureDecodeSize(); + + std::vector pending; + { + std::lock_guard lock(m_mutex); + pending.swap(m_pending); + } + + for (auto* surface : pending) + drawSurface(surface); +} + +void WallpaperService::addSurfaces() { + if (!started || !m_layerShell || !m_viewporter || !m_scaleManager) return; + + QList gone; + QList needed; + + { + std::lock_guard lock(m_mutex); + + QList kept; + for (auto* surface : m_surfaces) { + if (m_screens.contains(surface->screen())) + kept.append(surface); + else + gone.append(surface); + } + + m_surfaces = kept; + + for (const auto& screen : m_screens) { + bool exists = false; + for (auto* surface : m_surfaces) { + if (surface->screen() == screen) { + exists = true; + break; + } + } + + if (!exists) needed.append(screen); + } + } + + for (auto* surface : gone) + delete surface; + + for (const auto& screen : needed) + addSurface(screen); +} + +void WallpaperService::registryGlobal( + void* data, + struct wl_registry* registry, + uint32_t name, + const char* interface, + uint32_t version) { + auto* service = static_cast(data); + + void* proxy = nullptr; + + if (strcmp(interface, "zwlr_layer_shell_v1") == 0) { + proxy = new QtWayland::zwlr_layer_shell_v1( + registry, int(name), int(version)); + } else if (strcmp(interface, "wp_viewporter") == 0) { + proxy = new QtWayland::wp_viewporter(registry, int(name), int(version)); + } else if (strcmp(interface, "wp_fractional_scale_manager_v1") == 0) { + proxy = new QtWayland::wp_fractional_scale_manager_v1( + registry, int(name), int(version)); + } else { + return; + } + + service->storeGlobal(interface, proxy); + + service->queueAll(); +} + +void WallpaperService::storeGlobal(const char* interface, void* proxy) { + std::lock_guard lock(m_mutex); + + if (strcmp(interface, "zwlr_layer_shell_v1") == 0) + m_layerShell = static_cast(proxy); + else if (strcmp(interface, "wp_viewporter") == 0) + m_viewporter = static_cast(proxy); + else if (strcmp(interface, "wp_fractional_scale_manager_v1") == 0) + m_scaleManager = + static_cast(proxy); +} + +void WallpaperService::bindGlobals() { + m_registry = wl_display_get_registry(m_display); + + static const struct wl_registry_listener listener = { + registryGlobal, + nullptr, + }; + + wl_registry_add_listener(m_registry, &listener, this); +} + +bool WallpaperService::setupGl() { + const char* extensions = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS); + if (extensions && strstr(extensions, "EGL_EXT_platform_wayland")) { + m_eglDisplay = + eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_EXT, m_display, nullptr); + } else { + m_eglDisplay = + eglGetDisplay(reinterpret_cast(m_display)); + } + + if (m_eglDisplay == EGL_NO_DISPLAY) { + qWarning() << "WallpaperService: no EGL display"; + return false; + } + + EGLint major = 0, minor = 0; + if (!eglInitialize(m_eglDisplay, &major, &minor)) { + qWarning() << "WallpaperService: eglInitialize failed"; + return false; + } + + const EGLint configAttributes[] = { + EGL_SURFACE_TYPE, + EGL_WINDOW_BIT, + EGL_RENDERABLE_TYPE, + EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, + 8, + EGL_GREEN_SIZE, + 8, + EGL_BLUE_SIZE, + 8, + EGL_ALPHA_SIZE, + 8, + EGL_NONE}; + + EGLint count = 0; + if (!eglChooseConfig(m_eglDisplay, configAttributes, &m_config, 1, &count) || + count == 0) { + qWarning() << "WallpaperService: no EGL config for window surfaces"; + return false; + } + + const EGLint contextAttributes[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE}; + m_context = eglCreateContext( + m_eglDisplay, m_config, EGL_NO_CONTEXT, contextAttributes); + if (m_context == EGL_NO_CONTEXT) { + qWarning() << "WallpaperService: eglCreateContext failed"; + return false; + } + + return true; +} + +uint32_t WallpaperService::createProgram() { + uint32_t vertex = glCreateShader(GL_VERTEX_SHADER); + glShaderSource(vertex, 1, &kVertexShader, nullptr); + glCompileShader(vertex); + logShader(vertex, false); + + uint32_t fragment = glCreateShader(GL_FRAGMENT_SHADER); + glShaderSource(fragment, 1, &kFragmentShader, nullptr); + glCompileShader(fragment); + logShader(fragment, false); + + uint32_t program = glCreateProgram(); + glAttachShader(program, vertex); + glAttachShader(program, fragment); + glLinkProgram(program); + logShader(program, true); + + if (glGetUniformLocation(program, "size") < 0 || + glGetUniformLocation(program, "crop") < 0) { + qWarning() << "WallpaperService: the program does not have the " + "uniforms it needs"; + glDeleteProgram(program); + return 0; + } + + m_sizeUniform = glGetUniformLocation(program, "size"); + m_cropUniform = glGetUniformLocation(program, "crop"); + + glDeleteShader(vertex); + glDeleteShader(fragment); + return program; +} + +void WallpaperService::uploadImage() { + if (m_image.isNull()) return; + + if (m_texture == 0) { + glGenTextures(1, &m_texture); + glBindTexture(GL_TEXTURE_2D, m_texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + } else { + glBindTexture(GL_TEXTURE_2D, m_texture); + } + + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_RGBA8, + m_image.width(), + m_image.height(), + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + m_image.constBits()); +} + +bool WallpaperService::decode(const QString& path) { + if (path.isEmpty()) return false; + + QImageReader reader(path); + reader.setAutoTransform(true); + + QSize size = reader.size(); + if (size.isValid()) m_sourceSize = size; + + if (!m_decodeSize.isEmpty() && !m_sourceSize.isEmpty()) + reader.setScaledSize( + m_sourceSize.scaled(m_decodeSize, Qt::KeepAspectRatioByExpanding)); + + QImage image = reader.read(); + if (image.isNull()) { + qWarning() << "WallpaperService: could not read the image" << path; + return false; + } + + m_image = image.convertToFormat(QImage::Format_RGBA8888); + return !m_image.isNull(); +} + +bool WallpaperService::ensureDecodeSize() { + QSize largest; + QString path; + + { + std::lock_guard lock(m_mutex); + + largest = largestBufferSize(); + if (largest.isEmpty()) return false; + + if (!m_forceDecode && !m_decodeSize.isEmpty() && + qint64(largest.width()) * largest.height() <= + qint64(m_decodeSize.width()) * m_decodeSize.height()) + return false; + + QSize target = + m_sourceSize.isEmpty() + ? largest + : m_sourceSize.scaled(largest, Qt::KeepAspectRatioByExpanding); + + if (!m_forceDecode && !m_image.isNull() && target == m_image.size()) + return false; + + m_forceDecode = false; + m_decodeSize = largest; + path = m_path; + } + + if (!decode(path)) return false; + + { + std::lock_guard lock(m_mutex); + m_imageChanged = true; + } + + return true; +} + +QSize WallpaperService::largestBufferSize() { + QSize largest; + + for (auto* surface : m_surfaces) { + QSize buffer = surface->bufferSize(); + if (buffer.isEmpty()) continue; + + if (largest.isEmpty() || qint64(buffer.width()) * buffer.height() > + qint64(largest.width()) * largest.height()) + largest = buffer; + } + + return largest; +} + +QRectF WallpaperService::sourceRect( + const QVariantMap& crops, const QString& screen, const QSize& size) const { + QVariantMap crop = cropFor(crops, screen); + + qreal zoom = crop.value("zoom", 1.0).toDouble(); + if (zoom <= 0.0) + zoom = 1.0; + else if (zoom > 5.0) + zoom = 5.0; + + QRectF rect( + crop.value("x", 0.0).toDouble(), + crop.value("y", 0.0).toDouble(), + crop.value("width", 1.0).toDouble() / zoom, + crop.value("height", 1.0).toDouble() / zoom); + + if (size.isEmpty() || rect.isEmpty() || m_image.isNull()) return rect; + + qreal imageWidth = qreal(m_image.width()); + qreal imageHeight = qreal(m_image.height()); + + QRectF pixels( + rect.x() * imageWidth, + rect.y() * imageHeight, + rect.width() * imageWidth, + rect.height() * imageHeight); + + qreal targetRatio = qreal(size.width()) / qreal(size.height()); + qreal rectRatio = pixels.width() / pixels.height(); + + if (rectRatio > targetRatio) { + qreal newWidth = pixels.height() * targetRatio; + pixels.setX(pixels.x() + (pixels.width() - newWidth) / 2.0); + pixels.setWidth(newWidth); + } else if (rectRatio < targetRatio) { + qreal newHeight = pixels.width() / targetRatio; + pixels.setY(pixels.y() + (pixels.height() - newHeight) / 2.0); + pixels.setHeight(newHeight); + } + + return QRectF( + pixels.x() / imageWidth, + pixels.y() / imageHeight, + pixels.width() / imageWidth, + pixels.height() / imageHeight); +} + +void WallpaperService::drawSurface(WallpaperSurface* surface) { + EGLSurface target = surface->eglSurface(); + if (m_context == EGL_NO_CONTEXT || target == EGL_NO_SURFACE || + surface->bufferSize().isEmpty()) + return; + + if (!eglMakeCurrent(m_eglDisplay, target, target, m_context)) { + qWarning() << "WallpaperService: the context cannot be made current"; + return; + } + + QVariantMap crops; + QSize size; + bool upload = false; + + { + std::lock_guard lock(m_mutex); + + crops = m_crops; + size = surface->bufferSize(); + + upload = m_imageChanged; + m_imageChanged = false; + } + + if (size.isEmpty()) return; + + if (m_program == 0) m_program = createProgram(); + if (m_texture == 0 || upload) uploadImage(); + + if (m_program == 0 || m_texture == 0 || m_image.isNull()) return; + + glUseProgram(m_program); + glBindTexture(GL_TEXTURE_2D, m_texture); + + glUniform2f( + GLint(m_sizeUniform), GLfloat(size.width()), GLfloat(size.height())); + + QRectF rect = sourceRect(crops, surface->screen(), size); + glUniform4f( + GLint(m_cropUniform), + GLfloat(rect.x()), + GLfloat(rect.y()), + GLfloat(rect.width()), + GLfloat(rect.height())); + + glViewport(0, 0, size.width(), size.height()); + glDrawArrays(GL_TRIANGLES, 0, 3); + + if (!eglSwapBuffers(m_eglDisplay, target)) + qWarning() << "WallpaperService: eglSwapBuffers failed"; +} + +void WallpaperService::addSurface(const QString& screen) { + if (!m_layerShell || !m_viewporter || !m_scaleManager) return; + + QScreen* qscreen = nullptr; + for (auto* candidate : QGuiApplication::screens()) { + if (candidate->name() == screen) { + qscreen = candidate; + break; + } + } + + if (!qscreen) { + qWarning() << "WallpaperService:" << screen << "is not a screen"; + return; + } + + auto* waylandScreen = + dynamic_cast(qscreen->handle()); + if (!waylandScreen || !waylandScreen->output()) { + qWarning() << "WallpaperService:" << screen + << "is not a Wayland screen"; + return; + } + + wl_surface* wlSurface = wl_compositor_create_surface(m_compositor); + if (!wlSurface) return; + + auto* surface = + new WallpaperSurface(this, wlSurface, waylandScreen->output(), screen); + + { + std::lock_guard lock(m_mutex); + m_surfaces.append(surface); + } +} + +} // namespace ZShell::internal diff --git a/Plugins/ZShell/Internal/wallpaperservice.hpp b/Plugins/ZShell/Internal/wallpaperservice.hpp new file mode 100644 index 0000000..a68d84d --- /dev/null +++ b/Plugins/ZShell/Internal/wallpaperservice.hpp @@ -0,0 +1,147 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "wallpapersurface.hpp" + +namespace ZShell::internal { + +class WallpaperSurface; + +class WallpaperService : public QObject { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED) + Q_PROPERTY(QVariantMap crops READ crops WRITE setCrops NOTIFY cropsChanged) + + public: + explicit WallpaperService(QObject* parent = nullptr); + ~WallpaperService() override; + + [[nodiscard]] QString path() const; + void setPath(const QString& path); + + void addScreen(const QString& screen); + void removeScreen(const QString& screen); + + [[nodiscard]] const QVariantMap& crops() const; + void setCrops(const QVariantMap& crops); + + Q_INVOKABLE void redraw(); + + Q_INVOKABLE void setCrop(const QString& screen, const QVariantMap& crop); + + signals: + void pathChanged(); + void cropsChanged(); + + private: + friend class WallpaperSurface; + + void bindGlobals(); + static void registryGlobal( + void* data, + struct wl_registry* registry, + uint32_t name, + const char* interface, + uint32_t version); + + void storeGlobal(const char* interface, void* proxy); + + [[nodiscard]] bool setupGl(); + [[nodiscard]] uint32_t createProgram(); + + void uploadImage(); + + bool decode(const QString& path); + + bool ensureDecodeSize(); + + [[nodiscard]] QSize largestBufferSize(); + + void queue(WallpaperSurface* surface); + void queueAll(); + + void startDrawing(); + void loop(); + + void present(); + + void drawSurface(WallpaperSurface* surface); + + void wake(); + + [[nodiscard]] wl_event_queue* eventQueue() const { return m_queue; } + + [[nodiscard]] QRectF sourceRect( + const QVariantMap& crops, + const QString& screen, + const QSize& size) const; + + void start(); + + void addSurfaces(); + void addSurface(const QString& screen); + + QString m_path; + QStringList m_screens; + QVariantMap m_crops; + + QImage m_image; + + QSize m_sourceSize; + QSize m_decodeSize; + + wl_display* m_display = nullptr; + wl_registry* m_registry = nullptr; + wl_compositor* m_compositor = nullptr; + QtWayland::zwlr_layer_shell_v1* m_layerShell = nullptr; + QtWayland::wp_viewporter* m_viewporter = nullptr; + QtWayland::wp_fractional_scale_manager_v1* m_scaleManager = nullptr; + + EGLDisplay m_eglDisplay = EGL_NO_DISPLAY; + EGLConfig m_config = nullptr; + EGLContext m_context = EGL_NO_CONTEXT; + uint32_t m_texture = 0; + uint32_t m_program = 0; + int m_sizeUniform = -1; + int m_cropUniform = -1; + + QList m_surfaces; + + std::thread m_thread; + mutable std::recursive_mutex m_mutex; + std::vector m_pending; + wl_event_queue* m_queue = nullptr; + + int m_wake[2] = {-1, -1}; + + bool m_stop = false; + bool m_drawing = false; + + bool started = false; + + bool m_forceDecode = false; + bool m_imageChanged = false; +}; + +} // namespace ZShell::internal diff --git a/Plugins/ZShell/Internal/wallpapersurface.cpp b/Plugins/ZShell/Internal/wallpapersurface.cpp new file mode 100644 index 0000000..8fe99e6 --- /dev/null +++ b/Plugins/ZShell/Internal/wallpapersurface.cpp @@ -0,0 +1,147 @@ +#include "wallpapersurface.hpp" + +#include + +#include + +#include "wallpaperservice.hpp" + +namespace ZShell::internal { + +const QString kNamespace = "ZShell-Wallpaper"; + +void setQueue(void* proxy, struct wl_event_queue* queue) { + wl_proxy_set_queue(static_cast(proxy), queue); +} + +WallpaperSurface::WallpaperSurface( + WallpaperService* service, + wl_surface* surface, + wl_output* output, + const QString& screen) + : m_service(service), m_screen(screen), m_surface(surface) { + if (!surface) { + qWarning() << "WallpaperSurface: no surface for" << screen; + return; + } + + setQueue(m_surface, m_service->eventQueue()); + + auto* layer = m_service->m_layerShell->get_layer_surface( + m_surface, + output, + QtWayland::zwlr_layer_shell_v1::layer_background, + kNamespace); + setQueue(layer, m_service->eventQueue()); + QtWayland::zwlr_layer_surface_v1::init(layer); + + set_size(0, 0); + set_anchor(anchor_top | anchor_bottom | anchor_left | anchor_right); + set_exclusive_zone(-1); + set_keyboard_interactivity(keyboard_interactivity_none); + set_margin(0, 0, 0, 0); + + auto* viewport = m_service->m_viewporter->get_viewport(m_surface); + setQueue(viewport, m_service->eventQueue()); + QtWayland::wp_viewport::init(viewport); + + auto* scale = m_service->m_scaleManager->get_fractional_scale(m_surface); + setQueue(scale, m_service->eventQueue()); + QtWayland::wp_fractional_scale_v1::init(scale); + + wl_surface_commit(m_surface); +} + +WallpaperSurface::~WallpaperSurface() { + if (m_eglSurface != EGL_NO_SURFACE) + eglDestroySurface(m_service->m_eglDisplay, m_eglSurface); + + if (m_window) wl_egl_window_destroy(m_window); + + if (QtWayland::wp_viewport::isInitialized()) + QtWayland::wp_viewport::destroy(); + if (QtWayland::wp_fractional_scale_v1::isInitialized()) + QtWayland::wp_fractional_scale_v1::destroy(); + if (QtWayland::zwlr_layer_surface_v1::isInitialized()) + QtWayland::zwlr_layer_surface_v1::destroy(); + + if (m_surface) wl_surface_destroy(m_surface); +} + +void WallpaperSurface::redraw() { + m_service->queue(this); +} + +void WallpaperSurface::zwlr_layer_surface_v1_configure( + uint32_t serial, uint32_t width, uint32_t height) { + ack_configure(serial); + + if (width == 0 || height == 0) return; + + m_size = QSize(int(width), int(height)); + + if (!configured) configured = true; + + updateBuffer(); +} + +void WallpaperSurface::wp_fractional_scale_v1_preferred_scale(uint32_t scale) { + if (scale == 0) return; + + qreal newScale = qreal(scale) / 120.0; + if (qFuzzyCompare(newScale, m_scale)) return; + + m_scale = newScale; + updateBuffer(); +} + +void WallpaperSurface::zwlr_layer_surface_v1_closed() {} + +void WallpaperSurface::updateBuffer() { + if (m_size.isEmpty()) return; + + QSize buffer = QSize( + int(std::llround(m_size.width() * m_scale)), + int(std::llround(m_size.height() * m_scale))); + + if (buffer.isEmpty()) return; + + if (m_bufferSize != buffer) { + m_bufferSize = buffer; + + if (m_window) { + wl_egl_window_resize( + m_window, buffer.width(), buffer.height(), 0, 0); + } else { + m_window = + wl_egl_window_create(m_surface, buffer.width(), buffer.height()); + if (!m_window) { + qWarning() + << "WallpaperSurface: could not make a window surface for" + << m_screen; + return; + } + } + + if (m_eglSurface == EGL_NO_SURFACE) { + m_eglSurface = eglCreateWindowSurface( + m_service->m_eglDisplay, + m_service->m_config, + reinterpret_cast(m_window), + nullptr); + if (m_eglSurface == EGL_NO_SURFACE) { + qWarning() + << "WallpaperSurface: eglCreateWindowSurface failed for" + << m_screen; + return; + } + } + } + + if (QtWayland::wp_viewport::isInitialized()) + set_destination(m_size.width(), m_size.height()); + + m_service->queue(this); +} + +} // namespace ZShell::internal diff --git a/Plugins/ZShell/Internal/wallpapersurface.hpp b/Plugins/ZShell/Internal/wallpapersurface.hpp new file mode 100644 index 0000000..d534c99 --- /dev/null +++ b/Plugins/ZShell/Internal/wallpapersurface.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace ZShell::internal { + +class WallpaperService; + +class WallpaperSurface : public QtWayland::zwlr_layer_surface_v1, + public QtWayland::wp_viewport, + public QtWayland::wp_fractional_scale_v1 { + public: + WallpaperSurface( + WallpaperService* service, + wl_surface* surface, + wl_output* output, + const QString& screen); + ~WallpaperSurface() override; + + [[nodiscard]] const QString& screen() const { return m_screen; } + + [[nodiscard]] const QSize& size() const { return m_size; } + [[nodiscard]] const QSize& bufferSize() const { return m_bufferSize; } + [[nodiscard]] EGLSurface eglSurface() const { return m_eglSurface; } + + void redraw(); + + private: + void zwlr_layer_surface_v1_configure( + uint32_t serial, uint32_t width, uint32_t height) override; + void zwlr_layer_surface_v1_closed() override; + void wp_fractional_scale_v1_preferred_scale(uint32_t scale) override; + + void updateBuffer(); + + WallpaperService* m_service; + QString m_screen; + wl_surface* m_surface; + wl_egl_window* m_window = nullptr; + EGLSurface m_eglSurface = EGL_NO_SURFACE; + + QSize m_size; + QSize m_bufferSize; + qreal m_scale = 1.0; + + bool configured = false; +}; + +} // namespace ZShell::internal diff --git a/Plugins/cmake/wayland-protocol.cmake b/Plugins/cmake/wayland-protocol.cmake new file mode 100644 index 0000000..76b6989 --- /dev/null +++ b/Plugins/cmake/wayland-protocol.cmake @@ -0,0 +1,58 @@ +find_program(QTWAYLANDSCANNER_EXECUTABLE qtwaylandscanner REQUIRED) +find_program(WAYLAND_SCANNER_EXECUTABLE wayland-scanner REQUIRED) + +pkg_get_variable(WAYLAND_PROTOCOLS wayland-protocols pkgdatadir) +if(NOT WAYLAND_PROTOCOLS) + message(FATAL_ERROR "wayland-protocols not found") +endif() + +function(wl_proto arg_TARGET arg_PROTOCOL arg_XML_DIR) + set(out_dir "${CMAKE_BINARY_DIR}/wayland-protocols/${arg_TARGET}") + file(MAKE_DIRECTORY "${out_dir}") + + set(xml "${arg_XML_DIR}/${arg_PROTOCOL}.xml") + + add_custom_command( + OUTPUT "${out_dir}/wayland-${arg_PROTOCOL}-client-protocol.h" + "${out_dir}/wayland-${arg_PROTOCOL}.c" + COMMAND ${WAYLAND_SCANNER_EXECUTABLE} client-header "${xml}" + "${out_dir}/wayland-${arg_PROTOCOL}-client-protocol.h" + COMMAND ${WAYLAND_SCANNER_EXECUTABLE} private-code "${xml}" + "${out_dir}/wayland-${arg_PROTOCOL}.c" + DEPENDS "${xml}" + COMMENT "Generating ${arg_PROTOCOL} protocol files" + ) + + add_custom_command( + OUTPUT "${out_dir}/qwayland-${arg_PROTOCOL}.h" + "${out_dir}/qwayland-${arg_PROTOCOL}.cpp" + COMMAND ${QTWAYLANDSCANNER_EXECUTABLE} client-header "${xml}" + > "${out_dir}/qwayland-${arg_PROTOCOL}.h" + COMMAND ${QTWAYLANDSCANNER_EXECUTABLE} client-code "${xml}" + > "${out_dir}/qwayland-${arg_PROTOCOL}.cpp" + DEPENDS "${xml}" "${out_dir}/wayland-${arg_PROTOCOL}-client-protocol.h" + COMMENT "Generating ${arg_PROTOCOL} client wrapper" + ) + + add_library( + ${arg_TARGET} + STATIC + "${out_dir}/wayland-${arg_PROTOCOL}.c" + "${out_dir}/qwayland-${arg_PROTOCOL}.cpp" + ) + set_source_files_properties( + "${out_dir}/qwayland-${arg_PROTOCOL}.cpp" + PROPERTIES COMPILE_OPTIONS + "-Wno-old-style-cast;-Wno-sign-conversion;-Wno-conversion;-Wno-shadow" + ) + set_target_properties(${arg_TARGET} PROPERTIES POSITION_INDEPENDENT_CODE ON) + target_include_directories(${arg_TARGET} PUBLIC "${out_dir}") + target_link_libraries( + ${arg_TARGET} + PUBLIC + Qt::Core + Qt::WaylandClient + Qt::WaylandClientPrivate + PkgConfig::WAYLAND_CLIENT + ) +endfunction() diff --git a/shell.qml b/shell.qml index 75bde5a..57f6e6b 100644 --- a/shell.qml +++ b/shell.qml @@ -3,7 +3,6 @@ // @ pragma DefaultEnv QSG_RHI_BACKEND=vulkan //@ pragma DefaultEnv QSG_NO_VSYNC=1 //@ pragma DefaultEnv QS_NO_RELOAD_POPUP=1 -//@ pragma DefaultEnv QT_SCALE_FACTOR_ROUNDING_POLICY=Round //@ pragma DefaultEnv QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000 //@ pragma DropExpensiveFonts import Quickshell @@ -25,9 +24,6 @@ ShellRoot { Drawers { } - Wallpaper { - } - Lock { id: lock } @@ -48,4 +44,7 @@ ShellRoot { component: BatteryService { } } + + ServiceLoader { + } }