| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691 |
- /*
- 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 "base/qt_signal_producer.h"
- #include "lang/lang_keys.h"
- #include "ui/widgets/fields/input_field.h"
- #include "ui/wrap/fade_wrap.h"
- #include "ui/widgets/buttons.h"
- #include "ui/painter.h"
- #include "ui/rect.h"
- #include "ui/ui_utility.h"
- #include "styles/style_chat_helpers.h"
- #include <QtWidgets/QApplication>
- namespace Ui {
- namespace {
- constexpr auto kDebounceTimeout = crl::time(400);
- constexpr auto kCategoryIconSizeOverride = 22;
- class GroupsStrip final : public RpWidget {
- public:
- GroupsStrip(
- QWidget *parent,
- const style::TabbedSearch &st,
- rpl::producer<std::vector<EmojiGroup>> groups,
- Text::CustomEmojiFactory factory);
- void scrollByWheel(QWheelEvent *e);
- struct Chosen {
- not_null<const EmojiGroup*> group;
- int iconLeft = 0;
- int iconRight = 0;
- };
- [[nodiscard]] rpl::producer<Chosen> chosen() const;
- void clearChosen();
- [[nodiscard]] rpl::producer<int> moveRequests() const;
- private:
- struct Button {
- EmojiGroup group;
- QString iconId;
- std::unique_ptr<Text::CustomEmoji> icon;
- };
- void init(rpl::producer<std::vector<EmojiGroup>> groups);
- void set(std::vector<EmojiGroup> list);
- void paintEvent(QPaintEvent *e) override;
- void mouseMoveEvent(QMouseEvent *e) override;
- void mousePressEvent(QMouseEvent *e) override;
- void mouseReleaseEvent(QMouseEvent *e) override;
- void fireChosenGroup();
- 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<Chosen> _chosenGroup;
- rpl::event_stream<int> _moveRequests;
- QPoint _globalPressPoint, _globalLastPoint;
- bool _dragging = false;
- 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<GroupsStrip::Chosen> GroupsStrip::chosen() const {
- return _chosenGroup.events();
- }
- rpl::producer<int> GroupsStrip::moveRequests() const {
- return _moveRequests.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,
- { .repaint = 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));
- fireChosenGroup();
- } else {
- _chosen = -1;
- }
- }
- update();
- }
- void GroupsStrip::paintEvent(QPaintEvent *e) {
- auto p = QPainter(this);
- auto index = 0;
- const auto single = _st.groupWidth;
- const auto skip = _st.groupSkip;
- 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 = SearchWithGroups::IconSizeOverride();
- if (_chosen == index) {
- auto hq = PainterHighQualityEnabler(p);
- p.setPen(Qt::NoPen);
- p.setBrush(_st.bgActive);
- p.drawEllipse(
- left + skip,
- top + (height - single) / 2 + skip,
- single - 2 * skip,
- single - 2 * skip);
- }
- 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::scrollByWheel(QWheelEvent *e) {
- auto horizontal = (e->angleDelta().x() != 0);
- auto vertical = (e->angleDelta().y() != 0);
- if (!horizontal && !vertical) {
- return;
- }
- const auto delta = horizontal
- ? ((style::RightToLeft() ? -1 : 1) * (e->pixelDelta().x()
- ? e->pixelDelta().x()
- : e->angleDelta().x()))
- : (e->pixelDelta().y()
- ? e->pixelDelta().y()
- : e->angleDelta().y());
- _moveRequests.fire_copy(delta);
- }
- void GroupsStrip::mouseMoveEvent(QMouseEvent *e) {
- const auto point = e->globalPos();
- if (!_dragging) {
- const auto distance = (point - _globalPressPoint).manhattanLength();
- if (distance >= QApplication::startDragDistance()) {
- _dragging = true;
- _globalLastPoint = _globalPressPoint;
- }
- }
- if (_dragging) {
- const auto delta = (point - _globalLastPoint).x();
- _globalLastPoint = point;
- _moveRequests.fire_copy(delta);
- }
- }
- 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;
- _globalPressPoint = e->globalPos();
- }
- void GroupsStrip::mouseReleaseEvent(QMouseEvent *e) {
- const auto pressed = std::exchange(_pressed, -1);
- if (_dragging) {
- _dragging = false;
- return;
- }
- const auto index = e->pos().x() / _st.groupWidth;
- const auto chosen = (index < 0 || index >= _buttons.size())
- ? -1
- : index;
- if (pressed == chosen && chosen >= 0) {
- _chosen = pressed;
- fireChosenGroup();
- update();
- }
- }
- void GroupsStrip::fireChosenGroup() {
- Expects(_chosen >= 0 && _chosen < _buttons.size());
- _chosenGroup.fire({
- .group = &_buttons[_chosen].group,
- .iconLeft = _chosen * _st.groupWidth,
- .iconRight = (_chosen + 1) * _st.groupWidth,
- });
- }
- } // namespace
- const QString &PremiumGroupFakeEmoticon() {
- static const auto result = u"*premium"_q;
- return result;
- }
- SearchWithGroups::SearchWithGroups(
- QWidget *parent,
- SearchDescriptor descriptor)
- : RpWidget(parent)
- , _st(descriptor.st)
- , _search(CreateChild<FadeWrapScaled<IconButton>>(
- this,
- object_ptr<IconButton>(this, _st.search)))
- , _back(CreateChild<FadeWrapScaled<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<FadeWrapScaled<RpWidget>>(
- this,
- object_ptr<GroupsStrip>(
- this,
- _st,
- std::move(descriptor.groups),
- std::move(descriptor.customEmojiFactory))))
- , _fade(CreateChild<RpWidget>(this))
- , _debounceTimer([=] { _debouncedQuery = _query.current(); }) {
- initField();
- initGroups();
- initButtons();
- initEdges();
- _inited = true;
- }
- anim::type SearchWithGroups::animated() const {
- return _inited ? anim::type::normal : anim::type::instant;
- }
- void SearchWithGroups::initField() {
- _field->changes(
- ) | rpl::start_with_next([=] {
- const auto last = FieldQuery(_field);
- _query = last;
- const auto empty = last.empty();
- _fieldEmpty = empty;
- if (empty) {
- _debounceTimer.cancel();
- _debouncedQuery = last;
- } else {
- _debounceTimer.callOnce(kDebounceTimeout);
- _chosenGroup = QString();
- scrollGroupsToStart();
- }
- }, _field->lifetime());
- _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;
- _fieldEmpty = last.empty();
- _fieldEmpty.value(
- ) | rpl::start_with_next([=](bool empty) {
- _cancel->toggle(!empty, animated());
- _groups->toggle(empty, animated());
- resizeToWidth(width());
- }, lifetime());
- }
- void SearchWithGroups::initGroups() {
- const auto widget = static_cast<GroupsStrip*>(_groups->entity());
- const auto &search = _st.search;
- _fadeLeftStart = search.iconPosition.x() + search.icon.width();
- _groups->move(_fadeLeftStart + _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 GroupsStrip::Chosen &chosen) {
- _chosenGroup = chosen.group->iconId;
- _query = (chosen.group->type == EmojiGroupType::Premium)
- ? std::vector{ PremiumGroupFakeEmoticon() }
- : chosen.group->emoticons;
- _debouncedQuery = chosen.group->emoticons;
- _debounceTimer.cancel();
- scrollGroupsToIcon(chosen.iconLeft, chosen.iconRight);
- }, lifetime());
- widget->moveRequests(
- ) | rpl::start_with_next([=](int delta) {
- moveGroupsBy(width(), delta);
- }, 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();
- if (_field->getLastText().isEmpty()) {
- _query = {};
- _debouncedQuery = {};
- _debounceTimer.cancel();
- }
- } else {
- _field->setText({});
- }
- }, lifetime());
- }
- void SearchWithGroups::scrollGroupsToIcon(int iconLeft, int iconRight) {
- const auto single = _st.groupWidth;
- const auto fadeRight = _fadeLeftStart + _st.fadeLeft.width();
- if (_groups->x() < fadeRight + single - iconLeft) {
- scrollGroupsTo(fadeRight + single - iconLeft);
- } else if (_groups->x() > width() - single - iconRight) {
- scrollGroupsTo(width() - single - iconRight);
- } else {
- _groupsLeftAnimation.stop();
- }
- }
- void SearchWithGroups::scrollGroupsToStart() {
- scrollGroupsTo(width());
- }
- void SearchWithGroups::scrollGroupsTo(int left) {
- left = clampGroupsLeft(width(), left);
- _groupsLeftTo = left;
- const auto delta = _groupsLeftTo - _groups->x();
- if (!delta) {
- _groupsLeftAnimation.stop();
- return;
- }
- _groupsLeftAnimation.start([=] {
- const auto d = int(base::SafeRound(_groupsLeftAnimation.value(0)));
- moveGroupsTo(width(), _groupsLeftTo - d);
- }, delta, 0, st::slideWrapDuration, anim::sineInOut);
- }
- 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->setAttribute(Qt::WA_TransparentForMouseEvents);
- 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);
- _fadeOpacity.changes(
- ) | rpl::start_with_next([=] {
- _fade->update();
- }, _fade->lifetime());
- _fade->paintRequest(
- ) | rpl::start_with_next([=](QRect clip) {
- auto p = QPainter(_fade);
- p.setOpacity(_fadeOpacity.current());
- const auto fill = QRect(0, 0, _fadeLeftStart, _st.height);
- if (fill.intersects(clip)) {
- p.fillRect(fill, _st.bg);
- }
- const auto icon = QRect(
- _fadeLeftStart,
- 0,
- _st.fadeLeft.width(),
- _st.height);
- if (clip.intersects(icon)) {
- _st.fadeLeft.fill(p, icon);
- }
- }, _fade->lifetime());
- _fade->setAttribute(Qt::WA_TransparentForMouseEvents);
- style::PaletteChanged(
- ) | rpl::start_with_next([=] {
- _rounding = QImage();
- }, lifetime());
- }
- void SearchWithGroups::initButtons() {
- _cancel->setClickedCallback([=] {
- _field->setText(QString());
- });
- _back->entity()->setClickedCallback([=] {
- _chosenGroup = QString();
- scrollGroupsToStart();
- });
- _search->entity()->setClickedCallback([=] {
- _field->setFocus();
- scrollGroupsToStart();
- });
- _field->focusedChanges(
- ) | rpl::filter(rpl::mappers::_1) | rpl::start_with_next([=] {
- scrollGroupsToStart();
- }, _field->lifetime());
- _field->raise();
- _fade->raise();
- _search->raise();
- _back->raise();
- _cancel->raise();
- }
- 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<> SearchWithGroups::escapes() const {
- return _field->cancelled();
- }
- rpl::producer<std::vector<QString>> SearchWithGroups::queryValue() const {
- return _query.value();
- }
- auto SearchWithGroups::debouncedQueryValue() const
- -> rpl::producer<std::vector<QString>> {
- return _debouncedQuery.value();
- }
- void SearchWithGroups::cancel() {
- _field->setText(QString());
- _chosenGroup = QString();
- scrollGroupsToStart();
- }
- void SearchWithGroups::setLoading(bool loading) {
- _cancel->setLoadingAnimation(loading);
- }
- void SearchWithGroups::stealFocus() {
- if (!_focusTakenFrom) {
- _focusTakenFrom = QApplication::focusWidget();
- }
- _field->setFocus();
- }
- void SearchWithGroups::returnFocus() {
- if (_field && _focusTakenFrom) {
- if (_field->hasFocus()) {
- _focusTakenFrom->setFocus();
- }
- _focusTakenFrom = nullptr;
- }
- }
- int SearchWithGroups::IconSizeOverride() {
- return style::ConvertScale(kCategoryIconSizeOverride);
- }
- int SearchWithGroups::resizeGetHeight(int newWidth) {
- if (!newWidth) {
- return _st.height;
- }
- _back->moveToLeft(0, 0, newWidth);
- _search->moveToLeft(0, 0, newWidth);
- _cancel->moveToRight(0, 0, newWidth);
- moveGroupsBy(newWidth, 0);
- const auto fadeWidth = _fadeLeftStart + _st.fadeLeft.width();
- const auto fade = QRect(0, 0, fadeWidth, _st.height);
- _fade->setGeometry(fade);
- return _st.height;
- }
- void SearchWithGroups::wheelEvent(QWheelEvent *e) {
- static_cast<GroupsStrip*>(_groups->entity())->scrollByWheel(e);
- }
- int SearchWithGroups::clampGroupsLeft(int width, int desiredLeft) const {
- const auto groupsLeftDefault = _fadeLeftStart + _st.defaultFieldWidth;
- const auto groupsLeftMin = width - _groups->entity()->width();
- const auto groupsLeftMax = std::max(groupsLeftDefault, groupsLeftMin);
- return std::clamp(desiredLeft, groupsLeftMin, groupsLeftMax);
- }
- void SearchWithGroups::moveGroupsBy(int width, int delta) {
- moveGroupsTo(width, _groups->x() + delta);
- }
- void SearchWithGroups::moveGroupsTo(int width, int to) {
- const auto groupsLeft = clampGroupsLeft(width, to);
- _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 = _fieldEmpty.current()
- ? std::max(groupsLeft - _st.search.width, fieldWidthMin)
- : (width - _fadeLeftStart - _st.cancel.width);
- _field->resizeToWidth(fieldWidth);
- const auto fieldLeft = _fieldEmpty.current()
- ? (groupsLeft - fieldWidth)
- : _fadeLeftStart;
- _field->moveToLeft(fieldLeft, 0);
- if (fieldLeft >= _fadeLeftStart) {
- if (!_fade->isHidden()) {
- _fade->hide();
- }
- } else {
- if (_fade->isHidden()) {
- _fade->show();
- }
- _fadeOpacity = (fieldLeft < _fadeLeftStart / 2)
- ? 1.
- : (_fadeLeftStart - fieldLeft) / float64(_fadeLeftStart / 2);
- }
- }
- 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);
- }
- QImage TabbedSearch::grab() {
- return Ui::GrabWidgetToImage(&_search);
- }
- void TabbedSearch::cancel() {
- _search.cancel();
- }
- void TabbedSearch::setLoading(bool loading) {
- _search.setLoading(loading);
- }
- void TabbedSearch::stealFocus() {
- _search.stealFocus();
- }
- void TabbedSearch::returnFocus() {
- _search.returnFocus();
- }
- rpl::producer<> TabbedSearch::escapes() const {
- return _search.escapes();
- }
- 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
|