diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 2c6a0a196..fe175db3f 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -857,6 +857,8 @@ PRIVATE info/profile/info_profile_widget.h info/settings/info_settings_widget.cpp info/settings/info_settings_widget.h + info/userpic/info_userpic_emoji_builder_widget.cpp + info/userpic/info_userpic_emoji_builder_widget.h inline_bots/bot_attach_web_view.cpp inline_bots/bot_attach_web_view.h inline_bots/inline_bot_layout_internal.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 4f90377c4..edb4ba27d 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3753,6 +3753,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_request_channel_add_admins" = "add new admins"; "lng_request_channel_create" = "Create a New Channel for This"; +"lng_userpic_builder_color_subtitle" = "Choose background"; +"lng_userpic_builder_emoji_subtitle" = "Choose sticker or emoji"; + // Wnd specific "lng_wnd_choose_program_menu" = "Choose Default Program..."; diff --git a/Telegram/SourceFiles/info/userpic/info_userpic_builder.style b/Telegram/SourceFiles/info/userpic/info_userpic_builder.style index c83dbb829..4407a435e 100644 --- a/Telegram/SourceFiles/info/userpic/info_userpic_builder.style +++ b/Telegram/SourceFiles/info/userpic/info_userpic_builder.style @@ -7,7 +7,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ using "ui/basic.style"; +using "ui/widgets/widgets.style"; + +userpicBuilderEmojiPreviewPadding: margins(0px, 36px, 0px, 8px); +userpicBuilderEmojiSubtitle: FlatLabel(defaultFlatLabel) { + align: align(center); + minWidth: 356px; + maxHeight: 24px; + textFg: windowSubTextFg; +} +userpicBuilderEmojiSubtitlePadding: margins(0px, 9px, 0px, 2px); +userpicBuilderEmojiBubblePaletteSize: size(356px, 56px); +userpicBuilderEmojiBubblePalettePadding: margins(12px, 8px, 12px, 8px); + +userpicBuilderEmojiSelectorLeft: 5px; + userpicBuilderEmojiAccentColorSize: 30px; userpicBuilderEmojiBubblePadding: margins(5px, 5px, 5px, 5px); userpicBuilderEmojiLayerMinHeight: 496px; +userpicBuilderEmojiSelectorMinHeight: 216px; diff --git a/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_widget.cpp b/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_widget.cpp new file mode 100644 index 000000000..8b4e9186f --- /dev/null +++ b/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_widget.cpp @@ -0,0 +1,435 @@ +/* +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 "info/userpic/info_userpic_emoji_builder_widget.h" + +#include "apiwrap.h" +#include "api/api_peer_photo.h" +#include "chat_helpers/emoji_list_widget.h" +#include "chat_helpers/stickers_lottie.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_message_reactions.h" +#include "data/data_session.h" +#include "data/stickers/data_custom_emoji.h" +#include "editor/photo_editor_layer_widget.h" // Editor::kProfilePhotoSize. +#include "history/view/media/history_view_sticker_player.h" +#include "info/userpic/info_userpic_bubble_wrap.h" +#include "info/userpic/info_userpic_colors_palette_chooser.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "settings/settings_common.h" +#include "ui/painter.h" +#include "ui/rect.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/scroll_area.h" +#include "ui/wrap/padding_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "window/window_session_controller.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_info_userpic_builder.h" +#include "styles/style_layers.h" +#include "styles/style_settings.h" + +namespace UserpicBuilder { +namespace { + +class PreviewPainter final { +public: + PreviewPainter(int size); + + [[nodiscard]] not_null document() const; + + void setDocument( + not_null document, + Fn updateCallback); + + void paintBackground(QPainter &p, const QBrush &brush); + bool paintForeground(QPainter &p); + +private: + const int _size; + const int _emojiSize; + const QRect _frameRect; + + std::shared_ptr _media; + std::unique_ptr _player; + bool _paused = false; + rpl::lifetime _lifetime; + +}; + +PreviewPainter::PreviewPainter(int size) +: _size(size) +, _emojiSize(base::SafeRound(_size / M_SQRT2)) +, _frameRect(Rect(Size(_size)) - Margins((_size - _emojiSize) / 2)) { +} + +not_null PreviewPainter::document() const { + Expects(_media != nullptr); + return _media->owner(); +} + +void PreviewPainter::setDocument( + not_null document, + Fn updateCallback) { + if (_media && (document == _media->owner())) { + return; + } + _lifetime.destroy(); + + const auto sticker = document->sticker(); + Assert(sticker != nullptr); + _media = document->createMediaView(); + _media->checkStickerLarge(); + _media->goodThumbnailWanted(); + + rpl::single() | rpl::then( + document->owner().session().downloaderTaskFinished() + ) | rpl::start_with_next([=] { + if (!_media->loaded()) { + return; + } + _lifetime.destroy(); + const auto emojiSize = Size(_emojiSize); + if (sticker->isLottie()) { + _player = std::make_unique( + ChatHelpers::LottiePlayerFromDocument( + _media.get(), + // + ChatHelpers::StickerLottieSize::EmojiInteractionReserved7, + emojiSize, + Lottie::Quality::High)); + } else if (sticker->isWebm()) { + _player = std::make_unique( + _media->owner()->location(), + _media->bytes(), + emojiSize); + } else if (sticker) { + _player = std::make_unique( + _media->owner()->location(), + _media->bytes(), + emojiSize); + } + if (_player) { + _player->setRepaintCallback(updateCallback); + } else { + updateCallback(); + } + }, _lifetime); +} + +void PreviewPainter::paintBackground(QPainter &p, const QBrush &brush) { + PainterHighQualityEnabler hq(p); + p.setPen(Qt::NoPen); + p.setBrush(brush); + p.drawEllipse(0, 0, _size, _size); +} + +bool PreviewPainter::paintForeground(QPainter &p) { + if (_player && _player->ready()) { + // resolveIsColored(); + const auto frame = _player->frame( + Size(_emojiSize), + (/*_isColored + ? st::profileVerifiedCheckBg->c + : */QColor(0, 0, 0, 0)), + false, + crl::now(), + _paused); + + p.drawImage(_frameRect, frame.image); + if (!_paused) { + _player->markFrameShown(); + } + return true; + } + return false; +} + +class EmojiUserpic final : public Ui::RpWidget { +public: + EmojiUserpic(not_null parent, const QSize &size); + + void result(int size, Fn done); + void setGradientStops(QGradientStops stops); + void setDocument(not_null document); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + PreviewPainter _painter; + + QBrush _previousBrush; + QBrush _brush; + QGradientStops _stops; + + Ui::Animations::Simple _animation; + +}; + +EmojiUserpic::EmojiUserpic( + not_null parent, + const QSize &size) +: Ui::RpWidget(parent) +, _painter(size.width()) { + resize(size); +} + +void EmojiUserpic::setDocument(not_null document) { + _painter.setDocument(document, [=] { update(); }); +} + +void EmojiUserpic::result(int size, Fn done) { + const auto colors = ranges::views::all( + _stops + ) | ranges::views::transform([](const QGradientStop &stop) { + return stop.second; + }) | ranges::to_vector; + const auto painter = lifetime().make_state(size); + // Reset to the first frame. + painter->setDocument(_painter.document(), [=] { + auto background = Images::GenerateLinearGradient(Size(size), colors); + + auto p = QPainter(&background); + while (true) { + if (painter->paintForeground(p)) { + break; + } + } + done(std::move(background)); + }); +} + +void EmojiUserpic::setGradientStops(QGradientStops stops) { + if (_stops == stops) { + return; + } + if (!_stops.empty()) { + auto gradient = QLinearGradient(0, 0, width() / 2., height()); + gradient.setStops(base::take(_stops)); + _previousBrush = QBrush(std::move(gradient)); + } + _stops = std::move(stops); + { + auto gradient = QLinearGradient(0, 0, width() / 2., height()); + gradient.setStops(_stops); + _brush = QBrush(std::move(gradient)); + } + _animation.stop(); + _animation.start([=] { update(); }, 0., 1., st::slideWrapDuration); +} + +void EmojiUserpic::paintEvent(QPaintEvent *event) { + auto p = QPainter(this); + + if (_animation.animating() && (_previousBrush != Qt::NoBrush)) { + _painter.paintBackground(p, _previousBrush); + + p.setOpacity(_animation.value(1.)); + } + + _painter.paintBackground(p, _brush); + + p.setOpacity(1.); + _painter.paintForeground(p); +} + +class EmojiSelector final : public Ui::RpWidget { +public: + EmojiSelector( + not_null parent, + not_null controller); + + [[nodiscard]] rpl::producer> chosen() const; + +private: + void prepare(); + + const not_null _controller; + base::unique_qptr _scroll; + ChatHelpers::EmojiListWidget *_selector; + +}; + +EmojiSelector::EmojiSelector( + not_null parent, + not_null controller) +: RpWidget(parent) +, _controller(controller) { + prepare(); +} + +rpl::producer> EmojiSelector::chosen() const { + return _selector->customChosen( + ) | rpl::map([](const ChatHelpers::FileChosen &chosen) { + return chosen.document; + }); +} + +void EmojiSelector::prepare() { + const auto ref = Data::PossibleItemReactionsRef{ + .morePremiumAvailable = true, + .customAllowed = true, + }; + + const auto manager = &_controller->session().data().customEmojiManager(); + const auto tag = Data::CustomEmojiManager::SizeTag::Large; + const auto &stScroll = st::reactPanelScroll; + const auto session = &_controller->session(); + _scroll = base::make_unique_q(this, stScroll); + auto args = ChatHelpers::EmojiListDescriptor{ + .session = session, + .mode = ChatHelpers::EmojiListMode::FullReactions, + .controller = _controller, + .paused = [=, reason = Window::GifPauseReason::Layer] { + return _controller->isGifPausedAtLeastFor(reason); + }, + .customRecentList = session->api().peerPhoto().profileEmojiList(), + .customRecentFactory = [=](DocumentId id, Fn repaint) { + return manager->create(id, std::move(repaint), tag); + }, + .st = &st::reactPanelEmojiPan, + }; + _selector = _scroll->setOwnedWidget( + object_ptr(_scroll, std::move(args))); + const auto footer = _selector->createFooter().data(); + footer->setParent(this); + _selector->refreshEmoji(); + + const auto updateVisibleTopBottom = [=] { + const auto scrollTop = _scroll->scrollTop(); + const auto scrollBottom = scrollTop + _scroll->height(); + _selector->setVisibleTopBottom(scrollTop, scrollBottom); + }; + _scroll->scrollTopChanges( + ) | rpl::start_with_next(updateVisibleTopBottom, _selector->lifetime()); + + _selector->scrollToRequests( + ) | rpl::start_with_next([=](int y) { + _scroll->scrollToY(y); + // _shadow->update(); + }, _selector->lifetime()); + + const auto separator = Ui::CreateChild(this); + separator->paintRequest( + ) | rpl::start_with_next([=](const QRect &r) { + auto p = QPainter(separator); + p.fillRect(r, st::shadowFg); + }, separator->lifetime()); + + footer->show(); + separator->show(); + _scroll->show(); + + const auto scrollWidth = stScroll.width; + sizeValue( + ) | rpl::start_with_next([=](const QSize &s) { + const auto left = st::userpicBuilderEmojiSelectorLeft; + + footer->setGeometry( + left, + st::userpicBuilderEmojiSelectorLeft, + s.width() - left, + footer->height()); + + separator->setGeometry( + 0, + rect::bottom(footer), + s.width(), + st::lineWidth); + + _selector->resizeToWidth(s.width() - st::boxRadius * 2); + _scroll->setGeometry( + st::boxRadius, + rect::bottom(separator), + _selector->width() + scrollWidth, + s.height() - rect::bottom(separator)); + }, lifetime()); +} + +} // namespace + +not_null CreateUserpicBuilder( + not_null parent, + not_null controller, + BothWayCommunication communication) { + const auto container = Ui::CreateChild(parent.get()); + + const auto preview = container->add( + object_ptr>( + container, + object_ptr( + container, + Size(st::settingsInfoPhotoSize))), + st::userpicBuilderEmojiPreviewPadding)->entity(); + + container->add( + object_ptr>( + container, + object_ptr( + container, + tr::lng_userpic_builder_color_subtitle(), + st::userpicBuilderEmojiSubtitle)), + st::userpicBuilderEmojiSubtitlePadding); + + const auto paletteBg = Ui::AddBubbleWrap( + container, + st::userpicBuilderEmojiBubblePaletteSize, + [=] { return controller->chatStyle(); }); + + const auto palette = Ui::CreateChild(paletteBg.get()); + palette->stopsValue( + ) | rpl::start_with_next([=](QGradientStops stops) { + preview->setGradientStops(std::move(stops)); + }, preview->lifetime()); + paletteBg->innerRectValue( + ) | rpl::start_with_next([=](const QRect &r) { + palette->setGeometry(r + - st::userpicBuilderEmojiBubblePalettePadding); + }, palette->lifetime()); + + container->add( + object_ptr>( + container, + object_ptr( + container, + tr::lng_userpic_builder_emoji_subtitle(), + st::userpicBuilderEmojiSubtitle)), + st::userpicBuilderEmojiSubtitlePadding); + + const auto selectorBg = Ui::AddBubbleWrap( + container, + QSize( + st::userpicBuilderEmojiBubblePaletteSize.width(), + st::userpicBuilderEmojiSelectorMinHeight), + [=] { return controller->chatStyle(); }); + const auto selector = Ui::CreateChild( + selectorBg.get(), + controller); + selector->chosen( + ) | rpl::start_with_next([=](not_null document) { + preview->setDocument(document); + }, preview->lifetime()); + selectorBg->innerRectValue( + ) | rpl::start_with_next([=](const QRect &r) { + selector->setGeometry(r); + }, selector->lifetime()); + + base::take( + communication.triggers + ) | rpl::start_with_next([=, done = base::take(communication.result)] { + preview->result(Editor::kProfilePhotoSize, [=](QImage &&image) { + done(std::move(image)); + }); + }, preview->lifetime()); + + return container; +} + +} // namespace UserpicBuilder diff --git a/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_widget.h b/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_widget.h new file mode 100644 index 000000000..4c73f824a --- /dev/null +++ b/Telegram/SourceFiles/info/userpic/info_userpic_emoji_builder_widget.h @@ -0,0 +1,31 @@ +/* +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 + +namespace Ui { +class RpWidget; +class VerticalLayout; +} // namespace Ui + +namespace Window { +class SessionController; +} // namespace Window + +namespace UserpicBuilder { + +struct BothWayCommunication { + rpl::producer<> triggers; + Fn result; +}; + +not_null CreateUserpicBuilder( + not_null parent, + not_null controller, + BothWayCommunication communication); + +} // namespace UserpicBuilder