[graphics/krita] libs/ui/tool: WIP thread safe zoom preview

Emmet O'Neill <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 7f068b0ac622948c1c4c88d10c6885f216d22d6b by Emmet O'Neill, on behalf of Dat Le.
Committed on 13/08/2026 at 23:16.
Pushed by emmetoneill into branch 'master'.

WIP thread safe zoom preview

M  +62   -32   libs/ui/tool/KisAsyncColorSamplerHelper.cpp
M  +3    -1    libs/ui/tool/KisAsyncColorSamplerHelper.h
M  +6    -0    libs/ui/tool/strokes/kis_color_sampler_stroke_strategy.cpp
M  +17   -0    libs/ui/tool/strokes/kis_color_sampler_stroke_strategy.h

https://invent.kde.org/graphics/krita/-/commit/7f068b0ac622948c1c4c88d10c6885f216d22d6b

diff --git a/libs/ui/tool/KisAsyncColorSamplerHelper.cpp b/libs/ui/tool/KisAsyncColorSamplerHelper.cpp
index 3f533e30a29..7a250fd107f 100644
--- a/libs/ui/tool/KisAsyncColorSamplerHelper.cpp
+++ b/libs/ui/tool/KisAsyncColorSamplerHelper.cpp
@@ -82,6 +82,7 @@ struct KisAsyncColorSamplerHelper::Private
     QImage cacheCanvasPreviewImage;
     QPainterPath cacheCrosshairPath;
     int cacheCirclePreviewDiameter;
+    bool canvasPreviewFetchingStarted {false};
 
     QColor currentColor;
     QColor baseColor;
@@ -425,6 +426,8 @@ void KisAsyncColorSamplerHelper::startAction(const QPointF &docPoint, int radius
             this, &KisAsyncColorSamplerHelper::slotColorSamplingFinished);
     connect(strategy, &KisColorSamplerStrokeStrategy::sigFinalColorSelected,
             this, &KisAsyncColorSamplerHelper::sigFinalColorSelected);
+    connect(strategy, &KisColorSamplerStrokeStrategy::sigCanvasZoomPreviewUpdated,
+            this, &KisAsyncColorSamplerHelper::slotCanvasZoomPreviewUpdated);
 
     activatePreview();
     m_d->haveSample = true;
@@ -657,12 +660,8 @@ void KisAsyncColorSamplerHelper::paintCircle(QPainter &gc,
     cachePainter.setPen(Qt::NoPen);
     // If the cache update don't run, no brush is set. So set it
     cachePainter.setBrush(m_d->backgroundColor);
-    if (m_d->circleZoomPreviewEnabled){
-        cachePainter.setCompositionMode(QPainter::CompositionMode_SourceOver);
-    }
-    else {
-        cachePainter.setCompositionMode(QPainter::CompositionMode_Clear);
-    }
+
+    cachePainter.setCompositionMode(QPainter::CompositionMode_Clear);
     cachePainter.drawPath(tf.map(m_d->cacheCircleInnerClip));
 
     // Draw zoom preview
@@ -672,27 +671,19 @@ void KisAsyncColorSamplerHelper::paintCircle(QPainter &gc,
         zoomDocRectF.setSize(zoomDocRectF.size() / m_d->circleZoomPreviewScale);
         zoomDocRectF.moveCenter(m_d->sampleDocPoint);
 
-        paintCircleCanvasPreview(cachePainter, cacheRect, zoomDocRectF, tf.map(m_d->cacheCircleInnerClip));
-
-        paintCircleReferenceImagePreview(cachePainter, cacheRect, zoomDocRectF, tf.map(m_d->cacheCircleInnerClip));
+        bool paintLater = paintCircleCanvasPreview(cachePainter, cacheRect, zoomDocRectF, tf.map(m_d->cacheCircleInnerClip));
 
+        if (!paintLater) {
+            paintCircleReferenceImagePreview(cachePainter, cacheRect, zoomDocRectF, tf.map(m_d->cacheCircleInnerClip));
 
-        // Draw crosshair if preview is offseted
-        if (m_d->circlePreviewPosition != KisConfig::ColorSamplerPreviewCirclePosition::Center) {
-            QColor crosshairColor = Qt::black;
-            // TODO: Check back with Wolthera about the sRGB param
-            qreal luminance = KisPaintingTweaks::luminosityCoarse(currentColor, true);
-            if (luminance < 0.5) crosshairColor = Qt::white;
-
-            cachePainter.save();
-
-            cachePainter.setPen(crosshairColor);
-            QTransform tf;
-            tf.translate(cacheCenter.x(), cacheCenter.y());
-            tf.rotate(-canvasRotationAngle);
-            cachePainter.drawPath(tf.map(m_d->crosshairForOffsettedCircle()));
+            // Draw crosshair if preview is offseted
+            if (m_d->circlePreviewPosition != KisConfig::ColorSamplerPreviewCirclePosition::Center) {
+                paintCircleCrosshair(cachePainter, cacheRect, currentColor);
+            }
 
-            cachePainter.restore();
+            // Fill empty spaces to hide the underlying canvas
+            cachePainter.setCompositionMode(QPainter::CompositionMode_DestinationOver);
+            cachePainter.drawPath(tf.map(m_d->cacheCircleInnerClip));
         }
     }
 
@@ -701,10 +692,31 @@ void KisAsyncColorSamplerHelper::paintCircle(QPainter &gc,
     gc.restore();
 }
 
+void KisAsyncColorSamplerHelper::paintCircleCrosshair(QPainter &gc, const QRectF &viewRectF, const QColor &currentColor)
+{
+    QColor crosshairColor = Qt::black;
+    // TODO: Check back with Wolthera about the sRGB param
+    qreal luminance = KisPaintingTweaks::luminosityCoarse(currentColor, true);
+    if (luminance < 0.5) crosshairColor = Qt::white;
+
+    gc.save();
+
+    gc.setCompositionMode(QPainter::CompositionMode_SourceOver);
+    gc.setPen(crosshairColor);
+
+    QTransform tf;
+    tf.translate(viewRectF.center().x(), viewRectF.center().y());
+    tf.rotate(-m_d->cacheRotation);
+    gc.drawPath(tf.map(m_d->crosshairForOffsettedCircle()));
+
+    gc.restore();
+}
+
 QImage KisAsyncColorSamplerHelper::cacheCanvasImage(QRect &canvasPixelRect) {
     KisImageWSP canvasImage = m_d->canvas->image();
 
     if (!canvasImage->bounds().intersects(canvasPixelRect)) {
+        canvasPixelRect = QRect(); // Make this a null rect to imply no need to draw canvas
         return QImage();
     }
 
@@ -716,9 +728,9 @@ QImage KisAsyncColorSamplerHelper::cacheCanvasImage(QRect &canvasPixelRect) {
     if (m_d->cacheCanvasPreviewRect.isEmpty() || !m_d->cacheCanvasPreviewRect.contains(canvasPixelRect)) {
         // Cache an area larger than the needed preview area to avoid rapid small dynamic allocations
         // Attempt to reduce cache size when sample size is big to reduce delay
-        qreal cacheScale = 1;
-        if (canvasPixelRect.width() < 250) cacheScale = 4;
-        else if (canvasPixelRect.width() < 500) cacheScale = 2;
+        qreal cacheScale = 1000;
+        // if (canvasPixelRect.width() < 250) cacheScale = 4;
+        // else if (canvasPixelRect.width() < 500) cacheScale = 2;
 
         QRect cacheCanvasRect = canvasPixelRect;
         cacheCanvasRect.setSize(canvasPixelRect.size() * cacheScale);
@@ -729,17 +741,21 @@ QImage KisAsyncColorSamplerHelper::cacheCanvasImage(QRect &canvasPixelRect) {
             cacheCanvasRect = canvasImage->bounds();
         }
 
-        m_d->cacheCanvasPreviewImage = canvasImage->convertToQImage(cacheCanvasRect, canvasImage->profile());
+        // If already fetching, then do nothing
+        if (m_d->canvasPreviewFetchingStarted) return QImage();
 
-        m_d->cacheCanvasPreviewRect = cacheCanvasRect;
-    }
+        m_d->canvasPreviewFetchingStarted = true;
+        m_d->strokesFacade()->addJob(m_d->strokeId,
+            new KisColorSamplerStrokeStrategy::GenerateCanvasZoomPreviewData(canvasImage, cacheCanvasRect, canvasImage->profile()));
 
+        return QImage();
+    }
     canvasPixelRect.translate(-m_d->cacheCanvasPreviewRect.topLeft());
 
     return m_d->cacheCanvasPreviewImage;
 }
 
-void KisAsyncColorSamplerHelper::paintCircleCanvasPreview(QPainter &gc, const QRectF &viewRectF, const QRectF &zoomDocRectF, const QPainterPath &clip) {
+bool KisAsyncColorSamplerHelper::paintCircleCanvasPreview(QPainter &gc, const QRectF &viewRectF, const QRectF &zoomDocRectF, const QPainterPath &clip) {
     KisImageWSP image = m_d->canvas->image();
 
     QRectF canvasPixelRectF = image->documentToPixel(zoomDocRectF);
@@ -753,7 +769,10 @@ void KisAsyncColorSamplerHelper::paintCircleCanvasPreview(QPainter &gc, const QR
     canvasPixelRect.moveCenter(image->documentToImagePixelFloored(zoomDocRectF.center()));
 
     QImage cachedImage = cacheCanvasImage(canvasPixelRect);
-    if (cachedImage.isNull()) return;
+    // A null QImage and a null canvasPixelRect => No need to paint canvas
+    // A null QImage and a not null canvasPixelRect => Cache miss, skip painting and paint later when canvas image is ready
+    bool paintLater = cachedImage.isNull() && !canvasPixelRect.isNull();
+    if (cachedImage.isNull()) return paintLater;
 
     gc.save();
     gc.setCompositionMode(QPainter::CompositionMode_SourceOver);
@@ -766,6 +785,8 @@ void KisAsyncColorSamplerHelper::paintCircleCanvasPreview(QPainter &gc, const QR
     gc.drawImage(viewRectF, cachedImage, canvasPixelRect);
 
     gc.restore();
+
+    return false;
 }
 
 // Preview won't respect opacity because the color sampled doesn't respect opacity either
@@ -819,6 +840,15 @@ void KisAsyncColorSamplerHelper::paintCircleReferenceImagePreview(QPainter &gc,
     gc.restore();
 }
 
+void KisAsyncColorSamplerHelper::slotCanvasZoomPreviewUpdated(const QImage &canvasImage, QRect canvasRect) {
+    m_d->cacheCanvasPreviewImage = canvasImage;
+    m_d->cacheCanvasPreviewRect = canvasRect;
+
+    m_d->canvasPreviewFetchingStarted = false;
+
+    Q_EMIT sigRequestUpdateOutline();
+}
+
 void KisAsyncColorSamplerHelper::slotAddSamplingJob(const QPointF &docPoint)
 {
     /**
diff --git a/libs/ui/tool/KisAsyncColorSamplerHelper.h b/libs/ui/tool/KisAsyncColorSamplerHelper.h
index b16c909a57d..4b0513f8b32 100644
--- a/libs/ui/tool/KisAsyncColorSamplerHelper.h
+++ b/libs/ui/tool/KisAsyncColorSamplerHelper.h
@@ -75,13 +75,15 @@ private Q_SLOTS:
     void slotAddSamplingJob(const QPointF &docPoint);
     void slotColorSamplingFinished(const KoColor &rawColor);
     void slotUpdateBgColor();
+    void slotCanvasZoomPreviewUpdated(const QImage &canvasImage, QRect canvasRect);
 
 private:
     void activatePreview();
     void paintRectangle(QPainter &gc, const QRectF &viewRectF, const QColor &currentColor, const QColor &baseColor);
     void paintCircle(QPainter &gc, const QRectF &viewRectF, const QColor &currentColor, const QColor &baseColor);
 
-    void paintCircleCanvasPreview(QPainter &gc, const QRectF &viewRectF, const QRectF &zoomDocRectF, const QPainterPath &clip);
+    void paintCircleCrosshair(QPainter &gc, const QRectF &viewRectF, const QColor &currentColor);
+    bool paintCircleCanvasPreview(QPainter &gc, const QRectF &viewRectF, const QRectF &zoomDocRectF, const QPainterPath &clip);
     // Return true if at least one reference image preview was drawn
     void paintCircleReferenceImagePreview(QPainter &gc, const QRectF &viewRectF, const QRectF &zoomDocRectF, const QPainterPath &clip);
 
diff --git a/libs/ui/tool/strokes/kis_color_sampler_stroke_strategy.cpp b/libs/ui/tool/strokes/kis_color_sampler_stroke_strategy.cpp
index e19fb612e6e..31946545e4e 100644
--- a/libs/ui/tool/strokes/kis_color_sampler_stroke_strategy.cpp
+++ b/libs/ui/tool/strokes/kis_color_sampler_stroke_strategy.cpp
@@ -8,6 +8,7 @@
 
 #include "kis_tool_utils.h"
 #include "kis_paint_device.h"
+#include "kis_image.h"
 
 struct KisColorSamplerStrokeStrategy::Private
 {
@@ -42,6 +43,7 @@ void KisColorSamplerStrokeStrategy::doStrokeCallback(KisStrokeJobData *data)
 
     Data *d = dynamic_cast<Data*>(data);
     FinalizeData *finalize = dynamic_cast<FinalizeData*>(data);
+    GenerateCanvasZoomPreviewData *previewData = dynamic_cast<GenerateCanvasZoomPreviewData*>(data);
 
     if (d) {
         KoColor color;
@@ -54,6 +56,10 @@ void KisColorSamplerStrokeStrategy::doStrokeCallback(KisStrokeJobData *data)
         if (m_d->lastSelectedColor) {
             Q_EMIT sigFinalColorSelected(*m_d->lastSelectedColor);
         }
+    } else if (previewData) {
+        QImage image = previewData->canvasImage->convertToQImage(previewData->canvasPixelRect, previewData->colorProfile);
+
+        Q_EMIT sigCanvasZoomPreviewUpdated(image, previewData->canvasPixelRect);
     }
 }
 
diff --git a/libs/ui/tool/strokes/kis_color_sampler_stroke_strategy.h b/libs/ui/tool/strokes/kis_color_sampler_stroke_strategy.h
index bc6476304d3..2de6217b603 100644
--- a/libs/ui/tool/strokes/kis_color_sampler_stroke_strategy.h
+++ b/libs/ui/tool/strokes/kis_color_sampler_stroke_strategy.h
@@ -44,6 +44,22 @@ public:
             return new FinalizeData();
         }
     };
+
+    class GenerateCanvasZoomPreviewData : public KisStrokeJobData {
+    public:
+        GenerateCanvasZoomPreviewData(KisImageWSP _image, const QRect &_canvasPixelRect, const KoColorProfile *_colorProfile)
+            : canvasImage(_image), canvasPixelRect(_canvasPixelRect), colorProfile(_colorProfile)
+        {}
+
+        KisStrokeJobData* createLodClone(int levelOfDetail) override {
+            Q_UNUSED(levelOfDetail);
+            return new GenerateCanvasZoomPreviewData(canvasImage, canvasPixelRect, colorProfile);
+        }
+
+        KisImageWSP canvasImage;
+        QRect canvasPixelRect;
+        const KoColorProfile *colorProfile;
+    };
 public:
     KisColorSamplerStrokeStrategy(int radius, int blend, int lod = 0);
     ~KisColorSamplerStrokeStrategy() override;
@@ -54,6 +70,7 @@ public:
 Q_SIGNALS:
     void sigColorUpdated(const KoColor &color);
     void sigFinalColorSelected(const KoColor &color);
+    void sigCanvasZoomPreviewUpdated(const QImage &canvasImage, const QRect &canvasRect);
 
 private:
     struct Private;
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.