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