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