// This file is part of Desktop App Toolkit, // a set of libraries for developing nice desktop applications. // // For license and copyright information please follow this link: // https://github.com/desktop-app/legal/blob/master/LEGAL // #include "ui/widgets/fields/custom_field_object.h" #include "ui/effects/spoiler_mess.h" #include "ui/text/text.h" #include "ui/text/text_renderer.h" #include "ui/text/text_utilities.h" #include "ui/widgets/fields/input_field.h" #include "ui/integration.h" #include "styles/style_basic.h" #include "styles/style_widgets.h" #include #include namespace Ui { namespace { constexpr auto kSpoilerHiddenOpacity = 0.5; using SpoilerRect = InputFieldSpoilerRect; } // namespace class FieldSpoilerOverlay final : public RpWidget { public: FieldSpoilerOverlay( not_null field, Fn shown, Fn paused); private: void paintEvent(QPaintEvent *e) override; const not_null _field; const Fn _shown; const Fn _paused; SpoilerAnimation _animation; }; FieldSpoilerOverlay::FieldSpoilerOverlay( not_null field, Fn shown, Fn paused) : RpWidget(field->rawTextEdit()) , _field(field) , _shown(std::move(shown)) , _paused(std::move(paused)) , _animation([=] { update(); }) { setAttribute(Qt::WA_TransparentForMouseEvents); show(); } void FieldSpoilerOverlay::paintEvent(QPaintEvent *e) { auto p = std::optional(); auto topShift = std::optional(); auto frame = std::optional(); auto blockquoteBg = std::optional(); const auto clip = e->rect(); const auto shown = _shown(); const auto bgOpacity = shown; const auto fgOpacity = 1. * shown + kSpoilerHiddenOpacity * (1. - shown); for (const auto &rect : _field->_spoilerRects) { const auto fill = rect.geometry.intersected(clip); if (fill.isEmpty()) { continue; } else if (!p) { p.emplace(this); const auto paused = _paused && _paused(); frame.emplace( Text::DefaultSpoilerCache()->lookup( st::defaultTextPalette.spoilerFg->c)->frame( _animation.index(crl::now(), paused))); topShift = -_field->rawTextEdit()->verticalScrollBar()->value(); } if (bgOpacity > 0.) { p->setOpacity(bgOpacity); if (rect.blockquote && !blockquoteBg) { const auto bg = _field->_blockquoteBg; blockquoteBg = (bg.alphaF() < 1.) ? anim::color( _field->_st.textBg->c, QColor(bg.red(), bg.green(), bg.blue()), bg.alphaF()) : bg; } p->fillRect( fill, rect.blockquote ? *blockquoteBg : _field->_st.textBg->c); } p->setOpacity(fgOpacity); const auto shift = QPoint(0, *topShift) - rect.geometry.topLeft(); FillSpoilerRect(*p, rect.geometry, *frame, shift); } } CustomFieldObject::CustomFieldObject( not_null field, Text::MarkedContext context, Fn pausedEmoji, Fn pausedSpoiler) : _field(field) , _context(std::move(context)) , _pausedEmoji(std::move(pausedEmoji)) , _pausedSpoiler(std::move(pausedSpoiler)) , _factory(makeFactory()) , _now(crl::now()) { } CustomFieldObject::~CustomFieldObject() = default; void *CustomFieldObject::qt_metacast(const char *iid) { if (QLatin1String(iid) == qobject_interface_iid()) { return static_cast(this); } return QObject::qt_metacast(iid); } QSizeF CustomFieldObject::intrinsicSize( QTextDocument *doc, int posInDocument, const QTextFormat &format) { const auto line = _field->_st.style.font->height; if (format.objectType() == InputField::kCollapsedQuoteFormat) { const auto &padding = _field->_st.style.blockquote.padding; const auto paddings = padding.left() + padding.right(); const auto skip = 2 * doc->documentMargin(); const auto height = Text::kQuoteCollapsedLines * line; return QSizeF(doc->pageSize().width() - paddings - skip, height); } const auto size = st::emojiSize * 1.; const auto width = size + st::emojiPadding * 2.; const auto height = std::max(line * 1., size); if (!_skip) { const auto emoji = Text::AdjustCustomEmojiSize(st::emojiSize); _skip = (st::emojiSize - emoji) / 2; } return { width, height }; } void CustomFieldObject::drawObject( QPainter *painter, const QRectF &rect, QTextDocument *doc, int posInDocument, const QTextFormat &format) { if (format.objectType() == InputField::kCollapsedQuoteFormat) { const auto left = 0; const auto top = 0; const auto id = format.property(InputField::kQuoteId).toInt(); if (const auto i = _quotes.find(id); i != end(_quotes)) { i->second.string.draw(*painter, { .position = QPoint(left + rect.x(), top + rect.y()), .outerWidth = int(base::SafeRound(doc->pageSize().width())), .availableWidth = int(std::floor(rect.width())), .palette = nullptr, .spoiler = Text::DefaultSpoilerCache(), .now = _now, .pausedEmoji = _pausedEmoji(), .pausedSpoiler = _pausedSpoiler(), .elisionLines = Text::kQuoteCollapsedLines, .useFullWidth = true, }); } return; } const auto id = format.property(InputField::kCustomEmojiId).toULongLong(); if (!id) { return; } auto i = _emoji.find(id); if (i == end(_emoji)) { const auto link = format.property(InputField::kCustomEmojiLink); const auto data = InputField::CustomEmojiEntityData(link.toString()); if (auto emoji = _factory(data)) { i = _emoji.emplace(id, std::move(emoji)).first; } } if (i == end(_emoji)) { return; } i->second->paint(*painter, { .textColor = format.foreground().color(), .now = _now, .position = QPoint( int(base::SafeRound(rect.x())) + st::emojiPadding + _skip, int(base::SafeRound(rect.y())) + _skip), .paused = _pausedEmoji && _pausedEmoji(), }); } void CustomFieldObject::clearEmoji() { _emoji.clear(); } void CustomFieldObject::clearQuotes() { _quotes.clear(); } std::unique_ptr CustomFieldObject::createSpoilerOverlay() { return std::make_unique( _field, [=] { return _spoilerOpacity.value(_spoilerHidden ? 0. : 1.); }, _pausedSpoiler); } void CustomFieldObject::refreshSpoilerShown(InputFieldTextRange range) { auto hidden = false; using Range = InputFieldTextRange; const auto intersects = [](Range a, Range b) { return (a.from < b.till) && (b.from < a.till); }; if (range.till > range.from) { const auto check = [&](const std::vector &list) { if (!hidden) { for (const auto &spoiler : list) { if (intersects(spoiler, range)) { hidden = true; break; } } } }; check(_field->_spoilerRangesText); check(_field->_spoilerRangesEmoji); } else { auto touchesLeft = false; auto touchesRight = false; const auto cursor = range.from; const auto check = [&](const std::vector &list) { if (!touchesLeft || !touchesRight) { for (const auto &spoiler : list) { if (spoiler.from <= cursor && spoiler.till >= cursor) { if (spoiler.from < cursor) { touchesLeft = true; } if (spoiler.till >= cursor) { touchesRight = true; } if (touchesLeft && touchesRight) { break; } } } } }; check(_field->_spoilerRangesText); check(_field->_spoilerRangesEmoji); hidden = touchesLeft && touchesRight; } if (_spoilerHidden != hidden) { _spoilerHidden = hidden; _spoilerOpacity.start( [=] { _field->update(); }, hidden ? 1. : 0., hidden ? 0. : 1., st::fadeWrapDuration); } } void CustomFieldObject::setCollapsedText(int quoteId, TextWithTags text) { auto "e = _quotes[quoteId]; quote.string = Text::String(_field->_st.widthMin); quote.string.setMarkedText(_field->_st.style, { text.text, TextUtilities::ConvertTextTagsToEntities(text.tags), }, kMarkupTextOptions, makeFieldContext()); quote.text = std::move(text); } const TextWithTags &CustomFieldObject::collapsedText(int quoteId) const { if (const auto i = _quotes.find(quoteId); i != end(_quotes)) { return i->second.text; } static const auto kEmpty = TextWithTags(); return kEmpty; } Text::MarkedContext CustomFieldObject::makeFieldContext() { auto context = _context; context.repaint = [field = _field] { field->update(); }; return context; } CustomFieldObject::Factory CustomFieldObject::makeFactory() { return [context = makeFieldContext()](QStringView data) { return Text::MakeCustomEmoji(data, context); }; } void CustomFieldObject::setNow(crl::time now) { _now = now; } } // namespace Ui