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 "ui/chat/chat_theme.h"
9 
10 #include "ui/image/image_prepare.h"
11 #include "ui/ui_utility.h"
12 #include "ui/chat/message_bubble.h"
13 #include "ui/chat/chat_style.h"
14 #include "ui/style/style_core_palette.h"
15 #include "ui/style/style_palette_colorizer.h"
16 
17 #include <crl/crl_async.h>
18 #include <QtGui/QGuiApplication>
19 
20 namespace Ui {
21 namespace {
22 
23 constexpr auto kMaxChatEntryHistorySize = 50;
24 constexpr auto kCacheBackgroundTimeout = 1 * crl::time(1000);
25 constexpr auto kCacheBackgroundFastTimeout = crl::time(200);
26 constexpr auto kBackgroundFadeDuration = crl::time(200);
27 constexpr auto kMinimumTiledSize = 512;
28 constexpr auto kMaxSize = 2960;
29 constexpr auto kMaxContrastValue = 21.;
30 constexpr auto kMinAcceptableContrast = 1.14;// 4.5;
31 
DefaultBackgroundColor()32 [[nodiscard]] QColor DefaultBackgroundColor() {
33 	return QColor(213, 223, 233);
34 }
35 
ComputeRealRotation(const CacheBackgroundRequest & request)36 [[nodiscard]] int ComputeRealRotation(const CacheBackgroundRequest &request) {
37 	if (request.background.colors.size() < 3) {
38 		return request.background.gradientRotation;
39 	}
40 	const auto doubled = (request.background.gradientRotation
41 		+ request.gradientRotationAdd) % 720;
42 	return (((doubled % 2) ? (doubled - 45) : doubled) / 2) % 360;
43 }
44 
ComputeRealProgress(const CacheBackgroundRequest & request)45 [[nodiscard]] double ComputeRealProgress(
46 		const CacheBackgroundRequest &request) {
47 	if (request.background.colors.size() < 3) {
48 		return 1.;
49 	}
50 	const auto doubled = (request.background.gradientRotation
51 		+ request.gradientRotationAdd) % 720;
52 	return (doubled % 2) ? 0.5 : 1.;
53 }
54 
CacheBackground(const CacheBackgroundRequest & request)55 [[nodiscard]] CacheBackgroundResult CacheBackground(
56 		const CacheBackgroundRequest &request) {
57 	Expects(!request.area.isEmpty());
58 
59 	const auto gradient = request.background.gradientForFill.isNull()
60 		? QImage()
61 		: (request.gradientRotationAdd != 0)
62 		? Images::GenerateGradient(
63 			request.background.gradientForFill.size(),
64 			request.background.colors,
65 			ComputeRealRotation(request),
66 			ComputeRealProgress(request))
67 		: request.background.gradientForFill;
68 	if (request.background.isPattern
69 		|| request.background.tile
70 		|| request.background.prepared.isNull()) {
71 		auto result = gradient.isNull()
72 			? QImage(
73 				request.area * style::DevicePixelRatio(),
74 				QImage::Format_ARGB32_Premultiplied)
75 			: gradient.scaled(
76 				request.area * style::DevicePixelRatio(),
77 				Qt::IgnoreAspectRatio,
78 				Qt::SmoothTransformation);
79 		result.setDevicePixelRatio(style::DevicePixelRatio());
80 		if (!request.background.prepared.isNull()) {
81 			QPainter p(&result);
82 			if (!gradient.isNull()) {
83 				if (request.background.patternOpacity >= 0.) {
84 					p.setCompositionMode(QPainter::CompositionMode_SoftLight);
85 					p.setOpacity(request.background.patternOpacity);
86 				} else {
87 					p.setCompositionMode(
88 						QPainter::CompositionMode_DestinationIn);
89 				}
90 			}
91 			const auto tiled = request.background.isPattern
92 				? request.background.prepared.scaled(
93 					request.area.height() * style::DevicePixelRatio(),
94 					request.area.height() * style::DevicePixelRatio(),
95 					Qt::KeepAspectRatio,
96 					Qt::SmoothTransformation)
97 				: request.background.preparedForTiled;
98 			const auto w = tiled.width() / float(style::DevicePixelRatio());
99 			const auto h = tiled.height() / float(style::DevicePixelRatio());
100 			const auto cx = int(std::ceil(request.area.width() / w));
101 			const auto cy = int(std::ceil(request.area.height() / h));
102 			const auto rows = cy;
103 			const auto cols = request.background.isPattern
104 				? (((cx / 2) * 2) + 1)
105 				: cx;
106 			const auto xshift = request.background.isPattern
107 				? (request.area.width() - cols * w) / 2
108 				: 0;
109 			for (auto y = 0; y != rows; ++y) {
110 				for (auto x = 0; x != cols; ++x) {
111 					p.drawImage(QPointF(xshift + x * w, y * h), tiled);
112 				}
113 			}
114 			if (!gradient.isNull()
115 				&& request.background.patternOpacity < 0.
116 				&& request.background.patternOpacity > -1.) {
117 				p.setCompositionMode(QPainter::CompositionMode_SourceOver);
118 				p.setOpacity(1. + request.background.patternOpacity);
119 				p.fillRect(QRect(QPoint(), request.area), Qt::black);
120 			}
121 		}
122 		return {
123 			.image = std::move(result).convertToFormat(
124 				QImage::Format_ARGB32_Premultiplied),
125 			.gradient = gradient,
126 			.area = request.area,
127 			.waitingForNegativePattern
128 				= request.background.waitingForNegativePattern()
129 		};
130 	} else {
131 		const auto rects = ComputeChatBackgroundRects(
132 			request.area,
133 			request.background.prepared.size());
134 		auto result = request.background.prepared.copy(rects.from).scaled(
135 			rects.to.width() * style::DevicePixelRatio(),
136 			rects.to.height() * style::DevicePixelRatio(),
137 			Qt::IgnoreAspectRatio,
138 			Qt::SmoothTransformation);
139 		result.setDevicePixelRatio(style::DevicePixelRatio());
140 		return {
141 			.image = std::move(result).convertToFormat(
142 				QImage::Format_ARGB32_Premultiplied),
143 			.gradient = gradient,
144 			.area = request.area,
145 			.x = rects.to.x(),
146 			.y = rects.to.y(),
147 		};
148 	}
149 }
150 
PrepareBubblesBackground(const ChatThemeBubblesData & data)151 [[nodiscard]] QImage PrepareBubblesBackground(
152 		const ChatThemeBubblesData &data) {
153 	if (data.colors.size() < 2) {
154 		return QImage();
155 	}
156 	constexpr auto kSize = 512;
157 	return Images::GenerateLinearGradient(QSize(kSize, kSize), data.colors);
158 }
159 
160 // https://stackoverflow.com/a/9733420
CountContrast(const QColor & a,const QColor & b)161 [[nodiscard]] float64 CountContrast(const QColor &a, const QColor &b) {
162 	const auto luminance = [](const QColor &c) {
163 		const auto map = [](double value) {
164 			return (value <= 0.03928)
165 				? (value / 12.92)
166 				: std::pow((value + 0.055) / 1.055, 2.4);
167 		};
168 		return map(c.redF()) * 0.2126
169 			+ map(c.greenF()) * 0.7152
170 			+ map(c.blueF()) * 0.0722;
171 	};
172 	const auto luminance1 = luminance(a);
173 	const auto luminance2 = luminance(b);
174 	const auto brightest = std::max(luminance1, luminance2);
175 	const auto darkest = std::min(luminance1, luminance2);
176 	return (brightest + 0.05) / (darkest + 0.05);
177 }
178 
179 } // namespace
180 
operator ==(const ChatThemeBackground & a,const ChatThemeBackground & b)181 bool operator==(const ChatThemeBackground &a, const ChatThemeBackground &b) {
182 	return (a.prepared.cacheKey() == b.prepared.cacheKey())
183 		&& (a.gradientForFill.cacheKey() == b.gradientForFill.cacheKey())
184 		&& (a.tile == b.tile)
185 		&& (a.patternOpacity == b.patternOpacity);
186 }
187 
operator !=(const ChatThemeBackground & a,const ChatThemeBackground & b)188 bool operator!=(const ChatThemeBackground &a, const ChatThemeBackground &b) {
189 	return !(a == b);
190 }
191 
operator ==(const CacheBackgroundRequest & a,const CacheBackgroundRequest & b)192 bool operator==(
193 		const CacheBackgroundRequest &a,
194 		const CacheBackgroundRequest &b) {
195 	return (a.background == b.background)
196 		&& (a.area == b.area)
197 		&& (a.gradientRotationAdd == b.gradientRotationAdd)
198 		&& (a.gradientProgress == b.gradientProgress);
199 }
200 
operator !=(const CacheBackgroundRequest & a,const CacheBackgroundRequest & b)201 bool operator!=(
202 		const CacheBackgroundRequest &a,
203 		const CacheBackgroundRequest &b) {
204 	return !(a == b);
205 }
206 
CachedBackground(CacheBackgroundResult && result)207 CachedBackground::CachedBackground(CacheBackgroundResult &&result)
208 : pixmap(PixmapFromImage(std::move(result.image)))
209 , area(result.area)
210 , x(result.x)
211 , y(result.y)
212 , waitingForNegativePattern(result.waitingForNegativePattern) {
213 }
214 
ChatTheme()215 ChatTheme::ChatTheme() {
216 }
217 
218 // Runs from background thread.
ChatTheme(ChatThemeDescriptor && descriptor)219 ChatTheme::ChatTheme(ChatThemeDescriptor &&descriptor)
220 : _key(descriptor.key)
221 , _palette(std::make_unique<style::palette>()) {
222 	descriptor.preparePalette(*_palette);
223 	setBackground(PrepareBackgroundImage(descriptor.backgroundData));
224 	setBubblesBackground(PrepareBubblesBackground(descriptor.bubblesData));
225 	adjustPalette(descriptor);
226 }
227 
228 ChatTheme::~ChatTheme() = default;
229 
adjustPalette(const ChatThemeDescriptor & descriptor)230 void ChatTheme::adjustPalette(const ChatThemeDescriptor &descriptor) {
231 	auto &p = *_palette;
232 	const auto overrideOutBg = (descriptor.bubblesData.colors.size() == 1);
233 	if (overrideOutBg) {
234 		set(p.msgOutBg(), descriptor.bubblesData.colors.front());
235 	}
236 	const auto &background = descriptor.backgroundData.colors;
237 	if (!background.empty()) {
238 		const auto average = CountAverageColor(background);
239 		adjust(p.msgServiceBg(), average);
240 		adjust(p.msgServiceBgSelected(), average);
241 		adjust(p.historyScrollBg(), average);
242 		adjust(p.historyScrollBgOver(), average);
243 		adjust(p.historyScrollBarBg(), average);
244 		adjust(p.historyScrollBarBgOver(), average);
245 	}
246 	const auto bubblesAccent = descriptor.bubblesData.accent
247 		? descriptor.bubblesData.accent
248 		: (!descriptor.bubblesData.colors.empty())
249 		? ThemeAdjustedColor(
250 			p.msgOutReplyBarColor()->c,
251 			CountAverageColor(descriptor.bubblesData.colors))
252 		: std::optional<QColor>();
253 	if (bubblesAccent) {
254 		// First set hue/saturation the same for all those colors from accent.
255 		const auto by = *bubblesAccent;
256 		if (!overrideOutBg) {
257 			adjust(p.msgOutBg(), by);
258 		}
259 		adjust(p.msgOutShadow(), by);
260 		adjust(p.msgOutServiceFg(), by);
261 		adjust(p.msgOutDateFg(), by);
262 		adjust(p.msgFileThumbLinkOutFg(), by);
263 		adjust(p.msgFileOutBg(), by);
264 		adjust(p.msgOutReplyBarColor(), by);
265 		adjust(p.msgWaveformOutActive(), by);
266 		adjust(p.msgWaveformOutInactive(), by);
267 		adjust(p.historyFileOutRadialFg(), by); // historyFileOutIconFg
268 		adjust(p.mediaOutFg(), by);
269 
270 		adjust(p.historyLinkOutFg(), by);
271 		adjust(p.msgOutMonoFg(), by);
272 		adjust(p.historyOutIconFg(), by);
273 		adjust(p.historySendingOutIconFg(), by);
274 		adjust(p.historyCallArrowOutFg(), by);
275 		adjust(p.historyFileOutIconFg(), by); // msgOutBg
276 
277 		// After make msgFileOutBg exact accent and adjust some others.
278 		const auto colorizer = bubblesAccentColorizer(by);
279 		adjust(p.msgOutServiceFg(), colorizer);
280 		adjust(p.msgOutDateFg(), colorizer);
281 		adjust(p.msgFileThumbLinkOutFg(), colorizer);
282 		adjust(p.msgFileOutBg(), colorizer);
283 		adjust(p.msgOutReplyBarColor(), colorizer);
284 		adjust(p.msgWaveformOutActive(), colorizer);
285 		adjust(p.msgWaveformOutInactive(), colorizer);
286 		adjust(p.mediaOutFg(), colorizer);
287 		adjust(p.historyLinkOutFg(), colorizer);
288 		adjust(p.historyOutIconFg(), colorizer);
289 		adjust(p.historySendingOutIconFg(), colorizer);
290 		adjust(p.historyCallArrowOutFg(), colorizer);
291 
292 		if (!descriptor.basedOnDark) {
293 			adjust(p.msgOutBgSelected(), by);
294 			adjust(p.msgOutShadowSelected(), by);
295 			adjust(p.msgOutServiceFgSelected(), by);
296 			adjust(p.msgOutDateFgSelected(), by);
297 			adjust(p.msgFileThumbLinkOutFgSelected(), by);
298 			adjust(p.msgFileOutBgSelected(), by);
299 			adjust(p.msgOutReplyBarSelColor(), by);
300 			adjust(p.msgWaveformOutActiveSelected(), by);
301 			adjust(p.msgWaveformOutInactiveSelected(), by);
302 			adjust(p.historyFileOutRadialFgSelected(), by);
303 			adjust(p.mediaOutFgSelected(), by);
304 
305 			adjust(p.historyLinkOutFgSelected(), by);
306 			adjust(p.msgOutMonoFgSelected(), by);
307 			adjust(p.historyOutIconFgSelected(), by);
308 			// adjust(p.historySendingOutIconFgSelected(), by);
309 			adjust(p.historyCallArrowOutFgSelected(), by);
310 			adjust(p.historyFileOutIconFgSelected(), by); // msgOutBg
311 
312 			adjust(p.msgOutServiceFgSelected(), colorizer);
313 			adjust(p.msgOutDateFgSelected(), colorizer);
314 			adjust(p.msgFileThumbLinkOutFgSelected(), colorizer);
315 			adjust(p.msgFileOutBgSelected(), colorizer);
316 			adjust(p.msgOutReplyBarSelColor(), colorizer);
317 			adjust(p.msgWaveformOutActiveSelected(), colorizer);
318 			adjust(p.msgWaveformOutInactiveSelected(), colorizer);
319 			adjust(p.mediaOutFgSelected(), colorizer);
320 			adjust(p.historyLinkOutFgSelected(), colorizer);
321 			adjust(p.historyOutIconFgSelected(), colorizer);
322 			//adjust(p.historySendingOutIconFgSelected(), colorizer);
323 			adjust(p.historyCallArrowOutFgSelected(), colorizer);
324 		}
325 	}
326 	auto outBgColors = descriptor.bubblesData.colors;
327 	if (outBgColors.empty()) {
328 		outBgColors.push_back(p.msgOutBg()->c);
329 	}
330 	const auto colors = {
331 		p.msgOutServiceFg(),
332 		p.msgOutDateFg(),
333 		p.msgFileThumbLinkOutFg(),
334 		p.msgFileOutBg(),
335 		p.msgOutReplyBarColor(),
336 		p.msgWaveformOutActive(),
337 		p.historyTextOutFg(),
338 		p.mediaOutFg(),
339 		p.historyLinkOutFg(),
340 		p.msgOutMonoFg(),
341 		p.historyOutIconFg(),
342 		p.historyCallArrowOutFg(),
343 	};
344 	const auto minimal = [&](const QColor &with) {
345 		auto result = kMaxContrastValue;
346 		for (const auto &color : colors) {
347 			result = std::min(result, CountContrast(color->c, with));
348 		}
349 		return result;
350 	};
351 	const auto withBg = [&](auto &&count) {
352 		auto result = kMaxContrastValue;
353 		for (const auto &bg : outBgColors) {
354 			result = std::min(result, count(bg));
355 		}
356 		return result;
357 	};
358 	//const auto singleWithBg = [&](const QColor &c) {
359 	//	return withBg([&](const QColor &with) {
360 	//		return CountContrast(c, with);
361 	//	});
362 	//};
363 	if (withBg(minimal) < kMinAcceptableContrast) {
364 		const auto white = QColor(255, 255, 255);
365 		const auto black = QColor(0, 0, 0);
366 		// This one always gives black :)
367 		//const auto now = (singleWithBg(white) >= singleWithBg(black))
368 		//	? white
369 		//	: black;
370 		const auto now = descriptor.basedOnDark ? white : black;
371 		for (const auto &color : colors) {
372 			set(color, now);
373 		}
374 	}
375 }
376 
bubblesAccentColorizer(const QColor & accent) const377 style::colorizer ChatTheme::bubblesAccentColorizer(
378 		const QColor &accent) const {
379 	const auto color = [](const QColor &value) {
380 		auto hue = 0;
381 		auto saturation = 0;
382 		auto lightness = 0;
383 		value.getHsv(&hue, &saturation, &lightness);
384 		return style::colorizer::Color{ hue, saturation, lightness };
385 	};
386 	return {
387 		.hueThreshold = 255,
388 		.was = color(_palette->msgFileOutBg()->c),
389 		.now = color(accent),
390 	};
391 }
392 
set(const style::color & my,const QColor & color)393 void ChatTheme::set(const style::color &my, const QColor &color) {
394 	auto r = 0, g = 0, b = 0, a = 0;
395 	color.getRgb(&r, &g, &b, &a);
396 	my.set(uchar(r), uchar(g), uchar(b), uchar(a));
397 }
398 
adjust(const style::color & my,const QColor & by)399 void ChatTheme::adjust(const style::color &my, const QColor &by) {
400 	set(my, ThemeAdjustedColor(my->c, by));
401 }
402 
adjust(const style::color & my,const style::colorizer & by)403 void ChatTheme::adjust(const style::color &my, const style::colorizer &by) {
404 	if (const auto adjusted = style::colorize(my->c, by)) {
405 		set(my, *adjusted);
406 	}
407 }
408 
setBackground(ChatThemeBackground && background)409 void ChatTheme::setBackground(ChatThemeBackground &&background) {
410 	_mutableBackground = std::move(background);
411 	_backgroundState = {};
412 	_backgroundNext = {};
413 	_backgroundFade.stop();
414 	if (_cacheBackgroundTimer) {
415 		_cacheBackgroundTimer->cancel();
416 	}
417 	_repaintBackgroundRequests.fire({});
418 }
419 
updateBackgroundImageFrom(ChatThemeBackground && background)420 void ChatTheme::updateBackgroundImageFrom(ChatThemeBackground &&background) {
421 	_mutableBackground.prepared = std::move(background.prepared);
422 	_mutableBackground.preparedForTiled = std::move(
423 		background.preparedForTiled);
424 	if (!_backgroundState.now.pixmap.isNull()) {
425 		if (_cacheBackgroundTimer) {
426 			_cacheBackgroundTimer->cancel();
427 		}
428 		cacheBackgroundNow();
429 	} else {
430 		_repaintBackgroundRequests.fire({});
431 	}
432 }
433 
key() const434 ChatThemeKey ChatTheme::key() const {
435 	return _key;
436 }
437 
setBubblesBackground(QImage image)438 void ChatTheme::setBubblesBackground(QImage image) {
439 	if (image.isNull() && _bubblesBackgroundPrepared.isNull()) {
440 		return;
441 	}
442 	_bubblesBackgroundPrepared = std::move(image);
443 	if (_bubblesBackgroundPrepared.isNull()) {
444 		_bubblesBackgroundPattern = nullptr;
445 		_repaintBackgroundRequests.fire({});
446 		return;
447 	}
448 	_bubblesBackground = CacheBackground({
449 		.background = {
450 			.prepared = _bubblesBackgroundPrepared,
451 		},
452 		.area = (_bubblesBackground.area.isEmpty()
453 			? _bubblesBackgroundPrepared.size()
454 			: _bubblesBackground.area),
455 	});
456 	if (!_bubblesBackgroundPattern) {
457 		_bubblesBackgroundPattern = PrepareBubblePattern(palette());
458 	}
459 	_bubblesBackgroundPattern->pixmap = _bubblesBackground.pixmap;
460 	_repaintBackgroundRequests.fire({});
461 }
462 
preparePaintContext(not_null<const ChatStyle * > st,QRect viewport,QRect clip)463 ChatPaintContext ChatTheme::preparePaintContext(
464 		not_null<const ChatStyle*> st,
465 		QRect viewport,
466 		QRect clip) {
467 	const auto area = viewport.size();
468 	if (!_bubblesBackgroundPrepared.isNull()
469 		&& _bubblesBackground.area != area) {
470 		if (!_cacheBubblesTimer) {
471 			_cacheBubblesTimer.emplace([=] { cacheBubbles(); });
472 		}
473 		if (_cacheBubblesArea != area
474 			|| (!_cacheBubblesTimer->isActive()
475 				&& !_bubblesCachingRequest)) {
476 			_cacheBubblesArea = area;
477 			_lastBubblesAreaChangeTime = crl::now();
478 			_cacheBubblesTimer->callOnce(kCacheBackgroundFastTimeout);
479 		}
480 	}
481 	return {
482 		.st = st,
483 		.bubblesPattern = _bubblesBackgroundPattern.get(),
484 		.viewport = viewport,
485 		.clip = clip,
486 		.now = crl::now(),
487 	};
488 }
489 
backgroundState(QSize area)490 const BackgroundState &ChatTheme::backgroundState(QSize area) {
491 	if (!_cacheBackgroundTimer) {
492 		_cacheBackgroundTimer.emplace([=] { cacheBackground(); });
493 	}
494 	_backgroundState.shown = _backgroundFade.value(1.);
495 	if (_backgroundState.now.pixmap.isNull()
496 		&& !background().gradientForFill.isNull()) {
497 		// We don't support direct painting of patterned gradients.
498 		// So we need to sync-generate cache image here.
499 		_cacheBackgroundArea = area;
500 		setCachedBackground(CacheBackground(cacheBackgroundRequest(area)));
501 		_cacheBackgroundTimer->cancel();
502 	} else if (_backgroundState.now.area != area) {
503 		if (_cacheBackgroundArea != area
504 			|| (!_cacheBackgroundTimer->isActive()
505 				&& !_backgroundCachingRequest)) {
506 			_cacheBackgroundArea = area;
507 			_lastBackgroundAreaChangeTime = crl::now();
508 			_cacheBackgroundTimer->callOnce(kCacheBackgroundFastTimeout);
509 		}
510 	}
511 	generateNextBackgroundRotation();
512 	return _backgroundState;
513 }
514 
clearBackgroundState()515 void ChatTheme::clearBackgroundState() {
516 	_backgroundState = BackgroundState();
517 	_backgroundFade.stop();
518 }
519 
readyForBackgroundRotation() const520 bool ChatTheme::readyForBackgroundRotation() const {
521 	Expects(_cacheBackgroundTimer.has_value());
522 
523 	return !anim::Disabled()
524 		&& !_backgroundFade.animating()
525 		&& !_cacheBackgroundTimer->isActive()
526 		&& !_backgroundState.now.pixmap.isNull();
527 }
528 
generateNextBackgroundRotation()529 void ChatTheme::generateNextBackgroundRotation() {
530 	if (_backgroundCachingRequest
531 		|| !_backgroundNext.image.isNull()
532 		|| !readyForBackgroundRotation()) {
533 		return;
534 	}
535 	if (background().colors.size() < 3) {
536 		return;
537 	}
538 	constexpr auto kAddRotationDoubled = (720 - 45);
539 	const auto request = cacheBackgroundRequest(
540 		_backgroundState.now.area,
541 		kAddRotationDoubled);
542 	if (!request) {
543 		return;
544 	}
545 	cacheBackgroundAsync(request, [=](CacheBackgroundResult &&result) {
546 		const auto forRequest = base::take(_backgroundCachingRequest);
547 		if (!readyForBackgroundRotation()) {
548 			return;
549 		}
550 		const auto request = cacheBackgroundRequest(
551 			_backgroundState.now.area,
552 			kAddRotationDoubled);
553 		if (forRequest == request) {
554 			_mutableBackground.gradientRotation
555 				= (_mutableBackground.gradientRotation
556 					+ kAddRotationDoubled) % 720;
557 			_backgroundNext = std::move(result);
558 		}
559 	});
560 }
561 
cacheBackgroundRequest(QSize area,int addRotation) const562 auto ChatTheme::cacheBackgroundRequest(QSize area, int addRotation) const
563 -> CacheBackgroundRequest {
564 	if (background().colorForFill) {
565 		return {};
566 	}
567 	return {
568 		.background = background(),
569 		.area = area,
570 		.gradientRotationAdd = addRotation,
571 	};
572 }
573 
cacheBackground()574 void ChatTheme::cacheBackground() {
575 	Expects(_cacheBackgroundTimer.has_value());
576 
577 	const auto now = crl::now();
578 	if (now - _lastBackgroundAreaChangeTime < kCacheBackgroundTimeout
579 		&& QGuiApplication::mouseButtons() != 0) {
580 		_cacheBackgroundTimer->callOnce(kCacheBackgroundFastTimeout);
581 		return;
582 	}
583 	cacheBackgroundNow();
584 }
585 
cacheBackgroundNow()586 void ChatTheme::cacheBackgroundNow() {
587 	if (!_backgroundCachingRequest) {
588 		if (const auto request = cacheBackgroundRequest(
589 				_cacheBackgroundArea)) {
590 			cacheBackgroundAsync(request);
591 		}
592 	}
593 }
594 
cacheBackgroundAsync(const CacheBackgroundRequest & request,Fn<void (CacheBackgroundResult &&)> done)595 void ChatTheme::cacheBackgroundAsync(
596 		const CacheBackgroundRequest &request,
597 		Fn<void(CacheBackgroundResult&&)> done) {
598 	_backgroundCachingRequest = request;
599 	const auto weak = base::make_weak(this);
600 	crl::async([=] {
601 		if (!weak) {
602 			return;
603 		}
604 		crl::on_main(weak, [=, result = CacheBackground(request)]() mutable {
605 			if (done) {
606 				done(std::move(result));
607 			} else if (const auto request = cacheBackgroundRequest(
608 					_cacheBackgroundArea)) {
609 				if (_backgroundCachingRequest != request) {
610 					cacheBackgroundAsync(request);
611 				} else {
612 					_backgroundCachingRequest = {};
613 					setCachedBackground(std::move(result));
614 				}
615 			}
616 		});
617 	});
618 }
619 
setCachedBackground(CacheBackgroundResult && cached)620 void ChatTheme::setCachedBackground(CacheBackgroundResult &&cached) {
621 	_backgroundNext = {};
622 
623 	if (background().gradientForFill.isNull()
624 		|| _backgroundState.now.pixmap.isNull()
625 		|| anim::Disabled()) {
626 		_backgroundFade.stop();
627 		_backgroundState.shown = 1.;
628 		_backgroundState.now = std::move(cached);
629 		return;
630 	}
631 	// #TODO themes compose several transitions.
632 	_backgroundState.was = std::move(_backgroundState.now);
633 	_backgroundState.now = std::move(cached);
634 	_backgroundState.shown = 0.;
635 	const auto callback = [=] {
636 		if (!_backgroundFade.animating()) {
637 			_backgroundState.was = {};
638 			_backgroundState.shown = 1.;
639 		}
640 		_repaintBackgroundRequests.fire({});
641 	};
642 	_backgroundFade.start(
643 		callback,
644 		0.,
645 		1.,
646 		kBackgroundFadeDuration);
647 }
648 
cacheBubblesRequest(QSize area) const649 auto ChatTheme::cacheBubblesRequest(QSize area) const
650 -> CacheBackgroundRequest {
651 	if (_bubblesBackgroundPrepared.isNull()) {
652 		return {};
653 	}
654 	return {
655 		.background = {
656 			.gradientForFill = _bubblesBackgroundPrepared,
657 		},
658 		.area = area,
659 	};
660 }
661 
cacheBubbles()662 void ChatTheme::cacheBubbles() {
663 	Expects(_cacheBubblesTimer.has_value());
664 
665 	const auto now = crl::now();
666 	if (now - _lastBubblesAreaChangeTime < kCacheBackgroundTimeout
667 		&& QGuiApplication::mouseButtons() != 0) {
668 		_cacheBubblesTimer->callOnce(kCacheBackgroundFastTimeout);
669 		return;
670 	}
671 	cacheBubblesNow();
672 }
673 
cacheBubblesNow()674 void ChatTheme::cacheBubblesNow() {
675 	if (!_bubblesCachingRequest) {
676 		if (const auto request = cacheBackgroundRequest(
677 				_cacheBubblesArea)) {
678 			cacheBubblesAsync(request);
679 		}
680 	}
681 }
682 
cacheBubblesAsync(const CacheBackgroundRequest & request)683 void ChatTheme::cacheBubblesAsync(
684 		const CacheBackgroundRequest &request) {
685 	_bubblesCachingRequest = request;
686 	const auto weak = base::make_weak(this);
687 	crl::async([=] {
688 		if (!weak) {
689 			return;
690 		}
691 		crl::on_main(weak, [=, result = CacheBackground(request)]() mutable {
692 			if (const auto request = cacheBubblesRequest(
693 					_cacheBubblesArea)) {
694 				if (_bubblesCachingRequest != request) {
695 					cacheBubblesAsync(request);
696 				} else {
697 					_bubblesCachingRequest = {};
698 					_bubblesBackground = std::move(result);
699 					_bubblesBackgroundPattern->pixmap
700 						= _bubblesBackground.pixmap;
701 				}
702 			}
703 		});
704 	});
705 }
706 
repaintBackgroundRequests() const707 rpl::producer<> ChatTheme::repaintBackgroundRequests() const {
708 	return _repaintBackgroundRequests.events();
709 }
710 
rotateComplexGradientBackground()711 void ChatTheme::rotateComplexGradientBackground() {
712 	if (!_backgroundFade.animating() && !_backgroundNext.image.isNull()) {
713 		if (_mutableBackground.gradientForFill.size()
714 			== _backgroundNext.gradient.size()) {
715 			_mutableBackground.gradientForFill
716 				= std::move(_backgroundNext.gradient);
717 		}
718 		setCachedBackground(base::take(_backgroundNext));
719 	}
720 }
721 
ComputeChatBackgroundRects(QSize fillSize,QSize imageSize)722 ChatBackgroundRects ComputeChatBackgroundRects(
723 		QSize fillSize,
724 		QSize imageSize) {
725 	if (uint64(imageSize.width()) * fillSize.height()
726 		> uint64(imageSize.height()) * fillSize.width()) {
727 		const auto pxsize = fillSize.height() / float64(imageSize.height());
728 		auto takewidth = int(std::ceil(fillSize.width() / pxsize));
729 		if (takewidth > imageSize.width()) {
730 			takewidth = imageSize.width();
731 		} else if ((imageSize.width() % 2) != (takewidth % 2)) {
732 			++takewidth;
733 		}
734 		return {
735 			.from = QRect(
736 				(imageSize.width() - takewidth) / 2,
737 				0,
738 				takewidth,
739 				imageSize.height()),
740 			.to = QRect(
741 				int((fillSize.width() - takewidth * pxsize) / 2.),
742 				0,
743 				int(std::ceil(takewidth * pxsize)),
744 				fillSize.height()),
745 		};
746 	} else {
747 		const auto pxsize = fillSize.width() / float64(imageSize.width());
748 		auto takeheight = int(std::ceil(fillSize.height() / pxsize));
749 		if (takeheight > imageSize.height()) {
750 			takeheight = imageSize.height();
751 		} else if ((imageSize.height() % 2) != (takeheight % 2)) {
752 			++takeheight;
753 		}
754 		return {
755 			.from = QRect(
756 				0,
757 				(imageSize.height() - takeheight) / 2,
758 				imageSize.width(),
759 				takeheight),
760 			.to = QRect(
761 				0,
762 				int((fillSize.height() - takeheight * pxsize) / 2.),
763 				fillSize.width(),
764 				int(std::ceil(takeheight * pxsize))),
765 		};
766 	}
767 }
768 
CountAverageColor(const QImage & image)769 QColor CountAverageColor(const QImage &image) {
770 	Expects(image.format() == QImage::Format_ARGB32_Premultiplied
771 		|| image.format() == QImage::Format_RGB32);
772 
773 	uint64 components[3] = { 0 };
774 	const auto w = image.width();
775 	const auto h = image.height();
776 	const auto size = w * h;
777 	if (const auto pix = image.constBits()) {
778 		for (auto i = 0, l = size * 4; i != l; i += 4) {
779 			components[2] += pix[i + 0];
780 			components[1] += pix[i + 1];
781 			components[0] += pix[i + 2];
782 		}
783 	}
784 	if (size) {
785 		for (auto &component : components) {
786 			component /= size;
787 		}
788 	}
789 	return QColor(components[0], components[1], components[2]);
790 }
791 
CountAverageColor(const std::vector<QColor> & colors)792 QColor CountAverageColor(const std::vector<QColor> &colors) {
793 	Expects(colors.size() < (std::numeric_limits<int>::max() / 256));
794 
795 	int components[3] = { 0 };
796 	auto r = 0;
797 	auto g = 0;
798 	auto b = 0;
799 	for (const auto &color : colors) {
800 		color.getRgb(&r, &g, &b);
801 		components[0] += r;
802 		components[1] += g;
803 		components[2] += b;
804 	}
805 	if (const auto size = colors.size()) {
806 		for (auto &component : components) {
807 			component /= size;
808 		}
809 	}
810 	return QColor(components[0], components[1], components[2]);
811 }
812 
IsPatternInverted(const std::vector<QColor> & background,float64 patternOpacity)813 bool IsPatternInverted(
814 		const std::vector<QColor> &background,
815 		float64 patternOpacity) {
816 	return (patternOpacity > 0.)
817 		&& (CountAverageColor(background).toHsv().valueF() <= 0.3);
818 }
819 
ThemeAdjustedColor(QColor original,QColor background)820 QColor ThemeAdjustedColor(QColor original, QColor background) {
821 	return QColor::fromHslF(
822 		background.hslHueF(),
823 		background.hslSaturationF(),
824 		original.lightnessF(),
825 		original.alphaF()
826 	).toRgb();
827 }
828 
PreprocessBackgroundImage(QImage image)829 QImage PreprocessBackgroundImage(QImage image) {
830 	if (image.isNull()) {
831 		return image;
832 	}
833 	if (image.format() != QImage::Format_ARGB32_Premultiplied) {
834 		image = std::move(image).convertToFormat(
835 			QImage::Format_ARGB32_Premultiplied);
836 	}
837 	if (image.width() > 40 * image.height()) {
838 		const auto width = 40 * image.height();
839 		const auto height = image.height();
840 		image = image.copy((image.width() - width) / 2, 0, width, height);
841 	} else if (image.height() > 40 * image.width()) {
842 		const auto width = image.width();
843 		const auto height = 40 * image.width();
844 		image = image.copy(0, (image.height() - height) / 2, width, height);
845 	}
846 	if (image.width() > kMaxSize || image.height() > kMaxSize) {
847 		image = image.scaled(
848 			kMaxSize,
849 			kMaxSize,
850 			Qt::KeepAspectRatio,
851 			Qt::SmoothTransformation);
852 	}
853 	return image;
854 }
855 
CalculateImageMonoColor(const QImage & image)856 std::optional<QColor> CalculateImageMonoColor(const QImage &image) {
857 	Expects(image.bytesPerLine() == 4 * image.width());
858 
859 	if (image.isNull()) {
860 		return std::nullopt;
861 	}
862 	const auto bits = reinterpret_cast<const uint32*>(image.constBits());
863 	const auto first = bits[0];
864 	for (auto i = 0; i < image.width() * image.height(); i++) {
865 		if (first != bits[i]) {
866 			return std::nullopt;
867 		}
868 	}
869 	return image.pixelColor(QPoint());
870 }
871 
PrepareImageForTiled(const QImage & prepared)872 QImage PrepareImageForTiled(const QImage &prepared) {
873 	const auto width = prepared.width();
874 	const auto height = prepared.height();
875 	const auto isSmallForTiled = (width > 0 && height > 0)
876 		&& (width < kMinimumTiledSize || height < kMinimumTiledSize);
877 	if (!isSmallForTiled) {
878 		return prepared;
879 	}
880 	const auto repeatTimesX = (kMinimumTiledSize + width - 1) / width;
881 	const auto repeatTimesY = (kMinimumTiledSize + height - 1) / height;
882 	auto result = QImage(
883 		width * repeatTimesX,
884 		height * repeatTimesY,
885 		QImage::Format_ARGB32_Premultiplied);
886 	result.setDevicePixelRatio(prepared.devicePixelRatio());
887 	auto imageForTiledBytes = result.bits();
888 	auto bytesInLine = width * sizeof(uint32);
889 	for (auto timesY = 0; timesY != repeatTimesY; ++timesY) {
890 		auto imageBytes = prepared.constBits();
891 		for (auto y = 0; y != height; ++y) {
892 			for (auto timesX = 0; timesX != repeatTimesX; ++timesX) {
893 				memcpy(imageForTiledBytes, imageBytes, bytesInLine);
894 				imageForTiledBytes += bytesInLine;
895 			}
896 			imageBytes += prepared.bytesPerLine();
897 			imageForTiledBytes += result.bytesPerLine() - (repeatTimesX * bytesInLine);
898 		}
899 	}
900 	return result;
901 }
902 
ReadBackgroundImage(const QString & path,const QByteArray & content,bool gzipSvg)903 [[nodiscard]] QImage ReadBackgroundImage(
904 		const QString &path,
905 		const QByteArray &content,
906 		bool gzipSvg) {
907 	return Images::Read({
908 		.path = path,
909 		.content = content,
910 		.maxSize = QSize(kMaxSize, kMaxSize),
911 		.gzipSvg = gzipSvg,
912 	}).image;
913 }
914 
GenerateBackgroundImage(QSize size,const std::vector<QColor> & bg,int gradientRotation,float64 patternOpacity,Fn<void (QPainter &,bool)> drawPattern)915 QImage GenerateBackgroundImage(
916 		QSize size,
917 		const std::vector<QColor> &bg,
918 		int gradientRotation,
919 		float64 patternOpacity,
920 		Fn<void(QPainter&,bool)> drawPattern) {
921 	auto result = bg.empty()
922 		? Images::GenerateGradient(size, { DefaultBackgroundColor() })
923 		: Images::GenerateGradient(size, bg, gradientRotation);
924 	if (bg.size() > 1 && (!drawPattern || patternOpacity >= 0.)) {
925 		result = Images::DitherImage(std::move(result));
926 	}
927 	if (drawPattern) {
928 		auto p = QPainter(&result);
929 		if (patternOpacity >= 0.) {
930 			p.setCompositionMode(QPainter::CompositionMode_SoftLight);
931 			p.setOpacity(patternOpacity);
932 		} else {
933 			p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
934 		}
935 		drawPattern(p, IsPatternInverted(bg, patternOpacity));
936 		if (patternOpacity < 0. && patternOpacity > -1.) {
937 			p.setCompositionMode(QPainter::CompositionMode_SourceOver);
938 			p.setOpacity(1. + patternOpacity);
939 			p.fillRect(QRect{ QPoint(), size }, Qt::black);
940 		}
941 	}
942 
943 	return std::move(result).convertToFormat(
944 		QImage::Format_ARGB32_Premultiplied);
945 }
946 
PreparePatternImage(QImage pattern,const std::vector<QColor> & bg,int gradientRotation,float64 patternOpacity)947 QImage PreparePatternImage(
948 		QImage pattern,
949 		const std::vector<QColor> &bg,
950 		int gradientRotation,
951 		float64 patternOpacity) {
952 	auto result = GenerateBackgroundImage(
953 		pattern.size(),
954 		bg,
955 		gradientRotation,
956 		patternOpacity,
957 		[&](QPainter &p, bool inverted) {
958 			if (inverted) {
959 				pattern = InvertPatternImage(std::move(pattern));
960 			}
961 			p.drawImage(QRect(QPoint(), pattern.size()), pattern);
962 		});
963 
964 	pattern = QImage();
965 	return result;
966 }
967 
InvertPatternImage(QImage pattern)968 QImage InvertPatternImage(QImage pattern) {
969 	pattern = std::move(pattern).convertToFormat(
970 		QImage::Format_ARGB32_Premultiplied);
971 	const auto w = pattern.bytesPerLine() / 4;
972 	auto ints = reinterpret_cast<uint32*>(pattern.bits());
973 	for (auto y = 0, h = pattern.height(); y != h; ++y) {
974 		for (auto x = 0; x != w; ++x) {
975 			const auto value = (*ints >> 24);
976 			*ints++ = (value << 24)
977 				| (value << 16)
978 				| (value << 8)
979 				| value;
980 		}
981 	}
982 	return pattern;
983 }
984 
PrepareBlurredBackground(QImage image)985 QImage PrepareBlurredBackground(QImage image) {
986 	constexpr auto kSize = 900;
987 	constexpr auto kRadius = 24;
988 	if (image.width() > kSize || image.height() > kSize) {
989 		image = image.scaled(
990 			kSize,
991 			kSize,
992 			Qt::KeepAspectRatio,
993 			Qt::SmoothTransformation);
994 	}
995 	return Images::BlurLargeImage(image, kRadius);
996 }
997 
GenerateDitheredGradient(const std::vector<QColor> & colors,int rotation)998 QImage GenerateDitheredGradient(
999 		const std::vector<QColor> &colors,
1000 		int rotation) {
1001 	constexpr auto kSize = 512;
1002 	const auto size = QSize(kSize, kSize);
1003 	if (colors.empty()) {
1004 		return Images::GenerateGradient(size, { DefaultBackgroundColor() });
1005 	}
1006 	auto result = Images::GenerateGradient(size, colors, rotation);
1007 	if (colors.size() > 1) {
1008 		result = Images::DitherImage(std::move(result));
1009 	}
1010 	return result;
1011 }
1012 
PrepareBackgroundImage(const ChatThemeBackgroundData & data)1013 ChatThemeBackground PrepareBackgroundImage(
1014 		const ChatThemeBackgroundData &data) {
1015 	auto prepared = (data.isPattern || data.colors.empty())
1016 		? PreprocessBackgroundImage(
1017 			ReadBackgroundImage(data.path, data.bytes, data.gzipSvg))
1018 		: QImage();
1019 	if (data.isPattern && !prepared.isNull()) {
1020 		if (data.colors.size() < 2) {
1021 			prepared = PreparePatternImage(
1022 				std::move(prepared),
1023 				data.colors,
1024 				data.gradientRotation,
1025 				data.patternOpacity);
1026 		} else if (IsPatternInverted(data.colors, data.patternOpacity)) {
1027 			prepared = InvertPatternImage(std::move(prepared));
1028 		}
1029 		prepared.setDevicePixelRatio(style::DevicePixelRatio());
1030 	} else if (data.colors.empty()) {
1031 		prepared.setDevicePixelRatio(style::DevicePixelRatio());
1032 	}
1033 	const auto imageMonoColor = (data.colors.size() < 2)
1034 		? CalculateImageMonoColor(prepared)
1035 		: std::nullopt;
1036 	if (!prepared.isNull() && !data.isPattern && data.isBlurred) {
1037 		prepared = PrepareBlurredBackground(std::move(prepared));
1038 	}
1039 	auto gradientForFill = (data.generateGradient && data.colors.size() > 1)
1040 		? Ui::GenerateDitheredGradient(data.colors, data.gradientRotation)
1041 		: QImage();
1042 	return ChatThemeBackground{
1043 		.prepared = prepared,
1044 		.preparedForTiled = PrepareImageForTiled(prepared),
1045 		.gradientForFill = std::move(gradientForFill),
1046 		.colorForFill = (!prepared.isNull()
1047 			? imageMonoColor
1048 			: (data.colors.size() > 1 || data.colors.empty())
1049 			? std::nullopt
1050 			: std::make_optional(data.colors.front())),
1051 		.colors = data.colors,
1052 		.patternOpacity = data.patternOpacity,
1053 		.gradientRotation = data.generateGradient ? data.gradientRotation : 0,
1054 		.isPattern = data.isPattern,
1055 	};
1056 }
1057 
1058 } // namespace Window::Theme
1059