417 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			417 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
| This file is part of Telegram Desktop,
 | |
| the official desktop application for the Telegram messaging service.
 | |
| 
 | |
| For license and copyright information please follow this link:
 | |
| https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 | |
| */
 | |
| #include "ui/chat/group_call_userpics.h"
 | |
| 
 | |
| #include "ui/paint/blobs.h"
 | |
| #include "ui/painter.h"
 | |
| #include "base/random.h"
 | |
| #include "styles/style_chat.h"
 | |
| 
 | |
| namespace Ui {
 | |
| namespace {
 | |
| 
 | |
| constexpr auto kDuration = 160;
 | |
| constexpr auto kMaxUserpics = 4;
 | |
| constexpr auto kWideScale = 5;
 | |
| 
 | |
| constexpr auto kBlobsEnterDuration = crl::time(250);
 | |
| constexpr auto kLevelDuration = 100. + 500. * 0.23;
 | |
| constexpr auto kBlobScale = 0.605;
 | |
| constexpr auto kMinorBlobFactor = 0.9f;
 | |
| constexpr auto kUserpicMinScale = 0.8;
 | |
| constexpr auto kMaxLevel = 1.;
 | |
| constexpr auto kSendRandomLevelInterval = crl::time(100);
 | |
| 
 | |
| auto Blobs()->std::array<Ui::Paint::Blobs::BlobData, 2> {
 | |
| 	return { {
 | |
| 		{
 | |
| 			.segmentsCount = 6,
 | |
| 			.minScale = kBlobScale * kMinorBlobFactor,
 | |
| 			.minRadius = st::historyGroupCallBlobMinRadius * kMinorBlobFactor,
 | |
| 			.maxRadius = st::historyGroupCallBlobMaxRadius * kMinorBlobFactor,
 | |
| 			.speedScale = 1.,
 | |
| 			.alpha = .5,
 | |
| 		},
 | |
| 		{
 | |
| 			.segmentsCount = 8,
 | |
| 			.minScale = kBlobScale,
 | |
| 			.minRadius = (float)st::historyGroupCallBlobMinRadius,
 | |
| 			.maxRadius = (float)st::historyGroupCallBlobMaxRadius,
 | |
| 			.speedScale = 1.,
 | |
| 			.alpha = .2,
 | |
| 		},
 | |
| 	} };
 | |
| }
 | |
| 
 | |
| } // namespace
 | |
| 
 | |
| struct GroupCallUserpics::BlobsAnimation {
 | |
| 	BlobsAnimation(
 | |
| 		std::vector<Ui::Paint::Blobs::BlobData> blobDatas,
 | |
| 		float levelDuration,
 | |
| 		float maxLevel)
 | |
| 	: blobs(std::move(blobDatas), levelDuration, maxLevel) {
 | |
| 	}
 | |
| 
 | |
| 	Ui::Paint::Blobs blobs;
 | |
| 	crl::time lastTime = 0;
 | |
| 	crl::time lastSpeakingUpdateTime = 0;
 | |
| 	float64 enter = 0.;
 | |
| };
 | |
| 
 | |
| struct GroupCallUserpics::Userpic {
 | |
| 	User data;
 | |
| 	std::pair<uint64, uint64> cacheKey;
 | |
| 	crl::time speakingStarted = 0;
 | |
| 	QImage cache;
 | |
| 	Animations::Simple leftAnimation;
 | |
| 	Animations::Simple shownAnimation;
 | |
| 	std::unique_ptr<BlobsAnimation> blobsAnimation;
 | |
| 	int left = 0;
 | |
| 	bool positionInited = false;
 | |
| 	bool topMost = false;
 | |
| 	bool hiding = false;
 | |
| 	bool cacheMasked = false;
 | |
| };
 | |
| 
 | |
| GroupCallUserpics::GroupCallUserpics(
 | |
| 	const style::GroupCallUserpics &st,
 | |
| 	rpl::producer<bool> &&hideBlobs,
 | |
| 	Fn<void()> repaint)
 | |
| : _st(st)
 | |
| , _randomSpeakingTimer([=] { sendRandomLevels(); })
 | |
| , _repaint(std::move(repaint)) {
 | |
| 	const auto limit = kMaxUserpics;
 | |
| 	const auto single = _st.size;
 | |
| 	const auto shift = _st.shift;
 | |
| 	// + 1 * single for the blobs.
 | |
| 	_maxWidth = 2 * single + (limit - 1) * (single - shift);
 | |
| 
 | |
| 	style::PaletteChanged(
 | |
| 	) | rpl::start_with_next([=] {
 | |
| 		for (auto &userpic : _list) {
 | |
| 			userpic.cache = QImage();
 | |
| 		}
 | |
| 	}, lifetime());
 | |
| 
 | |
| 	_speakingAnimation.init([=](crl::time now) {
 | |
| 		if (const auto &last = _speakingAnimationHideLastTime; (last > 0)
 | |
| 			&& (now - last >= kBlobsEnterDuration)) {
 | |
| 			_speakingAnimation.stop();
 | |
| 		}
 | |
| 		for (auto &userpic : _list) {
 | |
| 			if (const auto blobs = userpic.blobsAnimation.get()) {
 | |
| 				blobs->blobs.updateLevel(now - blobs->lastTime);
 | |
| 				blobs->lastTime = now;
 | |
| 			}
 | |
| 		}
 | |
| 		if (const auto onstack = _repaint) {
 | |
| 			onstack();
 | |
| 		}
 | |
| 	});
 | |
| 
 | |
| 	rpl::combine(
 | |
| 		rpl::single(anim::Disabled()) | rpl::then(anim::Disables()),
 | |
| 		std::move(hideBlobs)
 | |
| 	) | rpl::start_with_next([=](bool animDisabled, bool deactivated) {
 | |
| 		const auto hide = animDisabled || deactivated;
 | |
| 
 | |
| 		if (!(hide && _speakingAnimationHideLastTime)) {
 | |
| 			_speakingAnimationHideLastTime = hide ? crl::now() : 0;
 | |
| 		}
 | |
| 		_skipLevelUpdate = hide;
 | |
| 		for (auto &userpic : _list) {
 | |
| 			if (const auto blobs = userpic.blobsAnimation.get()) {
 | |
| 				blobs->blobs.setLevel(0.);
 | |
| 			}
 | |
| 		}
 | |
| 		if (!hide && !_speakingAnimation.animating()) {
 | |
| 			_speakingAnimation.start();
 | |
| 		}
 | |
| 		_skipLevelUpdate = hide;
 | |
| 	}, lifetime());
 | |
| }
 | |
| 
 | |
| GroupCallUserpics::~GroupCallUserpics() = default;
 | |
| 
 | |
| void GroupCallUserpics::paint(QPainter &p, int x, int y, int size) {
 | |
| 	const auto factor = style::DevicePixelRatio();
 | |
| 	const auto &minScale = kUserpicMinScale;
 | |
| 	for (auto &userpic : ranges::views::reverse(_list)) {
 | |
| 		const auto shown = userpic.shownAnimation.value(
 | |
| 			userpic.hiding ? 0. : 1.);
 | |
| 		if (shown == 0.) {
 | |
| 			continue;
 | |
| 		}
 | |
| 		validateCache(userpic);
 | |
| 		p.setOpacity(shown);
 | |
| 		const auto left = x + userpic.leftAnimation.value(userpic.left);
 | |
| 		const auto blobs = userpic.blobsAnimation.get();
 | |
| 		const auto shownScale = 0.5 + shown / 2.;
 | |
| 		const auto scale = shownScale * (!blobs
 | |
| 			? 1.
 | |
| 			: (minScale
 | |
| 				+ (1. - minScale) * (_speakingAnimationHideLastTime
 | |
| 					? (1. - blobs->blobs.currentLevel())
 | |
| 					: blobs->blobs.currentLevel())));
 | |
| 		if (blobs) {
 | |
| 			auto hq = PainterHighQualityEnabler(p);
 | |
| 
 | |
| 			const auto shift = QPointF(left + size / 2., y + size / 2.);
 | |
| 			p.translate(shift);
 | |
| 			blobs->blobs.paint(p, st::windowActiveTextFg);
 | |
| 			p.translate(-shift);
 | |
| 			p.setOpacity(1.);
 | |
| 		}
 | |
| 		if (std::abs(scale - 1.) < 0.001) {
 | |
| 			const auto skip = ((kWideScale - 1) / 2) * size * factor;
 | |
| 			p.drawImage(
 | |
| 				QRect(left, y, size, size),
 | |
| 				userpic.cache,
 | |
| 				QRect(skip, skip, size * factor, size * factor));
 | |
| 		} else {
 | |
| 			auto hq = PainterHighQualityEnabler(p);
 | |
| 
 | |
| 			auto target = QRect(
 | |
| 				left + (1 - kWideScale) / 2 * size,
 | |
| 				y + (1 - kWideScale) / 2 * size,
 | |
| 				kWideScale * size,
 | |
| 				kWideScale * size);
 | |
| 			auto shrink = anim::interpolate(
 | |
| 				(1 - kWideScale) / 2 * size,
 | |
| 				0,
 | |
| 				scale);
 | |
| 			auto margins = QMargins(shrink, shrink, shrink, shrink);
 | |
| 			p.drawImage(target.marginsAdded(margins), userpic.cache);
 | |
| 		}
 | |
| 	}
 | |
| 	p.setOpacity(1.);
 | |
| 
 | |
| 	const auto hidden = [](const Userpic &userpic) {
 | |
| 		return userpic.hiding && !userpic.shownAnimation.animating();
 | |
| 	};
 | |
| 	_list.erase(ranges::remove_if(_list, hidden), end(_list));
 | |
| }
 | |
| 
 | |
| int GroupCallUserpics::maxWidth() const {
 | |
| 	return _maxWidth;
 | |
| }
 | |
| 
 | |
| rpl::producer<int> GroupCallUserpics::widthValue() const {
 | |
| 	return _width.value();
 | |
| }
 | |
| 
 | |
| bool GroupCallUserpics::needCacheRefresh(Userpic &userpic) {
 | |
| 	if (userpic.cache.isNull()) {
 | |
| 		return true;
 | |
| 	} else if (userpic.hiding) {
 | |
| 		return false;
 | |
| 	} else if (userpic.cacheKey != userpic.data.userpicKey) {
 | |
| 		return true;
 | |
| 	}
 | |
| 	const auto shouldBeMasked = !userpic.topMost;
 | |
| 	if (userpic.cacheMasked == shouldBeMasked || !shouldBeMasked) {
 | |
| 		return true;
 | |
| 	}
 | |
| 	return !userpic.leftAnimation.animating();
 | |
| }
 | |
| 
 | |
| void GroupCallUserpics::ensureBlobsAnimation(Userpic &userpic) {
 | |
| 	if (userpic.blobsAnimation) {
 | |
| 		return;
 | |
| 	}
 | |
| 	userpic.blobsAnimation = std::make_unique<BlobsAnimation>(
 | |
| 		Blobs() | ranges::to_vector,
 | |
| 		kLevelDuration,
 | |
| 		kMaxLevel);
 | |
| 	userpic.blobsAnimation->lastTime = crl::now();
 | |
| }
 | |
| 
 | |
| void GroupCallUserpics::sendRandomLevels() {
 | |
| 	if (_skipLevelUpdate) {
 | |
| 		return;
 | |
| 	}
 | |
| 	for (auto &userpic : _list) {
 | |
| 		if (const auto blobs = userpic.blobsAnimation.get()) {
 | |
| 			const auto value = 30 + base::RandomIndex(70);
 | |
| 			userpic.blobsAnimation->blobs.setLevel(float64(value) / 100.);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void GroupCallUserpics::validateCache(Userpic &userpic) {
 | |
| 	if (!needCacheRefresh(userpic)) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto factor = style::DevicePixelRatio();
 | |
| 	const auto size = _st.size;
 | |
| 	const auto shift = _st.shift;
 | |
| 	const auto full = QSize(size, size) * kWideScale * factor;
 | |
| 	if (userpic.cache.isNull()) {
 | |
| 		userpic.cache = QImage(full, QImage::Format_ARGB32_Premultiplied);
 | |
| 		userpic.cache.setDevicePixelRatio(factor);
 | |
| 	}
 | |
| 	userpic.cacheKey = userpic.data.userpicKey;
 | |
| 	userpic.cacheMasked = !userpic.topMost;
 | |
| 	userpic.cache.fill(Qt::transparent);
 | |
| 	{
 | |
| 		auto p = QPainter(&userpic.cache);
 | |
| 		const auto skip = (kWideScale - 1) / 2 * size;
 | |
| 		p.drawImage(QRect(skip, skip, size, size), userpic.data.userpic);
 | |
| 
 | |
| 		if (userpic.cacheMasked) {
 | |
| 			auto hq = PainterHighQualityEnabler(p);
 | |
| 			auto pen = QPen(Qt::transparent);
 | |
| 			pen.setWidth(_st.stroke);
 | |
| 			p.setCompositionMode(QPainter::CompositionMode_Source);
 | |
| 			p.setBrush(Qt::transparent);
 | |
| 			p.setPen(pen);
 | |
| 			p.drawEllipse(skip - size + shift, skip, size, size);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void GroupCallUserpics::update(
 | |
| 		const std::vector<GroupCallUser> &users,
 | |
| 		bool visible) {
 | |
| 	const auto idFromUserpic = [](const Userpic &userpic) {
 | |
| 		return userpic.data.id;
 | |
| 	};
 | |
| 
 | |
| 	// Use "topMost" as "willBeHidden" flag.
 | |
| 	for (auto &userpic : _list) {
 | |
| 		userpic.topMost = true;
 | |
| 	}
 | |
| 	for (const auto &user : users) {
 | |
| 		const auto i = ranges::find(_list, user.id, idFromUserpic);
 | |
| 		if (i == end(_list)) {
 | |
| 			_list.push_back(Userpic{ user });
 | |
| 			toggle(_list.back(), true);
 | |
| 			continue;
 | |
| 		}
 | |
| 		i->topMost = false;
 | |
| 
 | |
| 		if (i->hiding) {
 | |
| 			toggle(*i, true);
 | |
| 		}
 | |
| 		i->data = user;
 | |
| 
 | |
| 		// Put this one after the last we are not hiding.
 | |
| 		for (auto j = end(_list) - 1; j != i; --j) {
 | |
| 			if (!j->topMost) {
 | |
| 				ranges::rotate(i, i + 1, j + 1);
 | |
| 				break;
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Hide the ones that "willBeHidden" (currently having "topMost" flag).
 | |
| 	// Set correct real values of "topMost" flag.
 | |
| 	const auto userpicsBegin = begin(_list);
 | |
| 	const auto userpicsEnd = end(_list);
 | |
| 	auto markedTopMost = userpicsEnd;
 | |
| 	auto hasBlobs = false;
 | |
| 	for (auto i = userpicsBegin; i != userpicsEnd; ++i) {
 | |
| 		auto &userpic = *i;
 | |
| 		if (userpic.data.speaking) {
 | |
| 			ensureBlobsAnimation(userpic);
 | |
| 			hasBlobs = true;
 | |
| 		} else {
 | |
| 			userpic.blobsAnimation = nullptr;
 | |
| 		}
 | |
| 		if (userpic.topMost) {
 | |
| 			toggle(userpic, false);
 | |
| 			userpic.topMost = false;
 | |
| 		} else if (markedTopMost == userpicsEnd) {
 | |
| 			userpic.topMost = true;
 | |
| 			markedTopMost = i;
 | |
| 		}
 | |
| 	}
 | |
| 	if (markedTopMost != userpicsEnd && markedTopMost != userpicsBegin) {
 | |
| 		// Bring the topMost userpic to the very beginning, above all hiding.
 | |
| 		std::rotate(userpicsBegin, markedTopMost, markedTopMost + 1);
 | |
| 	}
 | |
| 	updatePositions();
 | |
| 
 | |
| 	if (!hasBlobs) {
 | |
| 		_randomSpeakingTimer.cancel();
 | |
| 		_speakingAnimation.stop();
 | |
| 	} else if (!_randomSpeakingTimer.isActive()) {
 | |
| 		_randomSpeakingTimer.callEach(kSendRandomLevelInterval);
 | |
| 		_speakingAnimation.start();
 | |
| 	}
 | |
| 
 | |
| 	if (!visible) {
 | |
| 		for (auto &userpic : _list) {
 | |
| 			userpic.shownAnimation.stop();
 | |
| 			userpic.leftAnimation.stop();
 | |
| 		}
 | |
| 	}
 | |
| 	recountAndRepaint();
 | |
| }
 | |
| 
 | |
| void GroupCallUserpics::toggle(Userpic &userpic, bool shown) {
 | |
| 	userpic.hiding = !shown;
 | |
| 	userpic.shownAnimation.start(
 | |
| 		[=] { recountAndRepaint(); },
 | |
| 		shown ? 0. : 1.,
 | |
| 		shown ? 1. : 0.,
 | |
| 		kDuration);
 | |
| }
 | |
| 
 | |
| void GroupCallUserpics::updatePositions() {
 | |
| 	const auto shownCount = ranges::count(_list, false, &Userpic::hiding);
 | |
| 	if (!shownCount) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto single = _st.size;
 | |
| 	const auto shift = _st.shift;
 | |
| 	// + 1 * single for the blobs.
 | |
| 	const auto fullWidth = single + (shownCount - 1) * (single - shift);
 | |
| 	auto left = (_st.align & Qt::AlignLeft)
 | |
| 		? 0
 | |
| 		: (_st.align & Qt::AlignHCenter)
 | |
| 		? (-fullWidth / 2)
 | |
| 		: -fullWidth;
 | |
| 	for (auto &userpic : _list) {
 | |
| 		if (userpic.hiding) {
 | |
| 			continue;
 | |
| 		}
 | |
| 		if (!userpic.positionInited) {
 | |
| 			userpic.positionInited = true;
 | |
| 			userpic.left = left;
 | |
| 		} else if (userpic.left != left) {
 | |
| 			userpic.leftAnimation.start(
 | |
| 				_repaint,
 | |
| 				userpic.left,
 | |
| 				left,
 | |
| 				kDuration);
 | |
| 			userpic.left = left;
 | |
| 		}
 | |
| 		left += (single - shift);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void GroupCallUserpics::recountAndRepaint() {
 | |
| 	auto width = 0;
 | |
| 	auto maxShown = 0.;
 | |
| 	for (const auto &userpic : _list) {
 | |
| 		const auto shown = userpic.shownAnimation.value(
 | |
| 			userpic.hiding ? 0. : 1.);
 | |
| 		if (shown > maxShown) {
 | |
| 			maxShown = shown;
 | |
| 		}
 | |
| 		width += anim::interpolate(0, _st.size - _st.shift, shown);
 | |
| 	}
 | |
| 	_width = width + anim::interpolate(0, _st.shift, maxShown);
 | |
| 	if (_repaint) {
 | |
| 		_repaint();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| } // namespace Ui
 | 
