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 }