Support suggestions of custom emoji.

This commit is contained in:
John Preston 2022-07-18 20:30:28 +03:00
parent bf286cf175
commit 04d4fdbf9b
5 changed files with 271 additions and 143 deletions

View file

@ -38,81 +38,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace ChatHelpers {
namespace {
constexpr auto kFakeEmojiDocumentIdBase = 0x7777'FFFF'FFFF'0000ULL;
using Core::RecentEmojiId;
using Core::RecentEmojiDocument;
[[nodiscard]] DocumentId FakeEmojiDocumentId(EmojiPtr emoji) {
return kFakeEmojiDocumentIdBase + emoji->index();
}
class DefaultEmojiLoader final : public Ui::CustomEmoji::Loader {
public:
DefaultEmojiLoader(EmojiPtr emoji, int size);
QString entityData() override;
void load(Fn<void(LoadResult)> loaded) override;
bool loading() override;
void cancel() override;
Ui::CustomEmoji::Preview preview() override;
private:
void validateImage();
EmojiPtr _emoji = nullptr;
QImage _image;
int _size = 0;
};
DefaultEmojiLoader::DefaultEmojiLoader(EmojiPtr emoji, int size)
: _emoji(emoji)
, _size(size) {
}
void DefaultEmojiLoader::load(Fn<void(LoadResult)> loaded) {
validateImage();
const auto data = entityData();
const auto unloader = [emoji = _emoji, size = _size] {
return std::make_unique<DefaultEmojiLoader>(emoji, size);
};
auto cache = Ui::CustomEmoji::Cache(_size);
cache.add(0, _image);
cache.finish();
loaded(Ui::CustomEmoji::Cached(data, unloader, std::move(cache)));
}
void DefaultEmojiLoader::validateImage() {
if (!_image.isNull()) {
return;
}
_image = QImage(
{ _size, _size },
QImage::Format_ARGB32_Premultiplied);
_image.setDevicePixelRatio(style::DevicePixelRatio());
_image.fill(Qt::transparent);
QPainter p(&_image);
Ui::Emoji::Draw(p, _emoji, _size, 0, 0);
}
QString DefaultEmojiLoader::entityData() {
return "default-emoji://" + _emoji->id();
}
bool DefaultEmojiLoader::loading() {
return false;
}
void DefaultEmojiLoader::cancel() {
}
Ui::CustomEmoji::Preview DefaultEmojiLoader::preview() {
validateImage();
return { _image };
}
} // namespace
class EmojiColorPicker : public Ui::RpWidget {
@ -181,7 +109,7 @@ struct EmojiListWidget::CustomInstance {
};
struct EmojiListWidget::RecentOne {
not_null<CustomInstance*> instance;
CustomInstance *instance = nullptr;
RecentEmojiId id;
};
@ -881,7 +809,7 @@ void EmojiListWidget::paintEvent(QPaintEvent *e) {
if (info.section == int(Section::Recent)) {
drawRecent(p, w, now, paused, index);
} else if (info.section < kEmojiSectionCount) {
drawEmoji(p, w, info.section, index);
drawEmoji(p, w, _emoji[info.section][index]);
} else {
const auto set = info.section - kEmojiSectionCount;
drawCustom(p, w, now, paused, set, index);
@ -919,24 +847,29 @@ void EmojiListWidget::drawRecent(
int index) {
const auto size = (_esize / cIntRetinaFactor());
_recentPainted = true;
_recent[index].instance->object.paint(
p,
position.x() + (_singleSize.width() - size) / 2,
position.y() + (_singleSize.height() - size) / 2,
now,
st::windowBgRipple->c,
paused);
if (const auto emoji = std::get_if<EmojiPtr>(&_recent[index].id.data)) {
drawEmoji(p, position, *emoji);
} else {
Assert(_recent[index].instance != nullptr);
_recent[index].instance->object.paint(
p,
position.x() + (_singleSize.width() - size) / 2,
position.y() + (_singleSize.height() - size) / 2,
now,
st::windowBgRipple->c,
paused);
}
}
void EmojiListWidget::drawEmoji(
QPainter &p,
QPoint position,
int section,
int index) {
EmojiPtr emoji) {
const auto size = (_esize / cIntRetinaFactor());
Ui::Emoji::Draw(
p,
_emoji[section][index],
emoji,
_esize,
position.x() + (_singleSize.width() - size) / 2,
position.y() + (_singleSize.height() - size) / 2);
@ -1385,7 +1318,7 @@ auto EmojiListWidget::resolveCustomInstance(
setId);
if (recentOnly) {
for (auto &recent : _recent) {
if (recent.instance == i->second.get()) {
if (recent.instance && recent.instance == i->second.get()) {
recent.instance = instance.get();
}
}
@ -1399,35 +1332,16 @@ auto EmojiListWidget::resolveCustomInstance(
auto EmojiListWidget::resolveCustomInstance(
RecentEmojiId customId)
-> not_null<CustomInstance*> {
-> CustomInstance* {
const auto &data = customId.data;
if (const auto document = std::get_if<RecentEmojiDocument>(&data)) {
return resolveCustomInstance(document->id);
} else if (const auto emoji = std::get_if<EmojiPtr>(&data)) {
return resolveCustomInstance(FakeEmojiDocumentId(*emoji), *emoji);
return nullptr;
}
Unexpected("Custom recent emoji id.");
}
auto EmojiListWidget::resolveCustomInstance(
DocumentId fakeId,
EmojiPtr emoji)
-> not_null<CustomInstance*> {
const auto i = _instances.find(fakeId);
if (i != end(_instances)) {
return i->second.get();
}
return _instances.emplace(
fakeId,
std::make_unique<CustomInstance>(
std::make_unique<DefaultEmojiLoader>(
emoji,
Ui::Emoji::GetSizeLarge()),
[](const auto&, const auto&) {},
[] {},
true)).first->second.get();
}
auto EmojiListWidget::resolveCustomInstance(
DocumentId documentId)
-> not_null<CustomInstance*> {

View file

@ -196,8 +196,7 @@ private:
void drawEmoji(
QPainter &p,
QPoint position,
int section,
int index);
EmojiPtr emoji);
void drawCustom(
QPainter &p,
QPoint position,
@ -234,11 +233,8 @@ private:
[[nodiscard]] not_null<CustomInstance*> resolveCustomInstance(
not_null<DocumentData*> document,
uint64 setId);
[[nodiscard]] not_null<CustomInstance*> resolveCustomInstance(
[[nodiscard]] CustomInstance *resolveCustomInstance(
Core::RecentEmojiId customId);
[[nodiscard]] not_null<CustomInstance*> resolveCustomInstance(
DocumentId fakeId,
EmojiPtr emoji);
[[nodiscard]] not_null<CustomInstance*> resolveCustomInstance(
DocumentId documentId);
[[nodiscard]] std::unique_ptr<CustomInstance> customInstanceWithLoader(

View file

@ -22,6 +22,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/application.h"
#include "base/event_filter.h"
#include "main/main_session.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/stickers/data_stickers.h"
#include "styles/style_chat_helpers.h"
#include <QtWidgets/QApplication>
@ -37,8 +41,36 @@ constexpr auto kAnimationDuration = crl::time(120);
} // namespace
SuggestionsWidget::SuggestionsWidget(QWidget *parent)
struct SuggestionsWidget::CustomInstance {
CustomInstance(
std::unique_ptr<Ui::CustomEmoji::Loader> loader,
Fn<void(
not_null<Ui::CustomEmoji::Instance*>,
Ui::CustomEmoji::RepaintRequest)> repaintLater,
Fn<void()> repaint);
Ui::CustomEmoji::Instance emoji;
Ui::CustomEmoji::Object object;
};
SuggestionsWidget::CustomInstance::CustomInstance(
std::unique_ptr<Ui::CustomEmoji::Loader> loader,
Fn<void(
not_null<Ui::CustomEmoji::Instance*>,
Ui::CustomEmoji::RepaintRequest)> repaintLater,
Fn<void()> repaint)
: emoji(
Ui::CustomEmoji::Loading(std::move(loader), Ui::CustomEmoji::Preview()),
std::move(repaintLater))
, object(&emoji, std::move(repaint)) {
}
SuggestionsWidget::SuggestionsWidget(
QWidget *parent,
not_null<Main::Session*> session)
: RpWidget(parent)
, _session(session)
, _repaintTimer([=] { invokeRepaints(); })
, _oneWidth(st::emojiSuggestionSize)
, _padding(st::emojiSuggestionsPadding) {
resize(
@ -47,11 +79,13 @@ SuggestionsWidget::SuggestionsWidget(QWidget *parent)
setMouseTracking(true);
}
SuggestionsWidget::~SuggestionsWidget() = default;
rpl::producer<bool> SuggestionsWidget::toggleAnimated() const {
return _toggleAnimated.events();
}
rpl::producer<QString> SuggestionsWidget::triggered() const {
auto SuggestionsWidget::triggered() const -> rpl::producer<Chosen> {
return _triggered.events();
}
@ -60,7 +94,7 @@ void SuggestionsWidget::showWithQuery(const QString &query, bool force) {
return;
}
_query = query;
auto rows = getRowsByQuery();
auto rows = prependCustom(getRowsByQuery());
if (rows.empty()) {
_toggleAnimated.fire(false);
}
@ -83,6 +117,142 @@ void SuggestionsWidget::selectFirstResult() {
}
}
auto SuggestionsWidget::prependCustom(std::vector<Row> rows)
-> std::vector<Row> {
if (rows.empty()) {
return {};
}
struct Custom {
not_null<DocumentData*> document;
not_null<EmojiPtr> emoji;
QString replacement;
};
auto custom = base::flat_multi_map<int, Custom>();
const auto premium = _session->premium();
const auto stickers = &_session->data().stickers();
for (const auto setId : stickers->emojiSetsOrder()) {
const auto i = stickers->sets().find(setId);
if (i == end(stickers->sets())) {
continue;
}
for (const auto &document : i->second->stickers) {
if (!premium && document->isPremiumEmoji()) {
// Skip the whole premium emoji set.
break;
}
if (const auto sticker = document->sticker()) {
if (const auto emoji = Ui::Emoji::Find(sticker->alt)) {
const auto j = ranges::find(
rows,
not_null{ emoji },
&Row::emoji);
if (j != end(rows)) {
custom.emplace(int(j - begin(rows)), Custom{
.document = document,
.emoji = j->emoji,
.replacement = j->replacement,
});
}
}
}
}
}
if (custom.empty()) {
return rows;
}
auto result = std::vector<Row>();
result.reserve(custom.size() + rows.size());
for (const auto &[position, one] : custom) {
result.push_back(Row(one.emoji, one.replacement));
result.back().document = one.document;
result.back().instance = resolveCustomInstance(one.document);
}
for (auto &row : rows) {
result.push_back(std::move(row));
}
return result;
}
auto SuggestionsWidget::resolveCustomInstance(
not_null<DocumentData*> document)
-> not_null<CustomInstance*> {
const auto i = _instances.find(document);
if (i != end(_instances)) {
return i->second.get();
}
const auto repaintDelayed = [=](
not_null<Ui::CustomEmoji::Instance*> instance,
Ui::CustomEmoji::RepaintRequest request) {
if (_instances.empty() || !request.when) {
return;
}
auto &when = _repaints[request.duration];
if (when < request.when) {
when = request.when;
}
if (_repaintTimerScheduled) {
return;
}
scheduleRepaintTimer();
};
const auto repaintNow = [=] {
update();
};
auto instance = std::make_unique<CustomInstance>(
_session->data().customEmojiManager().createLoader(
document,
Data::CustomEmojiManager::SizeTag::Large),
std::move(repaintDelayed),
std::move(repaintNow));
return _instances.emplace(
document,
std::move(instance)
).first->second.get();
}
void SuggestionsWidget::scheduleRepaintTimer() {
_repaintTimerScheduled = true;
Ui::PostponeCall(this, [=] {
_repaintTimerScheduled = false;
auto next = crl::time();
for (const auto &[duration, when] : _repaints) {
if (!next || next > when) {
next = when;
}
}
if (next && (!_repaintNext || _repaintNext > next)) {
const auto now = crl::now();
if (now >= next) {
_repaintNext = 0;
_repaintTimer.cancel();
invokeRepaints();
} else {
_repaintNext = next;
_repaintTimer.callOnce(next - now);
}
}
});
}
void SuggestionsWidget::invokeRepaints() {
_repaintNext = 0;
auto invoke = false;
const auto now = crl::now();
for (auto i = begin(_repaints); i != end(_repaints);) {
if (i->second > now) {
++i;
continue;
}
invoke = true;
i = _repaints.erase(i);
}
if (invoke) {
update();
}
scheduleRepaintTimer();
}
SuggestionsWidget::Row::Row(
not_null<EmojiPtr> emoji,
const QString &replacement)
@ -230,18 +400,20 @@ void SuggestionsWidget::paintEvent(QPaintEvent *e) {
Ui::StickerHoverCorners);
}
const auto now = crl::now();
const auto preview = st::windowBgOver->c;
for (auto i = from; i != till; ++i) {
const auto &row = _rows[i];
const auto emoji = row.emoji;
const auto esize = Ui::Emoji::GetSizeLarge();
const auto x = i * _oneWidth;
const auto y = 0;
Ui::Emoji::Draw(
p,
emoji,
esize,
x + (_oneWidth - (esize / cIntRetinaFactor())) / 2,
y + (_oneWidth - (esize / cIntRetinaFactor())) / 2);
const auto size = esize / style::DevicePixelRatio();
const auto x = i * _oneWidth + (_oneWidth - size) / 2;
const auto y = (_oneWidth - size) / 2;
if (row.instance) {
row.instance->object.paint(p, x, y, now, preview, false);
} else {
Ui::Emoji::Draw(p, emoji, esize, x, y);
}
}
paintFadings(p);
}
@ -496,7 +668,10 @@ bool SuggestionsWidget::triggerSelectedRow() const {
}
void SuggestionsWidget::triggerRow(const Row &row) const {
_triggered.fire(row.emoji->text());
_triggered.fire({
row.emoji->text(),
row.document ? Data::SerializeCustomEmojiId(row.document) : QString()
});
}
void SuggestionsWidget::enterEventHook(QEnterEvent *e) {
@ -525,7 +700,9 @@ SuggestionsController::SuggestionsController(
st::emojiSuggestionsDropdown);
_container->setAutoHiding(false);
_suggestions = _container->setOwnedWidget(
object_ptr<Ui::Emoji::SuggestionsWidget>(_container));
object_ptr<Ui::Emoji::SuggestionsWidget>(
_container,
session));
setReplaceCallback(nullptr);
@ -559,8 +736,8 @@ SuggestionsController::SuggestionsController(
suggestionsUpdated(visible);
}, _lifetime);
_suggestions->triggered(
) | rpl::start_with_next([=](QString replacement) {
replaceCurrent(replacement);
) | rpl::start_with_next([=](const SuggestionsWidget::Chosen &chosen) {
replaceCurrent(chosen.emoji, chosen.customData);
}, _lifetime);
Core::App().emojiKeywords().refreshed(
) | rpl::start_with_next([=] {
@ -589,8 +766,13 @@ SuggestionsController *SuggestionsController::Init(
result->setReplaceCallback([=](
int from,
int till,
const QString &replacement) {
field->commitInstantReplacement(from, till, replacement);
const QString &replacement,
const QString &customEmojiData) {
field->commitInstantReplacement(
from,
till,
replacement,
customEmojiData);
});
return result;
}
@ -599,11 +781,16 @@ void SuggestionsController::setReplaceCallback(
Fn<void(
int from,
int till,
const QString &replacement)> callback) {
const QString &replacement,
const QString &customEmojiData)> callback) {
if (callback) {
_replaceCallback = std::move(callback);
} else {
_replaceCallback = [=](int from, int till, const QString &replacement) {
_replaceCallback = [=](
int from,
int till,
const QString &replacement,
const QString &customEmojiData) {
auto cursor = _field->textCursor();
cursor.setPosition(from);
cursor.setPosition(till, QTextCursor::KeepAnchor);
@ -667,7 +854,9 @@ QString SuggestionsController::getEmojiQuery() {
if (from >= position || till < position) {
continue;
}
if (fragment.charFormat().isImageFormat()) {
const auto format = fragment.charFormat();
if (format.isImageFormat()
|| format.objectType() == InputField::kCustomEmojiFormat) {
continue;
}
_queryStartPosition = from;
@ -714,7 +903,9 @@ QString SuggestionsController::getEmojiQuery() {
return text;
}
void SuggestionsController::replaceCurrent(const QString &replacement) {
void SuggestionsController::replaceCurrent(
const QString &replacement,
const QString &customEmojiData) {
const auto suggestion = getEmojiQuery();
if (suggestion.isEmpty()) {
showWithQuery(QString());
@ -722,7 +913,7 @@ void SuggestionsController::replaceCurrent(const QString &replacement) {
const auto cursor = _field->textCursor();
const auto position = cursor.position();
const auto from = position - suggestion.size();
_replaceCallback(from, position, replacement);
_replaceCallback(from, position, replacement, customEmojiData);
}
}

View file

@ -27,19 +27,28 @@ namespace Emoji {
class SuggestionsWidget final : public Ui::RpWidget {
public:
SuggestionsWidget(QWidget *parent);
SuggestionsWidget(QWidget *parent, not_null<Main::Session*> session);
~SuggestionsWidget();
void showWithQuery(const QString &query, bool force = false);
void selectFirstResult();
bool handleKeyEvent(int key);
rpl::producer<bool> toggleAnimated() const;
rpl::producer<QString> triggered() const;
[[nodiscard]] rpl::producer<bool> toggleAnimated() const;
struct Chosen {
QString emoji;
QString customData;
};
[[nodiscard]] rpl::producer<Chosen> triggered() const;
private:
struct CustomInstance;
struct Row {
Row(not_null<EmojiPtr> emoji, const QString &replacement);
CustomInstance *instance = nullptr;
DocumentData *document = nullptr;
not_null<EmojiPtr> emoji;
QString replacement;
};
@ -56,7 +65,8 @@ private:
void scrollByWheelEvent(not_null<QWheelEvent*> e);
void paintFadings(Painter &p) const;
std::vector<Row> getRowsByQuery() const;
[[nodiscard]] std::vector<Row> getRowsByQuery() const;
[[nodiscard]] std::vector<Row> prependCustom(std::vector<Row> rows);
void resizeToRows();
void setSelected(
int selected,
@ -77,8 +87,21 @@ private:
void scrollTo(int value, anim::type animated = anim::type::instant);
void stopAnimations();
[[nodiscard]] not_null<CustomInstance*> resolveCustomInstance(
not_null<DocumentData*> document);
void scheduleRepaintTimer();
void invokeRepaints();
const not_null<Main::Session*> _session;
QString _query;
std::vector<Row> _rows;
base::flat_map<
not_null<DocumentData*>,
std::unique_ptr<CustomInstance>> _instances;
base::flat_map<crl::time, crl::time> _repaints;
bool _repaintTimerScheduled = false;
base::Timer _repaintTimer;
crl::time _repaintNext = 0;
std::optional<QPoint> _lastMousePosition;
bool _mouseSelection = false;
@ -96,7 +119,7 @@ private:
int _dragScrollStart = -1;
rpl::event_stream<bool> _toggleAnimated;
rpl::event_stream<QString> _triggered;
rpl::event_stream<Chosen> _triggered;
};
@ -119,7 +142,8 @@ public:
void setReplaceCallback(Fn<void(
int from,
int till,
const QString &replacement)> callback);
const QString &replacement,
const QString &customEmojiData)> callback);
static SuggestionsController *Init(
not_null<QWidget*> outer,
@ -135,7 +159,9 @@ private:
void suggestionsUpdated(bool visible);
void updateGeometry();
void updateForceHidden();
void replaceCurrent(const QString &replacement);
void replaceCurrent(
const QString &replacement,
const QString &customEmojiData);
bool fieldFilter(not_null<QEvent*> event);
bool outerFilter(not_null<QEvent*> event);
@ -149,7 +175,8 @@ private:
Fn<void(
int from,
int till,
const QString &replacement)> _replaceCallback;
const QString &replacement,
const QString &customEmojiData)> _replaceCallback;
base::unique_qptr<InnerDropdown> _container;
QPointer<SuggestionsWidget> _suggestions;
base::unique_qptr<QObject> _fieldFilter;

@ -1 +1 @@
Subproject commit 1d34c64da8bc234c4d5dd8ebaff7f249d897c7d7
Subproject commit 0daf3d4ac70e587c80abe7685e7ad7512f6f39cf