File indexing completed on 2024-05-12 16:02:02

0001 /*
0002  *  SPDX-FileCopyrightText: 2022 Sam Linnfer <littlelightlittlefire@gmail.com>
0003  *
0004  *  SPDX-License-Identifier: LGPL-2.0-or-later
0005  */
0006 #include "KisHsvColorSlider.h"
0007 
0008 #include "KoColorSpace.h"
0009 #include <KoMixColorsOp.h>
0010 #include <KoColor.h>
0011 #include <KoColorConversions.h>
0012 
0013 #include <QPainter>
0014 #include <QTimer>
0015 #include <QStyleOption>
0016 #include <QPointer>
0017 
0018 #include <memory>
0019 
0020 #include "KoColorDisplayRendererInterface.h"
0021 
0022 namespace {
0023 
0024 const qreal EPSILON = 1e-6;
0025 const int ARROW_SIZE = 8;
0026 const int FRAME_SIZE = 5;
0027 
0028 // Internal color representation used by the slider.
0029 // h, s, v are values between 0 and 1
0030 struct HSVColor {
0031     explicit HSVColor()
0032         : h(0), s(0), v(0)
0033     {
0034     }
0035 
0036     explicit HSVColor(qreal hh, qreal ss, qreal vv)
0037         : h(hh), s(ss), v(vv)
0038     {
0039     }
0040 
0041     qreal h, s, v;
0042 };
0043 
0044 void fromQColor(const QColor minC, const QColor maxC, HSVColor &min, HSVColor &max) {
0045     minC.getHsvF(&min.h, &min.s, &min.v);
0046     maxC.getHsvF(&max.h, &max.s, &max.v);
0047 
0048     // getHsvF can return -1 values for hue
0049     // this happens when the color whites out and the hue information is destroyed
0050     // e.g. for all x, hsv(x, 0, 1) is always rgb(255, 255, 255) white
0051     if (min.h < EPSILON && max.h < EPSILON) {
0052         min.h = 0;
0053         max.h = 0;
0054     } else if (min.h < EPSILON && max.h > EPSILON) {
0055         min.h = max.h;
0056     } else if (min.h > EPSILON && max.h < EPSILON) {
0057         max.h = min.h;
0058     }
0059 }
0060 
0061 class Mixer {
0062 public:
0063     // mix two colors
0064     // t is a qreal between 0 and 1
0065     virtual QColor mix(qreal t) const = 0;
0066     virtual ~Mixer() = default;
0067 };
0068 
0069 class ColorSpaceMixer : public Mixer {
0070 public:
0071     ColorSpaceMixer(KoColor minKColor, KoColor maxKColor, KoColorDisplayRendererInterface *renderer = KoDumbColorDisplayRenderer::instance())
0072         : minColor(minKColor)
0073         , maxColor(maxKColor)
0074         , mixOp(minKColor.colorSpace()->mixColorsOp())
0075         , displayRenderer(renderer)
0076     {
0077     }
0078 
0079     QColor mix(qreal t) const override
0080     {
0081         const quint8 *colors[2] = {
0082             minColor.data(),
0083             maxColor.data()
0084         };
0085 
0086         quint8 weight = static_cast<quint8>((1.0 - t) * 255);
0087         qint16 weights[2] = {
0088              weight,
0089              static_cast<qint16>(255 - weight),
0090         };
0091 
0092         KoColor color(minColor.colorSpace());
0093         mixOp->mixColors(colors, weights, 2, color.data());
0094 
0095         if (displayRenderer) {
0096             return displayRenderer->toQColor(color);
0097         }
0098 
0099         return color.toQColor();
0100     }
0101 
0102 private:
0103     KoColor minColor;
0104     KoColor maxColor;
0105     KoMixColorsOp *mixOp;
0106     KoColorDisplayRendererInterface *displayRenderer;
0107 };
0108 
0109 class HsxMixer : public Mixer {
0110 public:
0111     HsxMixer(const HSVColor &minColor, const HSVColor &maxColor, bool circular, KisHsvColorSlider::MIX_MODE mode)
0112         : Mixer()
0113         , min(minColor)
0114         , max(maxColor)
0115         , dH(), dS(), dV()
0116         , circularHue(circular)
0117         , mixMode(mode)
0118     {
0119 
0120         dH = max.h - min.h;
0121         if (circularHue) {
0122             if (dH < EPSILON) {
0123                 dH += 1;
0124             }
0125         } else {
0126             dH = 0;
0127         }
0128 
0129         dS = max.s - min.s;
0130         dV = max.v - min.v;
0131     }
0132 
0133     QColor mix(qreal t) const override
0134     {
0135         QColor color;
0136         const qreal h = fmod(min.h + t * dH, 1);
0137         const qreal s = min.s + t * dS;
0138         const qreal v = min.v + t * dV;
0139 
0140         switch (mixMode) {
0141             case KisHsvColorSlider::MIX_MODE::HSL:
0142                 color.setHslF(h, s, v);
0143                 break;
0144 
0145             case KisHsvColorSlider::MIX_MODE::HSY: {
0146                 qreal r, g, b;
0147                 HSYToRGB(h, s, v, &r, &g, &b);
0148                 color.setRgbF(r, g, b);
0149                 break;
0150             }
0151 
0152             case KisHsvColorSlider::MIX_MODE::HSI: {
0153                 qreal r, g, b;
0154                 HSIToRGB(h, s, v, &r, &g, &b);
0155                 color.setRgbF(r, g, b);
0156                 break;
0157             }
0158 
0159             default: // fallthrough
0160             case KisHsvColorSlider::MIX_MODE::HSV:
0161                 color.setHsvF(h, s, v);
0162                 break;
0163         }
0164 
0165         return color;
0166     }
0167 
0168 private:
0169     HSVColor min;
0170     HSVColor max;
0171     qreal dH, dS, dV;
0172     bool circularHue;
0173     KisHsvColorSlider::MIX_MODE mixMode;
0174 };
0175 
0176 }
0177 
0178 struct Q_DECL_HIDDEN KisHsvColorSlider::Private
0179 {
0180     Private()
0181         : minColor()
0182         , maxColor()
0183         , minKoColor()
0184         , maxKoColor()
0185         , pixmap()
0186         , upToDate(false)
0187         , displayRenderer(nullptr)
0188         , circularHue(false)
0189         , mixMode(KisHsvColorSlider::MIX_MODE::HSV)
0190     {
0191 
0192     }
0193 
0194     HSVColor minColor;
0195     HSVColor maxColor;
0196     KoColor minKoColor;
0197     KoColor maxKoColor;
0198     QPixmap pixmap;
0199     bool upToDate;
0200     QPointer<KoColorDisplayRendererInterface> displayRenderer;
0201 
0202     // If the min and max color has the same hue, should the widget display the entire gamut of hues.
0203     bool circularHue;
0204     MIX_MODE mixMode;
0205 };
0206 
0207 KisHsvColorSlider::KisHsvColorSlider(QWidget *parent, KoColorDisplayRendererInterface *displayRenderer)
0208     : KisHsvColorSlider(Qt::Horizontal, parent, displayRenderer)
0209 {
0210 }
0211 
0212 KisHsvColorSlider::KisHsvColorSlider(Qt::Orientation orientation, QWidget *parent, KoColorDisplayRendererInterface *displayRenderer)
0213     : KSelector(orientation, parent)
0214     , d(new Private)
0215 {
0216     setMaximum(255);
0217     d->displayRenderer = displayRenderer;
0218     connect(d->displayRenderer, SIGNAL(displayConfigurationChanged()), SLOT(update()), Qt::UniqueConnection);
0219 }
0220 
0221 KisHsvColorSlider::~KisHsvColorSlider()
0222 {
0223     delete d;
0224 }
0225 
0226 void KisHsvColorSlider::setColors(const KoColor min, const KoColor max)
0227 {
0228     fromQColor(min.toQColor(), max.toQColor(), d->minColor, d->maxColor);
0229     d->minKoColor = min;
0230     d->maxKoColor = max;
0231     d->upToDate = false;
0232     QTimer::singleShot(1, this, SLOT(update()));
0233 }
0234 
0235 void KisHsvColorSlider::setColors(const QColor min, const QColor max)
0236 {
0237     fromQColor(min, max, d->minColor, d->maxColor);
0238     d->minKoColor.fromQColor(min);
0239     d->maxKoColor.fromQColor(max);
0240     d->upToDate = false;
0241     QTimer::singleShot(1, this, SLOT(update()));
0242 }
0243 
0244 void KisHsvColorSlider::setColors(qreal minH, qreal minS, qreal minV, qreal maxH, qreal maxS, qreal maxV)
0245 {
0246     d->minColor = HSVColor(minH, minS, minV);
0247     d->maxColor = HSVColor(maxH, maxS, maxV);
0248 
0249     QColor minQ, maxQ;
0250     minQ.setHsvF(minH, minS, minV);
0251     maxQ.setHsvF(maxH, maxS, maxV);
0252     d->minKoColor.fromQColor(minQ);
0253     d->maxKoColor.fromQColor(maxQ);
0254     d->upToDate = false;
0255 
0256     QTimer::singleShot(1, this, SLOT(update()));
0257 }
0258 
0259 void KisHsvColorSlider::setCircularHue(bool value) {
0260     d->circularHue = value;
0261     d->upToDate = false;
0262     QTimer::singleShot(1, this, SLOT(update()));
0263 }
0264 
0265 void KisHsvColorSlider::setMixMode(MIX_MODE mode) {
0266     d->mixMode = mode;
0267     d->upToDate = false;
0268     QTimer::singleShot(1, this, SLOT(update()));
0269 }
0270 
0271 void KisHsvColorSlider::drawContents(QPainter *painter)
0272 {
0273     QPixmap checker(8, 8);
0274     QPainter p(&checker);
0275     p.fillRect(0, 0, 4, 4, Qt::lightGray);
0276     p.fillRect(4, 0, 4, 4, Qt::darkGray);
0277     p.fillRect(0, 4, 4, 4, Qt::darkGray);
0278     p.fillRect(4, 4, 4, 4, Qt::lightGray);
0279     p.end();
0280 
0281     QRect contentsRect_(contentsRect());
0282     painter->fillRect(contentsRect_, QBrush(checker));
0283 
0284     if (!d->upToDate || d->pixmap.isNull() || d->pixmap.width() != contentsRect_.width()
0285         || d->pixmap.height() != contentsRect_.height())
0286     {
0287         std::unique_ptr<Mixer> m;
0288         switch (d->mixMode) {
0289             case MIX_MODE::COLOR_SPACE:
0290                 m = std::make_unique<ColorSpaceMixer>(d->minKoColor, d->maxKoColor, d->displayRenderer);
0291                 break;
0292 
0293             default: // fallthrough
0294             case MIX_MODE::HSV: // fallthrough
0295             case MIX_MODE::HSL: // fallthrough
0296             case MIX_MODE::HSI: // fallthrough
0297             case MIX_MODE::HSY: // fallthrough
0298                 m = std::make_unique<HsxMixer>(d->minColor, d->maxColor, d->circularHue, d->mixMode);
0299                 break;
0300         }
0301 
0302         QImage image(contentsRect_.width(), contentsRect_.height(), QImage::Format_ARGB32);
0303 
0304         if (orientation() == Qt::Horizontal) {
0305             if (contentsRect_.width() > 0) {
0306                 for (int x = 0; x < contentsRect_.width(); x++) {
0307                     const qreal t = static_cast<qreal>(x) / (contentsRect_.width() - 1);
0308 
0309                     for (int y = 0; y < contentsRect_.height(); y++) {
0310                         image.setPixel(x, y, m->mix(t).rgba());
0311                     }
0312                 }
0313             }
0314         } else {
0315             if (contentsRect_.height() > 0) {
0316                 for (int y = 0; y < contentsRect_.height(); y++) {
0317                     const qreal t = static_cast<qreal>(y) / (contentsRect_.height() - 1);
0318 
0319                     for (int x = 0; x < contentsRect_.width(); x++) {
0320                         image.setPixel(x, y, m->mix(t).rgba());
0321                     }
0322                 }
0323             }
0324         }
0325 
0326         d->pixmap = QPixmap::fromImage(image);
0327         d->upToDate = true;
0328     }
0329 
0330     painter->drawPixmap(contentsRect_, d->pixmap, QRect(0, 0, d->pixmap.width(), d->pixmap.height()));
0331 }
0332 
0333 QPoint KisHsvColorSlider::calcArrowPos(int value) {
0334     QPoint p;
0335     int w = style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
0336     int iw = (w < FRAME_SIZE) ? FRAME_SIZE : w;
0337 
0338     double t = static_cast<double>(value - minimum()) / static_cast<double>(maximum() - minimum());
0339     if (orientation() == Qt::Vertical) {
0340         p.setY(height() - iw - 1 - (height() - 2 * iw - 1) * t);
0341 
0342         if (arrowDirection() == Qt::RightArrow) {
0343             p.setX(0);
0344         } else {
0345             p.setX(width() - FRAME_SIZE);
0346         }
0347     } else {
0348         p.setX(iw + (width() - 2 * iw - 1) * t);
0349 
0350         if (arrowDirection() == Qt::DownArrow) {
0351             p.setY(0);
0352         } else {
0353             p.setY(height() - FRAME_SIZE);
0354         }
0355     }
0356 
0357     return p;
0358 }
0359 
0360 void KisHsvColorSlider::drawArrow(QPainter *painter, const QPoint &)
0361 {
0362     painter->setPen(QPen(palette().text().color(), 0));
0363     painter->setBrush(palette().text());
0364 
0365     QStyleOption o;
0366     o.initFrom(this);
0367     o.state &= ~QStyle::State_MouseOver;
0368 
0369     // Recalculate pos since the value returned by the parent is bugged and doesn't account for negative setMinimum/setMaximum values
0370     QPoint pos = calcArrowPos(value());
0371 
0372     if (orientation() == Qt::Vertical) {
0373         o.rect = QRect(pos.x(), pos.y() - ARROW_SIZE / 2, ARROW_SIZE, ARROW_SIZE);
0374     } else {
0375         o.rect = QRect(pos.x() - ARROW_SIZE / 2, pos.y(), ARROW_SIZE, ARROW_SIZE);
0376     }
0377 
0378     QStyle::PrimitiveElement arrowPE;
0379     switch (arrowDirection()) {
0380     case Qt::UpArrow:
0381         arrowPE = QStyle::PE_IndicatorArrowUp;
0382         break;
0383     case Qt::DownArrow:
0384         arrowPE = QStyle::PE_IndicatorArrowDown;
0385         break;
0386     case Qt::RightArrow:
0387         arrowPE = QStyle::PE_IndicatorArrowRight;
0388         break;
0389     case Qt::LeftArrow:
0390         arrowPE = QStyle::PE_IndicatorArrowLeft;
0391         break;
0392     default:
0393         arrowPE = QStyle::PE_IndicatorArrowLeft;
0394         break;
0395     }
0396 
0397     style()->drawPrimitive(arrowPE, &o, painter, this);
0398 
0399 }