diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index d1921073f..adbf8543d 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -9,6 +9,7 @@ using "ui/basic.style"; using "ui/widgets/widgets.style"; using "ui/layers/layers.style"; +using "ui/chat/chat.style"; // GroupCallUserpics using "window/window.style"; CallSignalBars { @@ -605,9 +606,12 @@ groupCallButtonSkip: 43px; groupCallButtonBottomSkip: 145px; groupCallMuteBottomSkip: 160px; -groupCallTopBarUserpicSize: 28px; -groupCallTopBarUserpicShift: 8px; -groupCallTopBarUserpicStroke: 2px; +groupCallTopBarUserpics: GroupCallUserpics { + size: 28px; + shift: 8px; + stroke: 2px; + align: align(left); +} groupCallTopBarJoin: RoundButton(defaultActiveButton) { width: -26px; height: 26px; diff --git a/Telegram/SourceFiles/calls/calls_top_bar.cpp b/Telegram/SourceFiles/calls/calls_top_bar.cpp index ae8cf508f..e54ec8e5e 100644 --- a/Telegram/SourceFiles/calls/calls_top_bar.cpp +++ b/Telegram/SourceFiles/calls/calls_top_bar.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/paint/blobs_linear.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" +#include "ui/chat/group_call_userpics.h" // Ui::GroupCallUser. #include "ui/chat/group_call_bar.h" // Ui::GroupCallBarContent. #include "ui/layers/generic_box.h" #include "ui/wrap/padding_wrap.h" @@ -32,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "app.h" #include "styles/style_calls.h" +#include "styles/style_chat.h" // style::GroupCallUserpics #include "styles/style_layers.h" namespace Calls { @@ -165,7 +167,7 @@ void DebugInfoBox::updateText() { } // namespace struct TopBar::User { - Ui::GroupCallBarContent::User data; + Ui::GroupCallUser data; }; class Mute final : public Ui::IconButton { @@ -238,6 +240,12 @@ TopBar::TopBar( : RpWidget(parent) , _call(call) , _groupCall(groupCall) +, _userpics(call + ? nullptr + : std::make_unique( + st::groupCallTopBarUserpics, + rpl::single(true), + [=] { updateUserpics(); })) , _durationLabel(_call ? object_ptr(this, st::callBarLabel) : object_ptr(nullptr)) @@ -551,35 +559,31 @@ void TopBar::subscribeToMembersChanges(not_null call) { ) | rpl::map([=](not_null real) { return HistoryView::GroupCallTracker::ContentByCall( real, - HistoryView::UserpicsInRowStyle{ - .size = st::groupCallTopBarUserpicSize, - .shift = st::groupCallTopBarUserpicShift, - .stroke = st::groupCallTopBarUserpicStroke, - }); + st::groupCallTopBarUserpics.size); }) | rpl::flatten_latest( ) | rpl::filter([=](const Ui::GroupCallBarContent &content) { if (_users.size() != content.users.size()) { return true; } for (auto i = 0, count = int(_users.size()); i != count; ++i) { - if (_users[i].data.userpicKey != content.users[i].userpicKey - || _users[i].data.id != content.users[i].id) { + if (_users[i].userpicKey != content.users[i].userpicKey + || _users[i].id != content.users[i].id) { return true; } } return false; }) | rpl::start_with_next([=](const Ui::GroupCallBarContent &content) { - const auto sizeChanged = (_users.size() != content.users.size()); - _users = ranges::view::all( - content.users - ) | ranges::view::transform([](const auto &user) { - return User{ user }; - }) | ranges::to_vector; - generateUserpicsInRow(); - if (sizeChanged) { - updateControlsGeometry(); + _users = content.users; + for (auto &user : _users) { + user.speaking = false; } - update(); + _userpics->update(_users, !isHidden()); + }, lifetime()); + + _userpics->widthValue( + ) | rpl::start_with_next([=](int width) { + _userpicsWidth = width; + updateControlsGeometry(); }, lifetime()); call->peer()->session().changes().peerUpdates( @@ -591,41 +595,10 @@ void TopBar::subscribeToMembersChanges(not_null call) { }) | rpl::start_with_next([=] { updateInfoLabels(); }, lifetime()); - } -void TopBar::generateUserpicsInRow() { - const auto count = int(_users.size()); - if (!count) { - _userpics = QImage(); - return; - } - const auto limit = std::min(count, kMaxUsersInBar); - const auto single = st::groupCallTopBarUserpicSize; - const auto shift = st::groupCallTopBarUserpicShift; - const auto width = single + (limit - 1) * (single - shift); - if (_userpics.width() != width * cIntRetinaFactor()) { - _userpics = QImage( - QSize(width, single) * cIntRetinaFactor(), - QImage::Format_ARGB32_Premultiplied); - } - _userpics.fill(Qt::transparent); - _userpics.setDevicePixelRatio(cRetinaFactor()); - - auto q = Painter(&_userpics); - auto hq = PainterHighQualityEnabler(q); - auto pen = QPen(Qt::transparent); - pen.setWidth(st::groupCallTopBarUserpicStroke); - auto x = (count - 1) * (single - shift); - for (auto i = count; i != 0;) { - q.setCompositionMode(QPainter::CompositionMode_SourceOver); - q.drawImage(x, 0, _users[--i].data.userpic); - q.setCompositionMode(QPainter::CompositionMode_Source); - q.setBrush(Qt::NoBrush); - q.setPen(pen); - q.drawEllipse(x, 0, single, single); - x -= single - shift; - } +void TopBar::updateUserpics() { + update(_mute->width(), 0, _userpics->maxWidth(), height()); } void TopBar::updateInfoLabels() { @@ -688,9 +661,13 @@ void TopBar::updateControlsGeometry() { _durationLabel->moveToLeft(left, st::callBarLabelTop); left += _durationLabel->width() + st::callBarSkip; } - if (!_userpics.isNull()) { - left += (_userpics.width() / _userpics.devicePixelRatio()) - + st::callBarSkip; + if (_userpicsWidth) { + const auto single = st::groupCallTopBarUserpics.size; + const auto skip = anim::interpolate( + 0, + st::callBarSkip, + std::min(_userpicsWidth, single) / float64(single)); + left += _userpicsWidth + skip; } if (_signalBars) { _signalBars->moveToLeft(left, (height() - _signalBars->height()) / 2); @@ -742,11 +719,10 @@ void TopBar::paintEvent(QPaintEvent *e) { : (_muted ? st::callBarBgMuted : st::callBarBg); p.fillRect(e->rect(), std::move(brush)); - if (!_userpics.isNull()) { - const auto imageSize = _userpics.size() - / _userpics.devicePixelRatio(); - const auto top = (height() - imageSize.height()) / 2; - p.drawImage(_mute->width(), top, _userpics); + if (_userpicsWidth) { + const auto size = st::groupCallTopBarUserpics.size; + const auto top = (height() - size) / 2; + _userpics->paint(p, _mute->width(), top, size); } } diff --git a/Telegram/SourceFiles/calls/calls_top_bar.h b/Telegram/SourceFiles/calls/calls_top_bar.h index 2f89032de..79f65830c 100644 --- a/Telegram/SourceFiles/calls/calls_top_bar.h +++ b/Telegram/SourceFiles/calls/calls_top_bar.h @@ -20,6 +20,8 @@ class IconButton; class AbstractButton; class LabelSimple; class FlatLabel; +struct GroupCallUser; +class GroupCallUserpics; } // namespace Ui namespace Main { @@ -66,14 +68,15 @@ private: void setMuted(bool mute); void subscribeToMembersChanges(not_null call); - void generateUserpicsInRow(); + void updateUserpics(); const base::weak_ptr _call; const base::weak_ptr _groupCall; bool _muted = false; - std::vector _users; - QImage _userpics; + std::vector _users; + std::unique_ptr _userpics; + int _userpicsWidth = 0; object_ptr _durationLabel; object_ptr _signalBars; object_ptr _fullInfoLabel; diff --git a/Telegram/SourceFiles/history/view/history_view_group_call_tracker.cpp b/Telegram/SourceFiles/history/view/history_view_group_call_tracker.cpp index 0dd370922..5297a71ec 100644 --- a/Telegram/SourceFiles/history/view/history_view_group_call_tracker.cpp +++ b/Telegram/SourceFiles/history/view/history_view_group_call_tracker.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_group_call.h" #include "main/main_session.h" #include "ui/chat/group_call_bar.h" +#include "ui/chat/group_call_userpics.h" #include "ui/painter.h" #include "calls/calls_group_call.h" #include "calls/calls_instance.h" @@ -24,7 +25,7 @@ namespace HistoryView { void GenerateUserpicsInRow( QImage &result, const std::vector &list, - const UserpicsInRowStyle &st, + const style::GroupCallUserpics &st, int maxElements) { const auto count = int(list.size()); if (!count) { @@ -67,7 +68,7 @@ GroupCallTracker::GroupCallTracker(not_null peer) rpl::producer GroupCallTracker::ContentByCall( not_null call, - const UserpicsInRowStyle &st) { + int userpicSize) { struct State { std::vector userpics; Ui::GroupCallBarContent current; @@ -130,7 +131,7 @@ rpl::producer GroupCallTracker::ContentByCall( static const auto RegenerateUserpics = []( not_null state, not_null call, - const UserpicsInRowStyle &st, + int userpicSize, bool force = false) { const auto result = FillMissingUserpics(state, call) || force; if (!result) { @@ -141,7 +142,9 @@ rpl::producer GroupCallTracker::ContentByCall( state->someUserpicsNotLoaded = false; for (auto &userpic : state->userpics) { userpic.peer->loadUserpic(); - const auto pic = userpic.peer->genUserpic(userpic.view, st.size); + const auto pic = userpic.peer->genUserpic( + userpic.view, + userpicSize); userpic.uniqueKey = userpic.peer->userpicUniqueKey(userpic.view); state->current.users.push_back({ .userpic = pic.toImage(), @@ -161,7 +164,7 @@ rpl::producer GroupCallTracker::ContentByCall( not_null state, not_null call, not_null user, - const UserpicsInRowStyle &st) { + int userpicSize) { const auto i = ranges::find( state->userpics, user, @@ -170,7 +173,7 @@ rpl::producer GroupCallTracker::ContentByCall( return false; } state->userpics.erase(i); - RegenerateUserpics(state, call, st, true); + RegenerateUserpics(state, call, userpicSize, true); return true; }; @@ -178,7 +181,7 @@ rpl::producer GroupCallTracker::ContentByCall( not_null state, not_null call, not_null user, - const UserpicsInRowStyle &st) { + int userpicSize) { Expects(state->userpics.size() <= kLimit); const auto &participants = call->participants(); @@ -237,7 +240,7 @@ rpl::producer GroupCallTracker::ContentByCall( } Assert(state->userpics.size() <= kLimit); } - RegenerateUserpics(state, call, st, true); + RegenerateUserpics(state, call, userpicSize, true); return true; }; @@ -262,12 +265,12 @@ rpl::producer GroupCallTracker::ContentByCall( ) | rpl::start_with_next([=](const ParticipantUpdate &update) { const auto user = update.now ? update.now->user : update.was->user; if (!update.now) { - if (RemoveUserpic(state, call, user, st)) { + if (RemoveUserpic(state, call, user, userpicSize)) { pushNext(); } } else if (update.now->speaking && (!update.was || !update.was->speaking)) { - if (CheckPushToFront(state, call, user, st)) { + if (CheckPushToFront(state, call, user, userpicSize)) { pushNext(); } } else { @@ -287,7 +290,7 @@ rpl::producer GroupCallTracker::ContentByCall( updateSpeakingState = false; } } - if (RegenerateUserpics(state, call, st) + if (RegenerateUserpics(state, call, userpicSize) || updateSpeakingState) { pushNext(); } @@ -296,7 +299,7 @@ rpl::producer GroupCallTracker::ContentByCall( call->participantsSliceAdded( ) | rpl::filter([=] { - return RegenerateUserpics(state, call, st); + return RegenerateUserpics(state, call, userpicSize); }) | rpl::start_with_next(pushNext, lifetime); call->peer()->session().downloaderTaskFinished( @@ -306,14 +309,14 @@ rpl::producer GroupCallTracker::ContentByCall( for (const auto &userpic : state->userpics) { if (userpic.peer->userpicUniqueKey(userpic.view) != userpic.uniqueKey) { - RegenerateUserpics(state, call, st, true); + RegenerateUserpics(state, call, userpicSize, true); pushNext(); return; } } }, lifetime); - RegenerateUserpics(state, call, st); + RegenerateUserpics(state, call, userpicSize); call->fullCountValue( ) | rpl::start_with_next([=](int count) { @@ -345,12 +348,7 @@ rpl::producer GroupCallTracker::content() const { } else if (!call->fullCount() && !call->participantsLoaded()) { call->reload(); } - const auto st = UserpicsInRowStyle{ - .size = st::historyGroupCallUserpicSize, - .shift = st::historyGroupCallUserpicShift, - .stroke = st::historyGroupCallUserpicStroke, - }; - return ContentByCall(call, st); + return ContentByCall(call, st::historyGroupCallUserpics.size); }) | rpl::flatten_latest(); } diff --git a/Telegram/SourceFiles/history/view/history_view_group_call_tracker.h b/Telegram/SourceFiles/history/view/history_view_group_call_tracker.h index b1bed1790..c7b2ad1f0 100644 --- a/Telegram/SourceFiles/history/view/history_view_group_call_tracker.h +++ b/Telegram/SourceFiles/history/view/history_view_group_call_tracker.h @@ -18,6 +18,10 @@ class GroupCall; class CloudImageView; } // namespace Data +namespace style { +struct GroupCallUserpics; +} // namespace style + namespace HistoryView { struct UserpicInRow { @@ -27,16 +31,10 @@ struct UserpicInRow { mutable InMemoryKey uniqueKey; }; -struct UserpicsInRowStyle { - int size = 0; - int shift = 0; - int stroke = 0; -}; - void GenerateUserpicsInRow( QImage &result, const std::vector &list, - const UserpicsInRowStyle &st, + const style::GroupCallUserpics &st, int maxElements = 0); class GroupCallTracker final { @@ -48,7 +46,7 @@ public: [[nodiscard]] static rpl::producer ContentByCall( not_null call, - const UserpicsInRowStyle &st); + int userpicSize); private: const not_null _peer; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 71ac89589..9e81e2405 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -795,8 +795,8 @@ void Message::paintCommentsButton( auto &list = _comments->userpics; const auto limit = HistoryMessageViews::kMaxRecentRepliers; const auto count = std::min(int(views->recentRepliers.size()), limit); - const auto single = st::historyCommentsUserpicSize; - const auto shift = st::historyCommentsUserpicOverlap; + const auto single = st::historyCommentsUserpics.size; + const auto shift = st::historyCommentsUserpics.shift; const auto regenerate = [&] { if (list.size() != count) { return true; @@ -828,12 +828,11 @@ void Message::paintCommentsButton( while (list.size() > count) { list.pop_back(); } - const auto st = UserpicsInRowStyle{ - .size = single, - .shift = shift, - .stroke = st::historyCommentsUserpicStroke, - }; - GenerateUserpicsInRow(_comments->cachedUserpics, list, st, limit); + GenerateUserpicsInRow( + _comments->cachedUserpics, + list, + st::historyCommentsUserpics, + limit); } p.drawImage( left, @@ -2135,8 +2134,8 @@ int Message::minWidthForMedia() const { const auto views = data()->Get(); if (data()->repliesAreComments() && !views->replies.text.isEmpty()) { const auto limit = HistoryMessageViews::kMaxRecentRepliers; - const auto single = st::historyCommentsUserpicSize; - const auto shift = st::historyCommentsUserpicOverlap; + const auto single = st::historyCommentsUserpics.size; + const auto shift = st::historyCommentsUserpics.shift; const auto added = single + (limit - 1) * (single - shift) + st::historyCommentsSkipLeft diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index c8f7b4842..fdc66f585 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -18,6 +18,13 @@ MessageBar { duration: int; } +GroupCallUserpics { + size: pixels; + shift: pixels; + stroke: pixels; + align: align; +} + defaultMessageBar: MessageBar { title: semiboldTextStyle; titleFg: windowActiveTextFg; @@ -739,10 +746,13 @@ historyPollInChosenSelected: icon {{ "poll_select_check", historyFileInIconFgSel historyCommentsButtonHeight: 40px; historyCommentsSkipLeft: 9px; historyCommentsSkipText: 10px; -historyCommentsUserpicSize: 25px; -historyCommentsUserpicStroke: 2px; -historyCommentsUserpicOverlap: 6px; historyCommentsSkipRight: 8px; +historyCommentsUserpics: GroupCallUserpics { + size: 25px; + shift: 6px; + stroke: 2px; + align: align(left); +} boxAttachEmoji: IconButton(historyAttachEmoji) { width: 30px; @@ -798,9 +808,12 @@ historyCommentsOpenOutSelected: icon {{ "history_comments_open", msgFileThumbLin historySlowmodeCounterMargins: margins(0px, 0px, 10px, 0px); -historyGroupCallUserpicSize: 32px; -historyGroupCallUserpicShift: 12px; -historyGroupCallUserpicStroke: 4px; +historyGroupCallUserpics: GroupCallUserpics { + size: 32px; + shift: 12px; + stroke: 4px; + align: align(top); +} historyGroupCallBlobMinRadius: 23px; historyGroupCallBlobMaxRadius: 25px; diff --git a/Telegram/SourceFiles/ui/chat/group_call_bar.cpp b/Telegram/SourceFiles/ui/chat/group_call_bar.cpp index 04723a0ae..b74142371 100644 --- a/Telegram/SourceFiles/ui/chat/group_call_bar.cpp +++ b/Telegram/SourceFiles/ui/chat/group_call_bar.cpp @@ -7,12 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "ui/chat/group_call_bar.h" -#include "ui/chat/message_bar.h" +#include "ui/chat/group_call_userpics.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. @@ -21,71 +19,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include 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 { - 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 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 cacheKey; - crl::time speakingStarted = 0; - QImage cache; - Animations::Simple leftAnimation; - Animations::Simple shownAnimation; - std::unique_ptr blobsAnimation; - int left = 0; - bool positionInited = false; - bool topMost = false; - bool hiding = false; - bool cacheMasked = false; -}; GroupCallBar::GroupCallBar( not_null parent, @@ -98,16 +31,13 @@ GroupCallBar::GroupCallBar( tr::lng_group_call_join(), st::groupCallTopBarJoin)) , _shadow(std::make_unique(_wrap.parentWidget())) -, _randomSpeakingTimer([=] { sendRandomLevels(); }) { +, _userpics(std::make_unique( + st::historyGroupCallUserpics, + std::move(hideBlobs), + [=] { updateUserpics(); })) { _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); @@ -122,7 +52,7 @@ GroupCallBar::GroupCallBar( copy ) | rpl::start_with_next([=](GroupCallBarContent &&content) { _content = content; - updateUserpicsFromContent(); + _userpics->update(_content.users, !_wrap.isHidden()); _inner->update(); }, lifetime()); @@ -140,53 +70,10 @@ GroupCallBar::GroupCallBar( _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() { -} +GroupCallBar::~GroupCallBar() = default; void GroupCallBar::setupInner() { _inner->resize(0, st::historyReplyHeight); @@ -252,142 +139,11 @@ void GroupCallBar::paint(Painter &p) { ? tr::lng_group_call_members(tr::now, lt_count, _content.count) : tr::lng_group_call_no_members(tr::now))); + const auto size = st::historyGroupCallUserpics.size; // 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( - 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() % 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); - } - } + const auto top = (st::historyReplyHeight - st::lineWidth - size) / 2 + + st::lineWidth; + _userpics->paint(p, _inner->width() / 2, top, size); } void GroupCallBar::updateControlsGeometry(QRect wrapGeometry) { @@ -413,127 +169,14 @@ void GroupCallBar::updateShadowGeometry(QRect wrapGeometry) { : 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), + const auto width = _userpics->maxWidth(); + widget->update( + (middle - width / 2), 0, - _maxUserpicsWidth, + width, widget->height()); } diff --git a/Telegram/SourceFiles/ui/chat/group_call_bar.h b/Telegram/SourceFiles/ui/chat/group_call_bar.h index 2641ef586..f085a2d04 100644 --- a/Telegram/SourceFiles/ui/chat/group_call_bar.h +++ b/Telegram/SourceFiles/ui/chat/group_call_bar.h @@ -10,7 +10,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/wrap/slide_wrap.h" #include "ui/effects/animations.h" #include "base/object_ptr.h" -#include "base/timer.h" class Painter; @@ -18,17 +17,13 @@ namespace Ui { class PlainShadow; class RoundButton; +struct GroupCallUser; +class GroupCallUserpics; struct GroupCallBarContent { - struct User { - QImage userpic; - std::pair userpicKey = {}; - int32 id = 0; - bool speaking = false; - }; int count = 0; bool shown = false; - std::vector users; + std::vector users; }; class GroupCallBar final { @@ -58,24 +53,13 @@ public: } private: - using User = GroupCallBarContent::User; - struct BlobsAnimation; - struct Userpic; + using User = GroupCallUser; void updateShadowGeometry(QRect wrapGeometry); void updateControlsGeometry(QRect wrapGeometry); - void updateUserpicsFromContent(); + void updateUserpics(); void setupInner(); void paint(Painter &p); - void paintUserpics(Painter &p); - - void toggleUserpic(Userpic &userpic, bool shown); - void updateUserpics(); - void updateUserpicsPositions(); - void validateUserpicCache(Userpic &userpic); - [[nodiscard]] bool needUserpicCacheRefresh(Userpic &userpic); - void ensureBlobsAnimation(Userpic &userpic); - void sendRandomLevels(); SlideWrap<> _wrap; not_null _inner; @@ -83,17 +67,11 @@ private: std::unique_ptr _shadow; rpl::event_stream<> _barClicks; Fn _shadowGeometryPostprocess; - std::vector _userpics; - base::Timer _randomSpeakingTimer; - Ui::Animations::Basic _speakingAnimation; - int _maxUserpicsWidth = 0; bool _shouldBeShown = false; bool _forceHidden = false; - bool _skipLevelUpdate = false; - crl::time _speakingAnimationHideLastTime = 0; - GroupCallBarContent _content; + std::unique_ptr _userpics; }; diff --git a/Telegram/SourceFiles/ui/chat/group_call_userpics.cpp b/Telegram/SourceFiles/ui/chat/group_call_userpics.cpp new file mode 100644 index 000000000..68fab5f16 --- /dev/null +++ b/Telegram/SourceFiles/ui/chat/group_call_userpics.cpp @@ -0,0 +1,416 @@ +/* +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 "base/openssl_help.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 { + 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 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 cacheKey; + crl::time speakingStarted = 0; + QImage cache; + Animations::Simple leftAnimation; + Animations::Simple shownAnimation; + std::unique_ptr blobsAnimation; + int left = 0; + bool positionInited = false; + bool topMost = false; + bool hiding = false; + bool cacheMasked = false; +}; + +GroupCallUserpics::GroupCallUserpics( + const style::GroupCallUserpics &st, + rpl::producer &&hideBlobs, + Fn 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(Painter &p, int x, int y, int size) { + const auto factor = style::DevicePixelRatio(); + const auto &minScale = kUserpicMinScale; + for (auto &userpic : ranges::view::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 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( + 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 + (openssl::RandomValue() % 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); + { + 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.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 &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 diff --git a/Telegram/SourceFiles/ui/chat/group_call_userpics.h b/Telegram/SourceFiles/ui/chat/group_call_userpics.h new file mode 100644 index 000000000..af1da3dba --- /dev/null +++ b/Telegram/SourceFiles/ui/chat/group_call_userpics.h @@ -0,0 +1,73 @@ +/* +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 +*/ +#pragma once + +#include "base/timer.h" + +namespace style { +struct GroupCallUserpics; +} // namespace style + +namespace Ui { + +struct GroupCallUser { + QImage userpic; + std::pair userpicKey = {}; + int32 id = 0; + bool speaking = false; +}; + +class GroupCallUserpics final { +public: + GroupCallUserpics( + const style::GroupCallUserpics &st, + rpl::producer &&hideBlobs, + Fn repaint); + ~GroupCallUserpics(); + + void update( + const std::vector &users, + bool visible); + void paint(Painter &p, int x, int y, int size); + + [[nodiscard]] int maxWidth() const; + [[nodiscard]] rpl::producer widthValue() const; + + [[nodiscard]] rpl::lifetime &lifetime() { + return _lifetime; + } + +private: + using User = GroupCallUser; + struct BlobsAnimation; + struct Userpic; + + void toggle(Userpic &userpic, bool shown); + void updatePositions(); + void validateCache(Userpic &userpic); + [[nodiscard]] bool needCacheRefresh(Userpic &userpic); + void ensureBlobsAnimation(Userpic &userpic); + void sendRandomLevels(); + void recountAndRepaint(); + + const style::GroupCallUserpics &_st; + std::vector _list; + base::Timer _randomSpeakingTimer; + Fn _repaint; + Ui::Animations::Basic _speakingAnimation; + int _maxWidth = 0; + bool _skipLevelUpdate = false; + crl::time _speakingAnimationHideLastTime = 0; + + rpl::variable _width; + + rpl::lifetime _lifetime; + +}; + +} // namespace Ui diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 016c02a6d..bcdee7aae 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -80,6 +80,8 @@ PRIVATE ui/chat/attach/attach_single_media_preview.h ui/chat/group_call_bar.cpp ui/chat/group_call_bar.h + ui/chat/group_call_userpics.cpp + ui/chat/group_call_userpics.h ui/chat/message_bar.cpp ui/chat/message_bar.h ui/chat/pinned_bar.cpp