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