/* 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 "api/api_peer_photo.h" #include "apiwrap.h" #include "chat_helpers/emoji_list_widget.h" #include "chat_helpers/stickers_list_widget.h" #include "data/data_document.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 "info/userpic/info_userpic_bubble_wrap.h" #include "info/userpic/info_userpic_color_circle_button.h" #include "info/userpic/info_userpic_colors_editor.h" #include "info/userpic/info_userpic_emoji_builder_common.h" #include "info/userpic/info_userpic_emoji_builder_preview.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/controls/emoji_button.h" #include "ui/empty_userpic.h" #include "ui/layers/generic_box.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.h" #include "styles/style_chat_helpers.h" #include "styles/style_info_userpic_builder.h" #include "styles/style_layers.h" #include "styles/style_settings.h" #include "styles/style_menu_icons.h" namespace UserpicBuilder { namespace { void AlignChildren(not_null widget, int fullWidth) { const auto children = widget->children(); const auto widgets = ranges::views::all( children ) | ranges::views::filter([](not_null object) { return object->isWidgetType(); }) | ranges::views::transform([](not_null object) { return static_cast(object.get()); }) | ranges::to_vector; const auto widgetWidth = widgets.front()->width(); const auto widgetsCount = widgets.size(); const auto widgetsWidth = widgetWidth * widgetsCount; const auto step = (fullWidth - widgetsWidth) / (widgetsCount - 1); for (auto i = 0; i < widgetsCount; i++) { widgets[i]->move(i * (widgetWidth + step), widgets[i]->y()); } } [[nodiscard]] QImage GenerateSpecial( int size, const std::vector colors) { if (colors.empty()) { auto image = QImage( Size(size * style::DevicePixelRatio()), QImage::Format_ARGB32_Premultiplied); image.setDevicePixelRatio(style::DevicePixelRatio()); image.fill(Qt::transparent); { auto p = QPainter(&image); st::userpicBuilderEmojiColorPlus.icon.paintInCenter( p, Rect(Size(size))); } return image; } else { auto image = GenerateGradient(Size(size), colors); { auto p = QPainter(&image); constexpr auto kEllipseSize = 1; const auto center = QPointF(size / 2., size / 2.); const auto shift = QPointF(kEllipseSize * 4, 0); p.setPen(Qt::NoPen); p.setBrush(st::boxBg); p.drawEllipse(center, kEllipseSize, kEllipseSize); p.drawEllipse(center + shift, kEllipseSize, kEllipseSize); p.drawEllipse(center - shift, kEllipseSize, kEllipseSize); } return image; } } [[nodiscard]] std::vector> PaletteGradients() { auto v = std::vector>{ { QColor(32, 226, 205), QColor(14, 225, 241), QColor(77, 141, 255), QColor(43, 191, 255), }, { QColor(69, 247, 183), QColor(31, 241, 217), QColor(94, 182, 251), QColor(31, 206, 235), }, { QColor(193, 229, 38), QColor(128, 223, 43), QColor(9, 210, 96), QColor(94, 220, 64), }, { QColor(255, 212, 18), QColor(255, 167, 67), QColor(245, 105, 78), QColor(245, 119, 44), }, { QColor(246, 167, 48), QColor(255, 119, 66), QColor(246, 72, 132), QColor(239, 91, 65), }, { QColor(255, 178, 58), QColor(254, 126, 98), QColor(249, 75, 160), QColor(251, 92, 128), }, { QColor(255, 114, 169), QColor(226, 105, 255), QColor(131, 124, 255), QColor(176, 99, 255), }, }; for (auto &g : v) { // Rotate 180 degrees. std::swap(g[0], g[2]); std::swap(g[1], g[3]); } return v; } void ShowGradientEditor( not_null controller, StartData data, Fn)> &&doneCallback) { controller->show(Box([=](not_null box) { struct State { rpl::event_stream<> saveRequests; }; const auto state = box->lifetime().make_state(); box->setTitle(tr::lng_chat_theme_change()); box->addButton(tr::lng_settings_save(), [=] { state->saveRequests.fire({}); }); box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); auto content = CreateGradientEditor( box, (data.documentId ? controller->session().data().document( data.documentId).get() : nullptr), data.gradientEditorColors, BothWayCommunication>{ state->saveRequests.events(), [=](std::vector colors) { box->closeBox(); doneCallback(std::move(colors)); }, }); box->setWidth(content->width()); box->addRow(std::move(content), {}); })); } class EmojiSelector final : public Ui::RpWidget { public: EmojiSelector( not_null parent, not_null controller, rpl::producer> recent); [[nodiscard]] rpl::producer> chosen() const; private: using Footer = ChatHelpers::TabbedSelector::InnerFooter; using List = ChatHelpers::TabbedSelector::Inner; using Type = ChatHelpers::SelectorTab; void createSelector(Type type); struct Selector { not_null list; not_null footer; }; [[nodiscard]] Selector createEmojiList( not_null scroll); [[nodiscard]] Selector createStickersList( not_null scroll) const; const not_null _controller; base::unique_qptr _container; rpl::event_stream<> _recentChanges; std::vector _lastRecent; rpl::event_stream> _chosen; }; EmojiSelector::EmojiSelector( not_null parent, not_null controller, rpl::producer> recent) : RpWidget(parent) , _controller(controller) { std::move( recent ) | rpl::start_with_next([=](std::vector ids) { _lastRecent = std::move(ids); _recentChanges.fire({}); }, lifetime()); createSelector(Type::Emoji); } rpl::producer> EmojiSelector::chosen() const { return _chosen.events(); } EmojiSelector::Selector EmojiSelector::createEmojiList( not_null scroll) { const auto session = &_controller->session(); const auto manager = &session->data().customEmojiManager(); const auto tag = Data::CustomEmojiManager::SizeTag::Large; auto args = ChatHelpers::EmojiListDescriptor{ .show = _controller->uiShow(), .mode = ChatHelpers::EmojiListMode::UserpicBuilder, .paused = [=] { return true; }, .customRecentList = _lastRecent, .customRecentFactory = [=](DocumentId id, Fn repaint) { return manager->create(id, std::move(repaint), tag); }, .st = &st::userpicBuilderEmojiPan, }; const auto list = scroll->setOwnedWidget( object_ptr(scroll, std::move(args))); const auto footer = list->createFooter().data(); list->refreshEmoji(); list->customChosen( ) | rpl::start_with_next([=](const ChatHelpers::FileChosen &chosen) { _chosen.fire_copy(chosen.document); }, list->lifetime()); _recentChanges.events( ) | rpl::start_with_next([=] { createSelector(Type::Emoji); }, list->lifetime()); list->setAllowWithoutPremium(true); return { list, footer }; } EmojiSelector::Selector EmojiSelector::createStickersList( not_null scroll) const { const auto list = scroll->setOwnedWidget( object_ptr( scroll, _controller, Window::GifPauseReason::Any, ChatHelpers::StickersListMode::UserpicBuilder)); const auto footer = list->createFooter().data(); list->refreshRecent(); list->chosen( ) | rpl::start_with_next([=](const ChatHelpers::FileChosen &chosen) { _chosen.fire_copy(chosen.document); }, list->lifetime()); return { list, footer }; } void EmojiSelector::createSelector(Type type) { Expects((type == Type::Emoji) || (type == Type::Stickers)); const auto isEmoji = (type == Type::Emoji); const auto &stScroll = st::reactPanelScroll; _container = base::make_unique_q(this); const auto container = _container.get(); container->show(); sizeValue( ) | rpl::start_with_next([=](const QSize &s) { container->setGeometry(Rect(s)); }, container->lifetime()); const auto scroll = Ui::CreateChild(container, stScroll); const auto selector = isEmoji ? createEmojiList(scroll) : createStickersList(scroll); selector.footer->setParent(container); const auto toggleButton = Ui::CreateChild(container); const auto &togglePos = st::userpicBuilderEmojiSelectorTogglePosition; { const auto &pos = togglePos; toggleButton->resize(st::menuIconStickers.size() // Trying to overlap the settings button under. + QSize(pos.x() * 2, pos.y() * 2)); toggleButton->show(); toggleButton->paintRequest( ) | rpl::start_with_next([=] { auto p = QPainter(toggleButton); const auto r = toggleButton->rect() - QMargins(pos.x(), pos.y(), pos.x(), pos.y()); p.fillRect(r, st::boxBg); if (isEmoji) { st::userpicBuilderEmojiToggleStickersIcon.paintInCenter(p, r); } else { st::defaultEmojiPan.icons.people.paintInCenter(p, r); } }, toggleButton->lifetime()); } toggleButton->show(); toggleButton->setClickedCallback([=] { createSelector(isEmoji ? Type::Stickers : Type::Emoji); }); rpl::combine( scroll->scrollTopValue(), scroll->heightValue() ) | rpl::start_with_next([=](int scrollTop, int scrollHeight) { const auto scrollBottom = scrollTop + scrollHeight; selector.list->setVisibleTopBottom(scrollTop, scrollBottom); }, selector.list->lifetime()); selector.list->scrollToRequests( ) | rpl::start_with_next([=](int y) { scroll->scrollToY(y); // _shadow->update(); }, selector.list->lifetime()); const auto separator = Ui::CreateChild(container); separator->paintRequest( ) | rpl::start_with_next([=](const QRect &r) { auto p = QPainter(separator); p.fillRect(r, st::shadowFg); }, separator->lifetime()); selector.footer->show(); separator->show(); scroll->show(); const auto scrollWidth = stScroll.width; sizeValue( ) | rpl::start_with_next([=](const QSize &s) { const auto left = st::userpicBuilderEmojiSelectorLeft; const auto mostTop = st::userpicBuilderEmojiSelectorLeft; toggleButton->move(QPoint(left, mostTop)); selector.footer->setGeometry( (isEmoji ? (rect::right(toggleButton) - togglePos.x()) : left), mostTop, s.width() - left, selector.footer->height()); separator->setGeometry( 0, rect::bottom(selector.footer), s.width(), st::lineWidth); const auto listWidth = s.width() - st::boxRadius * 2; selector.list->resizeToWidth(listWidth); scroll->setGeometry( st::boxRadius, rect::bottom(separator), selector.list->width() + scrollWidth, s.height() - rect::bottom(separator)); selector.list->setMinimalHeight(listWidth, scroll->height()); }, lifetime()); // Reset all animations. selector.list->hideFinished(); } } // namespace not_null CreateUserpicBuilder( not_null parent, not_null controller, StartData data, BothWayCommunication communication) { const auto container = Ui::CreateChild(parent.get()); struct State { std::vector> circleButtons; Ui::Animations::Simple chosenColorAnimation; int colorIndex = -1; std::vector editorColors; StartData gradientEditorStartData; }; const auto state = container->lifetime().make_state(); const auto preview = container->add( object_ptr>( container, object_ptr( container, Size(st::settingsInfoPhotoSize), data.isForum)), st::userpicBuilderEmojiPreviewPadding)->entity(); if (const auto id = data.documentId) { const auto document = controller->session().data().document(id); if (document && document->sticker()) { preview->setDocument(document); } } container->add( object_ptr>( container, object_ptr( container, tr::lng_userpic_builder_color_subtitle(), st::userpicBuilderEmojiSubtitle)), st::userpicBuilderEmojiSubtitlePadding); const auto paletteBg = Ui::AddBubbleWrap( container, QSize( st::userpicBuilderEmojiBubblePaletteWidth, std::abs(Ui::BubbleWrapInnerRect(QRect(0, 0, 0, 0)).height()) + st::userpicBuilderEmojiAccentColorSize + rect::m::sum::v( st::userpicBuilderEmojiBubblePalettePadding))); const auto palette = Ui::CreateChild(paletteBg.get()); { constexpr auto kColorsCount = int(7); const auto checkIsSpecial = [=](int i) { return (i == kColorsCount); }; const auto size = st::userpicBuilderEmojiAccentColorSize; const auto paletteGradients = PaletteGradients(); for (auto i = 0; i < kColorsCount + 1; i++) { const auto isSpecial = checkIsSpecial(i); const auto colors = paletteGradients[i % kColorsCount]; const auto button = Ui::CreateChild(palette); state->circleButtons.push_back(button); button->resize(size, size); button->setBrush(isSpecial ? GenerateSpecial(size, state->editorColors) : GenerateGradient(Size(size), colors)); const auto openEditor = isSpecial ? Fn([=] { if (checkIsSpecial(state->colorIndex)) { state->colorIndex = -1; } ShowGradientEditor( controller, state->gradientEditorStartData, [=](std::vector colors) { state->editorColors = std::move(colors); button->setBrush( GenerateSpecial(size, state->editorColors)); button->clicked({}, Qt::LeftButton); }); }) : nullptr; button->setClickedCallback([=] { if (openEditor && state->editorColors.empty()) { return openEditor(); } const auto was = state->colorIndex; const auto now = i; if (was == now) { if (openEditor) { openEditor(); } return; } state->chosenColorAnimation.stop(); state->chosenColorAnimation.start([=](float64 progress) { if (was >= 0) { state->circleButtons[was]->setSelectedProgress( 1. - progress); } state->circleButtons[now]->setSelectedProgress(progress); }, 0., 1., st::universalDuration); state->colorIndex = now; const auto result = isSpecial ? state->editorColors : colors; state->gradientEditorStartData.gradientEditorColors = result; preview->setGradientColors(result); }); } const auto current = data.builderColorIndex % kColorsCount; state->circleButtons[current]->setSelectedProgress(1.); state->circleButtons[current]->clicked({}, Qt::LeftButton); } paletteBg->sizeValue( ) | rpl::start_with_next([=](const QSize &s) { palette->setGeometry(Ui::BubbleWrapInnerRect(Rect(s)) - st::userpicBuilderEmojiBubblePalettePadding); AlignChildren(palette, palette->width()); }, 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::userpicBuilderEmojiBubblePaletteWidth, st::userpicBuilderEmojiSelectorMinHeight)); const auto selector = Ui::CreateChild( selectorBg.get(), controller, base::take(data.documents)); selector->chosen( ) | rpl::start_with_next([=](not_null document) { state->gradientEditorStartData.documentId = document->id; preview->setDocument(document); }, preview->lifetime()); selectorBg->sizeValue( ) | rpl::start_with_next([=](const QSize &s) { selector->setGeometry(Ui::BubbleWrapInnerRect(Rect(s))); }, selector->lifetime()); base::take( communication.triggers ) | rpl::start_with_next([=, done = base::take(communication.result)] { preview->result(Editor::kProfilePhotoSize, [=](Result result) { done(std::move(result)); }); }, preview->lifetime()); return container; } not_null CreateEmojiUserpic( not_null parent, const QSize &size, rpl::producer> document, rpl::producer colorIndex, bool isForum) { const auto paletteGradients = PaletteGradients(); const auto widget = Ui::CreateChild( parent.get(), size, isForum); std::move( document ) | rpl::start_with_next([=](not_null d) { widget->setDocument(d); }, widget->lifetime()); std::move( colorIndex ) | rpl::start_with_next([=](int index) { widget->setGradientColors( paletteGradients[index % paletteGradients.size()]); }, widget->lifetime()); return widget; } } // namespace UserpicBuilder