599 lines
16 KiB
C++
599 lines
16 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_bar.h"
|
|
|
|
#include "ui/chat/message_bar.h"
|
|
#include "ui/widgets/shadow.h"
|
|
#include "ui/widgets/buttons.h"
|
|
#include "ui/paint/blobs.h"
|
|
#include "lang/lang_keys.h"
|
|
#include "base/openssl_help.h"
|
|
#include "styles/style_chat.h"
|
|
#include "styles/style_calls.h"
|
|
#include "styles/style_info.h" // st::topBarArrowPadding, like TopBarWidget.
|
|
#include "styles/palette.h"
|
|
|
|
#include <QtGui/QtEvents>
|
|
|
|
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 GroupCallBar::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 GroupCallBar::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;
|
|
};
|
|
|
|
GroupCallBar::GroupCallBar(
|
|
not_null<QWidget*> parent,
|
|
rpl::producer<GroupCallBarContent> content,
|
|
rpl::producer<bool> &&hideBlobs)
|
|
: _wrap(parent, object_ptr<RpWidget>(parent))
|
|
, _inner(_wrap.entity())
|
|
, _join(std::make_unique<RoundButton>(
|
|
_inner.get(),
|
|
tr::lng_group_call_join(),
|
|
st::groupCallTopBarJoin))
|
|
, _shadow(std::make_unique<PlainShadow>(_wrap.parentWidget()))
|
|
, _randomSpeakingTimer([=] { sendRandomLevels(); }) {
|
|
_wrap.hide(anim::type::instant);
|
|
_shadow->hide();
|
|
|
|
const auto limit = kMaxUserpics;
|
|
const auto single = st::historyGroupCallUserpicSize;
|
|
const auto shift = st::historyGroupCallUserpicShift;
|
|
// + 1 * single for the blobs.
|
|
_maxUserpicsWidth = 2 * single + (limit - 1) * (single - shift);
|
|
|
|
_wrap.entity()->paintRequest(
|
|
) | rpl::start_with_next([=](QRect clip) {
|
|
QPainter(_wrap.entity()).fillRect(clip, st::historyPinnedBg);
|
|
}, lifetime());
|
|
_wrap.setAttribute(Qt::WA_OpaquePaintEvent);
|
|
|
|
auto copy = std::move(
|
|
content
|
|
) | rpl::start_spawning(_wrap.lifetime());
|
|
|
|
rpl::duplicate(
|
|
copy
|
|
) | rpl::start_with_next([=](GroupCallBarContent &&content) {
|
|
_content = content;
|
|
updateUserpicsFromContent();
|
|
_inner->update();
|
|
}, lifetime());
|
|
|
|
std::move(
|
|
copy
|
|
) | rpl::map([=](const GroupCallBarContent &content) {
|
|
return !content.shown;
|
|
}) | rpl::start_with_next_done([=](bool hidden) {
|
|
_shouldBeShown = !hidden;
|
|
if (!_forceHidden) {
|
|
_wrap.toggle(_shouldBeShown, anim::type::normal);
|
|
}
|
|
}, [=] {
|
|
_forceHidden = true;
|
|
_wrap.toggle(false, anim::type::normal);
|
|
}, lifetime());
|
|
|
|
style::PaletteChanged(
|
|
) | rpl::start_with_next([=] {
|
|
for (auto &userpic : _userpics) {
|
|
userpic.cache = QImage();
|
|
}
|
|
}, lifetime());
|
|
|
|
_speakingAnimation.init([=](crl::time now) {
|
|
if (const auto &last = _speakingAnimationHideLastTime; (last > 0)
|
|
&& (now - last >= kBlobsEnterDuration)) {
|
|
_speakingAnimation.stop();
|
|
}
|
|
for (auto &userpic : _userpics) {
|
|
if (const auto blobs = userpic.blobsAnimation.get()) {
|
|
blobs->blobs.updateLevel(now - blobs->lastTime);
|
|
blobs->lastTime = now;
|
|
}
|
|
}
|
|
updateUserpics();
|
|
});
|
|
|
|
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 : _userpics) {
|
|
if (const auto blobs = userpic.blobsAnimation.get()) {
|
|
blobs->blobs.setLevel(0.);
|
|
}
|
|
}
|
|
if (!hide && !_speakingAnimation.animating()) {
|
|
_speakingAnimation.start();
|
|
}
|
|
_skipLevelUpdate = hide;
|
|
}, lifetime());
|
|
|
|
setupInner();
|
|
}
|
|
|
|
GroupCallBar::~GroupCallBar() {
|
|
}
|
|
|
|
void GroupCallBar::setupInner() {
|
|
_inner->resize(0, st::historyReplyHeight);
|
|
_inner->paintRequest(
|
|
) | rpl::start_with_next([=](QRect rect) {
|
|
auto p = Painter(_inner);
|
|
paint(p);
|
|
}, _inner->lifetime());
|
|
|
|
// Clicks.
|
|
_inner->setCursor(style::cur_pointer);
|
|
_inner->events(
|
|
) | rpl::filter([=](not_null<QEvent*> event) {
|
|
return (event->type() == QEvent::MouseButtonPress);
|
|
}) | rpl::map([=] {
|
|
return _inner->events(
|
|
) | rpl::filter([=](not_null<QEvent*> event) {
|
|
return (event->type() == QEvent::MouseButtonRelease);
|
|
}) | rpl::take(1) | rpl::filter([=](not_null<QEvent*> event) {
|
|
return _inner->rect().contains(
|
|
static_cast<QMouseEvent*>(event.get())->pos());
|
|
});
|
|
}) | rpl::flatten_latest(
|
|
) | rpl::map([] {
|
|
return rpl::empty_value();
|
|
}) | rpl::start_to_stream(_barClicks, _inner->lifetime());
|
|
|
|
rpl::combine(
|
|
_inner->widthValue(),
|
|
_join->widthValue()
|
|
) | rpl::start_with_next([=](int outerWidth, int) {
|
|
// Skip shadow of the bar above.
|
|
const auto top = (st::historyReplyHeight
|
|
- st::lineWidth
|
|
- _join->height()) / 2 + st::lineWidth;
|
|
_join->moveToRight(top, top, outerWidth);
|
|
}, _join->lifetime());
|
|
|
|
_wrap.geometryValue(
|
|
) | rpl::start_with_next([=](QRect rect) {
|
|
updateShadowGeometry(rect);
|
|
updateControlsGeometry(rect);
|
|
}, _inner->lifetime());
|
|
}
|
|
|
|
void GroupCallBar::paint(Painter &p) {
|
|
p.fillRect(_inner->rect(), st::historyComposeAreaBg);
|
|
|
|
const auto left = st::topBarArrowPadding.right();
|
|
const auto titleTop = st::msgReplyPadding.top();
|
|
const auto textTop = titleTop + st::msgServiceNameFont->height;
|
|
const auto width = _inner->width();
|
|
p.setPen(st::defaultMessageBar.textFg);
|
|
p.setFont(st::defaultMessageBar.title.font);
|
|
p.drawTextLeft(left, titleTop, width, tr::lng_group_call_title(tr::now));
|
|
p.setPen(st::historyStatusFg);
|
|
p.setFont(st::defaultMessageBar.text.font);
|
|
p.drawTextLeft(
|
|
left,
|
|
textTop,
|
|
width,
|
|
(_content.count > 0
|
|
? tr::lng_group_call_members(tr::now, lt_count, _content.count)
|
|
: tr::lng_group_call_no_members(tr::now)));
|
|
|
|
// Skip shadow of the bar above.
|
|
paintUserpics(p);
|
|
}
|
|
|
|
void GroupCallBar::paintUserpics(Painter &p) {
|
|
const auto top = (st::historyReplyHeight
|
|
- st::lineWidth
|
|
- st::historyGroupCallUserpicSize) / 2 + st::lineWidth;
|
|
const auto middle = _inner->width() / 2;
|
|
const auto size = st::historyGroupCallUserpicSize;
|
|
const auto factor = style::DevicePixelRatio();
|
|
const auto &minScale = kUserpicMinScale;
|
|
for (auto &userpic : ranges::view::reverse(_userpics)) {
|
|
const auto shown = userpic.shownAnimation.value(
|
|
userpic.hiding ? 0. : 1.);
|
|
if (shown == 0.) {
|
|
continue;
|
|
}
|
|
validateUserpicCache(userpic);
|
|
p.setOpacity(shown);
|
|
const auto left = middle + 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., top + 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, top, size, size),
|
|
userpic.cache,
|
|
QRect(skip, skip, size * factor, size * factor));
|
|
} else {
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
|
|
auto target = QRect(
|
|
left + (1 - kWideScale) / 2 * size,
|
|
top + (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();
|
|
};
|
|
_userpics.erase(ranges::remove_if(_userpics, hidden), end(_userpics));
|
|
}
|
|
|
|
bool GroupCallBar::needUserpicCacheRefresh(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 GroupCallBar::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 GroupCallBar::sendRandomLevels() {
|
|
if (_skipLevelUpdate) {
|
|
return;
|
|
}
|
|
for (auto &userpic : _userpics) {
|
|
if (const auto blobs = userpic.blobsAnimation.get()) {
|
|
const auto value = 30 + (openssl::RandomValue<uint32>() % 70);
|
|
userpic.blobsAnimation->blobs.setLevel(float64(value) / 100.);
|
|
}
|
|
}
|
|
}
|
|
|
|
void GroupCallBar::validateUserpicCache(Userpic &userpic) {
|
|
if (!needUserpicCacheRefresh(userpic)) {
|
|
return;
|
|
}
|
|
const auto factor = style::DevicePixelRatio();
|
|
const auto size = st::historyGroupCallUserpicSize;
|
|
const auto shift = st::historyGroupCallUserpicShift;
|
|
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);
|
|
{
|
|
Painter p(&userpic.cache);
|
|
const auto skip = (kWideScale - 1) / 2 * size;
|
|
p.drawImage(skip, skip, userpic.data.userpic);
|
|
|
|
if (userpic.cacheMasked) {
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
auto pen = QPen(Qt::transparent);
|
|
pen.setWidth(st::historyGroupCallUserpicStroke);
|
|
p.setCompositionMode(QPainter::CompositionMode_Source);
|
|
p.setBrush(Qt::transparent);
|
|
p.setPen(pen);
|
|
p.drawEllipse(skip - size + shift, skip, size, size);
|
|
}
|
|
}
|
|
}
|
|
|
|
void GroupCallBar::updateControlsGeometry(QRect wrapGeometry) {
|
|
const auto hidden = _wrap.isHidden() || !wrapGeometry.height();
|
|
if (_shadow->isHidden() != hidden) {
|
|
_shadow->setVisible(!hidden);
|
|
}
|
|
}
|
|
|
|
void GroupCallBar::setShadowGeometryPostprocess(Fn<QRect(QRect)> postprocess) {
|
|
_shadowGeometryPostprocess = std::move(postprocess);
|
|
updateShadowGeometry(_wrap.geometry());
|
|
}
|
|
|
|
void GroupCallBar::updateShadowGeometry(QRect wrapGeometry) {
|
|
const auto regular = QRect(
|
|
wrapGeometry.x(),
|
|
wrapGeometry.y() + wrapGeometry.height(),
|
|
wrapGeometry.width(),
|
|
st::lineWidth);
|
|
_shadow->setGeometry(_shadowGeometryPostprocess
|
|
? _shadowGeometryPostprocess(regular)
|
|
: regular);
|
|
}
|
|
|
|
void GroupCallBar::updateUserpicsFromContent() {
|
|
const auto idFromUserpic = [](const Userpic &userpic) {
|
|
return userpic.data.id;
|
|
};
|
|
|
|
// Use "topMost" as "willBeHidden" flag.
|
|
for (auto &userpic : _userpics) {
|
|
userpic.topMost = true;
|
|
}
|
|
for (const auto &user : _content.users) {
|
|
const auto i = ranges::find(_userpics, user.id, idFromUserpic);
|
|
if (i == end(_userpics)) {
|
|
_userpics.push_back(Userpic{ user });
|
|
toggleUserpic(_userpics.back(), true);
|
|
continue;
|
|
}
|
|
i->topMost = false;
|
|
|
|
if (i->hiding) {
|
|
toggleUserpic(*i, true);
|
|
}
|
|
i->data = user;
|
|
|
|
// Put this one after the last we are not hiding.
|
|
for (auto j = end(_userpics) - 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(_userpics);
|
|
const auto userpicsEnd = end(_userpics);
|
|
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) {
|
|
toggleUserpic(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);
|
|
}
|
|
updateUserpicsPositions();
|
|
|
|
if (!hasBlobs) {
|
|
_randomSpeakingTimer.cancel();
|
|
_speakingAnimation.stop();
|
|
} else if (!_randomSpeakingTimer.isActive()) {
|
|
_randomSpeakingTimer.callEach(kSendRandomLevelInterval);
|
|
_speakingAnimation.start();
|
|
}
|
|
|
|
if (_wrap.isHidden()) {
|
|
for (auto &userpic : _userpics) {
|
|
userpic.shownAnimation.stop();
|
|
userpic.leftAnimation.stop();
|
|
}
|
|
}
|
|
}
|
|
|
|
void GroupCallBar::toggleUserpic(Userpic &userpic, bool shown) {
|
|
userpic.hiding = !shown;
|
|
userpic.shownAnimation.start(
|
|
[=] { updateUserpics(); },
|
|
shown ? 0. : 1.,
|
|
shown ? 1. : 0.,
|
|
kDuration);
|
|
}
|
|
|
|
void GroupCallBar::updateUserpicsPositions() {
|
|
const auto shownCount = ranges::count(_userpics, false, &Userpic::hiding);
|
|
if (!shownCount) {
|
|
return;
|
|
}
|
|
const auto single = st::historyGroupCallUserpicSize;
|
|
const auto shift = st::historyGroupCallUserpicShift;
|
|
// + 1 * single for the blobs.
|
|
const auto fullWidth = single + (shownCount - 1) * (single - shift);
|
|
auto left = (-fullWidth / 2);
|
|
for (auto &userpic : _userpics) {
|
|
if (userpic.hiding) {
|
|
continue;
|
|
}
|
|
if (!userpic.positionInited) {
|
|
userpic.positionInited = true;
|
|
userpic.left = left;
|
|
} else if (userpic.left != left) {
|
|
userpic.leftAnimation.start(
|
|
[=] { updateUserpics(); },
|
|
userpic.left,
|
|
left,
|
|
kDuration);
|
|
userpic.left = left;
|
|
}
|
|
left += (single - shift);
|
|
}
|
|
}
|
|
|
|
void GroupCallBar::updateUserpics() {
|
|
const auto widget = _wrap.entity();
|
|
const auto middle = widget->width() / 2;
|
|
_wrap.entity()->update(
|
|
(middle - _maxUserpicsWidth / 2),
|
|
0,
|
|
_maxUserpicsWidth,
|
|
widget->height());
|
|
}
|
|
|
|
void GroupCallBar::show() {
|
|
if (!_forceHidden) {
|
|
return;
|
|
}
|
|
_forceHidden = false;
|
|
if (_shouldBeShown) {
|
|
_wrap.show(anim::type::instant);
|
|
_shadow->show();
|
|
}
|
|
}
|
|
|
|
void GroupCallBar::hide() {
|
|
if (_forceHidden) {
|
|
return;
|
|
}
|
|
_forceHidden = true;
|
|
_wrap.hide(anim::type::instant);
|
|
_shadow->hide();
|
|
}
|
|
|
|
void GroupCallBar::raise() {
|
|
_wrap.raise();
|
|
_shadow->raise();
|
|
}
|
|
|
|
void GroupCallBar::finishAnimating() {
|
|
_wrap.finishAnimating();
|
|
}
|
|
|
|
void GroupCallBar::move(int x, int y) {
|
|
_wrap.move(x, y);
|
|
}
|
|
|
|
void GroupCallBar::resizeToWidth(int width) {
|
|
_wrap.entity()->resizeToWidth(width);
|
|
_inner->resizeToWidth(width);
|
|
}
|
|
|
|
int GroupCallBar::height() const {
|
|
return !_forceHidden
|
|
? _wrap.height()
|
|
: _shouldBeShown
|
|
? st::historyReplyHeight
|
|
: 0;
|
|
}
|
|
|
|
rpl::producer<int> GroupCallBar::heightValue() const {
|
|
return _wrap.heightValue();
|
|
}
|
|
|
|
rpl::producer<> GroupCallBar::barClicks() const {
|
|
return _barClicks.events();
|
|
}
|
|
|
|
rpl::producer<> GroupCallBar::joinClicks() const {
|
|
return _join->clicks() | rpl::to_empty;
|
|
}
|
|
|
|
} // namespace Ui
|