From 4005e197eb5bde428bca087e7db382131366130f Mon Sep 17 00:00:00 2001 From: zach Date: Thu, 28 May 2026 14:40:47 +0200 Subject: [PATCH] better wallpaper cropping on load, cache images to disk and fix image aspect ratio from creating black bars --- Helpers/Wallpapers.qml | 4 +- .../Settings/Controls/WallpaperCropper.qml | 152 +++++++------ Modules/Wallpaper/WallBackground-old.qml | 80 ------- Modules/Wallpaper/WallBackground.qml | 67 +++--- Plugins/ZShell/Internal/CMakeLists.txt | 1 + Plugins/ZShell/Internal/arcgauge.cpp | 4 +- Plugins/ZShell/Internal/arcgauge.hpp | 4 +- Plugins/ZShell/Internal/wallpaperimage.cpp | 199 ++++++++++++++++++ Plugins/ZShell/Internal/wallpaperimage.hpp | 95 +++++++++ 9 files changed, 429 insertions(+), 177 deletions(-) delete mode 100644 Modules/Wallpaper/WallBackground-old.qml create mode 100644 Plugins/ZShell/Internal/wallpaperimage.cpp create mode 100644 Plugins/ZShell/Internal/wallpaperimage.hpp diff --git a/Helpers/Wallpapers.qml b/Helpers/Wallpapers.qml index 10c8f22..c2609ad 100644 --- a/Helpers/Wallpapers.qml +++ b/Helpers/Wallpapers.qml @@ -53,14 +53,12 @@ Searcher { }; root.crops = updated; - monitorCrops.writeAdapter(); - monitorCrops.reload(); } function setWallpaper(path: string): void { actualCurrent = path; WallpaperPath.currentWallpaperPath = path; - Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 0, 0), Qt.rect(0, 0, 0, 0), 1.0)); + Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), Qt.rect(0, 0, 0, 0), 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}`]); diff --git a/Modules/Settings/Controls/WallpaperCropper.qml b/Modules/Settings/Controls/WallpaperCropper.qml index d8a9e12..efe7cc6 100644 --- a/Modules/Settings/Controls/WallpaperCropper.qml +++ b/Modules/Settings/Controls/WallpaperCropper.qml @@ -80,12 +80,26 @@ Item { required property ShellScreen modelData function applyCrop(): void { - const croprect = cropRect.mapToItem(scaledImg, 0, 0, cropRect.width, cropRect.height); - const upscaledRect = Qt.rect((croprect.x - cropRect.imageX) / scaledImg.paintedWidth, (croprect.y - cropRect.imageY) / scaledImg.paintedHeight, croprect.width / scaledImg.paintedWidth, croprect.height / scaledImg.paintedHeight); - Wallpapers.setCrop(delegate.modelData.name, upscaledRect, croprect, cropRect.zoom); + if (!cropRectLoader.item) return; + const cropRect = cropRectLoader.item; + + // We need to calculate the exact percentage coordinates that map perfectly + // to our C++ backend, regardless of current display scaling + const cropXPercent = (cropRect.x - cropRect.imageX) / scaledImg.paintedWidth; + const cropYPercent = (cropRect.y - cropRect.imageY) / scaledImg.paintedHeight; + const cropWidthPercent = cropRect.width / scaledImg.paintedWidth; + const cropHeightPercent = cropRect.height / scaledImg.paintedHeight; + + const finalRect = Qt.rect(cropXPercent, cropYPercent, cropWidthPercent, cropHeightPercent); + + // We just pass the percentages directly to the backend + Wallpapers.setCrop(delegate.modelData.name, finalRect, finalRect, cropRect.zoom); } function zoomClipRect(zoom: real): void { + if (!cropRectLoader.item) return; + const cropRect = cropRectLoader.item; + let oldCenterX = cropRect.x + cropRect.width * 0.5; let oldCenterY = cropRect.y + cropRect.height * 0.5; @@ -128,7 +142,7 @@ Item { Layout.preferredHeight: 10 from: 1.0 to: 5.0 - value: cropRect.zoom + value: cropRectLoader.item ? cropRectLoader.item.zoom : 1.0 onMoved: { delegate.zoomClipRect(value); @@ -156,15 +170,20 @@ Item { sourceSize.width: parent.width onPaintedWidthChanged: { - if (paintedWidth > 0) { - scaledImg.displayData = Wallpapers.getCrop(delegate.modelData.name); - cropRect.zoom = Wallpapers.getCrop(delegate.modelData.name).zoom; - cropRect.restoreFromData(); + if (paintedWidth > 0 && cropRectLoader.item) { + cropRectLoader.item.restoreFromData(); + } + } + onSourceChanged: { + if (cropRectLoader.item) { + cropRectLoader.item.restoreFromData(); + } + } + onStatusChanged: { + if (scaledImg.status == Image.Ready && cropRectLoader.item) { + cropRectLoader.item.restoreFromData(); } } - onSourceChanged: cropRect.clampToBounds() - onStatusChanged: if (scaledImg.status == Image.Ready) - cropRect.clampToBounds() CustomText { id: monitorId @@ -177,72 +196,85 @@ Item { text: delegate.modelData.name } - CustomRect { - id: cropRect + Loader { + id: cropRectLoader + active: scaledImg.paintedWidth > 0 && scaledImg.status == Image.Ready + + sourceComponent: Component { + CustomRect { + id: cropRect - property real aspectRatio: delegate.modelData.width / delegate.modelData.height - readonly property real baseHeight: baseWidth / aspectRatio - readonly property real baseWidth: { - let fittedHeight = scaledImg.paintedHeight; - let fittedWidth = fittedHeight * aspectRatio; + property real aspectRatio: delegate.modelData.width / delegate.modelData.height + readonly property real baseHeight: baseWidth / aspectRatio + readonly property real baseWidth: { + let fittedHeight = scaledImg.paintedHeight; + let fittedWidth = fittedHeight * aspectRatio; - if (fittedWidth > scaledImg.paintedWidth) { - fittedWidth = scaledImg.paintedWidth; - fittedHeight = fittedWidth / aspectRatio; - } + if (fittedWidth > scaledImg.paintedWidth) { + fittedWidth = scaledImg.paintedWidth; + fittedHeight = fittedWidth / aspectRatio; + } - return fittedWidth; - } - readonly property real imageX: (scaledImg.width - scaledImg.paintedWidth) / 2 - readonly property real imageY: (scaledImg.height - scaledImg.paintedHeight) / 2 - property real imgAspectRatio: scaledImg.paintedWidth / scaledImg.paintedHeight - property real zoom: scaledImg.displayData.zoom + return fittedWidth; + } + readonly property real imageX: (scaledImg.width - scaledImg.paintedWidth) / 2 + readonly property real imageY: (scaledImg.height - scaledImg.paintedHeight) / 2 + property real imgAspectRatio: scaledImg.paintedWidth / scaledImg.paintedHeight + property real zoom: 1.0 - function centerInImage() { - x = imageX + (scaledImg.paintedWidth - width) / 2; - y = imageY + (scaledImg.paintedHeight - height) / 2; - } + function centerInImage() { + x = imageX + (scaledImg.paintedWidth - width) / 2; + y = imageY + (scaledImg.paintedHeight - height) / 2; + } - function clampToBounds() { - x = Math.max(imageX, Math.min(x, imageX + scaledImg.paintedWidth - width)); + function clampToBounds() { + x = Math.max(imageX, Math.min(x, imageX + scaledImg.paintedWidth - width)); - y = Math.max(imageY, Math.min(y, imageY + scaledImg.paintedHeight - height)); - } + y = Math.max(imageY, Math.min(y, imageY + scaledImg.paintedHeight - height)); + } - function restoreFromData() { - let data = scaledImg.displayData; + function restoreFromData() { + let data = Wallpapers.getCrop(delegate.modelData.name); - if (data && data.scaledX !== 0 || data.scaledY !== 0 || data.scaledWidth !== 0 || data.scaledHeight !== 0) { - x = data.scaledX; - y = data.scaledY; + if (data && (Math.abs(data.x) > 0.001 || Math.abs(data.y) > 0.001 || Math.abs(data.width - 1.0) > 0.001 || Math.abs(data.height - 1.0) > 0.001)) { + zoom = data.zoom > 0 ? data.zoom : 1.0; + x = imageX + (data.x * scaledImg.paintedWidth); + y = imageY + (data.y * scaledImg.paintedHeight); + + clampToBounds(); + } else { + zoom = 1.0; + centerInImage(); + } + } - clampToBounds(); - } else { - zoom = 1.0; - centerInImage(); + border.color: DynamicColors.palette.m3primary + border.width: 2 + height: baseHeight / zoom + opacity: 1 + width: baseWidth / zoom + + Behavior on opacity { + Anim { + } + } + + Component.onCompleted: { + restoreFromData(); + } + onHeightChanged: clampToBounds() + onWidthChanged: clampToBounds() } } - - border.color: DynamicColors.palette.m3primary - border.width: 2 - height: baseHeight / zoom - opacity: 1 - width: baseWidth / zoom - - Behavior on opacity { - Anim { - } - } - - Component.onCompleted: clampToBounds() - onHeightChanged: clampToBounds() - onWidthChanged: clampToBounds() } MouseArea { id: mouse function updateCrop(mouseX, mouseY) { + if (!cropRectLoader.item) return; + const cropRect = cropRectLoader.item; + let nx = mouseX - cropRect.width * 0.5; let ny = mouseY - cropRect.height * 0.5; diff --git a/Modules/Wallpaper/WallBackground-old.qml b/Modules/Wallpaper/WallBackground-old.qml deleted file mode 100644 index b8bb649..0000000 --- a/Modules/Wallpaper/WallBackground-old.qml +++ /dev/null @@ -1,80 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import qs.Components -import qs.Helpers -import qs.Config - -Item { - id: root - - property Image current: one - property string source: Wallpapers.current - - anchors.fill: parent - - Component.onCompleted: { - if (source) - Qt.callLater(() => one.update()); - } - onSourceChanged: { - if (!source) { - current = null; - } else if (current === one) { - two.update(); - } else { - one.update(); - } - } - - Img { - id: one - } - - Img { - id: two - } - - component Img: Image { - id: img - - function update(): void { - if (source === root.source) { - root.current = this; - } else { - source = root.source; - } - } - - anchors.fill: parent - asynchronous: true - fillMode: Image.PreserveAspectCrop - opacity: 0 - retainWhileLoading: true - scale: Wallpapers.showPreview ? 1 : 0.8 - sourceClipRect: Qt.rect(Config.background.sourceClipX, Config.background.sourceClipY, Config.background.sourceClipW, Config.background.sourceClipH) - - states: State { - name: "visible" - when: root.current === img - - PropertyChanges { - img.opacity: 1 - img.scale: 1 - } - } - transitions: Transition { - Anim { - duration: Config.background.wallFadeDuration - properties: "opacity,scale" - target: img - } - } - - onStatusChanged: { - if (status === Image.Ready) { - root.current = this; - } - } - } -} diff --git a/Modules/Wallpaper/WallBackground.qml b/Modules/Wallpaper/WallBackground.qml index e6a4675..1c8656a 100644 --- a/Modules/Wallpaper/WallBackground.qml +++ b/Modules/Wallpaper/WallBackground.qml @@ -6,6 +6,7 @@ import QtQuick import qs.Components import qs.Helpers import qs.Config +import ZShell.Internal Item { id: root @@ -15,58 +16,64 @@ Item { function refreshData(): void { Hyprland.refreshMonitors(); - const scale = Hyprland.monitorFor(root.screen).scale; - if (scale > 0 && img.resScale !== scale) { - img.resScale = scale; - img.sourceSize.width = root.screen.width * scale; + 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); - const displayRect = Qt.rect(img.sourceSize.width * displayData.x, img.implicitHeight * displayData.y, img.sourceSize.width * displayData.width, img.implicitHeight * displayData.height); - img.anchors.fill = null; - img.zoom = displayData.zoom; - img.x = -(displayRect.x * displayData.zoom / img.resScale); - img.y = -(displayRect.y * displayData.zoom / img.resScale); + + 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 - Image { + Component.onCompleted: root.refreshData() + + Connections { + function onHeightChanged() { + root.refreshData(); + } + + function onWidthChanged() { + root.refreshData(); + } + + target: root.screen + } + + WallpaperImage { id: img - property int displayH - property int displayW - property real resScale - property real zoom: 1.0 - - asynchronous: true - fillMode: Image.PreserveAspectCrop - height: implicitHeight * zoom / resScale - opacity: 1 - retainWhileLoading: true + anchors.fill: parent source: root.source - sourceSize.width: root.screen.width * resScale - width: implicitWidth * zoom / resScale - Behavior on height { + Behavior on cropHeight { Anim { } } - Behavior on width { + Behavior on cropWidth { Anim { } } - Behavior on x { + Behavior on cropX { Anim { } } - Behavior on y { + Behavior on cropY { Anim { } } - - onStatusChanged: { - if (img.status == Image.Ready) { - root.refreshData(); + Behavior on zoom { + Anim { } } diff --git a/Plugins/ZShell/Internal/CMakeLists.txt b/Plugins/ZShell/Internal/CMakeLists.txt index d49d62f..dd353a3 100644 --- a/Plugins/ZShell/Internal/CMakeLists.txt +++ b/Plugins/ZShell/Internal/CMakeLists.txt @@ -8,6 +8,7 @@ qml_module(ZShell-internal circularbuffer.hpp circularbuffer.cpp sparklineitem.hpp sparklineitem.cpp arcgauge.hpp arcgauge.cpp + wallpaperimage.hpp wallpaperimage.cpp LIBRARIES Qt::Gui Qt::Quick diff --git a/Plugins/ZShell/Internal/arcgauge.cpp b/Plugins/ZShell/Internal/arcgauge.cpp index 1ef0fb7..4f77b50 100644 --- a/Plugins/ZShell/Internal/arcgauge.cpp +++ b/Plugins/ZShell/Internal/arcgauge.cpp @@ -4,7 +4,7 @@ #include #include -namespace caelestia::internal { +namespace ZShell::internal { ArcGauge::ArcGauge(QQuickItem* parent) : QQuickPaintedItem(parent) { @@ -116,4 +116,4 @@ void ArcGauge::setLineWidth(qreal width) { update(); } -} // namespace caelestia::internal +} // namespace ZShell::internal diff --git a/Plugins/ZShell/Internal/arcgauge.hpp b/Plugins/ZShell/Internal/arcgauge.hpp index efa6800..453fd59 100644 --- a/Plugins/ZShell/Internal/arcgauge.hpp +++ b/Plugins/ZShell/Internal/arcgauge.hpp @@ -5,7 +5,7 @@ #include #include -namespace caelestia::internal { +namespace ZShell::internal { class ArcGauge : public QQuickPaintedItem { Q_OBJECT @@ -58,4 +58,4 @@ qreal m_sweepAngle = 1.5 * M_PI; qreal m_lineWidth = 10.0; }; -} // namespace caelestia::internal +} // namespace ZShell::internal diff --git a/Plugins/ZShell/Internal/wallpaperimage.cpp b/Plugins/ZShell/Internal/wallpaperimage.cpp new file mode 100644 index 0000000..ebe27e2 --- /dev/null +++ b/Plugins/ZShell/Internal/wallpaperimage.cpp @@ -0,0 +1,199 @@ +#include "wallpaperimage.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace ZShell::internal { + +WallpaperImage::WallpaperImage(QQuickItem *parent) + : QQuickItem(parent) +{ + setFlag(ItemHasContents, true); + connect(&m_imageWatcher, &QFutureWatcher::finished, this, &WallpaperImage::handleImageLoaded); +} + +WallpaperImage::~WallpaperImage() { + if (m_texture) delete m_texture; +} + +void WallpaperImage::setSource(const QUrl &source) { + if (m_source == source) return; + m_source = source; + emit sourceChanged(); + loadImage(); +} + +void WallpaperImage::setScreenResolution(const QSize &screenResolution) { + if (m_screenResolution == screenResolution) return; + m_screenResolution = screenResolution; + emit screenResolutionChanged(); + loadImage(); +} + +void WallpaperImage::setZoom(qreal zoom) { + if (qFuzzyCompare(m_zoom, zoom)) return; + m_zoom = zoom; + emit zoomChanged(); + update(); +} + +void WallpaperImage::setCropX(qreal x) { + if (qFuzzyCompare(m_cropX, x)) return; + m_cropX = x; + emit cropXChanged(); + update(); +} + +void WallpaperImage::setCropY(qreal y) { + if (qFuzzyCompare(m_cropY, y)) return; + m_cropY = y; + emit cropYChanged(); + update(); +} + +void WallpaperImage::setCropWidth(qreal w) { + 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(); +} + +QString WallpaperImage::getCacheFilePath() const { + if (m_source.isEmpty() || m_screenResolution.isEmpty()) return QString(); + + 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(); + + return cachePath + "/" + hash + ".png"; +} + +void WallpaperImage::loadImage() { + if (m_source.isEmpty()) 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; + + // 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; + } + + QImage original(sourceFile); + if (original.isNull()) return QImage(); + + if (targetRes.isEmpty()) { + // Screen resolution not set yet by QML, return the unscaled original for now to prevent a black screen + return original; + } + + // 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; + } + + // Otherwise just cache and return the original + if (!cacheFile.isEmpty()) original.save(cacheFile, "PNG"); + return original; + }); + + m_imageWatcher.setFuture(future); +} + +void WallpaperImage::handleImageLoaded() { + m_image = m_imageWatcher.result(); + m_textureDirty = true; + update(); // Request redraw +} + +QSGNode *WallpaperImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) { + auto *node = static_cast(oldNode); + + if (m_image.isNull()) { + delete node; + return nullptr; + } + + if (!node) { + node = window()->createImageNode(); + } + + if (m_textureDirty) { + if (m_texture) delete m_texture; + m_texture = window()->createTextureFromImage(m_image, QQuickWindow::TextureHasAlphaChannel); + m_textureDirty = false; + } + + if (m_texture) { + node->setTexture(m_texture); + node->setRect(boundingRect()); + node->setFiltering(QSGTexture::Linear); + + 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 bounds = boundingRect(); + if (bounds.isEmpty() || reqRect.isEmpty()) return node; + + qreal targetRatio = bounds.width() / bounds.height(); + qreal reqRatio = reqRect.width() / reqRect.height(); + + 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); + } + + node->setSourceRect(sourceRect); + } + + return node; +} + +} // namespace ZShell::internal diff --git a/Plugins/ZShell/Internal/wallpaperimage.hpp b/Plugins/ZShell/Internal/wallpaperimage.hpp new file mode 100644 index 0000000..04cb4ed --- /dev/null +++ b/Plugins/ZShell/Internal/wallpaperimage.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace ZShell::internal { + +class WallpaperImage : public QQuickItem { +Q_OBJECT +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(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) + +public: +explicit WallpaperImage(QQuickItem *parent = nullptr); +~WallpaperImage() override; + +QUrl source() const { + return m_source; +} +void setSource(const QUrl &source); + +QSize screenResolution() const { + return m_screenResolution; +} +void setScreenResolution(const QSize &screenResolution); + +qreal zoom() const { + return m_zoom; +} +void setZoom(qreal zoom); + +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); + +protected: +QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData) override; + +signals: +void sourceChanged(); +void screenResolutionChanged(); +void zoomChanged(); +void cropXChanged(); +void cropYChanged(); +void cropWidthChanged(); +void cropHeightChanged(); + +private: +void loadImage(); +void handleImageLoaded(); +QString getCacheFilePath() const; + +QUrl m_source; +QSize m_screenResolution; +qreal m_zoom = 1.0; + +qreal m_cropX = 0.0; +qreal m_cropY = 0.0; +qreal m_cropWidth = 1.0; +qreal m_cropHeight = 1.0; + +QImage m_image; +QSGTexture *m_texture = nullptr; +bool m_textureDirty = false; +QFutureWatcher m_imageWatcher; +}; + +} // namespace ZShell::internal