Implement emoji search / categories.

This commit is contained in:
John Preston 2023-01-20 20:44:08 +04:00
parent 826ec75c33
commit e73dbf5f65
81 changed files with 978 additions and 116 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 739 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 859 B

After

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 968 B

After

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 785 B

After

Width:  |  Height:  |  Size: 652 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 710 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 647 B

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 906 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 700 B

After

Width:  |  Height:  |  Size: 632 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 697 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 774 B

After

Width:  |  Height:  |  Size: 663 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 547 B

After

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 899 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 B

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 B

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 491 B

After

Width:  |  Height:  |  Size: 494 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 589 B

After

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1007 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -10,6 +10,22 @@ using "ui/basic.style";
using "boxes/boxes.style";
using "ui/widgets/widgets.style";
TabbedSearch {
outer: color;
bg: color;
fg: color;
fgActive: color;
fadeLeft: icon;
fadeRight: icon;
field: InputField;
search: IconButton;
back: IconButton;
cancel: CrossButton;
defaultFieldWidth: pixels;
groupWidth: pixels;
height: pixels;
}
EmojiPan {
margin: margins;
padding: margins;
@ -28,6 +44,8 @@ EmojiPan {
overBg: color;
fadeLeft: icon;
fadeRight: icon;
search: TabbedSearch;
searchMargin: margins;
}
switchPmButton: RoundButton(defaultBoxButton) {
@ -132,8 +150,7 @@ stickersTrendingUnread: icon {
{ "emoji/stickers_add_dot", dialogsUnreadBg }
};
stickersRecent: icon {{ "emoji/emoji_recent", emojiIconFg }};
stickersSearch: icon {{ "emoji/stickers_search", emojiIconFg }};
stickersPremium: icon {{ "emoji/stickers_premium", emojiIconFg }};
emojiStatusDefault: icon {{ "emoji/stickers_premium", emojiIconFg }};
stickersSettingsUnreadSize: 6px;
stickersSettingsUnreadPosition: point(6px, 10px);
@ -145,27 +162,27 @@ filtersRemove: IconButton(stickersRemove) {
emojiPanMargins: margins(10px, 10px, 10px, 10px);
emojiTabs: SettingsSlider(defaultTabsSlider) {
height: 55px;
barTop: 52px;
labelTop: 19px;
height: 45px;
barTop: 42px;
labelTop: 13px;
}
emojiScroll: defaultSolidScroll;
emojiRecent: icon {{ "emoji/emoji_recent", emojiIconFg }};
emojiRecentActive: icon {{ "emoji/emoji_recent", emojiSubIconFgActive }};
emojiPeople: icon {{ "emoji/emoji_people", emojiIconFg }};
emojiPeopleActive: icon {{ "emoji/emoji_people", emojiSubIconFgActive }};
emojiPeople: icon {{ "emoji/emoji_smile", emojiIconFg }};
emojiPeopleActive: icon {{ "emoji/emoji_smile", emojiSubIconFgActive }};
emojiNature: icon {{ "emoji/emoji_nature", emojiIconFg }};
emojiNatureActive: icon {{ "emoji/emoji_nature", emojiSubIconFgActive }};
emojiFood: icon {{ "emoji/emoji_food", emojiIconFg }};
emojiFoodActive: icon {{ "emoji/emoji_food", emojiSubIconFgActive }};
emojiActivity: icon {{ "emoji/emoji_activity", emojiIconFg }};
emojiActivityActive: icon {{ "emoji/emoji_activity", emojiSubIconFgActive }};
emojiActivity: icon {{ "emoji/emoji_activities", emojiIconFg }};
emojiActivityActive: icon {{ "emoji/emoji_activities", emojiSubIconFgActive }};
emojiTravel: icon {{ "emoji/emoji_travel", emojiIconFg }};
emojiTravelActive: icon {{ "emoji/emoji_travel", emojiSubIconFgActive }};
emojiObjects: icon {{ "emoji/emoji_objects", emojiIconFg }};
emojiObjectsActive: icon {{ "emoji/emoji_objects", emojiSubIconFgActive }};
emojiSymbols: icon {{ "emoji/emoji_symbols", emojiIconFg }};
emojiSymbolsActive: icon {{ "emoji/emoji_symbols", emojiSubIconFgActive }};
emojiSymbols: icon {{ "emoji/emoji_love", emojiIconFg }};
emojiSymbolsActive: icon {{ "emoji/emoji_love", emojiSubIconFgActive }};
emojiCategoryIconTop: 6px;
emojiPanAnimation: PanelAnimation(defaultPanelAnimation) {
@ -185,6 +202,37 @@ emojiPanLeft: 13px;
emojiPanRight: 17px;
emojiPanRadius: 8px;
defaultTabbedSearch: TabbedSearch {
outer: emojiPanBg;
bg: emojiPanCategories;
fg: emojiIconFg;
fgActive: emojiSubIconFgActive;
fadeLeft: icon {{ "fade_horizontal-flip_horizontal", emojiPanCategories }};
fadeRight: icon {{ "fade_horizontal", emojiPanCategories }};
field: InputField(defaultMultiSelectSearchField) {
textMargins: margins(2px, 7px, 2px, 0px);
}
search: IconButton(defaultIconButton) {
width: 33px;
height: 33px;
icon: icon{{ "emoji/emoji_search_input", emojiIconFg }};
iconOver: icon{{ "emoji/emoji_search_input", emojiIconFg }};
iconPosition: point(12px, -1px);
ripple: emptyRippleAnimation;
}
back: IconButton(defaultIconButton) {
width: 33px;
height: 33px;
icon: icon{{ "emoji/emoji_back", menuIconFg }};
iconOver: icon{{ "emoji/emoji_back", menuIconFg }};
iconPosition: point(12px, -1px);
ripple: emptyRippleAnimation;
}
cancel: defaultMultiSelectSearchCancel;
defaultFieldWidth: 95px;
groupWidth: 30px;
height: 33px;
}
defaultEmojiPan: EmojiPan {
margin: margins(roundRadiusSmall, 0px, 14px, 0px);
padding: margins(13px, 12px, 17px, 12px);
@ -203,6 +251,8 @@ defaultEmojiPan: EmojiPan {
overBg: emojiPanHover;
fadeLeft: icon {{ "fade_horizontal-flip_horizontal", emojiPanCategories }};
fadeRight: icon {{ "fade_horizontal", emojiPanCategories }};
search: defaultTabbedSearch;
searchMargin: margins(6px, 10px, 6px, 10px);
}
inlineResultsMinHeight: 278px;
@ -215,13 +265,6 @@ emojiColorsPadding: 5px;
emojiColorsSep: 1px;
emojiColorsSepColor: shadowFg;
emojiSwitchSkip: 27px;
emojiSwitchImgSkip: 21px;
emojiSwitchColor: windowActiveTextFg;
emojiSwitchStickers: icon {{ "emoji/emoji_switch", emojiSwitchColor }};
emojiSwitchEmoji: icon {{ "emoji/emoji_switch-flip_horizontal", emojiSwitchColor }};
emojiIconPadding: 7px;
emojiIconSelectSkip: 3px;
emojiPremiumRequired: icon{{ "emoji/premium_lock", windowSubTextFg }};

View File

@ -17,6 +17,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_domain.h"
#include "main/main_session.h"
#include "apiwrap.h"
#include "core/application.h"
#include "core/core_settings.h"
#include <QtGui/QGuiApplication>
@ -639,6 +641,52 @@ std::vector<Result> EmojiKeywords::query(
return result;
}
std::vector<Result> EmojiKeywords::queryMine(
const QString &query,
bool exact) const {
return ApplyVariants(PrioritizeRecent(this->query(query, exact)));
}
std::vector<Result> EmojiKeywords::PrioritizeRecent(
std::vector<Result> list) {
using Entry = Result;
auto lastRecent = begin(list);
const auto &recent = Core::App().settings().recentEmoji();
for (const auto &item : recent) {
const auto emoji = std::get_if<EmojiPtr>(&item.id.data);
if (!emoji) {
continue;
}
const auto original = (*emoji)->original()
? (*emoji)->original()
: (*emoji);
const auto it = ranges::find(list, original, [](const Entry &entry) {
return entry.emoji;
});
if (it > lastRecent && it != end(list)) {
std::rotate(lastRecent, it, it + 1);
++lastRecent;
}
}
return list;
}
std::vector<Result> EmojiKeywords::ApplyVariants(std::vector<Result> list) {
for (auto &item : list) {
item.emoji = [&] {
const auto result = item.emoji;
const auto &variants = Core::App().settings().emojiVariants();
const auto i = result->hasVariants()
? variants.find(result->nonColoredId())
: end(variants);
return (i != end(variants))
? result->variant(i->second)
: result;
}();
}
return list;
}
int EmojiKeywords::maxQueryLength() const {
if (_data.empty()) {
return 0;

View File

@ -43,6 +43,9 @@ public:
[[nodiscard]] std::vector<Result> query(
const QString &query,
bool exact = false) const;
[[nodiscard]] std::vector<Result> queryMine(
const QString &query,
bool exact = false) const;
[[nodiscard]] int maxQueryLength() const;
private:
@ -52,6 +55,11 @@ private:
ApiWrap *api() override;
void langPackRefreshed() override;
[[nodiscard]] static std::vector<Result> PrioritizeRecent(
std::vector<Result> list);
[[nodiscard]] static std::vector<Result> ApplyVariants(
std::vector<Result> list);
void handleSessionChanges();
void apiChanged(ApiWrap *api);
void refreshInputLanguages();

View File

@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/emoji_list_widget.h"
#include "base/unixtime.h"
#include "ui/controls/tabbed_search.h"
#include "ui/text/format_values.h"
#include "ui/effects/animations.h"
#include "ui/widgets/buttons.h"
@ -23,11 +24,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/sticker_set_box.h"
#include "lang/lang_keys.h"
#include "layout/layout_position.h"
#include "data/data_emoji_statuses.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "data/data_peer_values.h"
#include "data/stickers/data_stickers.h"
#include "data/stickers/data_custom_emoji.h"
#include "chat_helpers/emoji_keywords.h"
#include "chat_helpers/stickers_list_widget.h"
#include "chat_helpers/stickers_list_footer.h"
#include "emoji_suggestions_data.h"
@ -44,6 +47,8 @@ namespace {
constexpr auto kCollapsedRows = 3;
constexpr auto kAppearDuration = 0.3;
constexpr auto kPlainSearchLimit = 32;
constexpr auto kCustomSearchLimit = 256;
using Core::RecentEmojiId;
using Core::RecentEmojiDocument;
@ -402,6 +407,10 @@ EmojiListWidget::EmojiListWidget(
setAttribute(Qt::WA_OpaquePaintEvent);
}
if (_mode != Mode::RecentReactions) {
setupSearch();
}
_customSingleSize = Data::FrameSizeFromTag(
Data::CustomEmojiManager::SizeTag::Large
) / style::DevicePixelRatio();
@ -457,6 +466,142 @@ EmojiListWidget::~EmojiListWidget() {
base::take(_customEmoji);
}
void EmojiListWidget::setupSearch() {
using Descriptor = Ui::SearchDescriptor;
_search = std::make_unique<Ui::TabbedSearch>(this, st(), Descriptor{
.st = st().search,
.groups = (_mode == Mode::EmojiStatus
? session().data().emojiStatuses().statusGroupsValue()
: session().data().emojiStatuses().emojiGroupsValue()),
.customEmojiFactory = session().data().customEmojiManager().factory()
});
_search->queryValue(
) | rpl::start_with_next([=](std::vector<QString> &&query) {
_nextSearchQuery = std::move(query);
InvokeQueued(this, [=] {
applyNextSearchQuery();
});
}, lifetime());
}
void EmojiListWidget::applyNextSearchQuery() {
if (_searchQuery == _nextSearchQuery) {
return;
}
_searchQuery = _nextSearchQuery;
std::swap(_searchEmoji, _searchEmojiPrevious);
_searchEmoji.clear();
const auto finish = [&](bool searching = true) {
if (!_searchMode && !searching) {
return;
}
_searchMode = searching;
if (!searching) {
_searchResults.clear();
_searchCustomIds.clear();
}
clearSelection();
resizeToWidth(width());
update();
};
if (_searchQuery.empty()) {
finish(false);
return;
}
const auto guard = gsl::finally([&] { finish(); });
auto plain = collectPlainSearchResults();
if (_searchEmoji == _searchEmojiPrevious) {
return;
}
_searchResults.clear();
_searchCustomIds.clear();
if (_mode == Mode::EmojiStatus || session().premium()) {
appendPremiumSearchResults();
}
if (_mode != Mode::EmojiStatus) {
for (const auto emoji : plain) {
_searchResults.push_back({
.id = { emoji },
});
}
}
}
std::vector<EmojiPtr> EmojiListWidget::collectPlainSearchResults() {
auto result = std::vector<EmojiPtr>();
const auto pushPlain = [&](EmojiPtr emoji) {
if (result.size() < kPlainSearchLimit
&& _searchEmoji.emplace(emoji).second) {
result.push_back(emoji);
}
if (const auto original = emoji->original(); original != emoji) {
_searchEmoji.emplace(original);
}
};
auto refreshed = false;
auto &keywords = Core::App().emojiKeywords();
for (const auto &entry : _searchQuery) {
if (const auto emoji = Ui::Emoji::Find(entry)) {
pushPlain(emoji);
if (result.size() >= kPlainSearchLimit) {
return result;
}
} else if (!entry.isEmpty()) {
if (!refreshed) {
refreshed = true;
keywords.refresh();
}
const auto list = keywords.queryMine(entry);
for (const auto &entry : list) {
pushPlain(entry.emoji);
if (result.size() >= kPlainSearchLimit) {
return result;
}
}
}
}
return result;
}
void EmojiListWidget::appendPremiumSearchResults() {
const auto test = session().isTestMode();
auto &owner = session().data();
const auto checkCustom = [&](EmojiPtr emoji, DocumentId id) {
return emoji
&& _searchEmoji.contains(emoji)
&& (_searchResults.size() < kCustomSearchLimit)
&& _searchCustomIds.emplace(id).second;
};
for (const auto &recent : _recent) {
if (!recent.custom) {
continue;
}
const auto &idData = recent.id.data;
const auto id = std::get_if<Core::RecentEmojiDocument>(&idData);
if (!id || id->test != test) {
continue;
}
const auto sticker = owner.document(id->id)->sticker();
const auto emoji = sticker
? Ui::Emoji::Find(sticker->alt)
: nullptr;
if (checkCustom(emoji, id->id)) {
_searchResults.push_back(recent);
}
}
for (const auto &set : _custom) {
for (const auto &one : set.list) {
const auto id = one.document->id;
if (checkCustom(one.emoji, id)) {
_searchResults.push_back({
.custom = one.custom,
.id = { RecentEmojiDocument{ .id = id, .test = test } },
});
}
}
}
}
void EmojiListWidget::provideRecent(
const std::vector<DocumentId> &customRecentList) {
clearSelection();
@ -468,6 +613,13 @@ void EmojiListWidget::repaintCustom(uint64 setId) {
if (!_repaintsScheduled.emplace(setId).second) {
return;
}
const auto repaintSearch = (setId == SearchEmojiSectionSetId());
if (_searchMode) {
if (repaintSearch) {
update();
}
return;
}
const auto repaintRecent = (setId == RecentEmojiSectionSetId());
enumerateSections([&](const SectionInfo &info) {
const auto repaint1 = repaintRecent
@ -600,7 +752,7 @@ bool EmojiListWidget::enumerateSections(Callback callback) const {
? kCollapsedRows
: (info.count + _columnCount - 1) / _columnCount;
info.rowsTop = info.top
+ (i == 0 ? st().padding.top() : st().header);
+ (i == 0 ? _rowsTop : st().header);
info.rowsBottom = info.rowsTop
+ (info.rowsCount * _singleSize.height())
+ st::roundRadiusSmall;
@ -610,6 +762,11 @@ bool EmojiListWidget::enumerateSections(Callback callback) const {
info.top = info.rowsBottom;
return true;
};
if (_searchMode) {
info.section = i;
info.count = _searchResults.size();
return next();
}
for (; i != _staticCount; ++i) {
info.section = i;
info.count = i ? _counts[i] : _recent.size();
@ -660,7 +817,7 @@ EmojiListWidget::SectionInfo EmojiListWidget::sectionInfoByOffset(
}
int EmojiListWidget::sectionsCount() const {
return _staticCount + int(_custom.size());
return _searchMode ? 1 : (_staticCount + int(_custom.size()));
}
void EmojiListWidget::setSingleSize(QSize size) {
@ -686,6 +843,7 @@ int EmojiListWidget::countDesiredHeight(int newWidth) {
const auto innerWidth = fullWidth - padding.left() - padding.right();
_columnCount = std::max(innerWidth / st().desiredSize, 1);
const auto singleWidth = innerWidth / _columnCount;
_rowsTop = _search ? _search->height() : padding.top();
_rowsLeft = padding.left()
+ (innerWidth - _columnCount * singleWidth) / 2
- st().margin.left();
@ -698,9 +856,12 @@ int EmojiListWidget::countDesiredHeight(int newWidth) {
return info.top
+ qMax(info.rowsBottom - info.top, minimalLastHeight);
};
const auto minimalLastHeight = minimalHeight;
return qMax(minimalHeight, countResult(minimalLastHeight))
+ padding.bottom();
const auto minimalLastHeight = std::max(
minimalHeight - padding.bottom(),
0);
return qMax(
minimalHeight,
countResult(minimalLastHeight) + padding.bottom());
}
int EmojiListWidget::defaultMinimalHeight() const {
@ -953,7 +1114,7 @@ void EmojiListWidget::paint(
);
const auto w = position + _areaPosition;
if (context.expanding) {
const auto y = (position.y() - st().padding.top());
const auto y = (position.y() - _rowsTop);
const auto x = (position.x() - _rowsLeft);
const auto sum = y
+ std::max(std::min(y, width()) - x, 0);
@ -981,8 +1142,10 @@ void EmojiListWidget::paint(
}
_overBg.paint(p, QRect(tl, st::emojiPanArea));
}
if (info.section == int(Section::Recent)) {
drawRecent(p, context, w, index);
if (_searchMode) {
drawRecent(p, context, w, _searchResults[index]);
} else if (info.section == int(Section::Recent)) {
drawRecent(p, context, w, _recent[index]);
} else if (info.section < _staticCount) {
drawEmoji(p, context, w, _emoji[info.section][index]);
} else {
@ -1020,9 +1183,8 @@ void EmojiListWidget::drawRecent(
QPainter &p,
const ExpandingContext &context,
QPoint position,
int index) {
const RecentOne &recent) {
_recentPainted = true;
auto &recent = _recent[index];
if (const auto custom = recent.custom) {
_emojiPaintContext->scale = context.progress;
_emojiPaintContext->position = position
@ -1032,8 +1194,8 @@ void EmojiListWidget::drawRecent(
} else if (const auto emoji = std::get_if<EmojiPtr>(&recent.id.data)) {
if (_mode == Mode::EmojiStatus) {
position += QPoint(
(_singleSize.width() - st::stickersPremium.width()) / 2,
(_singleSize.height() - st::stickersPremium.height()) / 2
(_singleSize.width() - st::emojiStatusDefault.width()) / 2,
(_singleSize.height() - st::emojiStatusDefault.height()) / 2
) - _areaPosition;
p.drawImage(position, _premiumIcon->image());
} else {
@ -1087,7 +1249,16 @@ bool EmojiListWidget::checkPickerHide() {
DocumentData *EmojiListWidget::lookupCustomEmoji(
int index,
int section) const {
if (section == int(Section::Recent) && index < _recent.size()) {
if (_searchMode) {
if (index < _searchResults.size()) {
const auto document = std::get_if<RecentEmojiDocument>(
&_searchResults[index].id.data);
if (document) {
return session().data().document(document->id);
}
}
return nullptr;
} else if (section == int(Section::Recent) && index < _recent.size()) {
const auto document = std::get_if<RecentEmojiDocument>(
&_recent[index].id.data);
if (document) {
@ -1104,9 +1275,14 @@ DocumentData *EmojiListWidget::lookupCustomEmoji(
EmojiPtr EmojiListWidget::lookupOverEmoji(const OverEmoji *over) const {
const auto section = over ? over->section : -1;
const auto index = over ? over->index : -1;
return (section == int(Section::Recent)
&& index < _recent.size()
&& v::is<EmojiPtr>(_recent[index].id.data))
return _searchMode
? ((index < _searchResults.size()
&& v::is<EmojiPtr>(_searchResults[index].id.data))
? v::get<EmojiPtr>(_searchResults[index].id.data)
: nullptr)
: (section == int(Section::Recent)
&& index < _recent.size()
&& v::is<EmojiPtr>(_recent[index].id.data))
? v::get<EmojiPtr>(_recent[index].id.data)
: (section > int(Section::Recent)
&& section < _staticCount
@ -1119,9 +1295,10 @@ EmojiChosen EmojiListWidget::lookupChosen(
EmojiPtr emoji,
not_null<const OverEmoji*> over) {
const auto rect = emojiRect(over->section, over->index);
const auto size = st::emojiStatusDefault.size();
const auto icon = QRect(
rect.x() + (_singleSize.width() - st::stickersPremium.width()) / 2,
rect.y() + (_singleSize.height() - st::stickersPremium.height()) / 2,
rect.x() + (_singleSize.width() - size.width()) / 2,
rect.y() + (_singleSize.height() - size.height()) / 2,
rect.width(),
rect.height());
return {
@ -1622,10 +1799,11 @@ void EmojiListWidget::refreshCustom() {
auto set = std::vector<CustomOne>();
set.reserve(list.size());
for (const auto document : list) {
if (document->sticker()) {
if (const auto sticker = document->sticker()) {
set.push_back({
.custom = resolveCustomEmoji(document, setId),
.document = document,
.emoji = Ui::Emoji::Find(sticker->alt),
});
if (!premium && document->isPremiumEmoji()) {
premium = true;
@ -1668,6 +1846,9 @@ Fn<void()> EmojiListWidget::repaintCallback(
if (_recentCustomIds.contains(documentId)) {
repaintCustom(RecentEmojiSectionSetId());
}
if (_searchCustomIds.contains(documentId)) {
repaintCustom(SearchEmojiSectionSetId());
}
};
}

View File

@ -31,6 +31,7 @@ struct phrase;
namespace Ui {
class RippleAnimation;
class TabbedSearch;
} // namespace Ui
namespace Ui::Emoji {
@ -162,6 +163,7 @@ private:
struct CustomOne {
not_null<Ui::Text::CustomEmoji*> custom;
not_null<DocumentData*> document;
EmojiPtr emoji = nullptr;
};
struct CustomSet {
uint64 id = 0;
@ -243,6 +245,9 @@ private:
void unloadAllCustom();
void unloadCustomIn(const SectionInfo &info);
void setupSearch();
[[nodiscard]] std::vector<EmojiPtr> collectPlainSearchResults();
void appendPremiumSearchResults();
void ensureLoaded(int section);
void updateSelected();
void setSelected(OverState newSelected);
@ -267,7 +272,7 @@ private:
QPainter &p,
const ExpandingContext &context,
QPoint position,
int index);
const RecentOne &recent);
void drawEmoji(
QPainter &p,
const ExpandingContext &context,
@ -307,7 +312,6 @@ private:
void displaySet(uint64 setId);
void removeSet(uint64 setId);
void refreshColoredStatuses();
void initButton(RightButton &button, const QString &text, bool gradient);
[[nodiscard]] std::unique_ptr<Ui::RippleAnimation> createButtonRipple(
int section);
@ -328,8 +332,11 @@ private:
DocumentId documentId,
uint64 setId);
void applyNextSearchQuery();
Window::SessionController *_controller = nullptr;
Mode _mode = Mode::Full;
std::unique_ptr<Ui::TabbedSearch> _search;
const int _staticCount = 0;
StickersListFooter *_footer = nullptr;
std::unique_ptr<GradientPremiumStar> _premiumIcon;
@ -355,6 +362,15 @@ private:
bool _allowWithoutPremium = false;
Ui::RoundRect _overBg;
std::vector<QString> _nextSearchQuery;
std::vector<QString> _searchQuery;
base::flat_set<EmojiPtr> _searchEmoji;
base::flat_set<EmojiPtr> _searchEmojiPrevious;
base::flat_set<DocumentId> _searchCustomIds;
std::vector<RecentOne> _searchResults;
bool _searchMode = false;
int _rowsTop = 0;
int _rowsLeft = 0;
int _columnCount = 1;
QSize _singleSize;

View File

@ -213,49 +213,13 @@ auto SuggestionsWidget::getRowsByQuery(const QString &text) const
return ranges::none_of(text, [](QChar ch) { return ch.isLower(); });
}();
const auto exact = !middle || simple;
const auto list = Core::App().emojiKeywords().query(real, exact);
if (list.empty()) {
return {};
}
const auto list = Core::App().emojiKeywords().queryMine(real, exact);
using Entry = ChatHelpers::EmojiKeywords::Result;
auto result = ranges::views::all(
return ranges::views::all(
list
) | ranges::views::transform([](const Entry &result) {
return Row(result.emoji, result.replacement);
}) | ranges::to_vector;
auto lastRecent = begin(result);
const auto &recent = Core::App().settings().recentEmoji();
for (const auto &item : recent) {
const auto emoji = std::get_if<EmojiPtr>(&item.id.data);
if (!emoji) {
continue;
}
const auto original = (*emoji)->original()
? (*emoji)->original()
: (*emoji);
const auto it = ranges::find(result, original, [](const Row &row) {
return row.emoji.get();
});
if (it > lastRecent && it != end(result)) {
std::rotate(lastRecent, it, it + 1);
++lastRecent;
}
}
for (auto &item : result) {
item.emoji = [&] {
const auto result = item.emoji;
const auto &variants = Core::App().settings().emojiVariants();
const auto i = result->hasVariants()
? variants.find(result->nonColoredId())
: end(variants);
return (i != end(variants))
? result->variant(i->second)
: result.get();
}();
}
return result;
}
void SuggestionsWidget::resizeToRows() {

View File

@ -322,11 +322,9 @@ void InitMessageFieldHandlers(
const style::InputField *fieldStyle) {
field->setTagMimeProcessor(
FieldTagMimeProcessor(session, allowPremiumEmoji));
field->setCustomEmojiFactory([=](QStringView data, Fn<void()> update) {
return session->data().customEmojiManager().create(
data,
std::move(update));
}, std::move(customEmojiPaused));
field->setCustomEmojiFactory(
session->data().customEmojiManager().factory(),
std::move(customEmojiPaused));
field->setInstantReplaces(Ui::InstantReplaces::Default());
field->setInstantReplacesEnabled(
Core::App().settings().replaceEmojiValue());

View File

@ -71,6 +71,12 @@ uint64 AllEmojiSectionSetId() {
return kEmojiSectionSetIdBase;
}
uint64 SearchEmojiSectionSetId() {
return kEmojiSectionSetIdBase
+ static_cast<uint64>(EmojiSection::Symbols)
+ 2;
}
std::optional<EmojiSection> SetIdEmojiSection(uint64 id) {
const auto base = RecentEmojiSectionSetId();
if (id < base) {
@ -161,8 +167,8 @@ QImage GradientPremiumStar::image() const {
}
void GradientPremiumStar::renderOnDemand() const {
const auto size = st::stickersPremium.size();
const auto mask = st::stickersPremium.instance(Qt::white);
const auto size = st::emojiStatusDefault.size();
const auto mask = st::emojiStatusDefault.instance(Qt::white);
const auto factor = style::DevicePixelRatio();
_image = QImage(
size * factor,
@ -188,7 +194,6 @@ StickersListFooter::StickersListFooter(Descriptor &&descriptor)
descriptor.st ? *descriptor.st : st::defaultEmojiPan)
, _session(descriptor.session)
, _paused(descriptor.paused)
, _searchButtonVisible(descriptor.searchButtonVisible)
, _settingsButtonVisible(descriptor.settingsButtonVisible)
, _iconState([=] { update(); })
, _subiconState([=] { update(); })
@ -197,9 +202,7 @@ StickersListFooter::StickersListFooter(Descriptor &&descriptor)
, _barSelection(descriptor.barSelection) {
setMouseTracking(true);
_iconsLeft = st().iconSkip + (_searchButtonVisible
? st::stickerIconWidth
: 0);
_iconsLeft = st().iconSkip;
_iconsRight = st().iconSkip + (_settingsButtonVisible
? st::stickerIconWidth
: 0);
@ -568,9 +571,6 @@ void StickersListFooter::paintEvent(QPaintEvent *e) {
void StickersListFooter::paint(
Painter &p,
const ExpandingContext &context) const {
if (_searchButtonVisible) {
paintSearchIcon(p);
}
if (_icons.empty() || _searchShown) {
return;
}
@ -751,8 +751,6 @@ void StickersListFooter::mousePressEvent(QMouseEvent *e) {
if (_selected == SpecialOver::Settings) {
_openSettingsRequests.fire({});
} else if (_selected == SpecialOver::Search) {
toggleSearch(true);
} else {
_pressed = _selected;
_iconsMouseDown = _iconsMousePos;
@ -940,13 +938,7 @@ void StickersListFooter::updateSelected() {
const auto settingsLeft = width() - _iconsRight;
const auto searchLeft = _iconsLeft - _singleWidth;
auto newOver = OverState(SpecialOver::None);
if (_searchButtonVisible
&& x >= searchLeft
&& x < searchLeft + _singleWidth
&& y >= _iconsTop
&& y < _iconsTop + st().footer) {
newOver = SpecialOver::Search;
} else if (_settingsButtonVisible
if (_settingsButtonVisible
&& x >= settingsLeft
&& x < settingsLeft + _singleWidth
&& y >= _iconsTop
@ -1115,15 +1107,6 @@ void StickersListFooter::paintStickerSettingsIcon(QPainter &p) const {
width());
}
void StickersListFooter::paintSearchIcon(QPainter &p) const {
const auto searchLeft = _iconsLeft - _singleWidth;
st::stickersSearch.paint(
p,
searchLeft + (_singleWidth - st::stickersSearch.width()) / 2,
_iconsTop + st::emojiCategoryIconTop,
width());
}
void StickersListFooter::customEmojiRepaint() {
if (!_repaintScheduled) {
_repaintScheduled = true;
@ -1309,7 +1292,7 @@ void StickersListFooter::paintSetIcon(
width(),
st::stickerGroupCategorySize);
} else if (icon.setId == Data::Stickers::PremiumSetId) {
const auto size = st::stickersPremium.size();
const auto size = st::emojiStatusDefault.size();
p.drawImage(
info.adjustedLeft + (_singleWidth - size.width()) / 2,
_iconsTop + (st().footer - size.height()) / 2,

View File

@ -48,6 +48,7 @@ enum class ValidateIconAnimations {
[[nodiscard]] uint64 EmojiSectionSetId(Ui::Emoji::Section section);
[[nodiscard]] uint64 RecentEmojiSectionSetId();
[[nodiscard]] uint64 AllEmojiSectionSetId();
[[nodiscard]] uint64 SearchEmojiSectionSetId();
[[nodiscard]] std::optional<Ui::Emoji::Section> SetIdEmojiSection(uint64 id);
struct StickerIcon {
@ -99,7 +100,6 @@ public:
not_null<Main::Session*> session;
Fn<bool()> paused;
not_null<RpWidget*> parent;
bool searchButtonVisible = false;
bool settingsButtonVisible = false;
bool barSelection = false;
const style::EmojiPan *st = nullptr;
@ -153,7 +153,6 @@ protected:
private:
enum class SpecialOver {
None,
Search,
Settings,
};
struct IconId {
@ -227,7 +226,6 @@ private:
void paint(Painter &p, const ExpandingContext &context) const;
void paintStickerSettingsIcon(QPainter &p) const;
void paintSearchIcon(QPainter &p) const;
void paintSetIcon(
Painter &p,
const ExpandingContext &context,
@ -254,7 +252,6 @@ private:
const not_null<Main::Session*> _session;
const Fn<bool()> _paused;
const bool _searchButtonVisible = false;
const bool _settingsButtonVisible = false;
static constexpr auto kVisibleIconsCount = 8;

View File

@ -264,7 +264,6 @@ object_ptr<TabbedSelector::InnerFooter> StickersListWidget::createFooter() {
.session = &session(),
.paused = pausedMethod(),
.parent = this,
.searchButtonVisible = !_isMasks,
.settingsButtonVisible = true,
.barSelection = true,
});

View File

@ -1020,7 +1020,7 @@ void TabbedSelector::fillTabsSliderSections() {
return tr::lng_switch_masks;
}
Unexpected("SelectorTab value in fillTabsSliderSections.");
}()(tr::now).toUpper();
}()(tr::now);
}) | ranges::to_vector;
_tabsSlider->setSections(sections);
}

View File

@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/timer_rpl.h"
#include "base/call_delayed.h"
#include "apiwrap.h"
#include "ui/controls/tabbed_search.h"
namespace Data {
namespace {
@ -126,6 +127,70 @@ void EmojiStatuses::registerAutomaticClear(
}
}
auto EmojiStatuses::emojiGroupsValue() const -> rpl::producer<Groups> {
const_cast<EmojiStatuses*>(this)->requestEmojiGroups();
return _emojiGroups.data.value();
}
auto EmojiStatuses::statusGroupsValue() const -> rpl::producer<Groups> {
const_cast<EmojiStatuses*>(this)->requestStatusGroups();
return _statusGroups.data.value();
}
void EmojiStatuses::requestEmojiGroups() {
requestGroups(
&_emojiGroups,
MTPmessages_GetEmojiGroups(MTP_int(_emojiGroups.hash)));
}
void EmojiStatuses::requestStatusGroups() {
requestGroups(
&_statusGroups,
MTPmessages_GetEmojiStatusGroups(MTP_int(_statusGroups.hash)));
}
[[nodiscard]] std::vector<Ui::EmojiGroup> GroupsFromTL(
const MTPDmessages_emojiGroups &data) {
const auto &list = data.vgroups().v;
auto result = std::vector<Ui::EmojiGroup>();
result.reserve(list.size());
for (const auto &group : list) {
const auto &data = group.data();
auto emoticons = ranges::views::all(
data.vemoticons().v
) | ranges::view::transform([](const MTPstring &emoticon) {
return qs(emoticon);
}) | ranges::to_vector;
result.push_back({
.iconId = QString::number(data.vicon_emoji_id().v),
.emoticons = std::move(emoticons),
});
}
return result;
}
template <typename Request>
void EmojiStatuses::requestGroups(
not_null<GroupsType*> type,
Request &&request) {
if (type->requestId) {
return;
}
type->requestId = _owner->session().api().request(
std::forward<Request>(request)
).done([=](const MTPmessages_EmojiGroups &result) {
type->requestId = 0;
result.match([&](const MTPDmessages_emojiGroups &data) {
type->hash = data.vhash().v;
type->data = GroupsFromTL(data);
}, [](const MTPDmessages_emojiGroupsNotModified&) {
});
}).fail([=] {
type->requestId = 0;
}).send();
}
void EmojiStatuses::processClearing() {
auto minWait = TimeId(0);
const auto now = base::unixtime::now();

View File

@ -13,6 +13,10 @@ namespace Main {
class Session;
} // namespace Main
namespace Ui {
struct EmojiGroup;
} // namespace Ui
namespace Data {
class DocumentMedia;
@ -49,7 +53,19 @@ public:
void registerAutomaticClear(not_null<UserData*> user, TimeId until);
using Groups = std::vector<Ui::EmojiGroup>;
[[nodiscard]] rpl::producer<Groups> emojiGroupsValue() const;
[[nodiscard]] rpl::producer<Groups> statusGroupsValue() const;
void requestEmojiGroups();
void requestStatusGroups();
private:
struct GroupsType {
rpl::variable<Groups> data;
mtpRequestId requestId = 0;
int32 hash = 0;
};
void requestRecent();
void requestDefault();
void requestColored();
@ -61,6 +77,9 @@ private:
void processClearingIn(TimeId wait);
void processClearing();
template <typename Request>
void requestGroups(not_null<GroupsType*> type, Request &&request);
const not_null<Session*> _owner;
std::vector<DocumentId> _recent;
@ -84,6 +103,9 @@ private:
base::flat_map<not_null<UserData*>, TimeId> _clearing;
base::Timer _clearingTimer;
GroupsType _emojiGroups;
GroupsType _statusGroups;
rpl::lifetime _lifetime;
};

View File

@ -474,6 +474,14 @@ std::unique_ptr<Ui::Text::CustomEmoji> CustomEmojiManager::create(
std::move(update));
}
Ui::Text::CustomEmojiFactory CustomEmojiManager::factory(
SizeTag tag,
int sizeOverride) {
return [=](QStringView data, Fn<void()> update) {
return create(data, std::move(update), tag, sizeOverride);
};
}
Ui::CustomEmoji::Preview CustomEmojiManager::prepareNonExactPreview(
DocumentId documentId,
SizeTag tag,

View File

@ -52,6 +52,10 @@ public:
SizeTag tag = SizeTag::Normal,
int sizeOverride = 0);
[[nodiscard]] Ui::Text::CustomEmojiFactory factory(
SizeTag tag = SizeTag::Normal,
int sizeOverride = 0);
class Listener {
public:
virtual void customEmojiResolveDone(

View File

@ -0,0 +1,419 @@
/*
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/controls/tabbed_search.h"
#include "lang/lang_keys.h"
#include "ui/widgets/input_fields.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/widgets/buttons.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "styles/style_chat_helpers.h"
namespace Ui {
namespace {
constexpr auto kDebounceTimeout = crl::time(400);
class GroupsStrip final : public RpWidget {
public:
GroupsStrip(
QWidget *parent,
const style::TabbedSearch &st,
rpl::producer<std::vector<EmojiGroup>> groups,
Text::CustomEmojiFactory factory);
[[nodiscard]] rpl::producer<EmojiGroup> chosen() const;
void clearChosen();
private:
struct Button {
EmojiGroup group;
QString iconId;
std::unique_ptr<Ui::Text::CustomEmoji> icon;
};
void init(rpl::producer<std::vector<EmojiGroup>> groups);
void set(std::vector<EmojiGroup> list);
void paintEvent(QPaintEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
static inline auto FindById(auto &&buttons, QStringView id) {
return ranges::find(buttons, id, &Button::iconId);
}
const style::TabbedSearch &_st;
const Text::CustomEmojiFactory _factory;
std::vector<Button> _buttons;
rpl::event_stream<EmojiGroup> _chosenGroup;
int _selected = -1;
int _pressed = -1;
int _chosen = -1;
};
[[nodiscard]] std::vector<QString> FieldQuery(not_null<InputField*> field) {
if (const auto last = field->getLastText(); !last.isEmpty()) {
return { last };
}
return {};
}
GroupsStrip::GroupsStrip(
QWidget *parent,
const style::TabbedSearch &st,
rpl::producer<std::vector<EmojiGroup>> groups,
Text::CustomEmojiFactory factory)
: RpWidget(parent)
, _st(st)
, _factory(std::move(factory)) {
init(std::move(groups));
}
rpl::producer<EmojiGroup> GroupsStrip::chosen() const {
return _chosenGroup.events();
}
void GroupsStrip::clearChosen() {
if (const auto chosen = std::exchange(_chosen, -1); chosen >= 0) {
update();
}
}
void GroupsStrip::init(rpl::producer<std::vector<EmojiGroup>> groups) {
std::move(
groups
) | rpl::start_with_next([=](std::vector<EmojiGroup> &&list) {
set(std::move(list));
}, lifetime());
setCursor(style::cur_pointer);
}
void GroupsStrip::set(std::vector<EmojiGroup> list) {
const auto chosen = (_chosen >= 0)
? _buttons[_chosen].group.iconId
: QString();
auto existing = std::move(_buttons);
const auto updater = [=](const QString &iconId) {
return [=] {
const auto i = FindById(_buttons, iconId);
if (i != end(_buttons)) {
const auto index = i - begin(_buttons);
const auto single = _st.groupWidth;
update(index * single, 0, single, height());
}
};
};
for (auto &group : list) {
const auto i = FindById(existing, group.iconId);
if (i != end(existing)) {
_buttons.push_back(std::move(*i));
existing.erase(i);
} else {
const auto loopCount = 1;
const auto stopAtLastFrame = true;
_buttons.push_back({
.iconId = group.iconId,
.icon = std::make_unique<Text::LimitedLoopsEmoji>(
_factory(
group.iconId,
updater(group.iconId)),
loopCount,
stopAtLastFrame),
});
}
_buttons.back().group = std::move(group);
}
resize(_buttons.size() * _st.groupWidth, height());
if (!chosen.isEmpty()) {
const auto i = FindById(_buttons, chosen);
if (i != end(_buttons)) {
_chosen = (i - begin(_buttons));
_chosenGroup.fire_copy(i->group);
} else {
_chosen = -1;
}
}
update();
}
void GroupsStrip::paintEvent(QPaintEvent *e) {
auto p = QPainter(this);
auto index = 0;
const auto single = _st.groupWidth;
const auto height = this->height();
const auto clip = e->rect();
const auto now = crl::now();
for (const auto &button : _buttons) {
const auto left = index * single;
const auto top = 0;
const auto size = Ui::Text::AdjustCustomEmojiSize(st::emojiSize);
if (_chosen == index) {
p.setPen(Qt::NoPen);
p.setBrush(st::windowBgRipple);
p.drawEllipse(left, top + (height - single) / 2, single, single);
}
if (QRect(left, top, single, height).intersects(clip)) {
button.icon->paint(p, {
.textColor = (_chosen == index ? _st.fgActive : _st.fg)->c,
.now = now,
.position = QPoint(left, top) + QPoint(
(single - size) / 2,
(height - size) / 2),
});
}
++index;
}
}
void GroupsStrip::mousePressEvent(QMouseEvent *e) {
const auto index = e->pos().x() / _st.groupWidth;
const auto chosen = (index < 0 || index >= _buttons.size())
? -1
: index;
_pressed = chosen;
}
void GroupsStrip::mouseReleaseEvent(QMouseEvent *e) {
const auto index = e->pos().x() / _st.groupWidth;
const auto chosen = (index < 0 || index >= _buttons.size())
? -1
: index;
const auto pressed = std::exchange(_pressed, -1);
if (pressed == index && index >= 0) {
_chosen = pressed;
_chosenGroup.fire_copy(_buttons[index].group);
update();
}
}
} // namespace
SearchWithGroups::SearchWithGroups(
QWidget *parent,
SearchDescriptor descriptor)
: RpWidget(parent)
, _st(descriptor.st)
, _search(CreateChild<FadeWrap<IconButton>>(
this,
object_ptr<IconButton>(this, _st.search)))
, _back(CreateChild<FadeWrap<IconButton>>(
this,
object_ptr<IconButton>(this, _st.back)))
, _cancel(CreateChild<CrossButton>(this, _st.cancel))
, _field(CreateChild<InputField>(this, _st.field, tr::lng_dlg_filter()))
, _groups(CreateChild<FadeWrap<RpWidget>>(
this,
object_ptr<GroupsStrip>(
this,
_st,
std::move(descriptor.groups),
std::move(descriptor.customEmojiFactory))))
, _fadeLeft(CreateChild<FadeWrap<RpWidget>>(
this,
object_ptr<RpWidget>(this)))
, _fadeRight(CreateChild<FadeWrap<RpWidget>>(
this,
object_ptr<RpWidget>(this)))
, _debounceTimer([=] { _debouncedQuery = _query.current(); }) {
initField();
initGroups();
initEdges();
}
anim::type SearchWithGroups::animated() const {
return _inited ? anim::type::normal : anim::type::instant;
}
void SearchWithGroups::initField() {
connect(_field, &InputField::changed, [=] {
const auto last = FieldQuery(_field);
_query = last;
const auto empty = last.empty();
_cancel->toggle(!empty, animated());
_groups->toggle(empty, animated());
if (empty) {
_debounceTimer.cancel();
_debouncedQuery = last;
} else {
_debounceTimer.callOnce(kDebounceTimeout);
_chosenGroup = QString();
}
});
_fieldPlaceholderWidth = tr::lng_dlg_filter(
) | rpl::map([=](const QString &value) {
return _st.field.placeholderFont->width(value);
}) | rpl::after_next([=] {
resizeToWidth(width());
});
const auto last = FieldQuery(_field);
_query = last;
_debouncedQuery = last;
}
void SearchWithGroups::initGroups() {
const auto widget = static_cast<GroupsStrip*>(_groups->entity());
_groups->move(_search->entity()->width() + _st.defaultFieldWidth, 0);
widget->resize(widget->width(), _st.height);
widget->widthValue(
) | rpl::filter([=] {
return (width() > 0);
}) | rpl::start_with_next([=] {
resizeToWidth(width());
}, widget->lifetime());
widget->chosen(
) | rpl::start_with_next([=](const EmojiGroup &group) {
_chosenGroup = group.iconId;
_query = group.emoticons;
_debouncedQuery = group.emoticons;
_debounceTimer.cancel();
}, lifetime());
_chosenGroup.value(
) | rpl::map([=](const QString &id) {
return id.isEmpty();
}) | rpl::start_with_next([=](bool empty) {
_search->toggle(empty, animated());
_back->toggle(!empty, animated());
if (empty) {
widget->clearChosen();
} else {
_field->setText({});
}
}, lifetime());
}
void SearchWithGroups::initEdges() {
paintRequest() | rpl::start_with_next([=](QRect clip) {
QPainter(this).fillRect(clip, _st.bg);
}, lifetime());
const auto makeEdge = [&](bool left) {
const auto edge = CreateChild<RpWidget>(this);
const auto size = QSize(height() / 2, height());
edge->resize(size);
if (left) {
edge->move(0, 0);
} else {
widthValue(
) | rpl::start_with_next([=](int width) {
edge->move(width - edge->width(), 0);
}, edge->lifetime());
}
edge->paintRequest(
) | rpl::start_with_next([=] {
const auto ratio = edge->devicePixelRatioF();
ensureRounding(height(), ratio);
const auto size = _rounding.height();
const auto half = size / 2;
QPainter(edge).drawImage(
QPoint(),
_rounding,
QRect(left ? 0 : _rounding.width() - half, 0, half, size));
}, edge->lifetime());
};
makeEdge(true);
makeEdge(false);
style::PaletteChanged(
) | rpl::start_with_next([=] {
_rounding = QImage();
}, lifetime());
}
void SearchWithGroups::ensureRounding(int size, float64 ratio) {
const auto rounded = qRound(size * ratio);
const auto full = QSize(rounded + 4, rounded);
if (_rounding.size() != full) {
_rounding = QImage(full, QImage::Format_ARGB32_Premultiplied);
_rounding.fill(_st.outer->c);
auto p = QPainter(&_rounding);
auto hq = PainterHighQualityEnabler(p);
p.setCompositionMode(QPainter::CompositionMode_Source);
p.setBrush(Qt::transparent);
p.setPen(Qt::NoPen);
p.drawRoundedRect(QRect(QPoint(), full), rounded / 2., rounded / 2.);
}
_rounding.setDevicePixelRatio(ratio);
}
rpl::producer<std::vector<QString>> SearchWithGroups::queryValue() const {
return _query.value();
}
auto SearchWithGroups::debouncedQueryValue() const
-> rpl::producer<std::vector<QString>> {
return _debouncedQuery.value();
}
int SearchWithGroups::resizeGetHeight(int newWidth) {
_back->moveToLeft(0, 0, newWidth);
_search->moveToLeft(0, 0, newWidth);
_cancel->moveToRight(0, 0, newWidth);
const auto searchWidth = _search->entity()->width();
const auto groupsLeftDefault = searchWidth + _st.defaultFieldWidth;
const auto groupsLeftMin = newWidth - _groups->entity()->width();
const auto groupsLeftMax = std::max(groupsLeftDefault, groupsLeftMin);
const auto groupsLeft = std::clamp(
_groups->x(),
groupsLeftMin,
groupsLeftMax);
_groups->move(groupsLeft, 0);
const auto placeholderMargins = _st.field.textMargins
+ _st.field.placeholderMargins;
const auto placeholderWidth = _fieldPlaceholderWidth.current();
const auto fieldWidthMin = std::min(
rect::m::sum::h(placeholderMargins) + placeholderWidth,
_st.defaultFieldWidth);
const auto fieldWidth = std::max(
groupsLeft - searchWidth,
fieldWidthMin);
_field->resizeToWidth(fieldWidth);
_field->moveToLeft(groupsLeft - fieldWidth, 0);
return _st.height;
}
TabbedSearch::TabbedSearch(
not_null<RpWidget*> parent,
const style::EmojiPan &st,
SearchDescriptor &&descriptor)
: _st(st)
, _search(parent, std::move(descriptor)) {
_search.move(_st.searchMargin.left(), _st.searchMargin.top());
parent->widthValue(
) | rpl::start_with_next([=](int width) {
_search.resizeToWidth(width - rect::m::sum::h(_st.searchMargin));
}, _search.lifetime());
}
int TabbedSearch::height() const {
return _search.height() + rect::m::sum::v(_st.searchMargin);
}
rpl::producer<std::vector<QString>> TabbedSearch::queryValue() const {
return _search.queryValue();
}
auto TabbedSearch::debouncedQueryValue() const
-> rpl::producer<std::vector<QString>> {
return _search.debouncedQueryValue();
}
} // namespace Ui

View File

@ -0,0 +1,105 @@
/*
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"
#include "ui/rp_widget.h"
#include "ui/text/text_custom_emoji.h"
namespace style {
struct EmojiPan;
struct TabbedSearch;
} // namespace style
namespace anim {
enum class type : uchar;
} // namespace anim
namespace Ui {
class InputField;
class IconButton;
class CrossButton;
class RpWidget;
template <typename Widget>
class FadeWrap;
struct EmojiGroup {
QString iconId;
std::vector<QString> emoticons;
friend inline auto operator<=>(
const EmojiGroup &a,
const EmojiGroup &b) = default;
};
struct SearchDescriptor {
const style::TabbedSearch &st;
rpl::producer<std::vector<EmojiGroup>> groups;
Text::CustomEmojiFactory customEmojiFactory;
};
class SearchWithGroups final : public RpWidget {
public:
SearchWithGroups(QWidget *parent, SearchDescriptor descriptor);
[[nodiscard]] rpl::producer<std::vector<QString>> queryValue() const;
[[nodiscard]] auto debouncedQueryValue() const
-> rpl::producer<std::vector<QString>>;
private:
int resizeGetHeight(int newWidth) override;
[[nodiscard]] anim::type animated() const;
void initField();
void initGroups();
void initEdges();
void ensureRounding(int size, float64 rounding);
const style::TabbedSearch &_st;
not_null<FadeWrap<IconButton>*> _search;
not_null<FadeWrap<IconButton>*> _back;
not_null<CrossButton*> _cancel;
not_null<InputField*> _field;
not_null<FadeWrap<RpWidget>*> _groups;
not_null<FadeWrap<RpWidget>*> _fadeLeft;
not_null<FadeWrap<RpWidget>*> _fadeRight;
rpl::variable<int> _fieldPlaceholderWidth;
QImage _rounding;
rpl::variable<std::vector<QString>> _query;
rpl::variable<std::vector<QString>> _debouncedQuery;
rpl::variable<QString> _chosenGroup;
base::Timer _debounceTimer;
bool _inited = false;
};
class TabbedSearch final {
public:
TabbedSearch(
not_null<RpWidget*> parent,
const style::EmojiPan &st,
SearchDescriptor &&descriptor);
[[nodiscard]] int height() const;
[[nodiscard]] rpl::producer<std::vector<QString>> queryValue() const;
[[nodiscard]] auto debouncedQueryValue() const
->rpl::producer<std::vector<QString>>;
private:
const style::EmojiPan &_st;
SearchWithGroups _search;
};
} // namespace Ui

View File

@ -235,6 +235,8 @@ PRIVATE
ui/controls/send_as_button.h
ui/controls/send_button.cpp
ui/controls/send_button.h
ui/controls/tabbed_search.cpp
ui/controls/tabbed_search.h
ui/controls/who_reacted_context_action.cpp
ui/controls/who_reacted_context_action.h
ui/controls/window_outdated_bar.cpp