mirror of
https://github.com/telegramdesktop/tdesktop
synced 2025-02-24 01:06:59 +00:00
Support animated stickers in suggestions.
This commit is contained in:
parent
848ea16eef
commit
0dd1b4eae6
@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "mainwindow.h"
|
||||
#include "apiwrap.h"
|
||||
#include "storage/localstorage.h"
|
||||
#include "lottie/lottie_single_player.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/image/image.h"
|
||||
#include "auth_session.h"
|
||||
@ -23,11 +24,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "styles/style_widgets.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
|
||||
FieldAutocomplete::FieldAutocomplete(QWidget *parent) : TWidget(parent)
|
||||
FieldAutocomplete::FieldAutocomplete(QWidget *parent)
|
||||
: RpWidget(parent)
|
||||
, _scroll(this, st::mentionScroll) {
|
||||
_scroll->setGeometry(rect());
|
||||
|
||||
_inner = _scroll->setOwnedWidget(object_ptr<internal::FieldAutocompleteInner>(this, &_mrows, &_hrows, &_brows, &_srows));
|
||||
_inner = _scroll->setOwnedWidget(
|
||||
object_ptr<internal::FieldAutocompleteInner>(
|
||||
this,
|
||||
&_mrows,
|
||||
&_hrows,
|
||||
&_brows,
|
||||
&_srows));
|
||||
_inner->setGeometry(rect());
|
||||
|
||||
connect(_inner, SIGNAL(mentionChosen(UserData*, FieldAutocomplete::ChooseMethod)), this, SIGNAL(mentionChosen(UserData*, FieldAutocomplete::ChooseMethod)));
|
||||
@ -44,6 +52,8 @@ FieldAutocomplete::FieldAutocomplete(QWidget *parent) : TWidget(parent)
|
||||
connect(_scroll, SIGNAL(geometryChanged()), _inner, SLOT(onParentGeometryChanged()));
|
||||
}
|
||||
|
||||
FieldAutocomplete::~FieldAutocomplete() = default;
|
||||
|
||||
void FieldAutocomplete::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
|
||||
@ -70,7 +80,12 @@ void FieldAutocomplete::showFiltered(
|
||||
_channel = peer->asChannel();
|
||||
if (query.isEmpty()) {
|
||||
_type = Type::Mentions;
|
||||
rowsUpdated(internal::MentionRows(), internal::HashtagRows(), internal::BotCommandRows(), _srows, false);
|
||||
rowsUpdated(
|
||||
internal::MentionRows(),
|
||||
internal::HashtagRows(),
|
||||
internal::BotCommandRows(),
|
||||
base::take(_srows),
|
||||
false);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -108,7 +123,12 @@ void FieldAutocomplete::showStickers(EmojiPtr emoji) {
|
||||
_emoji = emoji;
|
||||
_type = Type::Stickers;
|
||||
if (!emoji) {
|
||||
rowsUpdated(_mrows, _hrows, _brows, internal::StickerRows(), false);
|
||||
rowsUpdated(
|
||||
base::take(_mrows),
|
||||
base::take(_hrows),
|
||||
base::take(_brows),
|
||||
internal::StickerRows(),
|
||||
false);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -137,6 +157,31 @@ inline int indexOfInFirstN(const T &v, const U &elem, int last) {
|
||||
}
|
||||
}
|
||||
|
||||
internal::StickerRows FieldAutocomplete::getStickerSuggestions() {
|
||||
const auto list = Stickers::GetListByEmoji(
|
||||
_emoji,
|
||||
_stickersSeed
|
||||
);
|
||||
auto result = ranges::view::all(
|
||||
list
|
||||
) | ranges::view::transform([](not_null<DocumentData*> sticker) {
|
||||
return internal::StickerSuggestion{ sticker };
|
||||
}) | ranges::to_vector;
|
||||
for (auto &suggestion : _srows) {
|
||||
if (!suggestion.animated) {
|
||||
continue;
|
||||
}
|
||||
const auto i = ranges::find(
|
||||
result,
|
||||
suggestion.document,
|
||||
&internal::StickerSuggestion::document);
|
||||
if (i != end(result)) {
|
||||
i->animated = std::move(suggestion.animated);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void FieldAutocomplete::updateFiltered(bool resetScroll) {
|
||||
int32 now = unixtime(), recentInlineBots = 0;
|
||||
internal::MentionRows mrows;
|
||||
@ -144,7 +189,7 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) {
|
||||
internal::BotCommandRows brows;
|
||||
internal::StickerRows srows;
|
||||
if (_emoji) {
|
||||
srows = Stickers::GetListByEmoji(_emoji, _stickersSeed);
|
||||
srows = getStickerSuggestions();
|
||||
} else if (_type == Type::Mentions) {
|
||||
int maxListSize = _addInlineBots ? cRecentInlineBots().size() : 0;
|
||||
if (_chat) {
|
||||
@ -326,11 +371,21 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) {
|
||||
}
|
||||
}
|
||||
}
|
||||
rowsUpdated(mrows, hrows, brows, srows, resetScroll);
|
||||
rowsUpdated(
|
||||
std::move(mrows),
|
||||
std::move(hrows),
|
||||
std::move(brows),
|
||||
std::move(srows),
|
||||
resetScroll);
|
||||
_inner->setRecentInlineBotsInRows(recentInlineBots);
|
||||
}
|
||||
|
||||
void FieldAutocomplete::rowsUpdated(const internal::MentionRows &mrows, const internal::HashtagRows &hrows, const internal::BotCommandRows &brows, const internal::StickerRows &srows, bool resetScroll) {
|
||||
void FieldAutocomplete::rowsUpdated(
|
||||
internal::MentionRows &&mrows,
|
||||
internal::HashtagRows &&hrows,
|
||||
internal::BotCommandRows &&brows,
|
||||
internal::StickerRows &&srows,
|
||||
bool resetScroll) {
|
||||
if (mrows.isEmpty() && hrows.isEmpty() && brows.isEmpty() && srows.empty()) {
|
||||
if (!isHidden()) {
|
||||
hideAnimated();
|
||||
@ -341,10 +396,10 @@ void FieldAutocomplete::rowsUpdated(const internal::MentionRows &mrows, const in
|
||||
_brows.clear();
|
||||
_srows.clear();
|
||||
} else {
|
||||
_mrows = mrows;
|
||||
_hrows = hrows;
|
||||
_brows = brows;
|
||||
_srows = srows;
|
||||
_mrows = std::move(mrows);
|
||||
_hrows = std::move(hrows);
|
||||
_brows = std::move(brows);
|
||||
_srows = std::move(srows);
|
||||
|
||||
bool hidden = _hiding || isHidden();
|
||||
if (hidden) {
|
||||
@ -358,6 +413,7 @@ void FieldAutocomplete::rowsUpdated(const internal::MentionRows &mrows, const in
|
||||
showAnimated();
|
||||
}
|
||||
}
|
||||
_inner->rowsUpdated();
|
||||
}
|
||||
|
||||
void FieldAutocomplete::setBoundings(QRect boundings) {
|
||||
@ -505,12 +561,14 @@ bool FieldAutocomplete::eventFilter(QObject *obj, QEvent *e) {
|
||||
return QWidget::eventFilter(obj, e);
|
||||
}
|
||||
|
||||
FieldAutocomplete::~FieldAutocomplete() {
|
||||
}
|
||||
|
||||
namespace internal {
|
||||
|
||||
FieldAutocompleteInner::FieldAutocompleteInner(FieldAutocomplete *parent, MentionRows *mrows, HashtagRows *hrows, BotCommandRows *brows, StickerRows *srows)
|
||||
FieldAutocompleteInner::FieldAutocompleteInner(
|
||||
not_null<FieldAutocomplete*> parent,
|
||||
not_null<MentionRows*> mrows,
|
||||
not_null<HashtagRows*> hrows,
|
||||
not_null<BotCommandRows*> brows,
|
||||
not_null<StickerRows*> srows)
|
||||
: _parent(parent)
|
||||
, _mrows(mrows)
|
||||
, _hrows(hrows)
|
||||
@ -551,9 +609,16 @@ void FieldAutocompleteInner::paintEvent(QPaintEvent *e) {
|
||||
int32 index = row * _stickersPerRow + col;
|
||||
if (index >= _srows->size()) break;
|
||||
|
||||
const auto document = _srows->at(index);
|
||||
auto &sticker = (*_srows)[index];
|
||||
const auto document = sticker.document;
|
||||
if (!document->sticker()) continue;
|
||||
|
||||
if (document->sticker()->animated
|
||||
&& !sticker.animated
|
||||
&& document->loaded()) {
|
||||
setupLottie(sticker);
|
||||
}
|
||||
|
||||
QPoint pos(st::stickerPanPadding + col * st::stickerPanSize.width(), st::stickerPanPadding + row * st::stickerPanSize.height());
|
||||
if (_sel == index) {
|
||||
QPoint tl(pos);
|
||||
@ -562,14 +627,23 @@ void FieldAutocompleteInner::paintEvent(QPaintEvent *e) {
|
||||
}
|
||||
|
||||
document->checkStickerSmall();
|
||||
|
||||
float64 coef = qMin((st::stickerPanSize.width() - st::buttonRadius * 2) / float64(document->dimensions.width()), (st::stickerPanSize.height() - st::buttonRadius * 2) / float64(document->dimensions.height()));
|
||||
if (coef > 1) coef = 1;
|
||||
int32 w = qRound(coef * document->dimensions.width()), h = qRound(coef * document->dimensions.height());
|
||||
if (w < 1) w = 1;
|
||||
if (h < 1) h = 1;
|
||||
QPoint ppos = pos + QPoint((st::stickerPanSize.width() - w) / 2, (st::stickerPanSize.height() - h) / 2);
|
||||
if (const auto image = document->getStickerSmall()) {
|
||||
if (sticker.animated && sticker.animated->ready()) {
|
||||
const auto frame = sticker.animated->frame();
|
||||
sticker.animated->markFrameShown();
|
||||
const auto size = frame.size() / cIntRetinaFactor();
|
||||
const auto ppos = pos + QPoint(
|
||||
(st::stickerPanSize.width() - size.width()) / 2,
|
||||
(st::stickerPanSize.height() - size.height()) / 2);
|
||||
p.drawImage(
|
||||
QRect(ppos, size),
|
||||
frame);
|
||||
} else if (const auto image = document->getStickerSmall()) {
|
||||
float64 coef = qMin((st::stickerPanSize.width() - st::buttonRadius * 2) / float64(document->dimensions.width()), (st::stickerPanSize.height() - st::buttonRadius * 2) / float64(document->dimensions.height()));
|
||||
if (coef > 1) coef = 1;
|
||||
int32 w = qRound(coef * document->dimensions.width()), h = qRound(coef * document->dimensions.height());
|
||||
if (w < 1) w = 1;
|
||||
if (h < 1) h = 1;
|
||||
QPoint ppos = pos + QPoint((st::stickerPanSize.width() - w) / 2, (st::stickerPanSize.height() - h) / 2);
|
||||
p.drawPixmapLeft(ppos, width(), image->pix(document->stickerSetOrigin(), w, h));
|
||||
}
|
||||
}
|
||||
@ -742,7 +816,7 @@ bool FieldAutocompleteInner::moveSel(int key) {
|
||||
bool FieldAutocompleteInner::chooseSelected(FieldAutocomplete::ChooseMethod method) const {
|
||||
if (!_srows->empty()) {
|
||||
if (_sel >= 0 && _sel < _srows->size()) {
|
||||
emit stickerChosen((*_srows)[_sel], method);
|
||||
emit stickerChosen((*_srows)[_sel].document, method);
|
||||
return true;
|
||||
}
|
||||
} else if (!_mrows->isEmpty()) {
|
||||
@ -872,6 +946,58 @@ void FieldAutocompleteInner::setSel(int sel, bool scroll) {
|
||||
}
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::rowsUpdated() {
|
||||
if (_srows->empty()) {
|
||||
_stickersLifetime.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
auto FieldAutocompleteInner::getLottieRenderer()
|
||||
-> std::shared_ptr<Lottie::FrameRenderer> {
|
||||
if (auto result = _lottieRenderer.lock()) {
|
||||
return result;
|
||||
}
|
||||
auto result = Lottie::MakeFrameRenderer();
|
||||
_lottieRenderer = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::setupLottie(StickerSuggestion &suggestion) {
|
||||
const auto document = suggestion.document;
|
||||
suggestion.animated = Stickers::LottiePlayerFromDocument(
|
||||
document,
|
||||
Stickers::LottieSize::InlineResults,
|
||||
QSize(
|
||||
st::stickerPanSize.width() - st::buttonRadius * 2,
|
||||
st::stickerPanSize.height() - st::buttonRadius * 2
|
||||
) * cIntRetinaFactor(),
|
||||
getLottieRenderer());
|
||||
|
||||
suggestion.animated->updates(
|
||||
) | rpl::start_with_next([=] {
|
||||
repaintSticker(document);
|
||||
}, _stickersLifetime);
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::repaintSticker(
|
||||
not_null<DocumentData*> document) {
|
||||
const auto i = ranges::find(
|
||||
*_srows,
|
||||
document,
|
||||
&StickerSuggestion::document);
|
||||
if (i == end(*_srows)) {
|
||||
return;
|
||||
}
|
||||
const auto index = (i - begin(*_srows));
|
||||
const auto row = (index / _stickersPerRow);
|
||||
const auto col = (index % _stickersPerRow);
|
||||
update(
|
||||
st::stickerPanPadding + col * st::stickerPanSize.width(),
|
||||
st::stickerPanPadding + row * st::stickerPanSize.height(),
|
||||
st::stickerPanSize.width(),
|
||||
st::stickerPanSize.height());
|
||||
}
|
||||
|
||||
void FieldAutocompleteInner::selectByMouse(QPoint globalPosition) {
|
||||
_mouseSelection = true;
|
||||
_lastMousePosition = globalPosition;
|
||||
@ -905,8 +1031,8 @@ void FieldAutocompleteInner::selectByMouse(QPoint globalPosition) {
|
||||
_down = _sel;
|
||||
if (_down >= 0 && _down < _srows->size()) {
|
||||
Ui::showMediaPreview(
|
||||
(*_srows)[_down]->stickerSetOrigin(),
|
||||
(*_srows)[_down]);
|
||||
(*_srows)[_down].document->stickerSetOrigin(),
|
||||
(*_srows)[_down].document);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -925,8 +1051,8 @@ void FieldAutocompleteInner::onParentGeometryChanged() {
|
||||
void FieldAutocompleteInner::showPreview() {
|
||||
if (_down >= 0 && _down < _srows->size()) {
|
||||
Ui::showMediaPreview(
|
||||
(*_srows)[_down]->stickerSetOrigin(),
|
||||
(*_srows)[_down]);
|
||||
(*_srows)[_down].document->stickerSetOrigin(),
|
||||
(*_srows)[_down].document);
|
||||
_previewShown = true;
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#pragma once
|
||||
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/twidget.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "base/timer.h"
|
||||
#include "chat_helpers/stickers.h"
|
||||
|
||||
@ -16,22 +16,33 @@ namespace Ui {
|
||||
class ScrollArea;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Lottie {
|
||||
class SinglePlayer;
|
||||
class FrameRenderer;
|
||||
} // namespace Lottie;
|
||||
|
||||
namespace internal {
|
||||
|
||||
struct StickerSuggestion {
|
||||
not_null<DocumentData*> document;
|
||||
std::unique_ptr<Lottie::SinglePlayer> animated;
|
||||
};
|
||||
|
||||
using MentionRows = QList<UserData*>;
|
||||
using HashtagRows = QList<QString>;
|
||||
using BotCommandRows = QList<QPair<UserData*, const BotCommand*>>;
|
||||
using StickerRows = std::vector<not_null<DocumentData*>>;
|
||||
using StickerRows = std::vector<StickerSuggestion>;
|
||||
|
||||
class FieldAutocompleteInner;
|
||||
|
||||
} // namespace internal
|
||||
|
||||
class FieldAutocomplete final : public TWidget {
|
||||
class FieldAutocomplete final : public Ui::RpWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
FieldAutocomplete(QWidget *parent);
|
||||
~FieldAutocomplete();
|
||||
|
||||
bool clearFilteredBotCommands();
|
||||
void showFiltered(
|
||||
@ -70,8 +81,6 @@ public:
|
||||
|
||||
void hideFast();
|
||||
|
||||
~FieldAutocomplete();
|
||||
|
||||
signals:
|
||||
void mentionChosen(UserData *user, FieldAutocomplete::ChooseMethod method) const;
|
||||
void hashtagChosen(QString hashtag, FieldAutocomplete::ChooseMethod method) const;
|
||||
@ -93,6 +102,7 @@ private:
|
||||
|
||||
void updateFiltered(bool resetScroll = false);
|
||||
void recount(bool resetScroll = false);
|
||||
internal::StickerRows getStickerSuggestions();
|
||||
|
||||
QPixmap _cache;
|
||||
internal::MentionRows _mrows;
|
||||
@ -100,7 +110,12 @@ private:
|
||||
internal::BotCommandRows _brows;
|
||||
internal::StickerRows _srows;
|
||||
|
||||
void rowsUpdated(const internal::MentionRows &mrows, const internal::HashtagRows &hrows, const internal::BotCommandRows &brows, const internal::StickerRows &srows, bool resetScroll);
|
||||
void rowsUpdated(
|
||||
internal::MentionRows &&mrows,
|
||||
internal::HashtagRows &&hrows,
|
||||
internal::BotCommandRows &&brows,
|
||||
internal::StickerRows &&srows,
|
||||
bool resetScroll);
|
||||
|
||||
object_ptr<Ui::ScrollArea> _scroll;
|
||||
QPointer<internal::FieldAutocompleteInner> _inner;
|
||||
@ -132,17 +147,25 @@ private:
|
||||
|
||||
namespace internal {
|
||||
|
||||
class FieldAutocompleteInner final : public TWidget, private base::Subscriber {
|
||||
class FieldAutocompleteInner final
|
||||
: public Ui::RpWidget
|
||||
, private base::Subscriber {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
FieldAutocompleteInner(FieldAutocomplete *parent, MentionRows *mrows, HashtagRows *hrows, BotCommandRows *brows, StickerRows *srows);
|
||||
FieldAutocompleteInner(
|
||||
not_null<FieldAutocomplete*> parent,
|
||||
not_null<MentionRows*> mrows,
|
||||
not_null<HashtagRows*> hrows,
|
||||
not_null<BotCommandRows*> brows,
|
||||
not_null<StickerRows*> srows);
|
||||
|
||||
void clearSel(bool hidden = false);
|
||||
bool moveSel(int key);
|
||||
bool chooseSelected(FieldAutocomplete::ChooseMethod method) const;
|
||||
|
||||
void setRecentInlineBotsInRows(int32 bots);
|
||||
void rowsUpdated();
|
||||
|
||||
signals:
|
||||
void mentionChosen(UserData *user, FieldAutocomplete::ChooseMethod method) const;
|
||||
@ -170,11 +193,17 @@ private:
|
||||
void showPreview();
|
||||
void selectByMouse(QPoint global);
|
||||
|
||||
FieldAutocomplete *_parent = nullptr;
|
||||
MentionRows *_mrows = nullptr;
|
||||
HashtagRows *_hrows = nullptr;
|
||||
BotCommandRows *_brows = nullptr;
|
||||
StickerRows *_srows = nullptr;
|
||||
void setupLottie(StickerSuggestion &suggestion);
|
||||
void repaintSticker(not_null<DocumentData*> document);
|
||||
std::shared_ptr<Lottie::FrameRenderer> getLottieRenderer();
|
||||
|
||||
not_null<FieldAutocomplete*> _parent;
|
||||
not_null<MentionRows*> _mrows;
|
||||
not_null<HashtagRows*> _hrows;
|
||||
not_null<BotCommandRows*> _brows;
|
||||
not_null<StickerRows*> _srows;
|
||||
rpl::lifetime _stickersLifetime;
|
||||
std::weak_ptr<Lottie::FrameRenderer> _lottieRenderer;
|
||||
int _stickersPerRow = 1;
|
||||
int _recentInlineBotsInRows = 0;
|
||||
int _sel = -1;
|
||||
|
@ -1154,10 +1154,11 @@ auto LottieFromDocument(
|
||||
std::unique_ptr<Lottie::SinglePlayer> LottiePlayerFromDocument(
|
||||
not_null<DocumentData*> document,
|
||||
LottieSize sizeTag,
|
||||
QSize box) {
|
||||
const auto method = [](auto &&...args) {
|
||||
QSize box,
|
||||
std::shared_ptr<Lottie::FrameRenderer> renderer) {
|
||||
const auto method = [&](auto &&...args) {
|
||||
return std::make_unique<Lottie::SinglePlayer>(
|
||||
std::forward<decltype(args)>(args)...);
|
||||
std::forward<decltype(args)>(args)..., std::move(renderer));
|
||||
};
|
||||
return LottieFromDocument(method, document, sizeTag, box);
|
||||
}
|
||||
|
@ -132,7 +132,8 @@ enum class LottieSize : uchar {
|
||||
[[nodiscard]] std::unique_ptr<Lottie::SinglePlayer> LottiePlayerFromDocument(
|
||||
not_null<DocumentData*> document,
|
||||
LottieSize sizeTag,
|
||||
QSize box);
|
||||
QSize box,
|
||||
std::shared_ptr<Lottie::FrameRenderer> renderer = nullptr);
|
||||
[[nodiscard]] not_null<Lottie::Animation*> LottieAnimationFromDocument(
|
||||
not_null<Lottie::MultiPlayer*> player,
|
||||
not_null<DocumentData*> document,
|
||||
|
@ -129,6 +129,10 @@ std::unique_ptr<rlottie::Animation> CreateFromContent(
|
||||
|
||||
} // namespace details
|
||||
|
||||
std::shared_ptr<FrameRenderer> MakeFrameRenderer() {
|
||||
return FrameRenderer::CreateIndependent();
|
||||
}
|
||||
|
||||
QImage ReadThumbnail(const QByteArray &content) {
|
||||
return Init(content, FrameRequest()).match([](
|
||||
const std::unique_ptr<SharedState> &state) {
|
||||
|
@ -22,6 +22,9 @@ namespace Lottie {
|
||||
|
||||
class Player;
|
||||
class SharedState;
|
||||
class FrameRenderer;
|
||||
|
||||
std::shared_ptr<FrameRenderer> MakeFrameRenderer();
|
||||
|
||||
QImage ReadThumbnail(const QByteArray &content);
|
||||
|
||||
|
@ -15,10 +15,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
namespace Lottie {
|
||||
|
||||
std::shared_ptr<FrameRenderer> MakeFrameRenderer() {
|
||||
return FrameRenderer::CreateIndependent();
|
||||
}
|
||||
|
||||
MultiPlayer::MultiPlayer(std::shared_ptr<FrameRenderer> renderer)
|
||||
: _timer([=] { checkNextFrameRender(); })
|
||||
, _renderer(renderer ? std::move(renderer) : FrameRenderer::Instance()) {
|
||||
|
@ -27,8 +27,6 @@ struct MultiUpdate {
|
||||
// std::pair<Animation*, Error>> data;
|
||||
};
|
||||
|
||||
std::shared_ptr<FrameRenderer> MakeFrameRenderer();
|
||||
|
||||
class MultiPlayer final : public Player {
|
||||
public:
|
||||
explicit MultiPlayer(std::shared_ptr<FrameRenderer> renderer = nullptr);
|
||||
|
Loading…
Reference in New Issue
Block a user