chat_theme.cpp 31 KB

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