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 {
+ }
}