From 2034f3a8db1c84061f984a341ecb0c13e57d8fd0 Mon Sep 17 00:00:00 2001 From: zach Date: Mon, 15 Jun 2026 15:52:55 +0200 Subject: [PATCH] fixed applying crop being overwritten by multiple write calls, added wallpaper fade anim --- Helpers/Wallpapers.qml | 27 +-- .../Settings/Controls/WallpaperCropper.qml | 4 +- Modules/Wallpaper/WallBackground.qml | 138 +++++++------ Plugins/ZShell/Internal/wallpaperimage.cpp | 182 ++++++++++-------- Plugins/ZShell/Internal/wallpaperimage.hpp | 44 +++++ 5 files changed, 245 insertions(+), 150 deletions(-) diff --git a/Helpers/Wallpapers.qml b/Helpers/Wallpapers.qml index c2609ad..e31ba1d 100644 --- a/Helpers/Wallpapers.qml +++ b/Helpers/Wallpapers.qml @@ -32,33 +32,26 @@ Searcher { showPreview = true; } - function setCrop(screen: string, rect: rect, scaledRect: rect, zoom: real): void { - let updated = Object.assign({}, root.crops); - + function setCrop(screen: string, rect: rect, zoom: real): void { if (zoom <= 0) zoom = 1.0; else if (zoom > 5.0) zoom = 5.0; - updated[screen] = { + root.crops[screen] = { x: rect.x, y: rect.y, width: rect.width, height: rect.height, - scaledX: scaledRect.x, - scaledY: scaledRect.y, - scaledWidth: scaledRect.width, - scaledHeight: scaledRect.height, zoom: zoom }; - - root.crops = updated; + // root.crops = updated; } function setWallpaper(path: string): void { actualCurrent = path; WallpaperPath.currentWallpaperPath = path; - Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), Qt.rect(0, 0, 0, 0), 1.0)); + Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), 1.0)); Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]); if (Config.general.color.schemeGeneration) Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]); @@ -91,7 +84,7 @@ Searcher { path: `${Paths.state}/wallpaper-crops.json` watchChanges: true - onAdapterUpdated: writeAdapter() + onAdapterUpdated: cropWriteDelay.restart() onFileChanged: reload() JsonAdapter { @@ -101,6 +94,16 @@ Searcher { } } + Timer { + id: cropWriteDelay + + interval: 100 + repeat: false + running: false + + onTriggered: monitorCrops.writeAdapter() + } + FileSystemModel { id: wallpapers diff --git a/Modules/Settings/Controls/WallpaperCropper.qml b/Modules/Settings/Controls/WallpaperCropper.qml index 20b92d0..3e90864 100644 --- a/Modules/Settings/Controls/WallpaperCropper.qml +++ b/Modules/Settings/Controls/WallpaperCropper.qml @@ -165,7 +165,7 @@ Item { anchors.top: parent.top asynchronous: true fillMode: Image.PreserveAspectFit - // retainWhileLoading: true + retainWhileLoading: true source: Wallpapers.current sourceSize.height: parent.height sourceSize.width: parent.width @@ -200,7 +200,7 @@ Item { Loader { id: cropRectLoader - active: scaledImg.paintedWidth > 0 && scaledImg.status == Image.Ready + active: scaledImg.paintedWidth > 0 sourceComponent: Component { CustomRect { diff --git a/Modules/Wallpaper/WallBackground.qml b/Modules/Wallpaper/WallBackground.qml index 1c8656a..abe2c35 100644 --- a/Modules/Wallpaper/WallBackground.qml +++ b/Modules/Wallpaper/WallBackground.qml @@ -11,82 +11,98 @@ import ZShell.Internal Item { id: root + property bool completed + property real cropHeight: displayData.height ?? 1.0 + property real cropWidth: displayData.width ?? 1.0 + property real cropX: displayData.x ?? 0.0 + property real cropY: displayData.y ?? 0.0 + property WallpaperImage current + 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 property string source: Wallpapers.current - function refreshData(): void { - Hyprland.refreshMonitors(); - let scale = Hyprland.monitorFor(root.screen).scale; - if (scale <= 0) - scale = 1.0; // Fallback to avoid zeroes on initialization - - if (root.screen.width > 0 && root.screen.height > 0) { - img.screenResolution = Qt.size(root.screen.width * scale, root.screen.height * scale); - } - - const displayData = Wallpapers.getCrop(root.screen.name); - - if (displayData) { - img.cropX = displayData.x !== undefined ? displayData.x : 0.0; - img.cropY = displayData.y !== undefined ? displayData.y : 0.0; - img.cropWidth = (displayData.width !== undefined && displayData.width > 0) ? displayData.width : 1.0; - img.cropHeight = (displayData.height !== undefined && displayData.height > 0) ? displayData.height : 1.0; - } - } - anchors.fill: parent - Component.onCompleted: root.refreshData() + Component.onCompleted: { + Hyprland.refreshMonitors(); - Connections { - function onHeightChanged() { - root.refreshData(); - } - - function onWidthChanged() { - root.refreshData(); - } - - target: root.screen + if (source) + Qt.callLater(() => { + current = imgComp.createObject(this, { + source + }); + completed = true; + }); + } + onSourceChanged: { + if (!source) + current = null; + else + current = imgComp.createObject(this, { + source: source + }); } - WallpaperImage { - id: img + Component { + id: imgComp - anchors.fill: parent - source: root.source + WallpaperImage { + id: img - Behavior on cropHeight { - Anim { - } - } - Behavior on cropWidth { - Anim { - } - } - Behavior on cropX { - Anim { - } - } - Behavior on cropY { - Anim { - } - } - Behavior on zoom { - Anim { - } - } + anchors.fill: parent + cropHeight: root.cropHeight + cropWidth: root.cropWidth + cropX: root.cropX + cropY: root.cropY + opacity: 0 + screenResolution: root.screenResolution + source: root.source - Connections { - function onAdapterUpdated(): void { - root.refreshData(); + Behavior on cropHeight { + Anim { + id: heightAnim + } + } + Behavior on cropWidth { + Anim { + id: widthAnim + } + } + Behavior on cropX { + Anim { + id: xAnim + } + } + Behavior on cropY { + Anim { + id: yAnim + } + } + Anim on opacity { + id: anim + + from: 0 + running: false + to: 1 + type: Anim.SlowEffects } - function onLoaded(): void { - root.refreshData(); + onStatusChanged: { + if (status === Image.Ready) { + anim.start(); + } } - target: Wallpapers.monitorCrops + Timer { + id: destroyTimer + + interval: anim.duration * 2 + running: root.current !== img && root.current?.status === Image.Ready + + onTriggered: Qt.callLater(() => img.destroy()) + } } } } diff --git a/Plugins/ZShell/Internal/wallpaperimage.cpp b/Plugins/ZShell/Internal/wallpaperimage.cpp index ebe27e2..c48aab4 100644 --- a/Plugins/ZShell/Internal/wallpaperimage.cpp +++ b/Plugins/ZShell/Internal/wallpaperimage.cpp @@ -7,6 +7,7 @@ #include #include #include +#include namespace ZShell::internal { @@ -21,10 +22,20 @@ WallpaperImage::~WallpaperImage() { if (m_texture) delete m_texture; } +void WallpaperImage::setStatus(const Status s) { + if (m_status == s) + return; + + m_status = s; + emit statusChanged(); +} + void WallpaperImage::setSource(const QUrl &source) { if (m_source == source) return; m_source = source; emit sourceChanged(); + + setStatus(Loading); loadImage(); } @@ -57,28 +68,27 @@ void WallpaperImage::setCropY(qreal y) { } void WallpaperImage::setCropWidth(qreal w) { - if (w <= 0.0) w = 1.0; - if (qFuzzyCompare(m_cropWidth, w)) return; - m_cropWidth = w; - emit cropWidthChanged(); - update(); + if (w <= 0.0) w = 1.0; + if (qFuzzyCompare(m_cropWidth, w)) return; + m_cropWidth = w; + emit cropWidthChanged(); + update(); } void WallpaperImage::setCropHeight(qreal h) { - if (h <= 0.0) h = 1.0; - if (qFuzzyCompare(m_cropHeight, h)) return; - m_cropHeight = h; - emit cropHeightChanged(); - update(); + if (h <= 0.0) h = 1.0; + if (qFuzzyCompare(m_cropHeight, h)) return; + m_cropHeight = h; + emit cropHeightChanged(); + update(); } QString WallpaperImage::getCacheFilePath() const { if (m_source.isEmpty() || m_screenResolution.isEmpty()) return QString(); - QString cachePath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/zshell/imagecache"; + QString cachePath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/zshell/imagecache"; QDir().mkpath(cachePath); - // Hash the source URL + resolution QString id = m_source.toString() + "_" + QString::number(m_screenResolution.width()) + "x" + QString::number(m_screenResolution.height()); QByteArray hash = QCryptographicHash::hash(id.toUtf8(), QCryptographicHash::Md5).toHex(); @@ -86,52 +96,53 @@ QString WallpaperImage::getCacheFilePath() const { } void WallpaperImage::loadImage() { - if (m_source.isEmpty()) return; + if (m_source.isEmpty()) { + setStatus(Null); + return; + } - QString cacheFile = getCacheFilePath(); - QString sourceFile = m_source.isLocalFile() ? m_source.toLocalFile() : m_source.toString(); - - // Qt resource path correction if passed as a standard URL string - if (sourceFile.startsWith("qrc:/")) { - sourceFile = sourceFile.mid(3); // Converts "qrc:/" to ":/" - } - - QSize targetRes = m_screenResolution; + QString cacheFile = getCacheFilePath(); + QString sourceFile = m_source.isLocalFile() ? m_source.toLocalFile() : m_source.toString(); - // Run off the main thread to avoid blocking the UI - QFuture future = QtConcurrent::run([sourceFile, cacheFile, targetRes]() -> QImage { - if (!targetRes.isEmpty() && !cacheFile.isEmpty() && QFileInfo::exists(cacheFile)) { - QImage cached(cacheFile); - if (!cached.isNull()) return cached; - } + if (sourceFile.startsWith("qrc:/")) { + sourceFile = sourceFile.mid(3); + } - QImage original(sourceFile); - if (original.isNull()) return QImage(); + QSize targetRes = m_screenResolution; - if (targetRes.isEmpty()) { - // Screen resolution not set yet by QML, return the unscaled original for now to prevent a black screen - return original; - } + QFuture future = QtConcurrent::run([sourceFile, cacheFile, targetRes]() -> QImage { + if (!targetRes.isEmpty() && !cacheFile.isEmpty() && QFileInfo::exists(cacheFile)) { + QImage cached(cacheFile); + if (!cached.isNull()) return cached; + } - // Check if original is strictly larger than screen resolution - if (original.width() > targetRes.width() || original.height() > targetRes.height()) { - QImage scaled = original.scaled(targetRes, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); - if (!cacheFile.isEmpty()) scaled.save(cacheFile, "PNG"); - return scaled; - } + QImage original(sourceFile); + if (original.isNull()) return QImage(); - // Otherwise just cache and return the original - if (!cacheFile.isEmpty()) original.save(cacheFile, "PNG"); - return original; - }); + if (targetRes.isEmpty()) { + return original; + } - m_imageWatcher.setFuture(future); + if (original.width() > targetRes.width() || original.height() > targetRes.height()) { + QImage scaled = original.scaled(targetRes, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + if (!cacheFile.isEmpty()) scaled.save(cacheFile, "PNG"); + return scaled; + } + + if (!cacheFile.isEmpty()) original.save(cacheFile, "PNG"); + return original; + }); + + m_imageWatcher.setFuture(future); } void WallpaperImage::handleImageLoaded() { m_image = m_imageWatcher.result(); + + setStatus(m_image.isNull() ? Error : Ready); + m_textureDirty = true; - update(); // Request redraw + update(); } QSGNode *WallpaperImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) { @@ -148,7 +159,7 @@ QSGNode *WallpaperImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData * if (m_textureDirty) { if (m_texture) delete m_texture; - m_texture = window()->createTextureFromImage(m_image, QQuickWindow::TextureHasAlphaChannel); + m_texture = window()->createTextureFromImage(m_image, QQuickWindow::TextureHasAlphaChannel); m_textureDirty = false; } @@ -157,40 +168,61 @@ QSGNode *WallpaperImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData * node->setRect(boundingRect()); node->setFiltering(QSGTexture::Linear); - qreal cW = m_cropWidth / m_zoom; - qreal cH = m_cropHeight / m_zoom; + qreal cW = m_cropWidth / m_zoom; + qreal cH = m_cropHeight / m_zoom; - QRectF reqRect( - m_cropX * m_texture->textureSize().width(), - m_cropY * m_texture->textureSize().height(), - cW * m_texture->textureSize().width(), - cH * m_texture->textureSize().height() - ); + QRectF reqRect( + m_cropX * m_texture->textureSize().width(), + m_cropY * m_texture->textureSize().height(), + cW * m_texture->textureSize().width(), + cH * m_texture->textureSize().height() + ); - QRectF bounds = boundingRect(); - if (bounds.isEmpty() || reqRect.isEmpty()) return node; + QRectF bounds = boundingRect(); + if (bounds.isEmpty() || reqRect.isEmpty()) return node; - qreal targetRatio = bounds.width() / bounds.height(); - qreal reqRatio = reqRect.width() / reqRect.height(); + qreal targetRatio = bounds.width() / bounds.height(); + qreal reqRatio = reqRect.width() / reqRect.height(); - QRectF sourceRect = reqRect; + QRectF sourceRect = reqRect; - // Force 'PreserveAspectCrop' behavior on the requested region - if (reqRatio > targetRatio) { - // Requested region is too wide, center-crop the sides - qreal newWidth = reqRect.height() * targetRatio; - qreal xOffset = (reqRect.width() - newWidth) / 2.0; - sourceRect.setX(reqRect.x() + xOffset); - sourceRect.setWidth(newWidth); - } else if (reqRatio < targetRatio) { - // Requested region is too tall, center-crop the top/bottom - qreal newHeight = reqRect.width() / targetRatio; - qreal yOffset = (reqRect.height() - newHeight) / 2.0; - sourceRect.setY(reqRect.y() + yOffset); - sourceRect.setHeight(newHeight); - } + if (reqRatio > targetRatio) { + qreal newWidth = reqRect.height() * targetRatio; + qreal xOffset = (reqRect.width() - newWidth) / 2.0; + sourceRect.setX(reqRect.x() + xOffset); + sourceRect.setWidth(newWidth); + } else if (reqRatio < targetRatio) { + qreal newHeight = reqRect.width() / targetRatio; + qreal yOffset = (reqRect.height() - newHeight) / 2.0; + sourceRect.setY(reqRect.y() + yOffset); + sourceRect.setHeight(newHeight); + } - node->setSourceRect(sourceRect); + QRectF normalizedActual( + sourceRect.x() / m_texture->textureSize().width(), + sourceRect.y() / m_texture->textureSize().height(), + sourceRect.width() / m_texture->textureSize().width(), + sourceRect.height() / m_texture->textureSize().height() + ); + + bool changed = false; + + auto updateIfChanged = [&](qreal &dst, qreal value) { + if (!qFuzzyCompare(dst, value)) { + dst = value; + changed = true; + } + }; + + updateIfChanged(m_actualCropX, normalizedActual.x()); + updateIfChanged(m_actualCropY, normalizedActual.y()); + updateIfChanged(m_actualCropWidth, normalizedActual.width()); + updateIfChanged(m_actualCropHeight, normalizedActual.height()); + + if (changed) + emit actualCropChanged(); + + node->setSourceRect(sourceRect); } return node; diff --git a/Plugins/ZShell/Internal/wallpaperimage.hpp b/Plugins/ZShell/Internal/wallpaperimage.hpp index 04cb4ed..3c00fe4 100644 --- a/Plugins/ZShell/Internal/wallpaperimage.hpp +++ b/Plugins/ZShell/Internal/wallpaperimage.hpp @@ -6,6 +6,7 @@ #include #include #include +#include namespace ZShell::internal { @@ -15,6 +16,12 @@ QML_NAMED_ELEMENT(WallpaperImage) Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged) Q_PROPERTY(QSize screenResolution READ screenResolution WRITE setScreenResolution NOTIFY screenResolutionChanged) Q_PROPERTY(qreal zoom READ zoom WRITE setZoom NOTIFY zoomChanged) +Q_PROPERTY(Status status READ status NOTIFY statusChanged) + +Q_PROPERTY(qreal actualCropX READ actualCropX NOTIFY actualCropChanged) +Q_PROPERTY(qreal actualCropY READ actualCropY NOTIFY actualCropChanged) +Q_PROPERTY(qreal actualCropWidth READ actualCropWidth NOTIFY actualCropChanged) +Q_PROPERTY(qreal actualCropHeight READ actualCropHeight NOTIFY actualCropChanged) Q_PROPERTY(qreal cropX READ cropX WRITE setCropX NOTIFY cropXChanged) Q_PROPERTY(qreal cropY READ cropY WRITE setCropY NOTIFY cropYChanged) @@ -25,6 +32,18 @@ public: explicit WallpaperImage(QQuickItem *parent = nullptr); ~WallpaperImage() override; +enum Status { + Null, + Ready, + Loading, + Error +}; +Q_ENUM(Status) + +Status status() const { + return m_status; +} + QUrl source() const { return m_source; } @@ -40,6 +59,22 @@ qreal zoom() const { } void setZoom(qreal zoom); +qreal actualCropX() const { + return m_actualCropX; +} + +qreal actualCropY() const { + return m_actualCropY; +} + +qreal actualCropWidth() const { + return m_actualCropWidth; +} + +qreal actualCropHeight() const { + return m_actualCropHeight; +} + qreal cropX() const { return m_cropX; } @@ -67,20 +102,29 @@ signals: void sourceChanged(); void screenResolutionChanged(); void zoomChanged(); +void actualCropChanged(); void cropXChanged(); void cropYChanged(); void cropWidthChanged(); void cropHeightChanged(); +void statusChanged(); private: void loadImage(); void handleImageLoaded(); QString getCacheFilePath() const; +void setStatus(const Status s); +Status m_status = Null; QUrl m_source; QSize m_screenResolution; qreal m_zoom = 1.0; +qreal m_actualCropX = 0.0; +qreal m_actualCropY = 0.0; +qreal m_actualCropWidth = 1.0; +qreal m_actualCropHeight = 1.0; + qreal m_cropX = 0.0; qreal m_cropY = 0.0; qreal m_cropWidth = 1.0;