Krita Source Code Documentation
Loading...
Searching...
No Matches
KisHsvColorSlider.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2022 Sam Linnfer <littlelightlittlefire@gmail.com>
3 *
4 * SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6#include "KisHsvColorSlider.h"
7
8#include "KoColorSpace.h"
9#include <KoMixColorsOp.h>
10#include <KoColor.h>
11#include <KoColorConversions.h>
12
13#include <QPainter>
14#include <QTimer>
15#include <QStyleOption>
16#include <QPointer>
17
18#include <memory>
19
22
23namespace {
24
25const qreal EPSILON = 1e-6;
26const int ARROW_SIZE = 8;
27const int FRAME_SIZE = 5;
28
29// Internal color representation used by the slider.
30// h, s, v are values between 0 and 1
31struct HSVColor {
32 explicit HSVColor()
33 : h(0), s(0), v(0)
34 {
35 }
36
37 explicit HSVColor(qreal hh, qreal ss, qreal vv)
38 : h(hh), s(ss), v(vv)
39 {
40 }
41#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
42 qreal h;
43 qreal s;
44 qreal v;
45#else
46 float h;
47 float s;
48 float v;
49#endif
50};
51
52void fromQColor(const QColor minC, const QColor maxC, HSVColor &min, HSVColor &max) {
53 minC.getHsvF(&min.h, &min.s, &min.v);
54 maxC.getHsvF(&max.h, &max.s, &max.v);
55
56 // getHsvF can return -1 values for hue
57 // this happens when the color whites out and the hue information is destroyed
58 // e.g. for all x, hsv(x, 0, 1) is always rgb(255, 255, 255) white
59 if (min.h < EPSILON && max.h < EPSILON) {
60 min.h = 0;
61 max.h = 0;
62 } else if (min.h < EPSILON && max.h > EPSILON) {
63 min.h = max.h;
64 } else if (min.h > EPSILON && max.h < EPSILON) {
65 max.h = min.h;
66 }
67}
68
69class Mixer {
70public:
71 // mix two colors
72 // t is a qreal between 0 and 1
73 virtual QColor mix(qreal t) const = 0;
74 virtual ~Mixer() = default;
75};
76
77class ColorSpaceMixer : public Mixer {
78public:
79 ColorSpaceMixer(KoColor minKColor, KoColor maxKColor, KoColorDisplayRendererInterface *renderer = KoDumbColorDisplayRenderer::instance())
80 : minColor(minKColor)
81 , maxColor(maxKColor)
82 , mixOp(minKColor.colorSpace()->mixColorsOp())
83 , displayRenderer(renderer)
84 {
85 }
86
87 QColor mix(qreal t) const override
88 {
89 const quint8 *colors[2] = {
90 minColor.data(),
91 maxColor.data()
92 };
93
94 quint8 weight = static_cast<quint8>((1.0 - t) * 255);
95 qint16 weights[2] = {
96 weight,
97 static_cast<qint16>(255 - weight),
98 };
99
100 KoColor color(minColor.colorSpace());
101 mixOp->mixColors(colors, weights, 2, color.data());
102
103 if (displayRenderer) {
104 return displayRenderer->toQColor(color);
105 }
106
107 return color.toQColor();
108 }
109
110private:
111 KoColor minColor;
112 KoColor maxColor;
113 KoMixColorsOp *mixOp;
114 KoColorDisplayRendererInterface *displayRenderer;
115};
116
117class HsxMixer : public Mixer {
118public:
119 HsxMixer(const HSVColor &minColor, const HSVColor &maxColor, bool circular, KisHsvColorSlider::MIX_MODE mode)
120 : Mixer()
121 , min(minColor)
122 , max(maxColor)
123 , dH(), dS(), dV()
124 , circularHue(circular)
125 , mixMode(mode)
126 {
127
128 dH = max.h - min.h;
129 if (circularHue) {
130 if (dH < EPSILON) {
131 dH += 1;
132 }
133 } else {
134 dH = 0;
135 }
136
137 dS = max.s - min.s;
138 dV = max.v - min.v;
139 }
140
141 QColor mix(qreal t) const override
142 {
143 QColor color;
144 const qreal h = fmod(min.h + t * dH, 1);
145 const qreal s = min.s + t * dS;
146 const qreal v = min.v + t * dV;
147
148 switch (mixMode) {
150 color.setHslF(h, s, v);
151 break;
152
154 qreal r, g, b;
155 HSYToRGB(h, s, v, &r, &g, &b);
156 color.setRgbF(r, g, b);
157 break;
158 }
159
161 qreal r, g, b;
162 HSIToRGB(h, s, v, &r, &g, &b);
163 color.setRgbF(r, g, b);
164 break;
165 }
166
167 default: // fallthrough
169 color.setHsvF(h, s, v);
170 break;
171 }
172
173 return color;
174 }
175
176private:
177 HSVColor min;
178 HSVColor max;
179 qreal dH, dS, dV;
180 bool circularHue;
182};
183
184}
185
186struct Q_DECL_HIDDEN KisHsvColorSlider::Private
187{
189 : minColor()
190 , maxColor()
191 , minKoColor()
192 , maxKoColor()
193 , pixmap()
194 , upToDate(false)
195 , displayRenderer(nullptr)
196 , updateCompressor(100, KisSignalCompressor::FIRST_ACTIVE)
197 , circularHue(false)
198 , mixMode(KisHsvColorSlider::MIX_MODE::HSV)
199 {
200
201 }
202
203 HSVColor minColor;
204 HSVColor maxColor;
207 QPixmap pixmap;
211
212 // If the min and max color has the same hue, should the widget display the entire gamut of hues.
215};
216
218 : KisHsvColorSlider(Qt::Horizontal, parent, displayRenderer)
219{
220}
221
222KisHsvColorSlider::KisHsvColorSlider(Qt::Orientation orientation, QWidget *parent, KoColorDisplayRendererInterface *displayRenderer)
223 : KSelector(orientation, parent)
224 , d(new Private)
225{
226 setMaximum(255);
227 d->displayRenderer = displayRenderer;
228 connect(d->displayRenderer, SIGNAL(displayConfigurationChanged()), SLOT(update()), Qt::UniqueConnection);
229 connect(&d->updateCompressor, SIGNAL(timeout()), SLOT(update()), Qt::UniqueConnection);
230}
231
236
238{
239 fromQColor(min.toQColor(), max.toQColor(), d->minColor, d->maxColor);
240 d->minKoColor = min;
241 d->maxKoColor = max;
242 d->upToDate = false;
243 d->updateCompressor.start();
244}
245
246void KisHsvColorSlider::setColors(const QColor min, const QColor max)
247{
248 fromQColor(min, max, d->minColor, d->maxColor);
249 d->minKoColor.fromQColor(min);
250 d->maxKoColor.fromQColor(max);
251 d->upToDate = false;
252 d->updateCompressor.start();
253}
254
255void KisHsvColorSlider::setColors(qreal minH, qreal minS, qreal minV, qreal maxH, qreal maxS, qreal maxV)
256{
257 d->minColor = HSVColor(minH, minS, minV);
258 d->maxColor = HSVColor(maxH, maxS, maxV);
259
260 QColor minQ, maxQ;
261 minQ.setHsvF(minH, minS, minV);
262 maxQ.setHsvF(maxH, maxS, maxV);
263 d->minKoColor.fromQColor(minQ);
264 d->maxKoColor.fromQColor(maxQ);
265 d->upToDate = false;
266
267 d->updateCompressor.start();
268}
269
271 d->circularHue = value;
272 d->upToDate = false;
273 d->updateCompressor.start();
274}
275
277 d->mixMode = mode;
278 d->upToDate = false;
279 d->updateCompressor.start();
280}
281
282void KisHsvColorSlider::drawContents(QPainter *painter)
283{
284 QPixmap checker(8, 8);
285 QPainter p(&checker);
286 p.fillRect(0, 0, 4, 4, Qt::lightGray);
287 p.fillRect(4, 0, 4, 4, Qt::darkGray);
288 p.fillRect(0, 4, 4, 4, Qt::darkGray);
289 p.fillRect(4, 4, 4, 4, Qt::lightGray);
290 p.end();
291
292 QRect contentsRect_(contentsRect());
293 painter->fillRect(contentsRect_, QBrush(checker));
294
295 if (!d->upToDate || d->pixmap.isNull() || d->pixmap.width() != contentsRect_.width()
296 || d->pixmap.height() != contentsRect_.height())
297 {
298 std::unique_ptr<Mixer> m;
299 switch (d->mixMode) {
301 m = std::make_unique<ColorSpaceMixer>(d->minKoColor, d->maxKoColor, d->displayRenderer);
302 break;
303
304 default: // fallthrough
305 case MIX_MODE::HSV: // fallthrough
306 case MIX_MODE::HSL: // fallthrough
307 case MIX_MODE::HSI: // fallthrough
308 case MIX_MODE::HSY: // fallthrough
309 m = std::make_unique<HsxMixer>(d->minColor, d->maxColor, d->circularHue, d->mixMode);
310 break;
311 }
312
313 QImage image(contentsRect_.width(), contentsRect_.height(), QImage::Format_ARGB32);
314
315 if (orientation() == Qt::Horizontal) {
316 if (contentsRect_.width() > 0) {
317 for (int x = 0; x < contentsRect_.width(); x++) {
318 const qreal t = static_cast<qreal>(x) / (contentsRect_.width() - 1);
319
320 for (int y = 0; y < contentsRect_.height(); y++) {
321 image.setPixel(x, y, m->mix(t).rgba());
322 }
323 }
324 }
325 } else {
326 if (contentsRect_.height() > 0) {
327 for (int y = 0; y < contentsRect_.height(); y++) {
328 const qreal t = static_cast<qreal>(y) / (contentsRect_.height() - 1);
329
330 for (int x = 0; x < contentsRect_.width(); x++) {
331 image.setPixel(x, y, m->mix(t).rgba());
332 }
333 }
334 }
335 }
336
337 d->pixmap = QPixmap::fromImage(image);
338 d->upToDate = true;
339 }
340
341 painter->drawPixmap(contentsRect_, d->pixmap, QRect(0, 0, d->pixmap.width(), d->pixmap.height()));
342}
343
345 QPoint p;
346 int w = style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
347 int iw = (w < FRAME_SIZE) ? FRAME_SIZE : w;
348
349 double t = static_cast<double>(value - minimum()) / static_cast<double>(maximum() - minimum());
350 if (orientation() == Qt::Vertical) {
351 p.setY(height() - iw - 1 - (height() - 2 * iw - 1) * t);
352
353 if (arrowDirection() == Qt::RightArrow) {
354 p.setX(0);
355 } else {
356 p.setX(width() - FRAME_SIZE);
357 }
358 } else {
359 p.setX(iw + (width() - 2 * iw - 1) * t);
360
361 if (arrowDirection() == Qt::DownArrow) {
362 p.setY(0);
363 } else {
364 p.setY(height() - FRAME_SIZE);
365 }
366 }
367
368 return p;
369}
370
371void KisHsvColorSlider::drawArrow(QPainter *painter, const QPoint &)
372{
373 painter->setPen(QPen(palette().text().color(), 0));
374 painter->setBrush(palette().text());
375
376 QStyleOption o;
377 o.initFrom(this);
378 o.state &= ~QStyle::State_MouseOver;
379
380 // Recalculate pos since the value returned by the parent is bugged and doesn't account for negative setMinimum/setMaximum values
381 QPoint pos = calcArrowPos(value());
382
383 if (orientation() == Qt::Vertical) {
384 o.rect = QRect(pos.x(), pos.y() - ARROW_SIZE / 2, ARROW_SIZE, ARROW_SIZE);
385 } else {
386 o.rect = QRect(pos.x() - ARROW_SIZE / 2, pos.y(), ARROW_SIZE, ARROW_SIZE);
387 }
388
389 QStyle::PrimitiveElement arrowPE;
390 switch (arrowDirection()) {
391 case Qt::UpArrow:
392 arrowPE = QStyle::PE_IndicatorArrowUp;
393 break;
394 case Qt::DownArrow:
395 arrowPE = QStyle::PE_IndicatorArrowDown;
396 break;
397 case Qt::RightArrow:
398 arrowPE = QStyle::PE_IndicatorArrowRight;
399 break;
400 case Qt::LeftArrow:
401 arrowPE = QStyle::PE_IndicatorArrowLeft;
402 break;
403 default:
404 arrowPE = QStyle::PE_IndicatorArrowLeft;
405 break;
406 }
407
408 style()->drawPrimitive(arrowPE, &o, painter, this);
409
410}
float value(const T *src, size_t ch)
const Params2D p
qreal v
#define EPSILON
void HSYToRGB(const qreal h, const qreal s, const qreal y, qreal *red, qreal *green, qreal *blue, qreal R, qreal G, qreal B)
void HSIToRGB(const qreal h, const qreal s, const qreal i, qreal *red, qreal *green, qreal *blue)
connect(this, SIGNAL(optionsChanged()), this, SLOT(saveOptions()))
static KoColorDisplayRendererInterface * instance()
T min(T a, T b, T c)
constexpr std::enable_if< sizeof...(values)==0, size_t >::type max()
rgba palette[MAX_PALETTE]
Definition palette.c:35
Definition flatten.c:229
void drawArrow(QPainter *painter, const QPoint &pos) override
void setColors(const KoColor minColor, const KoColor maxColor)
QPointer< KoColorDisplayRendererInterface > displayRenderer
KisSignalCompressor updateCompressor
QPoint calcArrowPos(int value)
void drawContents(QPainter *) override
void setMixMode(MIX_MODE mode)
KisHsvColorSlider(QWidget *parent=0, KoColorDisplayRendererInterface *displayRenderer=KoDumbColorDisplayRenderer::instance())