From 0faadc8fa0791d61bdbe3a0320b380446f7f3dcc Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 29 Mar 2023 17:23:21 +0400 Subject: [PATCH] Implement folder link add / join design. --- Telegram/SourceFiles/api/api_chat_filters.cpp | 260 ++++++---- .../boxes/filters/edit_filter_links.cpp | 8 +- Telegram/SourceFiles/boxes/peer_list_box.cpp | 1 + .../SourceFiles/mtproto/session_private.cpp | 5 + .../platform/platform_overlay_widget.cpp | 17 +- Telegram/SourceFiles/settings/settings.style | 48 +- .../ui/controls/filter_link_header.cpp | 446 ++++++++++++++++++ .../ui/controls/filter_link_header.h | 49 ++ Telegram/cmake/td_ui.cmake | 2 + Telegram/lib_ui | 2 +- 10 files changed, 725 insertions(+), 113 deletions(-) create mode 100644 Telegram/SourceFiles/ui/controls/filter_link_header.cpp create mode 100644 Telegram/SourceFiles/ui/controls/filter_link_header.h diff --git a/Telegram/SourceFiles/api/api_chat_filters.cpp b/Telegram/SourceFiles/api/api_chat_filters.cpp index 5396f14f7c..03149b6e30 100644 --- a/Telegram/SourceFiles/api/api_chat_filters.cpp +++ b/Telegram/SourceFiles/api/api_chat_filters.cpp @@ -15,11 +15,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "lang/lang_keys.h" #include "main/main_session.h" +#include "settings/settings_common.h" #include "ui/boxes/confirm_box.h" +#include "ui/controls/filter_link_header.h" #include "ui/text/text_utilities.h" #include "ui/toasts/common_toasts.h" #include "ui/widgets/buttons.h" #include "window/window_session_controller.h" +#include "styles/style_filter_icons.h" #include "styles/style_layers.h" #include "styles/style_settings.h" @@ -31,19 +34,6 @@ enum class ToggleAction { Removing, }; -enum class HeaderType { - AddingFilter, - AddingChats, - AllAdded, - Removing, -}; - -struct HeaderDescriptor { - base::required type; - base::required title; - int badge = 0; -}; - class ToggleChatsController final : public PeerListController , public base::has_weak_ptr { @@ -63,11 +53,14 @@ public: [[nodiscard]] auto selectedValue() const -> rpl::producer>>; + void setAddedTopHeight(int addedTopHeight); + private: void setupAboveWidget(); void setupBelowWidget(); const not_null _window; + Ui::RpWidget *_addedTopWidget = nullptr; ToggleAction _action = ToggleAction::Adding; QString _slug; @@ -82,44 +75,42 @@ private: }; -[[nodiscard]] rpl::producer TitleText(HeaderType type) { +[[nodiscard]] tr::phrase<> TitleText(Ui::FilterLinkHeaderType type) { + using Type = Ui::FilterLinkHeaderType; switch (type) { - case HeaderType::AddingFilter: - return tr::lng_filters_by_link_title(); - case HeaderType::AddingChats: - return tr::lng_filters_by_link_more(); - case HeaderType::AllAdded: - return tr::lng_filters_by_link_already(); - case HeaderType::Removing: - return tr::lng_filters_by_link_remove(); + case Type::AddingFilter: return tr::lng_filters_by_link_title; + case Type::AddingChats: return tr::lng_filters_by_link_more; + case Type::AllAdded: return tr::lng_filters_by_link_already; + case Type::Removing: return tr::lng_filters_by_link_remove; } - Unexpected("HeaderType in TitleText."); + Unexpected("Ui::FilterLinkHeaderType in TitleText."); } -void FillHeader( - not_null container, - HeaderDescriptor descriptor) { - const auto phrase = (descriptor.type == HeaderType::AddingFilter) +[[nodiscard]] TextWithEntities AboutText( + Ui::FilterLinkHeaderType type, + const QString &title) { + using Type = Ui::FilterLinkHeaderType; + const auto phrase = (type == Type::AddingFilter) ? tr::lng_filters_by_link_sure - : (descriptor.type == HeaderType::AddingChats) + : (type == Type::AddingChats) ? tr::lng_filters_by_link_more_sure - : (descriptor.type == HeaderType::AllAdded) + : (type == Type::AllAdded) ? tr::lng_filters_by_link_already_about : tr::lng_filters_by_link_remove_sure; - auto boldTitle = Ui::Text::Bold(descriptor.title); - auto description = (descriptor.type == HeaderType::AddingFilter) + auto boldTitle = Ui::Text::Bold(title); + return (type == Type::AddingFilter) ? tr::lng_filters_by_link_sure( tr::now, lt_folder, std::move(boldTitle), Ui::Text::WithEntities) - : (descriptor.type == HeaderType::AddingChats) + : (type == Type::AddingChats) ? tr::lng_filters_by_link_more_sure( tr::now, lt_folder, std::move(boldTitle), Ui::Text::WithEntities) - : (descriptor.type == HeaderType::AllAdded) + : (type == Type::AllAdded) ? tr::lng_filters_by_link_already_about( tr::now, lt_folder, @@ -130,35 +121,92 @@ void FillHeader( lt_folder, std::move(boldTitle), Ui::Text::WithEntities); - container->add( - object_ptr( - container, - phrase( - lt_folder, - rpl::single(Ui::Text::Bold(descriptor.title)), - Ui::Text::WithEntities), - st::boxDividerLabel), - st::boxRowPadding); +} + +void InitFilterLinkHeader( + not_null box, + Fn setAddedTopHeight, + Ui::FilterLinkHeaderType type, + const QString &title, + rpl::producer count) { + auto header = Ui::MakeFilterLinkHeader(box, { + .type = type, + .title = TitleText(type)(tr::now), + .about = AboutText(type, title), + .folderTitle = title, + .folderIcon = &st::foldersCustomActive, + .badge = (type == Ui::FilterLinkHeaderType::AddingChats + ? std::move(count) + : rpl::single(0)), + }); + const auto widget = header.widget; + widget->resizeToWidth(st::boxWideWidth); + Ui::SendPendingMoveResizeEvents(widget); + + const auto min = widget->minimumHeight(), max = widget->maximumHeight(); + widget->resize(st::boxWideWidth, max); + + box->setAddedTopScrollSkip(max); + std::move( + header.wheelEvents + ) | rpl::start_with_next([=](not_null e) { + box->sendScrollViewportEvent(e); + }, widget->lifetime()); + + struct State { + bool processing = false; + int addedTopHeight = 0; + }; + const auto state = widget->lifetime().make_state(); + + box->scrolls( + ) | rpl::filter([=] { + return !state->processing; + }) | rpl::start_with_next([=] { + state->processing = true; + const auto guard = gsl::finally([&] { state->processing = false; }); + + const auto top = box->scrollTop(); + const auto height = box->scrollHeight(); + const auto headerHeight = std::max(max - top, min); + const auto addedTopHeight = max - headerHeight; + widget->resize(widget->width(), headerHeight); + if (state->addedTopHeight < addedTopHeight) { + setAddedTopHeight(addedTopHeight); + box->setAddedTopScrollSkip(headerHeight); + } else { + box->setAddedTopScrollSkip(headerHeight); + setAddedTopHeight(addedTopHeight); + } + state->addedTopHeight = addedTopHeight; + box->peerListRefreshRows(); + }, widget->lifetime()); + + box->setNoContentMargin(true); } void ImportInvite( base::weak_ptr weak, const QString &slug, - const base::flat_set> &peers) { + const base::flat_set> &peers, + Fn done, + Fn fail) { Expects(!peers.empty()); const auto peer = peers.front(); const auto api = &peer->session().api(); const auto callback = [=](const MTPUpdates &result) { api->applyUpdates(result); + done(); }; const auto error = [=](const MTP::Error &error) { if (const auto strong = weak.get()) { Ui::ShowMultilineToast({ .parentOverride = Window::Show(strong).toastParent(), - .text = { error.description() }, + .text = { error.type() }, }); } + fail(); }; auto inputs = peers | ranges::views::transform([](auto peer) { return MTPInputPeer(peer->input); @@ -182,6 +230,7 @@ ToggleChatsController::ToggleChatsController( , _filterId(filterId) , _filterTitle(title) , _chats(std::move(chats)) { + setStyleOverrides(&st::filterLinkChatsList); } void ToggleChatsController::prepare() { @@ -218,19 +267,14 @@ void ToggleChatsController::setupAboveWidget() { auto wrap = object_ptr((QWidget*)nullptr); const auto container = wrap.data(); - const auto type = !_filterId - ? HeaderType::AddingFilter - : (_action == ToggleAction::Adding) - ? HeaderType::AddingChats - : HeaderType::Removing; - delegate()->peerListSetTitle(TitleText(type)); - FillHeader(container, { - .type = type, - .title = _filterTitle, - .badge = (type == HeaderType::AddingChats) ? int(_chats.size()) : 0, - }); - - // lng_filters_by_link_join; // langs + _addedTopWidget = container->add(object_ptr(container)); + AddDivider(container); + AddSubsectionTitle( + container, + tr::lng_filters_by_link_join( + lt_count, + rpl::single(float64(_chats.size()))), + st::filterLinkSubsectionTitlePadding); delegate()->peerListSetAboveWidget(std::move(wrap)); } @@ -241,7 +285,7 @@ void ToggleChatsController::setupBelowWidget() { (QWidget*)nullptr, object_ptr( (QWidget*)nullptr, - tr::lng_filters_by_link_about(), + tr::lng_filters_by_link_about(tr::now), st::boxDividerLabel), st::settingsDividerLabelPadding)); } @@ -255,17 +299,10 @@ auto ToggleChatsController::selectedValue() const return _selected.value(); } -[[nodiscard]] void AlreadyFilterBox( - not_null box, - const QString &title) { - box->setTitle(TitleText(HeaderType::AllAdded)); +void ToggleChatsController::setAddedTopHeight(int addedTopHeight) { + Expects(addedTopHeight >= 0); - FillHeader(box->verticalLayout(), { - .type = HeaderType::AllAdded, - .title = title, - }); - - box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); }); + _addedTopWidget->resize(_addedTopWidget->width(), addedTopHeight); } void ProcessFilterInvite( @@ -279,15 +316,11 @@ void ProcessFilterInvite( return; } Core::App().hideMediaView(); - if (peers.empty()) { - if (filterId) { - strong->show(Box(AlreadyFilterBox, title)); - } else { - Ui::ShowMultilineToast({ - .parentOverride = Window::Show(strong).toastParent(), - .text = { tr::lng_group_invite_bad_link(tr::now) }, - }); - } + if (peers.empty() && !filterId) { + Ui::ShowMultilineToast({ + .parentOverride = Window::Show(strong).toastParent(), + .text = { tr::lng_group_invite_bad_link(tr::now) }, + }); return; } auto controller = std::make_unique( @@ -298,42 +331,61 @@ void ProcessFilterInvite( title, std::move(peers)); const auto raw = controller.get(); - auto initBox = [=](not_null box) { + auto initBox = [=](not_null box) { box->setStyle(st::filterInviteBox); + + using Type = Ui::FilterLinkHeaderType; + const auto type = !filterId + ? Type::AddingFilter + : Type::AddingChats; + auto badge = raw->selectedValue( + ) | rpl::map([=](const base::flat_set> &peers) { + return int(peers.size()); + }); + InitFilterLinkHeader(box, [=](int addedTopHeight) { + raw->setAddedTopHeight(addedTopHeight); + }, type, title, rpl::duplicate(badge)); + + auto owned = Ui::FilterLinkProcessButton( + box, + type, + title, + std::move(badge)); + + const auto button = owned.data(); + box->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto &padding = st::filterInviteBox.buttonPadding; + button->resizeToWidth(width + - padding.left() + - padding.right()); + button->moveToLeft(padding.left(), padding.top()); + }, button->lifetime()); + + box->addButton(std::move(owned)); + + struct State { + bool importing = false; + }; + const auto state = box->lifetime().make_state(); + raw->selectedValue( ) | rpl::start_with_next([=]( base::flat_set> &&peers) { - const auto count = int(peers.size()); - - box->clearButtons(); - auto button = object_ptr( - box, - rpl::single(count - ? u"Add %1 Chats"_q.arg(count) - : u"Don't add chats"_q), - st::defaultActiveButton); - const auto raw = button.data(); - - box->widthValue( - ) | rpl::start_with_next([=](int width) { - const auto &padding = st::filterInviteBox.buttonPadding; - raw->resizeToWidth(width - - padding.left() - - padding.right()); - raw->moveToLeft(padding.left(), padding.top()); - }, raw->lifetime()); - - raw->setClickedCallback([=] { - if (!count) { + button->setClickedCallback([=] { + if (peers.empty()) { box->closeBox(); //} else if (count + alreadyInFilter() >= ...) { // #TODO filters - } else { - ImportInvite(weak, slug, peers); + } else if (!state->importing) { + state->importing = true; + ImportInvite(weak, slug, peers, crl::guard(box, [=] { + box->closeBox(); + }), crl::guard(box, [=] { + state->importing = false; + })); } }); - - box->addButton(std::move(button)); }, box->lifetime()); }; strong->show( diff --git a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp index a181d3dac2..d9f01e55fa 100644 --- a/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp +++ b/Telegram/SourceFiles/boxes/filters/edit_filter_links.cpp @@ -531,7 +531,10 @@ void LinkController::addLinkBlock(not_null container) { &st::menuIconDelete); return result; }; - AddSubsectionTitle(container, tr::lng_filters_link_subtitle()); + AddSubsectionTitle( + container, + tr::lng_filters_link_subtitle(), + st::filterLinkSubsectionTitlePadding); const auto prefix = u"https://"_q; const auto label = container->lifetime().make_state( @@ -640,7 +643,8 @@ void LinkController::setupAboveWidget() { }); Settings::AddSubsectionTitle( container, - std::move(subtitle)); + std::move(subtitle), + st::filterLinkSubsectionTitlePadding); delegate()->peerListSetAboveWidget(std::move(wrap)); } diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index 2dbe7c67ab..db5076952e 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -140,6 +140,7 @@ void PeerListBox::createMultiSelect() { void PeerListBox::setAddedTopScrollSkip(int skip) { _addedTopScrollSkip = skip; + _scrollBottomFixed = false; updateScrollSkips(); } diff --git a/Telegram/SourceFiles/mtproto/session_private.cpp b/Telegram/SourceFiles/mtproto/session_private.cpp index fe4beb8356..729a95ed60 100644 --- a/Telegram/SourceFiles/mtproto/session_private.cpp +++ b/Telegram/SourceFiles/mtproto/session_private.cpp @@ -1630,6 +1630,11 @@ SessionPrivate::HandleResult SessionPrivate::handleOneReceived( _sessionSalt = data.vnew_server_salt().v; correctUnixtimeWithBadLocal(info.serverTime); + if (_bindMsgId) { + LOG(("Message Info: bad_server_salt received while binding temp key, restarting.")); + return HandleResult::RestartConnection; + } + if (setState(ConnectedState, ConnectingState)) { resendAll(); } diff --git a/Telegram/SourceFiles/platform/platform_overlay_widget.cpp b/Telegram/SourceFiles/platform/platform_overlay_widget.cpp index 5707b54f3c..90bda56247 100644 --- a/Telegram/SourceFiles/platform/platform_overlay_widget.cpp +++ b/Telegram/SourceFiles/platform/platform_overlay_widget.cpp @@ -224,9 +224,20 @@ rpl::producer<> DefaultOverlayWidgetHelper::controlsActivations() { } rpl::producer DefaultOverlayWidgetHelper::controlsSideRightValue() { - return Ui::Platform::TitleControlsLayoutValue() | rpl::map([=] { - return _controls->controls.geometry().center().x() - > _controls->wrap.geometry().center().x(); + using namespace Ui::Platform; + + return TitleControlsLayoutValue( + ) | rpl::map([=](const TitleControls::Layout &layout) { + // See TitleControls::updateControlsPosition. + if (ranges::contains(layout.left, TitleControl::Close)) { + return false; + } else if (ranges::contains(layout.right, TitleControl::Close)) { + return true; + } else if (layout.left.size() > layout.right.size()) { + return false; + } else { + return true; + } }) | rpl::distinct_until_changed(); } diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index b5b304ec73..2af9de650b 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -540,11 +540,53 @@ powerSavingButtonNoIcon: SettingsButton(powerSavingButton) { powerSavingSubtitlePadding: margins(0px, 4px, 0px, -2px); filterInviteBox: Box(defaultBox) { - buttonPadding: margins(12px, 12px, 12px, 12px); - buttonHeight: 44px; + buttonPadding: margins(10px, 9px, 10px, 9px); + buttonHeight: 42px; button: RoundButton(defaultActiveButton) { - height: 44px; + height: 42px; textTop: 12px; font: font(13px semibold); } } +filterInviteButtonStyle: TextStyle(defaultTextStyle) { + font: font(13px semibold); + linkFont: font(13px underline); + linkFontOver: font(13px underline); +} +filterInviteButtonBadgeStyle: TextStyle(defaultTextStyle) { + font: font(12px semibold); + linkFont: font(12px underline); + linkFontOver: font(12px underline); +} +filterInviteButtonBadgePadding: margins(5px, 0px, 5px, 2px); +filterInviteButtonBadgeSkip: 5px; +filterLinkTitlePadding: margins(0px, 15px, 0px, 17px); +filterLinkAboutTextStyle: TextStyle(defaultTextStyle) { + font: font(12px); + linkFont: font(12px underline); + linkFontOver: font(12px underline); + lineHeight: 17px; +} +filterLinkAbout: FlatLabel(defaultFlatLabel) { + style: filterLinkAboutTextStyle; + align: align(top); + minWidth: 190px; +} +filterLinkAboutTop: 170px; +filterLinkAboutBottom: 15px; +filterLinkPreview: 96px; +filterLinkPreviewRadius: 13px; +filterLinkPreviewTop: 30px; +filterLinkPreviewColumn: 65px; +filterLinkPreviewAllBottom: 18px; +filterLinkPreviewAllTop: 17px; +filterLinkPreviewMyBottom: 74px; +filterLinkPreviewMyTop: 73px; +filterLinkPreviewChatSize: 36px; +filterLinkPreviewChatSkip: 10px; +filterLinkPreviewBadgeLeft: 40px; +filterLinkPreviewBadgeTop: 38px; +filterLinkSubsectionTitlePadding: margins(0px, 5px, 0px, -4px); +filterLinkChatsList: PeerList(peerListBox) { + padding: margins(0px, 0px, 0px, membersMarginBottom); +} diff --git a/Telegram/SourceFiles/ui/controls/filter_link_header.cpp b/Telegram/SourceFiles/ui/controls/filter_link_header.cpp new file mode 100644 index 0000000000..f35ad1a615 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/filter_link_header.cpp @@ -0,0 +1,446 @@ +/* +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/filter_link_header.h" + +#include "lang/lang_keys.h" +#include "ui/painter.h" +#include "ui/rp_widget.h" +#include "ui/image/image_prepare.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/labels.h" +#include "styles/style_filter_icons.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" +#include "styles/style_window.h" + +namespace Ui { +namespace { + +constexpr auto kBodyAnimationPart = 0.90; +constexpr auto kTitleAdditionalScale = 0.05; + +class Widget final : public RpWidget { +public: + Widget( + not_null parent, + FilterLinkHeaderDescriptor &&descriptor); + + void setTitlePosition(int x, int y); + void updateDimensions(int newWidth); + + [[nodiscard]] rpl::producer> wheelEvents() const; + +private: + void resizeEvent(QResizeEvent *e) override; + void paintEvent(QPaintEvent *e) override; + void wheelEvent(QWheelEvent *e) override; + + [[nodiscard]] QRectF previewRect( + float64 topProgress, + float64 sizeProgress) const; + void refreshTitleText(); + + const not_null _about; + QMargins _aboutPadding; + + struct { + float64 top = 0.; + float64 body = 0.; + float64 title = 0.; + float64 scaleTitle = 0.; + } _progress; + + rpl::variable _badge; + QImage _preview; + QRectF _previewRect; + + QString _titleText; + style::font _titleFont; + QMargins _titlePadding; + QPoint _titlePosition; + QPainterPath _titlePath; + + QString _folderTitle; + not_null _folderIcon; + + int _maxHeight = 0; + + rpl::event_stream> _wheelEvents; + +}; + +[[nodiscard]] QImage GeneratePreview( + const QString &title, + not_null icon, + int badge) { + const auto size = st::filterLinkPreview; + const auto ratio = style::DevicePixelRatio(); + const auto radius = st::filterLinkPreviewRadius; + const auto full = QSize(size, size) * ratio; + auto result = QImage(full, QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(ratio); + result.fill(st::windowBg->c); + + auto p = QPainter(&result); + + const auto column = st::filterLinkPreviewColumn; + p.fillRect(0, 0, column, size, st::sideBarBg); + p.fillRect(column, 0, size - column, size, st::emojiPanCategories); + + const auto &st = st::windowFiltersButton; + const auto skip = st.style.font->spacew; + const auto available = column - 2 * skip; + const auto iconWidth = st::foldersAll.width(); + const auto iconHeight = st::foldersAll.height(); + const auto iconLeft = (column - iconWidth) / 2; + const auto allIconTop = st::filterLinkPreviewAllBottom - iconHeight; + st::foldersAll.paint(p, iconLeft, allIconTop, size); + const auto myIconTop = st::filterLinkPreviewMyBottom - iconHeight; + icon->paint(p, iconLeft, myIconTop, size); + + const auto paintName = [&](const QString &text, int top) { + const auto &font = st.style.font; + p.drawText( + QRect(0, top, column, font->height), + font->elided(text, available), + style::al_top); + }; + p.setFont(st.style.font); + p.setPen(st.textFg); + paintName(tr::lng_filters_all(tr::now), st::filterLinkPreviewAllTop); + p.setPen(st.textFgActive); + paintName(title, st::filterLinkPreviewMyTop); + + auto hq = PainterHighQualityEnabler(p); + + const auto chatSize = st::filterLinkPreviewChatSize; + const auto chatLeft = size + st::lineWidth - (chatSize / 2); + const auto paintChat = [&](int top, const style::color &bg) { + p.setBrush(bg); + p.drawEllipse(chatLeft, top, chatSize, chatSize); + }; + const auto chatSkip = st::filterLinkPreviewChatSkip; + const auto chat1Top = (size - 2 * chatSize - chatSkip) / 2; + const auto chat2Top = size - chat1Top - chatSize; + p.setPen(Qt::NoPen); + paintChat(chat1Top, st::historyPeer4UserpicBg); + paintChat(chat2Top, st::historyPeer8UserpicBg); + + if (badge > 0) { + const auto font = st.badgeStyle.font; + const auto badgeHeight = st.badgeHeight; + const auto countBadgeWidth = [&](const QString &text) { + return std::max( + font->width(text) + 2 * st.badgeSkip, + badgeHeight); + }; + const auto defaultBadgeWidth = countBadgeWidth(u"+3"_q); + const auto badgeText = '+' + QString::number(badge); + const auto badgeWidth = countBadgeWidth(badgeText); + const auto defaultBadgeLeft = st::filterLinkPreviewBadgeLeft; + const auto badgeLeft = defaultBadgeLeft + + (defaultBadgeWidth - badgeWidth) / 2; + const auto badgeTop = st::filterLinkPreviewBadgeTop; + + const auto add = st::lineWidth; + auto pen = st.textBg->p; + pen.setWidthF(add * 2.); + p.setPen(pen); + p.setBrush(st.badgeBg); + const auto radius = (badgeHeight / 2) + add; + const auto rect = QRect(badgeLeft, badgeTop, badgeWidth, badgeHeight) + + QMargins(add, add, add, add); + p.drawRoundedRect(rect, radius, radius); + + p.setPen(st.badgeFg); + p.setFont(st.badgeStyle.font); + p.drawText(rect, badgeText, style::al_center); + } + + auto pen = st::shadowFg->p; + pen.setWidthF(st::lineWidth * 2.); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + p.drawRoundedRect(0, 0, size, size, radius, radius); + p.end(); + + return Images::Round(std::move(result), Images::CornersMask(radius)); +} + +Widget::Widget( + not_null parent, + FilterLinkHeaderDescriptor &&descriptor) +: RpWidget(parent) +, _about(CreateChild( + this, + rpl::single(descriptor.about.value()), + st::filterLinkAbout)) +, _aboutPadding(st::boxRowPadding) +, _badge(std::move(descriptor.badge)) +, _titleText(descriptor.title) +, _titleFont(st::boxTitle.style.font) +, _titlePadding(st::filterLinkTitlePadding) +, _folderTitle(descriptor.folderTitle) +, _folderIcon(descriptor.folderIcon) { + setMinimumHeight(st::boxTitleHeight); + refreshTitleText(); + setTitlePosition(st::boxTitlePosition.x(), st::boxTitlePosition.y()); + + style::PaletteChanged( + ) | rpl::start_with_next([=] { + _preview = QImage(); + }, lifetime()); + + _badge.changes() | rpl::start_with_next([=] { + _preview = QImage(); + update(); + }, lifetime()); +} + +void Widget::refreshTitleText() { + _titlePath = QPainterPath(); + _titlePath.addText(0, _titleFont->ascent, _titleFont, _titleText); + update(); +} + +void Widget::setTitlePosition(int x, int y) { + _titlePosition = { x, y }; +} + +rpl::producer> Widget::wheelEvents() const { + return _wheelEvents.events(); +} + +void Widget::resizeEvent(QResizeEvent *e) { + const auto &padding = _aboutPadding; + const auto availableWidth = width() - padding.left() - padding.right(); + if (availableWidth <= 0) { + return; + } + _about->resizeToWidth(availableWidth); + + const auto minHeight = minimumHeight(); + const auto maxHeight = st::filterLinkAboutTop + + _about->height() + + st::filterLinkAboutBottom; + if (maxHeight <= minHeight) { + return; + } else if (_maxHeight != maxHeight) { + _maxHeight = maxHeight; + setMaximumHeight(maxHeight); + } + + const auto progress = (height() - minHeight) + / float64(_maxHeight - minHeight); + _progress.top = 1. - + std::clamp( + (1. - progress) / kBodyAnimationPart, + 0., + 1.); + _progress.body = _progress.top; + _progress.title = 1. - progress; + _progress.scaleTitle = 1. + kTitleAdditionalScale * progress; + + _previewRect = previewRect(_progress.top, _progress.body); + + const auto titleTop = _previewRect.top() + + _previewRect.height() + + _titlePadding.top(); + const auto titlePathRect = _titlePath.boundingRect(); + const auto aboutTop = titleTop + + titlePathRect.height() + + _titlePadding.bottom(); + _about->moveToLeft(_aboutPadding.left(), aboutTop); + _about->setOpacity(_progress.body); + + update(); +} + +QRectF Widget::previewRect( + float64 topProgress, + float64 sizeProgress) const { + const auto size = st::filterLinkPreview; + return QRectF( + (width() - size) / 2., + st::filterLinkPreviewTop * topProgress, + size, + size); +}; + +void Widget::paintEvent(QPaintEvent *e) { + auto p = QPainter(this); + + p.setOpacity(_progress.body); + p.translate(_previewRect.center()); + p.scale(_progress.body, _progress.body); + p.translate(-_previewRect.center()); + if (_progress.top) { + auto hq = PainterHighQualityEnabler(p); + if (_preview.isNull()) { + _preview = GeneratePreview( + _folderTitle, + _folderIcon, + _badge.current()); + } + p.drawImage(_previewRect, _preview); + } + p.resetTransform(); + + const auto titlePathRect = _titlePath.boundingRect(); + + // Title. + PainterHighQualityEnabler hq(p); + p.setOpacity(1.); + p.setFont(_titleFont); + p.setPen(st::boxTitleFg); + const auto fullPreviewRect = previewRect(1., 1.); + const auto fullTitleTop = fullPreviewRect.top() + + fullPreviewRect.height() + + _titlePadding.top(); + p.translate( + anim::interpolate( + (width() - titlePathRect.width()) / 2, + _titlePosition.x(), + _progress.title), + anim::interpolate(fullTitleTop, _titlePosition.y(), _progress.title)); + + p.translate(titlePathRect.center()); + p.scale(_progress.scaleTitle, _progress.scaleTitle); + p.translate(-titlePathRect.center()); + p.fillPath(_titlePath, st::boxTitleFg); +} + +void Widget::wheelEvent(QWheelEvent *e) { + _wheelEvents.fire(e); +} + +} // namespace + +[[nodiscard]] FilterLinkHeader MakeFilterLinkHeader( + not_null parent, + FilterLinkHeaderDescriptor &&descriptor) { + const auto result = CreateChild( + parent.get(), + std::move(descriptor)); + return { .widget = result, .wheelEvents = result->wheelEvents() }; +} + +object_ptr FilterLinkProcessButton( + not_null parent, + FilterLinkHeaderType type, + const QString &title, + rpl::producer badge) { + const auto st = &st::filterInviteBox.button; + const auto badgeSt = &st::filterInviteButtonBadgeStyle; + auto result = object_ptr(parent, rpl::single(u""_q), *st); + + struct Data { + QString text; + QString badge; + }; + auto data = std::move( + badge + ) | rpl::map([=](int count) { + const auto badge = count ? QString::number(count) : QString(); + const auto with = [&](QString badge) { + return rpl::map([=](QString text) { + return Data{ text, badge }; + }); + }; + switch (type) { + case FilterLinkHeaderType::AddingFilter: + return badge.isEmpty() + ? tr::lng_filters_by_link_add_no() | with(QString()) + : tr::lng_filters_by_link_add_button( + lt_folder, + rpl::single(title) + ) | with(badge); + case FilterLinkHeaderType::AddingChats: + return badge.isEmpty() + ? tr::lng_filters_by_link_join_no() | with(QString()) + : tr::lng_filters_by_link_join_button() | with(badge); + case FilterLinkHeaderType::AllAdded: + return tr::lng_box_ok() | with(QString()); + case FilterLinkHeaderType::Removing: + return badge.isEmpty() + ? tr::lng_filters_by_link_remove_button() | with(QString()) + : tr::lng_filters_by_link_quit_button() | with(badge); + } + Unexpected("Type in FilterLinkProcessButton."); + }) | rpl::flatten_latest(); + + struct Label : RpWidget { + using RpWidget::RpWidget; + + Text::String text; + Text::String badge; + }; + const auto label = result->lifetime().make_state