fixed applying crop being overwritten by multiple write calls, added wallpaper fade anim
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 14s
Python / test (pull_request) Successful in 31s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m6s

This commit is contained in:
2026-06-15 15:52:55 +02:00
parent 7e38f3f428
commit 2034f3a8db
5 changed files with 245 additions and 150 deletions
+15 -12
View File
@@ -32,33 +32,26 @@ Searcher {
showPreview = true; showPreview = true;
} }
function setCrop(screen: string, rect: rect, scaledRect: rect, zoom: real): void { function setCrop(screen: string, rect: rect, zoom: real): void {
let updated = Object.assign({}, root.crops);
if (zoom <= 0) if (zoom <= 0)
zoom = 1.0; zoom = 1.0;
else if (zoom > 5.0) else if (zoom > 5.0)
zoom = 5.0; zoom = 5.0;
updated[screen] = { root.crops[screen] = {
x: rect.x, x: rect.x,
y: rect.y, y: rect.y,
width: rect.width, width: rect.width,
height: rect.height, height: rect.height,
scaledX: scaledRect.x,
scaledY: scaledRect.y,
scaledWidth: scaledRect.width,
scaledHeight: scaledRect.height,
zoom: zoom zoom: zoom
}; };
// root.crops = updated;
root.crops = updated;
} }
function setWallpaper(path: string): void { function setWallpaper(path: string): void {
actualCurrent = path; actualCurrent = path;
WallpaperPath.currentWallpaperPath = 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}`]); 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) if (Config.general.color.schemeGeneration)
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]); 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` path: `${Paths.state}/wallpaper-crops.json`
watchChanges: true watchChanges: true
onAdapterUpdated: writeAdapter() onAdapterUpdated: cropWriteDelay.restart()
onFileChanged: reload() onFileChanged: reload()
JsonAdapter { JsonAdapter {
@@ -101,6 +94,16 @@ Searcher {
} }
} }
Timer {
id: cropWriteDelay
interval: 100
repeat: false
running: false
onTriggered: monitorCrops.writeAdapter()
}
FileSystemModel { FileSystemModel {
id: wallpapers id: wallpapers
@@ -165,7 +165,7 @@ Item {
anchors.top: parent.top anchors.top: parent.top
asynchronous: true asynchronous: true
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
// retainWhileLoading: true retainWhileLoading: true
source: Wallpapers.current source: Wallpapers.current
sourceSize.height: parent.height sourceSize.height: parent.height
sourceSize.width: parent.width sourceSize.width: parent.width
@@ -200,7 +200,7 @@ Item {
Loader { Loader {
id: cropRectLoader id: cropRectLoader
active: scaledImg.paintedWidth > 0 && scaledImg.status == Image.Ready active: scaledImg.paintedWidth > 0
sourceComponent: Component { sourceComponent: Component {
CustomRect { CustomRect {
+77 -61
View File
@@ -11,82 +11,98 @@ import ZShell.Internal
Item { Item {
id: root 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 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 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 anchors.fill: parent
Component.onCompleted: root.refreshData() Component.onCompleted: {
Hyprland.refreshMonitors();
Connections { if (source)
function onHeightChanged() { Qt.callLater(() => {
root.refreshData(); current = imgComp.createObject(this, {
} source
});
function onWidthChanged() { completed = true;
root.refreshData(); });
} }
onSourceChanged: {
target: root.screen if (!source)
current = null;
else
current = imgComp.createObject(this, {
source: source
});
} }
WallpaperImage { Component {
id: img id: imgComp
anchors.fill: parent WallpaperImage {
source: root.source id: img
Behavior on cropHeight { anchors.fill: parent
Anim { cropHeight: root.cropHeight
} cropWidth: root.cropWidth
} cropX: root.cropX
Behavior on cropWidth { cropY: root.cropY
Anim { opacity: 0
} screenResolution: root.screenResolution
} source: root.source
Behavior on cropX {
Anim {
}
}
Behavior on cropY {
Anim {
}
}
Behavior on zoom {
Anim {
}
}
Connections { Behavior on cropHeight {
function onAdapterUpdated(): void { Anim {
root.refreshData(); 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 { onStatusChanged: {
root.refreshData(); 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())
}
} }
} }
} }
+107 -75
View File
@@ -7,6 +7,7 @@
#include <QtConcurrent> #include <QtConcurrent>
#include <QSGImageNode> #include <QSGImageNode>
#include <QQuickWindow> #include <QQuickWindow>
#include <set>
namespace ZShell::internal { namespace ZShell::internal {
@@ -21,10 +22,20 @@ WallpaperImage::~WallpaperImage() {
if (m_texture) delete m_texture; 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) { void WallpaperImage::setSource(const QUrl &source) {
if (m_source == source) return; if (m_source == source) return;
m_source = source; m_source = source;
emit sourceChanged(); emit sourceChanged();
setStatus(Loading);
loadImage(); loadImage();
} }
@@ -57,28 +68,27 @@ void WallpaperImage::setCropY(qreal y) {
} }
void WallpaperImage::setCropWidth(qreal w) { void WallpaperImage::setCropWidth(qreal w) {
if (w <= 0.0) w = 1.0; if (w <= 0.0) w = 1.0;
if (qFuzzyCompare(m_cropWidth, w)) return; if (qFuzzyCompare(m_cropWidth, w)) return;
m_cropWidth = w; m_cropWidth = w;
emit cropWidthChanged(); emit cropWidthChanged();
update(); update();
} }
void WallpaperImage::setCropHeight(qreal h) { void WallpaperImage::setCropHeight(qreal h) {
if (h <= 0.0) h = 1.0; if (h <= 0.0) h = 1.0;
if (qFuzzyCompare(m_cropHeight, h)) return; if (qFuzzyCompare(m_cropHeight, h)) return;
m_cropHeight = h; m_cropHeight = h;
emit cropHeightChanged(); emit cropHeightChanged();
update(); update();
} }
QString WallpaperImage::getCacheFilePath() const { QString WallpaperImage::getCacheFilePath() const {
if (m_source.isEmpty() || m_screenResolution.isEmpty()) return QString(); 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); QDir().mkpath(cachePath);
// Hash the source URL + resolution
QString id = m_source.toString() + "_" + QString::number(m_screenResolution.width()) + "x" + QString::number(m_screenResolution.height()); 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(); QByteArray hash = QCryptographicHash::hash(id.toUtf8(), QCryptographicHash::Md5).toHex();
@@ -86,52 +96,53 @@ QString WallpaperImage::getCacheFilePath() const {
} }
void WallpaperImage::loadImage() { void WallpaperImage::loadImage() {
if (m_source.isEmpty()) return; if (m_source.isEmpty()) {
setStatus(Null);
return;
}
QString cacheFile = getCacheFilePath(); QString cacheFile = getCacheFilePath();
QString sourceFile = m_source.isLocalFile() ? m_source.toLocalFile() : m_source.toString(); 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;
// Run off the main thread to avoid blocking the UI if (sourceFile.startsWith("qrc:/")) {
QFuture<QImage> future = QtConcurrent::run([sourceFile, cacheFile, targetRes]() -> QImage { sourceFile = sourceFile.mid(3);
if (!targetRes.isEmpty() && !cacheFile.isEmpty() && QFileInfo::exists(cacheFile)) { }
QImage cached(cacheFile);
if (!cached.isNull()) return cached;
}
QImage original(sourceFile); QSize targetRes = m_screenResolution;
if (original.isNull()) return QImage();
if (targetRes.isEmpty()) { QFuture<QImage> future = QtConcurrent::run([sourceFile, cacheFile, targetRes]() -> QImage {
// Screen resolution not set yet by QML, return the unscaled original for now to prevent a black screen if (!targetRes.isEmpty() && !cacheFile.isEmpty() && QFileInfo::exists(cacheFile)) {
return original; QImage cached(cacheFile);
} if (!cached.isNull()) return cached;
}
// Check if original is strictly larger than screen resolution QImage original(sourceFile);
if (original.width() > targetRes.width() || original.height() > targetRes.height()) { if (original.isNull()) return QImage();
QImage scaled = original.scaled(targetRes, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
if (!cacheFile.isEmpty()) scaled.save(cacheFile, "PNG");
return scaled;
}
// Otherwise just cache and return the original if (targetRes.isEmpty()) {
if (!cacheFile.isEmpty()) original.save(cacheFile, "PNG"); return original;
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() { void WallpaperImage::handleImageLoaded() {
m_image = m_imageWatcher.result(); m_image = m_imageWatcher.result();
setStatus(m_image.isNull() ? Error : Ready);
m_textureDirty = true; m_textureDirty = true;
update(); // Request redraw update();
} }
QSGNode *WallpaperImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) { QSGNode *WallpaperImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) {
@@ -148,7 +159,7 @@ QSGNode *WallpaperImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *
if (m_textureDirty) { if (m_textureDirty) {
if (m_texture) delete m_texture; 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; m_textureDirty = false;
} }
@@ -157,40 +168,61 @@ QSGNode *WallpaperImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *
node->setRect(boundingRect()); node->setRect(boundingRect());
node->setFiltering(QSGTexture::Linear); node->setFiltering(QSGTexture::Linear);
qreal cW = m_cropWidth / m_zoom; qreal cW = m_cropWidth / m_zoom;
qreal cH = m_cropHeight / m_zoom; qreal cH = m_cropHeight / m_zoom;
QRectF reqRect( QRectF reqRect(
m_cropX * m_texture->textureSize().width(), m_cropX * m_texture->textureSize().width(),
m_cropY * m_texture->textureSize().height(), m_cropY * m_texture->textureSize().height(),
cW * m_texture->textureSize().width(), cW * m_texture->textureSize().width(),
cH * m_texture->textureSize().height() cH * m_texture->textureSize().height()
); );
QRectF bounds = boundingRect(); QRectF bounds = boundingRect();
if (bounds.isEmpty() || reqRect.isEmpty()) return node; if (bounds.isEmpty() || reqRect.isEmpty()) return node;
qreal targetRatio = bounds.width() / bounds.height(); qreal targetRatio = bounds.width() / bounds.height();
qreal reqRatio = reqRect.width() / reqRect.height(); qreal reqRatio = reqRect.width() / reqRect.height();
QRectF sourceRect = reqRect; QRectF sourceRect = reqRect;
// Force 'PreserveAspectCrop' behavior on the requested region if (reqRatio > targetRatio) {
if (reqRatio > targetRatio) { qreal newWidth = reqRect.height() * targetRatio;
// Requested region is too wide, center-crop the sides qreal xOffset = (reqRect.width() - newWidth) / 2.0;
qreal newWidth = reqRect.height() * targetRatio; sourceRect.setX(reqRect.x() + xOffset);
qreal xOffset = (reqRect.width() - newWidth) / 2.0; sourceRect.setWidth(newWidth);
sourceRect.setX(reqRect.x() + xOffset); } else if (reqRatio < targetRatio) {
sourceRect.setWidth(newWidth); qreal newHeight = reqRect.width() / targetRatio;
} else if (reqRatio < targetRatio) { qreal yOffset = (reqRect.height() - newHeight) / 2.0;
// Requested region is too tall, center-crop the top/bottom sourceRect.setY(reqRect.y() + yOffset);
qreal newHeight = reqRect.width() / targetRatio; sourceRect.setHeight(newHeight);
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; return node;
@@ -6,6 +6,7 @@
#include <QSGTexture> #include <QSGTexture>
#include <QFutureWatcher> #include <QFutureWatcher>
#include <QtQml/qqml.h> #include <QtQml/qqml.h>
#include <qtmetamacros.h>
namespace ZShell::internal { namespace ZShell::internal {
@@ -15,6 +16,12 @@ QML_NAMED_ELEMENT(WallpaperImage)
Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged) Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged)
Q_PROPERTY(QSize screenResolution READ screenResolution WRITE setScreenResolution NOTIFY screenResolutionChanged) Q_PROPERTY(QSize screenResolution READ screenResolution WRITE setScreenResolution NOTIFY screenResolutionChanged)
Q_PROPERTY(qreal zoom READ zoom WRITE setZoom NOTIFY zoomChanged) 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 cropX READ cropX WRITE setCropX NOTIFY cropXChanged)
Q_PROPERTY(qreal cropY READ cropY WRITE setCropY NOTIFY cropYChanged) Q_PROPERTY(qreal cropY READ cropY WRITE setCropY NOTIFY cropYChanged)
@@ -25,6 +32,18 @@ public:
explicit WallpaperImage(QQuickItem *parent = nullptr); explicit WallpaperImage(QQuickItem *parent = nullptr);
~WallpaperImage() override; ~WallpaperImage() override;
enum Status {
Null,
Ready,
Loading,
Error
};
Q_ENUM(Status)
Status status() const {
return m_status;
}
QUrl source() const { QUrl source() const {
return m_source; return m_source;
} }
@@ -40,6 +59,22 @@ qreal zoom() const {
} }
void setZoom(qreal zoom); 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 { qreal cropX() const {
return m_cropX; return m_cropX;
} }
@@ -67,20 +102,29 @@ signals:
void sourceChanged(); void sourceChanged();
void screenResolutionChanged(); void screenResolutionChanged();
void zoomChanged(); void zoomChanged();
void actualCropChanged();
void cropXChanged(); void cropXChanged();
void cropYChanged(); void cropYChanged();
void cropWidthChanged(); void cropWidthChanged();
void cropHeightChanged(); void cropHeightChanged();
void statusChanged();
private: private:
void loadImage(); void loadImage();
void handleImageLoaded(); void handleImageLoaded();
QString getCacheFilePath() const; QString getCacheFilePath() const;
void setStatus(const Status s);
Status m_status = Null;
QUrl m_source; QUrl m_source;
QSize m_screenResolution; QSize m_screenResolution;
qreal m_zoom = 1.0; 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_cropX = 0.0;
qreal m_cropY = 0.0; qreal m_cropY = 0.0;
qreal m_cropWidth = 1.0; qreal m_cropWidth = 1.0;