1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "boxes/edit_color_box.h"
9
10 #include "lang/lang_keys.h"
11 #include "ui/widgets/shadow.h"
12 #include "ui/widgets/input_fields.h"
13 #include "ui/ui_utility.h"
14 #include "base/platform/base_platform_info.h"
15 #include "styles/style_boxes.h"
16 #include "styles/style_media_view.h"
17
18 class EditColorBox::Picker : public TWidget {
19 public:
20 Picker(QWidget *parent, Mode mode, QColor color);
21
valueX() const22 float64 valueX() const {
23 return _x;
24 }
valueY() const25 float64 valueY() const {
26 return _y;
27 }
28
changed() const29 rpl::producer<> changed() const {
30 return _changed.events();
31 }
32 void setHSB(HSB hsb);
33 void setRGB(int red, int green, int blue);
34
35 protected:
36 void paintEvent(QPaintEvent *e);
37
38 void mousePressEvent(QMouseEvent *e);
39 void mouseMoveEvent(QMouseEvent *e);
40 void mouseReleaseEvent(QMouseEvent *e);
41
42 private:
43 void setFromColor(QColor color);
44 QCursor generateCursor();
45
46 void preparePalette();
47 void preparePaletteRGBA();
48 void preparePaletteHSL();
49 void updateCurrentPoint(QPoint localPosition);
50
51 Mode _mode;
52 QColor _topleft;
53 QColor _topright;
54 QColor _bottomleft;
55 QColor _bottomright;
56
57 QImage _palette;
58 bool _paletteInvalidated = false;
59 float64 _x = 0.;
60 float64 _y = 0.;
61
62 bool _choosing = false;
63 rpl::event_stream<> _changed;
64
65 };
66
generateCursor()67 QCursor EditColorBox::Picker::generateCursor() {
68 auto diameter = style::ConvertScale(16);
69 auto line = style::ConvertScale(1);
70 auto size = ((diameter + 2 * line) >= 32) ? 64 : 32;
71 auto cursor = QImage(QSize(size, size) * cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied);
72 cursor.setDevicePixelRatio(cRetinaFactor());
73 cursor.fill(Qt::transparent);
74 {
75 Painter p(&cursor);
76 PainterHighQualityEnabler hq(p);
77
78 p.setBrush(Qt::NoBrush);
79 auto pen = QPen(Qt::white);
80 pen.setWidth(3 * line);
81 p.setPen(pen);
82 p.drawEllipse((size - diameter) / 2, (size - diameter) / 2, diameter, diameter);
83 pen = QPen(Qt::black);
84 pen.setWidth(line);
85 p.setPen(pen);
86 p.drawEllipse((size - diameter) / 2, (size - diameter) / 2, diameter, diameter);
87 }
88 return QCursor(QPixmap::fromImage(cursor));
89 }
90
Picker(QWidget * parent,Mode mode,QColor color)91 EditColorBox::Picker::Picker(QWidget *parent, Mode mode, QColor color)
92 : TWidget(parent)
93 , _mode(mode) {
94 setCursor(generateCursor());
95
96 auto size = QSize(st::colorPickerSize, st::colorPickerSize);
97 resize(size);
98
99 _palette = QImage(size * cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied);
100
101 setFromColor(color);
102 }
103
paintEvent(QPaintEvent * e)104 void EditColorBox::Picker::paintEvent(QPaintEvent *e) {
105 Painter p(this);
106
107 preparePalette();
108
109 p.drawImage(0, 0, _palette);
110
111 auto left = anim::color(_topleft, _bottomleft, _y);
112 auto right = anim::color(_topright, _bottomright, _y);
113 auto color = anim::color(left, right, _x);
114 auto lightness = 0.2989 * color.redF() + 0.5870 * color.greenF() + 0.1140 * color.blueF();
115 auto pen = QPen((lightness > 0.6) ? QColor(0, 0, 0) : QColor(255, 255, 255));
116 pen.setWidth(st::colorPickerMarkLine);
117 p.setPen(pen);
118 p.setBrush(Qt::NoBrush);
119
120 auto x = anim::interpolate(0, width() - 1, _x);
121 auto y = anim::interpolate(0, height() - 1, _y);
122 PainterHighQualityEnabler hq(p);
123
124 p.drawEllipse(QRect(x - st::colorPickerMarkRadius, y - st::colorPickerMarkRadius, 2 * st::colorPickerMarkRadius, 2 * st::colorPickerMarkRadius));
125 }
126
mousePressEvent(QMouseEvent * e)127 void EditColorBox::Picker::mousePressEvent(QMouseEvent *e) {
128 _choosing = true;
129 updateCurrentPoint(e->pos());
130 }
131
mouseMoveEvent(QMouseEvent * e)132 void EditColorBox::Picker::mouseMoveEvent(QMouseEvent *e) {
133 if (_choosing) {
134 updateCurrentPoint(e->pos());
135 }
136 }
137
mouseReleaseEvent(QMouseEvent * e)138 void EditColorBox::Picker::mouseReleaseEvent(QMouseEvent *e) {
139 _choosing = false;
140 }
141
preparePalette()142 void EditColorBox::Picker::preparePalette() {
143 if (!_paletteInvalidated) return;
144 _paletteInvalidated = false;
145
146 if (_mode == Mode::RGBA) {
147 preparePaletteRGBA();
148 } else {
149 preparePaletteHSL();
150 }
151 _palette.setDevicePixelRatio(cRetinaFactor());
152 }
153
preparePaletteRGBA()154 void EditColorBox::Picker::preparePaletteRGBA() {
155 const auto size = _palette.width();
156 auto ints = reinterpret_cast<uint32*>(_palette.bits());
157 const auto intsAddPerLine = (_palette.bytesPerLine() - size * sizeof(uint32)) / sizeof(uint32);
158
159 constexpr auto Large = 1024 * 1024;
160 constexpr auto LargeBit = 20; // n / Large == (n >> LargeBit)
161 const auto part = Large / size;
162
163 const auto topleft = anim::shifted(_topleft);
164 const auto topright = anim::shifted(_topright);
165 const auto bottomleft = anim::shifted(_bottomleft);
166 const auto bottomright = anim::shifted(_bottomright);
167
168 auto y_accumulated = 0;
169 for (auto y = 0; y != size; ++y, y_accumulated += part) {
170 auto y_ratio = y_accumulated >> (LargeBit - 8); // (y_accumulated * 256) / Large;
171 // 0 <= y_accumulated < Large
172 // 0 <= y_ratio < 256
173
174 const auto top_ratio = 256 - y_ratio;
175 const auto bottom_ratio = y_ratio;
176
177 const auto left = anim::reshifted(bottomleft * bottom_ratio + topleft * top_ratio);
178 const auto right = anim::reshifted(bottomright * bottom_ratio + topright * top_ratio);
179
180 auto x_accumulated = 0;
181 for (auto x = 0; x != size; ++x, x_accumulated += part) {
182 auto x_ratio = x_accumulated >> (LargeBit - 8); // (x_accumulated * 256) / Large;
183 // 0 <= x_accumulated < Large
184 // 0 <= x_ratio < 256
185
186 auto left_ratio = 256 - x_ratio;
187 auto right_ratio = x_ratio;
188
189 *ints++ = anim::unshifted(left * left_ratio + right * right_ratio);
190 }
191 ints += intsAddPerLine;
192 }
193 }
194
preparePaletteHSL()195 void EditColorBox::Picker::preparePaletteHSL() {
196 const auto size = _palette.width();
197 const auto intsAddPerLine = (_palette.bytesPerLine() - size * sizeof(uint32)) / sizeof(uint32);
198 auto ints = reinterpret_cast<uint32*>(_palette.bits());
199
200 constexpr auto Large = 1024 * 1024;
201 constexpr auto LargeBit = 20; // n / Large == (n >> LargeBit)
202 const auto part = Large / size;
203
204 const auto lightness = _topleft.lightness();
205 const auto right = anim::shifted(_bottomright);
206
207 for (auto y = 0; y != size; ++y) {
208 const auto hue = y * 360 / size;
209 const auto color = QColor::fromHsl(hue, 255, lightness).toRgb();
210 const auto left = anim::shifted(anim::getPremultiplied(color));
211
212 auto x_accumulated = 0;
213 for (auto x = 0; x != size; ++x, x_accumulated += part) {
214 auto x_ratio = x_accumulated >> (LargeBit - 8); // (x_accumulated * 256) / Large;
215 // 0 <= x_accumulated < Large
216 // 0 <= x_ratio < 256
217
218 auto left_ratio = 256 - x_ratio;
219 auto right_ratio = x_ratio;
220 *ints++ = anim::unshifted(left * left_ratio + right * right_ratio);
221 }
222 ints += intsAddPerLine;
223 }
224
225 _palette = std::move(_palette).transformed(
226 QTransform(0, 1, 1, 0, 0, 0));
227 }
228
updateCurrentPoint(QPoint localPosition)229 void EditColorBox::Picker::updateCurrentPoint(QPoint localPosition) {
230 auto x = std::clamp(localPosition.x(), 0, width()) / float64(width());
231 auto y = std::clamp(localPosition.y(), 0, height()) / float64(height());
232 if (_x != x || _y != y) {
233 _x = x;
234 _y = y;
235 update();
236 _changed.fire({});
237 }
238 }
239
setHSB(HSB hsb)240 void EditColorBox::Picker::setHSB(HSB hsb) {
241 if (_mode == Mode::RGBA) {
242 _topleft = QColor(255, 255, 255);
243 _topright.setHsv(qMax(0, hsb.hue), 255, 255);
244 _topright = _topright.toRgb();
245 _bottomleft = _bottomright = QColor(0, 0, 0);
246
247 _x = std::clamp(hsb.saturation / 255., 0., 1.);
248 _y = 1. - std::clamp(hsb.brightness / 255., 0., 1.);
249 } else {
250 _topleft = _topright = QColor::fromHsl(0, 255, hsb.brightness);
251 _bottomleft = _bottomright = QColor::fromHsl(0, 0, hsb.brightness);
252
253 _x = std::clamp(hsb.hue / 360., 0., 1.);
254 _y = 1. - std::clamp(hsb.saturation / 255., 0., 1.);
255 }
256
257 _paletteInvalidated = true;
258 update();
259 }
260
setRGB(int red,int green,int blue)261 void EditColorBox::Picker::setRGB(int red, int green, int blue) {
262 setFromColor(QColor(red, green, blue));
263 }
264
setFromColor(QColor color)265 void EditColorBox::Picker::setFromColor(QColor color) {
266 if (_mode == Mode::RGBA) {
267 setHSB({ color.hsvHue(), color.hsvSaturation(), color.value() });
268 } else {
269 setHSB({ color.hslHue(), color.hslSaturation(), color.lightness() });
270 }
271 }
272
273 class EditColorBox::Slider : public TWidget {
274 public:
275 enum class Direction {
276 Horizontal,
277 Vertical,
278 };
279 enum class Type {
280 Hue,
281 Opacity,
282 Lightness
283 };
284 Slider(QWidget *parent, Direction direction, Type type, QColor color);
285
changed() const286 rpl::producer<> changed() const {
287 return _changed.events();
288 }
value() const289 float64 value() const {
290 return _value;
291 }
setValue(float64 value)292 void setValue(float64 value) {
293 _value = std::clamp(value, 0., 1.);
294 update();
295 }
296 void setHSB(HSB hsb);
297 void setRGB(int red, int green, int blue);
298 void setAlpha(int alpha);
299
300 void setLightnessLimits(int min, int max);
301
302 protected:
303 void paintEvent(QPaintEvent *e) override;
304 void resizeEvent(QResizeEvent *e) override;
305
306 void mousePressEvent(QMouseEvent *e) override;
307 void mouseMoveEvent(QMouseEvent *e) override;
308 void mouseReleaseEvent(QMouseEvent *e) override;
309
310 private:
311 float64 valueFromColor(QColor color) const;
312 float64 valueFromHue(int hue) const;
isHorizontal() const313 bool isHorizontal() const {
314 return (_direction == Direction::Horizontal);
315 }
316 void colorUpdated();
317 void prepareMinSize();
318 void generatePixmap();
319 void updatePixmapFromMask();
320 void updateCurrentPoint(QPoint localPosition);
321 [[nodiscard]] QColor applyLimits(QColor color) const;
322
323 Direction _direction = Direction::Horizontal;
324 Type _type = Type::Hue;
325
326 int _lightnessMin = 0;
327 int _lightnessMax = 255;
328
329 QColor _color;
330 float64 _value = 0;
331
332 QImage _mask;
333 QPixmap _pixmap;
334 QBrush _transparent;
335
336 bool _choosing = false;
337 rpl::event_stream<> _changed;
338
339 };
340
Slider(QWidget * parent,Direction direction,Type type,QColor color)341 EditColorBox::Slider::Slider(
342 QWidget *parent,
343 Direction direction,
344 Type type,
345 QColor color)
346 : TWidget(parent)
347 , _direction(direction)
348 , _type(type)
349 , _color(color.red(), color.green(), color.blue())
350 , _value(valueFromColor(color))
351 , _transparent((_type == Type::Opacity)
352 ? style::TransparentPlaceholder()
353 : QBrush()) {
354 prepareMinSize();
355 }
356
prepareMinSize()357 void EditColorBox::Slider::prepareMinSize() {
358 auto minSize = st::colorSliderSkip + st::colorSliderWidth + st::colorSliderSkip;
359 resize(minSize, minSize);
360 }
361
paintEvent(QPaintEvent * e)362 void EditColorBox::Slider::paintEvent(QPaintEvent *e) {
363 Painter p(this);
364 auto to = rect().marginsRemoved(QMargins(st::colorSliderSkip, st::colorSliderSkip, st::colorSliderSkip, st::colorSliderSkip));
365 Ui::Shadow::paint(p, to, width(), st::defaultRoundShadow);
366 if (_type == Type::Opacity) {
367 p.fillRect(to, _transparent);
368 }
369 p.drawPixmap(to, _pixmap, _pixmap.rect());
370 if (isHorizontal()) {
371 auto x = st::colorSliderSkip + qRound(_value * to.width());
372 st::colorSliderArrowTop.paint(p, x - st::colorSliderArrowTop.width() / 2, 0, width());
373 st::colorSliderArrowBottom.paint(p, x - st::colorSliderArrowBottom.width() / 2, height() - st::colorSliderArrowBottom.height(), width());
374 } else {
375 auto y = st::colorSliderSkip + qRound(_value * to.height());
376 st::colorSliderArrowLeft.paint(p, 0, y - st::colorSliderArrowLeft.height() / 2, width());
377 st::colorSliderArrowRight.paint(p, width() - st::colorSliderArrowRight.width(), y - st::colorSliderArrowRight.height() / 2, width());
378 }
379 }
380
resizeEvent(QResizeEvent * e)381 void EditColorBox::Slider::resizeEvent(QResizeEvent *e) {
382 generatePixmap();
383 update();
384 }
385
mousePressEvent(QMouseEvent * e)386 void EditColorBox::Slider::mousePressEvent(QMouseEvent *e) {
387 _choosing = true;
388 updateCurrentPoint(e->pos());
389 }
390
mouseMoveEvent(QMouseEvent * e)391 void EditColorBox::Slider::mouseMoveEvent(QMouseEvent *e) {
392 if (_choosing) {
393 updateCurrentPoint(e->pos());
394 }
395 }
396
mouseReleaseEvent(QMouseEvent * e)397 void EditColorBox::Slider::mouseReleaseEvent(QMouseEvent *e) {
398 _choosing = false;
399 }
400
generatePixmap()401 void EditColorBox::Slider::generatePixmap() {
402 auto size = (isHorizontal() ? width() : height()) * cIntRetinaFactor();
403 auto image = QImage(size, cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied);
404 image.setDevicePixelRatio(cRetinaFactor());
405 auto ints = reinterpret_cast<uint32*>(image.bits());
406 auto intsPerLine = image.bytesPerLine() / sizeof(uint32);
407 auto intsPerLineAdded = intsPerLine - size;
408
409 constexpr auto Large = 1024 * 1024;
410 constexpr auto LargeBit = 20; // n / Large == (n >> LargeBit)
411 auto part = Large / size;
412
413 if (_type == Type::Hue) {
414 for (auto x = 0; x != size; ++x) {
415 const auto color = QColor::fromHsv(x * 360 / size, 255, 255);
416 const auto value = anim::getPremultiplied(color.toRgb());
417 for (auto y = 0; y != cIntRetinaFactor(); ++y) {
418 ints[y * intsPerLine] = value;
419 }
420 ++ints;
421 }
422 if (!isHorizontal()) {
423 image = std::move(image).transformed(QTransform(0, -1, 1, 0, 0, 0));
424 }
425 _pixmap = Ui::PixmapFromImage(std::move(image));
426 } else if (_type == Type::Opacity) {
427 auto color = anim::shifted(QColor(255, 255, 255, 255));
428 auto transparent = anim::shifted(QColor(255, 255, 255, 0));
429 for (auto y = 0; y != cIntRetinaFactor(); ++y) {
430 auto x_accumulated = 0;
431 for (auto x = 0; x != size; ++x, x_accumulated += part) {
432 auto x_ratio = x_accumulated >> (LargeBit - 8);
433 // 0 <= x_accumulated < Large
434 // 0 <= x_ratio < 256
435
436 *ints++ = anim::unshifted(color * x_ratio + transparent * (256 - x_ratio));
437 }
438 ints += intsPerLineAdded;
439 }
440 if (!isHorizontal()) {
441 image = std::move(image).transformed(QTransform(0, -1, 1, 0, 0, 0));
442 }
443 _mask = std::move(image);
444 updatePixmapFromMask();
445 } else {
446 const auto range = _lightnessMax - _lightnessMin;
447 for (auto x = 0; x != size; ++x) {
448 const auto color = QColor::fromHsl(
449 _color.hslHue(),
450 _color.hslSaturation(),
451 _lightnessMin + x * range / size);
452 const auto value = anim::getPremultiplied(color.toRgb());
453 for (auto y = 0; y != cIntRetinaFactor(); ++y) {
454 ints[y * intsPerLine] = value;
455 }
456 ++ints;
457 }
458 if (!isHorizontal()) {
459 image = std::move(image).transformed(QTransform(0, -1, 1, 0, 0, 0));
460 }
461 _pixmap = Ui::PixmapFromImage(std::move(image));
462 }
463 }
464
setHSB(HSB hsb)465 void EditColorBox::Slider::setHSB(HSB hsb) {
466 if (_type == Type::Hue) {
467 // hue == 360 converts to 0 if done in general way
468 _value = valueFromHue(hsb.hue);
469 update();
470 } else if (_type == Type::Opacity) {
471 _color.setHsv(hsb.hue, hsb.saturation, hsb.brightness);
472 colorUpdated();
473 } else {
474 _color.setHsl(
475 hsb.hue,
476 hsb.saturation,
477 std::clamp(hsb.brightness, _lightnessMin, _lightnessMax));
478 colorUpdated();
479 }
480 }
481
setRGB(int red,int green,int blue)482 void EditColorBox::Slider::setRGB(int red, int green, int blue) {
483 _color = applyLimits(QColor(red, green, blue));
484 colorUpdated();
485 }
486
colorUpdated()487 void EditColorBox::Slider::colorUpdated() {
488 if (_type == Type::Hue) {
489 _value = valueFromColor(_color);
490 } else if (!_mask.isNull()) {
491 updatePixmapFromMask();
492 } else {
493 _value = valueFromColor(_color);
494 generatePixmap();
495 }
496 update();
497 }
498
valueFromColor(QColor color) const499 float64 EditColorBox::Slider::valueFromColor(QColor color) const {
500 return (_type == Type::Hue)
501 ? valueFromHue(color.hsvHue())
502 : (_type == Type::Opacity)
503 ? color.alphaF()
504 : std::clamp(
505 ((color.lightness() - _lightnessMin)
506 / float64(_lightnessMax - _lightnessMin)),
507 0.,
508 1.);
509 }
510
valueFromHue(int hue) const511 float64 EditColorBox::Slider::valueFromHue(int hue) const {
512 return (1. - std::clamp(hue, 0, 360) / 360.);
513 }
514
setAlpha(int alpha)515 void EditColorBox::Slider::setAlpha(int alpha) {
516 if (_type == Type::Opacity) {
517 _value = std::clamp(alpha, 0, 255) / 255.;
518 update();
519 }
520 }
521
setLightnessLimits(int min,int max)522 void EditColorBox::Slider::setLightnessLimits(int min, int max) {
523 Expects(max > min);
524
525 _lightnessMin = min;
526 _lightnessMax = max;
527 _color = applyLimits(_color);
528 colorUpdated();
529 }
530
updatePixmapFromMask()531 void EditColorBox::Slider::updatePixmapFromMask() {
532 _pixmap = Ui::PixmapFromImage(style::colorizeImage(_mask, _color));
533 }
534
updateCurrentPoint(QPoint localPosition)535 void EditColorBox::Slider::updateCurrentPoint(QPoint localPosition) {
536 auto coord = (isHorizontal() ? localPosition.x() : localPosition.y()) - st::colorSliderSkip;
537 auto maximum = (isHorizontal() ? width() : height()) - 2 * st::colorSliderSkip;
538 auto value = std::clamp(coord, 0, maximum) / float64(maximum);
539 if (_value != value) {
540 _value = value;
541 update();
542 _changed.fire({});
543 }
544 }
545
applyLimits(QColor color) const546 QColor EditColorBox::Slider::applyLimits(QColor color) const {
547 if (_type != Type::Lightness) {
548 return color;
549 }
550
551 const auto lightness = color.lightness();
552 const auto clamped = std::clamp(lightness, _lightnessMin, _lightnessMax);
553 if (clamped == lightness) {
554 return color;
555 }
556 return QColor::fromHsl(color.hslHue(), color.hslSaturation(), clamped);
557 }
558
559 class EditColorBox::Field : public Ui::MaskedInputField {
560 public:
561 Field(QWidget *parent, const style::InputField &st, const QString &placeholder, int limit, const QString &units = QString());
562
value() const563 int value() const {
564 return getLastText().toInt();
565 }
566
setTextWithFocus(const QString & text)567 void setTextWithFocus(const QString &text) {
568 setText(text);
569 if (hasFocus()) {
570 selectAll();
571 }
572 }
573
574 protected:
575 void correctValue(const QString &was, int wasCursor, QString &now, int &nowCursor) override;
576 void paintAdditionalPlaceholder(Painter &p) override;
577
578 void wheelEvent(QWheelEvent *e) override;
579 void keyPressEvent(QKeyEvent *e) override;
580
581 private:
582 void changeValue(int delta);
583
584 QString _placeholder, _units;
585 int _limit = 0;
586 int _digitLimit = 1;
587 int _wheelDelta = 0;
588
589 };
590
Field(QWidget * parent,const style::InputField & st,const QString & placeholder,int limit,const QString & units)591 EditColorBox::Field::Field(QWidget *parent, const style::InputField &st, const QString &placeholder, int limit, const QString &units) : Ui::MaskedInputField(parent, st)
592 , _placeholder(placeholder)
593 , _units(units)
594 , _limit(limit)
595 , _digitLimit(QString::number(_limit).size()) {
596 }
597
correctValue(const QString & was,int wasCursor,QString & now,int & nowCursor)598 void EditColorBox::Field::correctValue(const QString &was, int wasCursor, QString &now, int &nowCursor) {
599 QString newText;
600 int oldPos(nowCursor), newPos(-1), oldLen(now.length());
601
602 newText.reserve(oldLen);
603 for (int i = 0; i < oldLen; ++i) {
604 if (i == oldPos) {
605 newPos = newText.length();
606 }
607
608 QChar ch(now[i]);
609 if (ch.isDigit()) {
610 newText += ch;
611 }
612 if (newText.size() >= _digitLimit) {
613 break;
614 }
615 }
616 if (newPos < 0 || newPos > newText.size()) {
617 newPos = newText.size();
618 }
619 if (newText.toInt() > _limit) {
620 newText = QString::number(_limit);
621 newPos = newText.size();
622 }
623 if (newText != now) {
624 now = newText;
625 setText(now);
626 startPlaceholderAnimation();
627 nowCursor = -1;
628 }
629 if (newPos != nowCursor) {
630 nowCursor = newPos;
631 setCursorPosition(nowCursor);
632 }
633 }
634
paintAdditionalPlaceholder(Painter & p)635 void EditColorBox::Field::paintAdditionalPlaceholder(Painter &p) {
636 p.setFont(_st.font);
637 p.setPen(_st.placeholderFg);
638 auto inner = QRect(_st.textMargins.right(), _st.textMargins.top(), width() - 2 * _st.textMargins.right(), height() - _st.textMargins.top() - _st.textMargins.bottom());
639 p.drawText(inner, _placeholder, style::al_topleft);
640 if (!_units.isEmpty()) {
641 p.drawText(inner, _units, style::al_topright);
642 }
643 }
644
wheelEvent(QWheelEvent * e)645 void EditColorBox::Field::wheelEvent(QWheelEvent *e) {
646 if (!hasFocus()) {
647 return;
648 }
649
650 auto deltaX = e->angleDelta().x(), deltaY = e->angleDelta().y();
651 if (Platform::IsMac()) {
652 deltaY *= -1;
653 } else {
654 deltaX *= -1;
655 }
656 _wheelDelta += (qAbs(deltaX) > qAbs(deltaY)) ? deltaX : deltaY;
657
658 constexpr auto step = 5;
659 if (auto delta = _wheelDelta / step) {
660 _wheelDelta -= delta * step;
661 changeValue(delta);
662 }
663 }
664
changeValue(int delta)665 void EditColorBox::Field::changeValue(int delta) {
666 auto currentValue = value();
667 auto newValue = std::clamp(currentValue + delta, 0, _limit);
668 if (newValue != currentValue) {
669 setText(QString::number(newValue));
670 setFocus();
671 selectAll();
672 changed();
673 }
674 }
675
keyPressEvent(QKeyEvent * e)676 void EditColorBox::Field::keyPressEvent(QKeyEvent *e) {
677 if (e->key() == Qt::Key_Up) {
678 changeValue(1);
679 } else if (e->key() == Qt::Key_Down) {
680 changeValue(-1);
681 } else {
682 MaskedInputField::keyPressEvent(e);
683 }
684 }
685
686 class EditColorBox::ResultField : public Ui::MaskedInputField {
687 public:
688 ResultField(QWidget *parent, const style::InputField &st);
689
setTextWithFocus(const QString & text)690 void setTextWithFocus(const QString &text) {
691 setText(text);
692 if (hasFocus()) {
693 selectAll();
694 }
695 }
696
697 protected:
698 void correctValue(const QString &was, int wasCursor, QString &now, int &nowCursor) override;
699 void paintAdditionalPlaceholder(Painter &p) override;
700
701 };
702
ResultField(QWidget * parent,const style::InputField & st)703 EditColorBox::ResultField::ResultField(QWidget *parent, const style::InputField &st) : Ui::MaskedInputField(parent, st) {
704 }
705
correctValue(const QString & was,int wasCursor,QString & now,int & nowCursor)706 void EditColorBox::ResultField::correctValue(const QString &was, int wasCursor, QString &now, int &nowCursor) {
707 QString newText;
708 int oldPos(nowCursor), newPos(-1), oldLen(now.length());
709
710 newText.reserve(oldLen);
711 for (int i = 0; i < oldLen; ++i) {
712 if (i == oldPos) {
713 newPos = newText.length();
714 }
715
716 QChar ch(now[i]);
717 auto code = ch.unicode();
718 if ((code >= '0' && code <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')) {
719 newText += ch;
720 }
721 if (newText.size() >= 8) {
722 break;
723 }
724 }
725 if (newPos < 0 || newPos > newText.size()) {
726 newPos = newText.size();
727 }
728 if (newText != now) {
729 now = newText;
730 setText(now);
731 startPlaceholderAnimation();
732 nowCursor = -1;
733 }
734 if (newPos != nowCursor) {
735 nowCursor = newPos;
736 setCursorPosition(nowCursor);
737 }
738 }
739
paintAdditionalPlaceholder(Painter & p)740 void EditColorBox::ResultField::paintAdditionalPlaceholder(Painter &p) {
741 p.setFont(_st.font);
742 p.setPen(_st.placeholderFg);
743 p.drawText(QRect(_st.textMargins.right(), _st.textMargins.top(), width(), height() - _st.textMargins.top() - _st.textMargins.bottom()), "#", style::al_topleft);
744 }
745
EditColorBox(QWidget *,const QString & title,Mode mode,QColor current)746 EditColorBox::EditColorBox(
747 QWidget*,
748 const QString &title,
749 Mode mode,
750 QColor current)
751 : BoxContent()
752 , _title(title)
753 , _mode(mode)
754 , _picker(this, mode, current)
755 , _hueField(this, st::colorValueInput, "H", 360, QString() + QChar(176)) // degree character
756 , _saturationField(this, st::colorValueInput, "S", 100, "%")
757 , _brightnessField(this, st::colorValueInput, (mode == Mode::RGBA) ? "B" : "L", 100, "%")
758 , _redField(this, st::colorValueInput, "R", 255)
759 , _greenField(this, st::colorValueInput, "G", 255)
760 , _blueField(this, st::colorValueInput, "B", 255)
761 , _result(this, st::colorResultInput)
762 , _transparent(style::TransparentPlaceholder())
763 , _current(current)
764 , _new(current) {
765 if (_mode == Mode::RGBA) {
766 _hueSlider.create(
767 this,
768 Slider::Direction::Vertical,
769 Slider::Type::Hue,
770 current);
771 _opacitySlider.create(
772 this,
773 Slider::Direction::Horizontal,
774 Slider::Type::Opacity,
775 current);
776 } else if (_mode == Mode::HSL) {
777 _lightnessSlider.create(
778 this,
779 Slider::Direction::Horizontal,
780 Slider::Type::Lightness,
781 current);
782 }
783 }
784
setLightnessLimits(int min,int max)785 void EditColorBox::setLightnessLimits(int min, int max) {
786 Expects(_mode == Mode::HSL);
787
788 _lightnessMin = min;
789 _lightnessMax = max;
790 _lightnessSlider->setLightnessLimits(min, max);
791
792 const auto adjusted = applyLimits(_new);
793 if (_new != adjusted) {
794 updateFromColor(adjusted);
795 }
796 }
797
prepare()798 void EditColorBox::prepare() {
799 setTitle(rpl::single(_title));
800
801 const auto hsbChanged = [=] { updateFromHSBFields(); };
802 const auto rgbChanged = [=] { updateFromRGBFields(); };
803 connect(_hueField, &Ui::MaskedInputField::changed, hsbChanged);
804 connect(_saturationField, &Ui::MaskedInputField::changed, hsbChanged);
805 connect(_brightnessField, &Ui::MaskedInputField::changed, hsbChanged);
806 connect(_redField, &Ui::MaskedInputField::changed, rgbChanged);
807 connect(_greenField, &Ui::MaskedInputField::changed, rgbChanged);
808 connect(_blueField, &Ui::MaskedInputField::changed, rgbChanged);
809 connect(_result, &Ui::MaskedInputField::changed, [=] {
810 updateFromResultField();
811 });
812
813 const auto submitted = [=] { fieldSubmitted(); };
814 connect(_hueField, &Ui::MaskedInputField::submitted, submitted);
815 connect(_saturationField, &Ui::MaskedInputField::submitted, submitted);
816 connect(_brightnessField, &Ui::MaskedInputField::submitted, submitted);
817 connect(_redField, &Ui::MaskedInputField::submitted, submitted);
818 connect(_greenField, &Ui::MaskedInputField::submitted, submitted);
819 connect(_blueField, &Ui::MaskedInputField::submitted, submitted);
820 connect(_result, &Ui::MaskedInputField::submitted, submitted);
821
822 addButton(tr::lng_settings_save(), [=] { saveColor(); });
823 addButton(tr::lng_cancel(), [=] { closeBox(); });
824
825 auto height = st::colorEditSkip + st::colorPickerSize + st::colorEditSkip + st::colorSliderWidth + st::colorEditSkip;
826 setDimensions(st::colorEditWidth, height);
827
828 rpl::merge(
829 _picker->changed(),
830 (_hueSlider ? _hueSlider->changed() : rpl::never<>()),
831 (_opacitySlider ? _opacitySlider->changed() : rpl::never<>()),
832 (_lightnessSlider ? _lightnessSlider->changed() : rpl::never<>())
833 ) | rpl::start_with_next([=] {
834 updateFromControls();
835 }, lifetime());
836
837 boxClosing() | rpl::start_with_next([=] {
838 if (_cancelCallback) {
839 _cancelCallback();
840 }
841 }, lifetime());
842
843 updateRGBFields();
844 updateHSBFields();
845 updateResultField();
846 update();
847 }
848
setInnerFocus()849 void EditColorBox::setInnerFocus() {
850 _result->setFocus();
851 _result->selectAll();
852 }
853
fieldSubmitted()854 void EditColorBox::fieldSubmitted() {
855 Ui::MaskedInputField *fields[] = {
856 _hueField,
857 _saturationField,
858 _brightnessField,
859 _redField,
860 _greenField,
861 _blueField,
862 _result
863 };
864 for (auto i = 0, count = int(base::array_size(fields)); i + 1 != count; ++i) {
865 if (fields[i]->hasFocus()) {
866 fields[i + 1]->setFocus();
867 fields[i + 1]->selectAll();
868 return;
869 }
870 }
871 if (_result->hasFocus()) {
872 saveColor();
873 }
874 }
875
saveColor()876 void EditColorBox::saveColor() {
877 const auto weak = Ui::MakeWeak(this);
878 _cancelCallback = nullptr;
879 if (_saveCallback) {
880 _saveCallback(_new.toRgb());
881 }
882 if (weak) {
883 closeBox();
884 }
885 }
886
updateHSBFields()887 void EditColorBox::updateHSBFields() {
888 const auto hsb = hsbFromControls();
889 _hueField->setTextWithFocus(QString::number(hsb.hue));
890 _saturationField->setTextWithFocus(QString::number(percentFromByte(hsb.saturation)));
891 _brightnessField->setTextWithFocus(QString::number(percentFromByte(hsb.brightness)));
892 }
893
updateRGBFields()894 void EditColorBox::updateRGBFields() {
895 _redField->setTextWithFocus(QString::number(_new.red()));
896 _greenField->setTextWithFocus(QString::number(_new.green()));
897 _blueField->setTextWithFocus(QString::number(_new.blue()));
898 }
899
updateResultField()900 void EditColorBox::updateResultField() {
901 auto text = QString();
902 auto addHex = [&text](int value) {
903 if (value >= 0 && value <= 9) {
904 text.append('0' + value);
905 } else if (value >= 10 && value <= 15) {
906 text.append('a' + (value - 10));
907 }
908 };
909 auto addValue = [addHex](int value) {
910 addHex(value / 16);
911 addHex(value % 16);
912 };
913 addValue(_new.red());
914 addValue(_new.green());
915 addValue(_new.blue());
916 if (_new.alpha() != 255) {
917 addValue(_new.alpha());
918 }
919 _result->setTextWithFocus(text);
920 }
921
resizeEvent(QResizeEvent * e)922 void EditColorBox::resizeEvent(QResizeEvent *e) {
923 const auto fullwidth = _picker->width()
924 + ((_mode == Mode::RGBA)
925 ? (2 * (st::colorEditSkip - st::colorSliderSkip) + _hueSlider->width())
926 : (2 * st::colorEditSkip))
927 + st::colorSampleSize.width();
928 auto left = (width() - fullwidth) / 2;
929 _picker->moveToLeft(left, st::colorEditSkip);
930 if (_hueSlider) {
931 _hueSlider->setGeometryToLeft(_picker->x() + _picker->width() + st::colorEditSkip - st::colorSliderSkip, st::colorEditSkip - st::colorSliderSkip, _hueSlider->width(), st::colorPickerSize + 2 * st::colorSliderSkip);
932 }
933 if (_opacitySlider) {
934 _opacitySlider->setGeometryToLeft(_picker->x() - st::colorSliderSkip, _picker->y() + _picker->height() + st::colorEditSkip - st::colorSliderSkip, _picker->width() + 2 * st::colorSliderSkip, _opacitySlider->height());
935 }
936 if (_lightnessSlider) {
937 _lightnessSlider->setGeometryToLeft(_picker->x() - st::colorSliderSkip, _picker->y() + _picker->height() + st::colorEditSkip - st::colorSliderSkip, _picker->width() + 2 * st::colorSliderSkip, _lightnessSlider->height());
938 }
939 const auto fieldLeft = (_mode == Mode::RGBA)
940 ? (_hueSlider->x() + _hueSlider->width() + st::colorEditSkip - st::colorSliderSkip)
941 : (_picker->x() + _picker->width() + st::colorEditSkip);
942 const auto addWidth = (_mode == Mode::RGBA) ? 0 : st::colorEditSkip;
943 const auto fieldWidth = st::colorSampleSize.width() + addWidth;
944 const auto fieldHeight = _hueField->height();
945 _newRect = QRect(fieldLeft, st::colorEditSkip, fieldWidth, st::colorSampleSize.height());
946 _currentRect = _newRect.translated(0, st::colorSampleSize.height());
947 _hueField->setGeometryToLeft(fieldLeft, _currentRect.y() + _currentRect.height() + st::colorFieldSkip, fieldWidth, fieldHeight);
948 _saturationField->setGeometryToLeft(fieldLeft, _hueField->y() + _hueField->height(), fieldWidth, fieldHeight);
949 _brightnessField->setGeometryToLeft(fieldLeft, _saturationField->y() + _saturationField->height(), fieldWidth, fieldHeight);
950 _redField->setGeometryToLeft(fieldLeft, _brightnessField->y() + _brightnessField->height() + st::colorFieldSkip, fieldWidth, fieldHeight);
951 _greenField->setGeometryToLeft(fieldLeft, _redField->y() + _redField->height(), fieldWidth, fieldHeight);
952 _blueField->setGeometryToLeft(fieldLeft, _greenField->y() + _greenField->height(), fieldWidth, fieldHeight);
953 const auto resultDelta = (_mode == Mode::RGBA)
954 ? (st::colorEditSkip + st::colorSliderWidth)
955 : 0;
956 const auto resultBottom = (_mode == Mode::RGBA)
957 ? (_opacitySlider->y() + _opacitySlider->height())
958 : (_lightnessSlider->y() + _lightnessSlider->height());
959 _result->setGeometryToLeft(fieldLeft - resultDelta, resultBottom - st::colorSliderSkip - _result->height(), fieldWidth + resultDelta, fieldHeight);
960 }
961
paintEvent(QPaintEvent * e)962 void EditColorBox::paintEvent(QPaintEvent *e) {
963 BoxContent::paintEvent(e);
964
965 Painter p(this);
966 Ui::Shadow::paint(p, _picker->geometry(), width(), st::defaultRoundShadow);
967
968 Ui::Shadow::paint(p, QRect(_newRect.x(), _newRect.y(), _newRect.width(), _newRect.height() + _currentRect.height()), width(), st::defaultRoundShadow);
969 if (_new.alphaF() < 1.) {
970 p.fillRect(myrtlrect(_newRect), _transparent);
971 }
972 p.fillRect(myrtlrect(_newRect), _new);
973 if (_current.alphaF() < 1.) {
974 p.fillRect(myrtlrect(_currentRect), _transparent);
975 }
976 p.fillRect(myrtlrect(_currentRect), _current);
977 }
978
mousePressEvent(QMouseEvent * e)979 void EditColorBox::mousePressEvent(QMouseEvent *e) {
980 if (myrtlrect(_currentRect).contains(e->pos())) {
981 updateFromColor(_current);
982 }
983 }
984
hsbFromControls() const985 EditColorBox::HSB EditColorBox::hsbFromControls() const {
986 const auto hue = (_mode == Mode::RGBA)
987 ? qRound((1. - _hueSlider->value()) * 360)
988 : qRound(_picker->valueX() * 360);
989 const auto saturation = (_mode == Mode::RGBA)
990 ? qRound(_picker->valueX() * 255)
991 : qRound((1. - _picker->valueY()) * 255);
992 const auto brightness = (_mode == Mode::RGBA)
993 ? qRound((1. - _picker->valueY()) * 255)
994 : (_lightnessMin
995 + qRound(_lightnessSlider->value()
996 * (_lightnessMax - _lightnessMin)));
997 return { hue, saturation, brightness };
998 }
999
applyLimits(QColor color) const1000 QColor EditColorBox::applyLimits(QColor color) const {
1001 if (_mode != Mode::HSL) {
1002 return color;
1003 }
1004
1005 const auto lightness = color.lightness();
1006 const auto clamped = std::clamp(lightness, _lightnessMin, _lightnessMax);
1007 if (clamped == lightness) {
1008 return color;
1009 }
1010 return QColor::fromHsl(color.hslHue(), color.hslSaturation(), clamped);
1011 }
1012
updateFromColor(QColor color)1013 void EditColorBox::updateFromColor(QColor color) {
1014 _new = applyLimits(color);
1015 updateControlsFromColor();
1016 updateRGBFields();
1017 updateHSBFields();
1018 updateResultField();
1019 update();
1020 }
1021
updateFromControls()1022 void EditColorBox::updateFromControls() {
1023 const auto hsb = hsbFromControls();
1024 const auto alpha = _opacitySlider
1025 ? qRound(_opacitySlider->value() * 255)
1026 : 255;
1027 setHSB(hsb, alpha);
1028 updateHSBFields();
1029 updateControlsFromHSB(hsb);
1030 }
1031
updateFromHSBFields()1032 void EditColorBox::updateFromHSBFields() {
1033 const auto hue = _hueField->value();
1034 const auto saturation = percentToByte(_saturationField->value());
1035 const auto brightness = std::clamp(
1036 percentToByte(_brightnessField->value()),
1037 _lightnessMin,
1038 _lightnessMax);
1039 const auto alpha = _opacitySlider
1040 ? qRound(_opacitySlider->value() * 255)
1041 : 255;
1042 setHSB({ hue, saturation, brightness }, alpha);
1043 updateControlsFromHSB({ hue, saturation, brightness });
1044 }
1045
updateFromRGBFields()1046 void EditColorBox::updateFromRGBFields() {
1047 const auto red = _redField->value();
1048 const auto blue = _blueField->value();
1049 const auto green = _greenField->value();
1050 const auto alpha = _opacitySlider
1051 ? qRound(_opacitySlider->value() * 255)
1052 : 255;
1053 setRGB(red, green, blue, alpha);
1054 updateResultField();
1055 }
1056
updateFromResultField()1057 void EditColorBox::updateFromResultField() {
1058 auto text = _result->getLastText();
1059 if (text.size() != 6 && text.size() != 8) {
1060 return;
1061 }
1062
1063 auto fromHex = [](QChar hex) {
1064 auto code = hex.unicode();
1065 if (code >= 'A' && code <= 'F') {
1066 return (code - 'A' + 10);
1067 } else if (code >= 'a' && code <= 'f') {
1068 return (code - 'a' + 10);
1069 }
1070 return code - '0';
1071 };
1072 auto fromChars = [fromHex](QChar a, QChar b) {
1073 return fromHex(a) * 0x10 + fromHex(b);
1074 };
1075 auto red = fromChars(text[0], text[1]);
1076 auto green = fromChars(text[2], text[3]);
1077 auto blue = fromChars(text[4], text[5]);
1078 auto alpha = (text.size() == 8) ? fromChars(text[6], text[7]) : 255;
1079 setRGB(red, green, blue, alpha);
1080 updateRGBFields();
1081 }
1082
updateControlsFromHSB(HSB hsb)1083 void EditColorBox::updateControlsFromHSB(HSB hsb) {
1084 _picker->setHSB(hsb);
1085 if (_hueSlider) {
1086 _hueSlider->setHSB(hsb);
1087 }
1088 if (_opacitySlider) {
1089 _opacitySlider->setHSB(hsb);
1090 }
1091 if (_lightnessSlider) {
1092 _lightnessSlider->setHSB(hsb);
1093 }
1094 }
1095
updateControlsFromColor()1096 void EditColorBox::updateControlsFromColor() {
1097 auto red = _new.red();
1098 auto green = _new.green();
1099 auto blue = _new.blue();
1100 auto alpha = _new.alpha();
1101 _picker->setRGB(red, green, blue);
1102 if (_hueSlider) {
1103 _hueSlider->setRGB(red, green, blue);
1104 }
1105 if (_opacitySlider) {
1106 _opacitySlider->setRGB(red, green, blue);
1107 _opacitySlider->setAlpha(alpha);
1108 }
1109 if (_lightnessSlider) {
1110 _lightnessSlider->setRGB(red, green, blue);
1111 }
1112 }
1113
setHSB(HSB hsb,int alpha)1114 void EditColorBox::setHSB(HSB hsb, int alpha) {
1115 if (_mode == Mode::RGBA) {
1116 _new.setHsv(hsb.hue, hsb.saturation, hsb.brightness, alpha);
1117 } else {
1118 _new.setHsl(hsb.hue, hsb.saturation, hsb.brightness, alpha);
1119 }
1120 updateRGBFields();
1121 updateResultField();
1122 update();
1123 }
1124
setRGB(int red,int green,int blue,int alpha)1125 void EditColorBox::setRGB(int red, int green, int blue, int alpha) {
1126 _new = applyLimits(QColor(red, green, blue, alpha));
1127 updateControlsFromColor();
1128 updateHSBFields();
1129 update();
1130 }
1131