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/group_call_userpics.h"
9
10 #include "ui/paint/blobs.h"
11 #include "base/random.h"
12 #include "styles/style_chat.h"
13
14 namespace Ui {
15 namespace {
16
17 constexpr auto kDuration = 160;
18 constexpr auto kMaxUserpics = 4;
19 constexpr auto kWideScale = 5;
20
21 constexpr auto kBlobsEnterDuration = crl::time(250);
22 constexpr auto kLevelDuration = 100. + 500. * 0.23;
23 constexpr auto kBlobScale = 0.605;
24 constexpr auto kMinorBlobFactor = 0.9f;
25 constexpr auto kUserpicMinScale = 0.8;
26 constexpr auto kMaxLevel = 1.;
27 constexpr auto kSendRandomLevelInterval = crl::time(100);
28
Blobs()29 auto Blobs()->std::array<Ui::Paint::Blobs::BlobData, 2> {
30 return { {
31 {
32 .segmentsCount = 6,
33 .minScale = kBlobScale * kMinorBlobFactor,
34 .minRadius = st::historyGroupCallBlobMinRadius * kMinorBlobFactor,
35 .maxRadius = st::historyGroupCallBlobMaxRadius * kMinorBlobFactor,
36 .speedScale = 1.,
37 .alpha = .5,
38 },
39 {
40 .segmentsCount = 8,
41 .minScale = kBlobScale,
42 .minRadius = (float)st::historyGroupCallBlobMinRadius,
43 .maxRadius = (float)st::historyGroupCallBlobMaxRadius,
44 .speedScale = 1.,
45 .alpha = .2,
46 },
47 } };
48 }
49
50 } // namespace
51
52 struct GroupCallUserpics::BlobsAnimation {
BlobsAnimationUi::GroupCallUserpics::BlobsAnimation53 BlobsAnimation(
54 std::vector<Ui::Paint::Blobs::BlobData> blobDatas,
55 float levelDuration,
56 float maxLevel)
57 : blobs(std::move(blobDatas), levelDuration, maxLevel) {
58 }
59
60 Ui::Paint::Blobs blobs;
61 crl::time lastTime = 0;
62 crl::time lastSpeakingUpdateTime = 0;
63 float64 enter = 0.;
64 };
65
66 struct GroupCallUserpics::Userpic {
67 User data;
68 std::pair<uint64, uint64> cacheKey;
69 crl::time speakingStarted = 0;
70 QImage cache;
71 Animations::Simple leftAnimation;
72 Animations::Simple shownAnimation;
73 std::unique_ptr<BlobsAnimation> blobsAnimation;
74 int left = 0;
75 bool positionInited = false;
76 bool topMost = false;
77 bool hiding = false;
78 bool cacheMasked = false;
79 };
80
GroupCallUserpics(const style::GroupCallUserpics & st,rpl::producer<bool> && hideBlobs,Fn<void ()> repaint)81 GroupCallUserpics::GroupCallUserpics(
82 const style::GroupCallUserpics &st,
83 rpl::producer<bool> &&hideBlobs,
84 Fn<void()> repaint)
85 : _st(st)
86 , _randomSpeakingTimer([=] { sendRandomLevels(); })
87 , _repaint(std::move(repaint)) {
88 const auto limit = kMaxUserpics;
89 const auto single = _st.size;
90 const auto shift = _st.shift;
91 // + 1 * single for the blobs.
92 _maxWidth = 2 * single + (limit - 1) * (single - shift);
93
94 style::PaletteChanged(
__anoncafbd57f0302null95 ) | rpl::start_with_next([=] {
96 for (auto &userpic : _list) {
97 userpic.cache = QImage();
98 }
99 }, lifetime());
100
__anoncafbd57f0402(crl::time now) 101 _speakingAnimation.init([=](crl::time now) {
102 if (const auto &last = _speakingAnimationHideLastTime; (last > 0)
103 && (now - last >= kBlobsEnterDuration)) {
104 _speakingAnimation.stop();
105 }
106 for (auto &userpic : _list) {
107 if (const auto blobs = userpic.blobsAnimation.get()) {
108 blobs->blobs.updateLevel(now - blobs->lastTime);
109 blobs->lastTime = now;
110 }
111 }
112 if (const auto onstack = _repaint) {
113 onstack();
114 }
115 });
116
117 rpl::combine(
118 rpl::single(anim::Disabled()) | rpl::then(anim::Disables()),
119 std::move(hideBlobs)
__anoncafbd57f0502(bool animDisabled, bool deactivated) 120 ) | rpl::start_with_next([=](bool animDisabled, bool deactivated) {
121 const auto hide = animDisabled || deactivated;
122
123 if (!(hide && _speakingAnimationHideLastTime)) {
124 _speakingAnimationHideLastTime = hide ? crl::now() : 0;
125 }
126 _skipLevelUpdate = hide;
127 for (auto &userpic : _list) {
128 if (const auto blobs = userpic.blobsAnimation.get()) {
129 blobs->blobs.setLevel(0.);
130 }
131 }
132 if (!hide && !_speakingAnimation.animating()) {
133 _speakingAnimation.start();
134 }
135 _skipLevelUpdate = hide;
136 }, lifetime());
137 }
138
139 GroupCallUserpics::~GroupCallUserpics() = default;
140
paint(Painter & p,int x,int y,int size)141 void GroupCallUserpics::paint(Painter &p, int x, int y, int size) {
142 const auto factor = style::DevicePixelRatio();
143 const auto &minScale = kUserpicMinScale;
144 for (auto &userpic : ranges::views::reverse(_list)) {
145 const auto shown = userpic.shownAnimation.value(
146 userpic.hiding ? 0. : 1.);
147 if (shown == 0.) {
148 continue;
149 }
150 validateCache(userpic);
151 p.setOpacity(shown);
152 const auto left = x + userpic.leftAnimation.value(userpic.left);
153 const auto blobs = userpic.blobsAnimation.get();
154 const auto shownScale = 0.5 + shown / 2.;
155 const auto scale = shownScale * (!blobs
156 ? 1.
157 : (minScale
158 + (1. - minScale) * (_speakingAnimationHideLastTime
159 ? (1. - blobs->blobs.currentLevel())
160 : blobs->blobs.currentLevel())));
161 if (blobs) {
162 auto hq = PainterHighQualityEnabler(p);
163
164 const auto shift = QPointF(left + size / 2., y + size / 2.);
165 p.translate(shift);
166 blobs->blobs.paint(p, st::windowActiveTextFg);
167 p.translate(-shift);
168 p.setOpacity(1.);
169 }
170 if (std::abs(scale - 1.) < 0.001) {
171 const auto skip = ((kWideScale - 1) / 2) * size * factor;
172 p.drawImage(
173 QRect(left, y, size, size),
174 userpic.cache,
175 QRect(skip, skip, size * factor, size * factor));
176 } else {
177 auto hq = PainterHighQualityEnabler(p);
178
179 auto target = QRect(
180 left + (1 - kWideScale) / 2 * size,
181 y + (1 - kWideScale) / 2 * size,
182 kWideScale * size,
183 kWideScale * size);
184 auto shrink = anim::interpolate(
185 (1 - kWideScale) / 2 * size,
186 0,
187 scale);
188 auto margins = QMargins(shrink, shrink, shrink, shrink);
189 p.drawImage(target.marginsAdded(margins), userpic.cache);
190 }
191 }
192 p.setOpacity(1.);
193
194 const auto hidden = [](const Userpic &userpic) {
195 return userpic.hiding && !userpic.shownAnimation.animating();
196 };
197 _list.erase(ranges::remove_if(_list, hidden), end(_list));
198 }
199
maxWidth() const200 int GroupCallUserpics::maxWidth() const {
201 return _maxWidth;
202 }
203
widthValue() const204 rpl::producer<int> GroupCallUserpics::widthValue() const {
205 return _width.value();
206 }
207
needCacheRefresh(Userpic & userpic)208 bool GroupCallUserpics::needCacheRefresh(Userpic &userpic) {
209 if (userpic.cache.isNull()) {
210 return true;
211 } else if (userpic.hiding) {
212 return false;
213 } else if (userpic.cacheKey != userpic.data.userpicKey) {
214 return true;
215 }
216 const auto shouldBeMasked = !userpic.topMost;
217 if (userpic.cacheMasked == shouldBeMasked || !shouldBeMasked) {
218 return true;
219 }
220 return !userpic.leftAnimation.animating();
221 }
222
ensureBlobsAnimation(Userpic & userpic)223 void GroupCallUserpics::ensureBlobsAnimation(Userpic &userpic) {
224 if (userpic.blobsAnimation) {
225 return;
226 }
227 userpic.blobsAnimation = std::make_unique<BlobsAnimation>(
228 Blobs() | ranges::to_vector,
229 kLevelDuration,
230 kMaxLevel);
231 userpic.blobsAnimation->lastTime = crl::now();
232 }
233
sendRandomLevels()234 void GroupCallUserpics::sendRandomLevels() {
235 if (_skipLevelUpdate) {
236 return;
237 }
238 for (auto &userpic : _list) {
239 if (const auto blobs = userpic.blobsAnimation.get()) {
240 const auto value = 30 + base::RandomIndex(70);
241 userpic.blobsAnimation->blobs.setLevel(float64(value) / 100.);
242 }
243 }
244 }
245
validateCache(Userpic & userpic)246 void GroupCallUserpics::validateCache(Userpic &userpic) {
247 if (!needCacheRefresh(userpic)) {
248 return;
249 }
250 const auto factor = style::DevicePixelRatio();
251 const auto size = _st.size;
252 const auto shift = _st.shift;
253 const auto full = QSize(size, size) * kWideScale * factor;
254 if (userpic.cache.isNull()) {
255 userpic.cache = QImage(full, QImage::Format_ARGB32_Premultiplied);
256 userpic.cache.setDevicePixelRatio(factor);
257 }
258 userpic.cacheKey = userpic.data.userpicKey;
259 userpic.cacheMasked = !userpic.topMost;
260 userpic.cache.fill(Qt::transparent);
261 {
262 Painter p(&userpic.cache);
263 const auto skip = (kWideScale - 1) / 2 * size;
264 p.drawImage(skip, skip, userpic.data.userpic);
265
266 if (userpic.cacheMasked) {
267 auto hq = PainterHighQualityEnabler(p);
268 auto pen = QPen(Qt::transparent);
269 pen.setWidth(_st.stroke);
270 p.setCompositionMode(QPainter::CompositionMode_Source);
271 p.setBrush(Qt::transparent);
272 p.setPen(pen);
273 p.drawEllipse(skip - size + shift, skip, size, size);
274 }
275 }
276 }
277
update(const std::vector<GroupCallUser> & users,bool visible)278 void GroupCallUserpics::update(
279 const std::vector<GroupCallUser> &users,
280 bool visible) {
281 const auto idFromUserpic = [](const Userpic &userpic) {
282 return userpic.data.id;
283 };
284
285 // Use "topMost" as "willBeHidden" flag.
286 for (auto &userpic : _list) {
287 userpic.topMost = true;
288 }
289 for (const auto &user : users) {
290 const auto i = ranges::find(_list, user.id, idFromUserpic);
291 if (i == end(_list)) {
292 _list.push_back(Userpic{ user });
293 toggle(_list.back(), true);
294 continue;
295 }
296 i->topMost = false;
297
298 if (i->hiding) {
299 toggle(*i, true);
300 }
301 i->data = user;
302
303 // Put this one after the last we are not hiding.
304 for (auto j = end(_list) - 1; j != i; --j) {
305 if (!j->topMost) {
306 ranges::rotate(i, i + 1, j + 1);
307 break;
308 }
309 }
310 }
311
312 // Hide the ones that "willBeHidden" (currently having "topMost" flag).
313 // Set correct real values of "topMost" flag.
314 const auto userpicsBegin = begin(_list);
315 const auto userpicsEnd = end(_list);
316 auto markedTopMost = userpicsEnd;
317 auto hasBlobs = false;
318 for (auto i = userpicsBegin; i != userpicsEnd; ++i) {
319 auto &userpic = *i;
320 if (userpic.data.speaking) {
321 ensureBlobsAnimation(userpic);
322 hasBlobs = true;
323 } else {
324 userpic.blobsAnimation = nullptr;
325 }
326 if (userpic.topMost) {
327 toggle(userpic, false);
328 userpic.topMost = false;
329 } else if (markedTopMost == userpicsEnd) {
330 userpic.topMost = true;
331 markedTopMost = i;
332 }
333 }
334 if (markedTopMost != userpicsEnd && markedTopMost != userpicsBegin) {
335 // Bring the topMost userpic to the very beginning, above all hiding.
336 std::rotate(userpicsBegin, markedTopMost, markedTopMost + 1);
337 }
338 updatePositions();
339
340 if (!hasBlobs) {
341 _randomSpeakingTimer.cancel();
342 _speakingAnimation.stop();
343 } else if (!_randomSpeakingTimer.isActive()) {
344 _randomSpeakingTimer.callEach(kSendRandomLevelInterval);
345 _speakingAnimation.start();
346 }
347
348 if (!visible) {
349 for (auto &userpic : _list) {
350 userpic.shownAnimation.stop();
351 userpic.leftAnimation.stop();
352 }
353 }
354 recountAndRepaint();
355 }
356
toggle(Userpic & userpic,bool shown)357 void GroupCallUserpics::toggle(Userpic &userpic, bool shown) {
358 userpic.hiding = !shown;
359 userpic.shownAnimation.start(
360 [=] { recountAndRepaint(); },
361 shown ? 0. : 1.,
362 shown ? 1. : 0.,
363 kDuration);
364 }
365
updatePositions()366 void GroupCallUserpics::updatePositions() {
367 const auto shownCount = ranges::count(_list, false, &Userpic::hiding);
368 if (!shownCount) {
369 return;
370 }
371 const auto single = _st.size;
372 const auto shift = _st.shift;
373 // + 1 * single for the blobs.
374 const auto fullWidth = single + (shownCount - 1) * (single - shift);
375 auto left = (_st.align & Qt::AlignLeft)
376 ? 0
377 : (_st.align & Qt::AlignHCenter)
378 ? (-fullWidth / 2)
379 : -fullWidth;
380 for (auto &userpic : _list) {
381 if (userpic.hiding) {
382 continue;
383 }
384 if (!userpic.positionInited) {
385 userpic.positionInited = true;
386 userpic.left = left;
387 } else if (userpic.left != left) {
388 userpic.leftAnimation.start(
389 _repaint,
390 userpic.left,
391 left,
392 kDuration);
393 userpic.left = left;
394 }
395 left += (single - shift);
396 }
397 }
398
recountAndRepaint()399 void GroupCallUserpics::recountAndRepaint() {
400 auto width = 0;
401 auto maxShown = 0.;
402 for (const auto &userpic : _list) {
403 const auto shown = userpic.shownAnimation.value(
404 userpic.hiding ? 0. : 1.);
405 if (shown > maxShown) {
406 maxShown = shown;
407 }
408 width += anim::interpolate(0, _st.size - _st.shift, shown);
409 }
410 _width = width + anim::interpolate(0, _st.shift, maxShown);
411 if (_repaint) {
412 _repaint();
413 }
414 }
415
416 } // namespace Ui
417