/* 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 "iv/markdown/iv_markdown_article_text.h" #include "iv/markdown/iv_markdown_article_layout_blocks.h" #include "iv/markdown/iv_markdown_prepare_links.h" #include "iv/markdown/iv_markdown_prepare_serialize.h" #include "lang/lang_keys.h" #include "ui/style/style_core.h" #include "ui/style/style_core_scale.h" #include "ui/text/text_custom_emoji.h" #include "ui/basic_click_handlers.h" #include "ui/dynamic_image.h" #include "ui/integration.h" #include "styles/palette.h" #include "styles/style_iv.h" #include #include #include #include #include namespace Iv::Markdown { namespace { constexpr auto kIvMarkedTextOptions = TextParseOptions{ TextParseMultiline, 0, 0, Qt::LayoutDirectionAuto, }; struct PreparedLinkExternalData { ClickHandler::TextEntity entity; QString copyText; QString copyLabel; }; [[nodiscard]] QString OpenableTargetForLink(const PreparedLink &link) { return link.fragment.isEmpty() ? link.target : (link.target + u"#"_q + link.fragment); } [[nodiscard]] std::optional ExternalDataForLink( const PreparedLink &link) { if (link.kind != PreparedLinkKind::External && link.kind != PreparedLinkKind::InstantViewPage) { return std::nullopt; } const auto target = OpenableTargetForLink(link); auto type = link.entityType; if (type != EntityType::Url && type != EntityType::CustomUrl && type != EntityType::Email) { if (target.isEmpty()) { return std::nullopt; } type = UrlClickHandler::IsEmail(target) ? EntityType::Email : EntityType::CustomUrl; } return PreparedLinkExternalData{ .entity = { type, target }, .copyText = link.fragment.isEmpty() && !link.copyText.isEmpty() ? link.copyText : target, .copyLabel = (type == EntityType::Email) ? Ui::Integration::Instance().phraseContextCopyEmail() : Ui::Integration::Instance().phraseContextCopyLink(), }; } [[nodiscard]] ClickHandler::TextEntity TextEntityForLink( const PreparedLink &link) { if (const auto external = ExternalDataForLink(link)) { return external->entity; } return {}; } [[nodiscard]] QString CopyTextForLink(const PreparedLink &link) { if (const auto external = ExternalDataForLink(link)) { return external->copyText; } switch (link.kind) { case PreparedLinkKind::Anchor: case PreparedLinkKind::FootnoteBacklink: return link.target.isEmpty() ? QString() : (u"#"_q + link.target); case PreparedLinkKind::Footnote: return !link.copyText.isEmpty() ? link.copyText : link.target; case PreparedLinkKind::LocalFile: return link.fragment.isEmpty() ? link.target : (link.target + u"#"_q + link.fragment); case PreparedLinkKind::External: case PreparedLinkKind::InstantViewPage: return link.target; case PreparedLinkKind::RejectedRelative: case PreparedLinkKind::ToggleDetails: return QString(); } return QString(); } [[nodiscard]] QString CopyLabelForLink(const PreparedLink &link) { if (const auto external = ExternalDataForLink(link)) { return external->copyLabel; } switch (link.kind) { case PreparedLinkKind::RejectedRelative: case PreparedLinkKind::ToggleDetails: return QString(); case PreparedLinkKind::External: case PreparedLinkKind::InstantViewPage: case PreparedLinkKind::Anchor: case PreparedLinkKind::Footnote: case PreparedLinkKind::FootnoteBacklink: case PreparedLinkKind::LocalFile: return tr::lng_context_copy_link(tr::now); } return QString(); } class PreparedLinkClickHandler final : public ClickHandler { public: explicit PreparedLinkClickHandler(PreparedLink link); void onClick(ClickContext) const override; [[nodiscard]] const PreparedLink &link() const; QString url() const override; QString copyToClipboardText() const override; QString copyToClipboardContextItemText() const override; TextEntity getTextEntity() const override; QString tooltip() const override; private: PreparedLink _link; }; PreparedLinkClickHandler::PreparedLinkClickHandler(PreparedLink link) : _link(std::move(link)) { } void PreparedLinkClickHandler::onClick(ClickContext context) const { if (context.button != Qt::LeftButton && context.button != Qt::MiddleButton) { return; } const auto data = ExternalEntityLinkData(_link); if (!data) { return; } if (const auto handler = Ui::Integration::Instance().createLinkHandler( *data, Ui::Text::MarkedContext())) { handler->onClick(std::move(context)); } } const PreparedLink &PreparedLinkClickHandler::link() const { return _link; } QString PreparedLinkClickHandler::url() const { return _link.target; } QString PreparedLinkClickHandler::copyToClipboardText() const { return CopyTextForLink(_link); } QString PreparedLinkClickHandler::copyToClipboardContextItemText() const { return copyToClipboardText().isEmpty() ? QString() : CopyLabelForLink(_link); } ClickHandler::TextEntity PreparedLinkClickHandler::getTextEntity() const { return TextEntityForLink(_link); } QString PreparedLinkClickHandler::tooltip() const { return TooltipForPreparedLink(_link); } [[nodiscard]] int FormulaTextSize(const style::TextStyle &textStyle) { return std::max(textStyle.font->height, 1); } [[nodiscard]] int ScaleFormulaCap(int cap, int textSize, int baseTextSize) { if (cap <= 0) { return 0; } const auto numerator = int64(cap) * std::max(textSize, 1); const auto denominator = std::max(baseTextSize, 1); return std::max(int((numerator + denominator - 1) / denominator), 1); } [[nodiscard]] PreparedFormulaMeasurementSignature FormulaRenderSignature( const PreparedFormulaSlot &slot, const style::Markdown &st) { const auto &displayMath = st.displayMath; return { .trimmedTex = slot.trimmedTex.trimmed(), .kind = slot.kind, .textSize = slot.textSize ? slot.textSize : displayMath.textSize, .renderWidthCap = slot.renderWidthCap ? slot.renderWidthCap : displayMath.maxRenderWidth, .renderHeightCap = slot.renderHeightCap ? slot.renderHeightCap : displayMath.maxRenderHeight, }; } [[nodiscard]] PreparedFormulaMeasurementSignature InlineFormulaSignature( QString trimmedTex, const style::TextStyle &textStyle, const style::Markdown &st) { const auto &displayMath = st.displayMath; const auto textSize = FormulaTextSize(textStyle); return { .trimmedTex = std::move(trimmedTex).trimmed(), .kind = MathKind::Inline, .textSize = textSize, .renderWidthCap = ScaleFormulaCap( displayMath.maxRenderWidth, textSize, displayMath.textSize), .renderHeightCap = ScaleFormulaCap( displayMath.maxRenderHeight, textSize, displayMath.textSize), }; } template void NormalizeInlineFormulaRasterMetrics(Formula *formula); [[nodiscard]] RenderedFormula MeasuredFallback(const MeasuredFormula &measured) { auto result = RenderedFormula(); result.logicalSize = measured.logicalSize; result.logicalDepth = measured.logicalDepth; result.exact = measured.exact; result.fallbackText = measured.fallbackText; result.error = measured.error; result.success = false; result.overflow = measured.overflow; result.tooLarge = measured.tooLarge; NormalizeInlineFormulaRasterMetrics(&result); return result; } [[nodiscard]] int RenderFormulaDevicePixelRatio(const RenderedFormula &formula) { const auto ratio = formula.image.devicePixelRatio(); return (ratio > 0.) ? int(std::round(ratio)) : 0; } [[nodiscard]] int RoundedInlineFormulaMetric(int scaledValue) { return (scaledValue > 0) ? ((scaledValue + kFormulaExactMetricScale - 1) / kFormulaExactMetricScale) : 0; } [[nodiscard]] int FlooredInlineFormulaMetric(int scaledValue) { return (scaledValue >= 0) ? (scaledValue / kFormulaExactMetricScale) : -((-scaledValue + kFormulaExactMetricScale - 1) / kFormulaExactMetricScale); } [[nodiscard]] qreal LogicalInlineFormulaMetric(int scaledValue) { return qreal(scaledValue) / qreal(kFormulaExactMetricScale); } struct InlineFormulaColorizedKey { QRgb color = 0; int devicePixelRatio = 0; friend bool operator<( InlineFormulaColorizedKey a, InlineFormulaColorizedKey b); }; bool operator<( InlineFormulaColorizedKey a, InlineFormulaColorizedKey b) { if (a.color != b.color) { return a.color < b.color; } return a.devicePixelRatio < b.devicePixelRatio; } class InlineFormulaSharedState final { public: InlineFormulaSharedState( PreparedFormulaMeasurementSignature signature, std::shared_ptr measuredData, QString displayFallbackText, std::shared_ptr renderer); [[nodiscard]] int width() const; [[nodiscard]] bool failed() const; [[nodiscard]] std::optional vertical( const style::TextStyle &textStyle) const; void paint( QPainter &p, const Ui::Text::CustomEmoji::Context &context, const QString &replacementText, int fallbackWidth) const; void setRenderer(std::shared_ptr renderer); void invalidatePaletteCache(); void invalidateRasterCache(); private: [[nodiscard]] const MeasuredFormula &measured() const; [[nodiscard]] MathRenderer *renderer() const; [[nodiscard]] RenderedFormula ensureRendered(int devicePixelRatio) const; [[nodiscard]] const QImage *colorizedImage( const QColor &color, int devicePixelRatio) const; PreparedFormulaMeasurementSignature _signature; std::shared_ptr _measuredData; QString _displayFallbackText; mutable std::shared_ptr _renderer; mutable std::map _rendered; mutable std::map _colorized; }; struct InlineFormulaGeometry { int width = 1; int imageHeight = 0; int imageDescent = 0; int ascent = 0; int descent = 0; int paintOffsetYScaled = 0; }; template [[nodiscard]] InlineFormulaGeometry InlineFormulaGeometryFrom( const Formula &formula) { const auto imageHeight = std::max(formula.logicalSize.height(), 0); const auto imageDescent = std::clamp(formula.logicalDepth, 0, imageHeight); auto result = InlineFormulaGeometry{ .width = std::max(formula.logicalSize.width(), 1), .imageHeight = imageHeight, .imageDescent = imageDescent, .ascent = imageHeight - imageDescent, .descent = imageDescent, }; const auto &exact = formula.exact; const auto exactHeight = exact.scaledSize.height(); if ((exact.scaledSize.width() <= 0) || (exactHeight <= 0)) { return result; } const auto exactAscent = std::clamp(exact.scaledAscent, 0, exactHeight); const auto exactDescent = std::max(exactHeight - exactAscent, 0); const auto topInset = std::clamp(exact.scaledInsets.top(), 0, exactAscent); const auto bottomInset = std::clamp( exact.scaledInsets.bottom(), 0, exactDescent); const auto paintTop = topInset - exactAscent; const auto paintBottom = std::max(exactDescent - bottomInset, 0); const auto imageWidth = RoundedInlineFormulaMetric( exact.scaledSize.width()); const auto exactImageHeight = RoundedInlineFormulaMetric(exactHeight); const auto exactImageAscent = std::clamp( RoundedInlineFormulaMetric(exactAscent), 0, exactImageHeight); const auto top = FlooredInlineFormulaMetric(paintTop); const auto bottom = RoundedInlineFormulaMetric(paintBottom); result.width = std::max(imageWidth, 1); result.imageHeight = exactImageHeight; result.imageDescent = std::max(exactImageHeight - exactImageAscent, 0); if (top < 0 || bottom > 0) { result.ascent = std::max(-top, 0); result.descent = std::max(bottom, 0); result.paintOffsetYScaled = (-exactAscent) - (top * kFormulaExactMetricScale); } else if (exactImageHeight > 0) { result.ascent = exactImageHeight - result.imageDescent; result.descent = result.imageDescent; } return result; } template void NormalizeInlineFormulaRasterMetrics(Formula *formula) { if (!formula) { return; } const auto geometry = InlineFormulaGeometryFrom(*formula); formula->logicalSize = QSize(geometry.width, geometry.imageHeight); formula->logicalDepth = geometry.imageDescent; } class InlineFormulaObject final : public Ui::Text::CustomEmoji { public: InlineFormulaObject( QString entityData, QString replacementText, int fallbackWidth, std::shared_ptr state); int width() override; QString entityData() override; std::optional vertical( const style::TextStyle &textStyle) override; QString replacementText() override; Ui::Text::CustomEmojiSemantics semantics() override; void paint(QPainter &p, const Context &context) override; void unload() override; bool ready() override; bool readyInDefaultState() override; private: QString _entityData; QString _replacementText; int _fallbackWidth = 1; const std::shared_ptr _state; }; class InlineIvImageObject final : public Ui::Text::CustomEmoji { public: InlineIvImageObject( QString replacementText, int width, int height, std::shared_ptr image, Fn repaint, Fn repaintRect); int width() override; QString entityData() override; std::optional vertical( const style::TextStyle &textStyle) override; QString replacementText() override; Ui::Text::CustomEmojiSemantics semantics() override; void paint(QPainter &p, const Context &context) override; void unload() override; bool ready() override; bool readyInDefaultState() override; private: QString _replacementText; int _width = 1; int _height = 1; const std::shared_ptr _image; const Fn _repaint; const Fn _repaintRect; const std::shared_ptr _lastPaintRect = std::make_shared(); bool _subscribed = false; }; [[nodiscard]] QString InlineFormulaDisplayFallbackText( const PreparedFormulaMeasurementSignature &signature, const MeasuredFormula &measured) { if (!measured.fallbackText.isEmpty()) { return measured.fallbackText; } else if (!signature.trimmedTex.isEmpty()) { return signature.trimmedTex; } return u"[math]"_q; } [[nodiscard]] std::shared_ptr FindInlineFormulaMeasuredData( const std::vector *formulas, const PreparedFormulaMeasurementSignature &signature, const style::Markdown &st, MeasuredFormula *measured) { if (!formulas) { return nullptr; } for (const auto &slot : *formulas) { if (!slot.present || (FormulaRenderSignature(slot, st) != signature)) { continue; } if (measured) { *measured = slot.measured; } if (slot.measuredData) { return slot.measuredData; } return std::make_shared(slot.measured); } return nullptr; } } // namespace ClickHandlerPtr CreatePreparedLinkHandler(PreparedLink link) { return std::make_shared(std::move(link)); } std::optional ExtractPreparedLink(const ClickHandlerPtr &link) { if (const auto prepared = std::dynamic_pointer_cast( link)) { return prepared->link(); } return std::nullopt; } void BindLinks( Ui::Text::String *leaf, const std::vector &links) { for (const auto &link : links) { leaf->setLink( link.index, CreatePreparedLinkHandler(link)); } } void SetTextLeafSpoilerLinkFilter( Ui::Text::String *leaf, Fn spoilerLinkFilter) { if (!leaf->hasSpoilers()) { return; } if (spoilerLinkFilter) { leaf->setSpoilerLinkFilter(std::move(spoilerLinkFilter)); } else { leaf->setSpoilerLinkFilter([](const ClickContext &context) { return context.button == Qt::LeftButton; }); } } const PreparedFormulaSlot *PreparedFormulaFor( const std::vector &formulas, int formulaIndex) { if (formulaIndex < 0 || formulaIndex >= int(formulas.size())) { return nullptr; } else if (!formulas[formulaIndex].present) { return nullptr; } return &formulas[formulaIndex]; } PreparedFormulaSlot *PreparedFormulaFor( std::vector *formulas, int formulaIndex) { if (!formulas || formulaIndex < 0 || formulaIndex >= int(formulas->size())) { return nullptr; } else if (!(*formulas)[formulaIndex].present) { return nullptr; } return &(*formulas)[formulaIndex]; } RenderedFormula *FormulaRasterSlot( std::vector *rendered, int formulaIndex) { if (!rendered || formulaIndex < 0) { return nullptr; } if (formulaIndex >= int(rendered->size())) { rendered->resize(formulaIndex + 1); } return &(*rendered)[formulaIndex]; } RenderedFormula EnsureFormulaRendered( const PreparedFormulaMeasurementSignature &signature, const MeasuredFormula &measured, RenderedFormula *rendered, MathRenderer *renderer, int devicePixelRatio) { if (!measured.success) { return MeasuredFallback(measured); } if (rendered && rendered->success && (RenderFormulaDevicePixelRatio(*rendered) == devicePixelRatio)) { return *rendered; } auto ownedRenderer = std::shared_ptr(); if (!renderer) { ownedRenderer = std::make_shared(); renderer = ownedRenderer.get(); } auto local = renderer->renderFormula({ .trimmedTex = signature.trimmedTex, .kind = signature.kind, .textSize = signature.textSize, .renderWidthCap = signature.renderWidthCap, .renderHeightCap = signature.renderHeightCap, .devicePixelRatio = devicePixelRatio, }); if (local.logicalSize.isEmpty()) { local.logicalSize = measured.logicalSize; local.logicalDepth = measured.logicalDepth; local.exact = measured.exact; local.fallbackText = measured.fallbackText; local.error = measured.error; local.overflow = measured.overflow; local.tooLarge = measured.tooLarge; NormalizeInlineFormulaRasterMetrics(&local); } if (rendered) { *rendered = std::move(local); return rendered->success ? *rendered : MeasuredFallback(measured); } return local.success ? local : MeasuredFallback(measured); } RenderedFormula EnsureFormulaRendered( const PreparedFormulaSlot *slot, RenderedFormula *rendered, MathRenderer *renderer, int devicePixelRatio, const style::Markdown &st) { if (!slot) { return RenderedFormula(); } return EnsureFormulaRendered( FormulaRenderSignature(*slot, st), slot->measured, rendered, renderer, devicePixelRatio); } class InlineFormulaObjectCache final { public: InlineFormulaObjectCache() = default; void setRenderer(std::shared_ptr renderer); void clear(); void invalidatePaletteCache(); void invalidateRasterCache(); [[nodiscard]] std::unique_ptr create( const InlineTextObjectFormulaData &data, const style::TextStyle &textStyle, const style::Markdown &st, const std::vector *formulas); private: [[nodiscard]] std::shared_ptr lookupOrCreate( const PreparedFormulaMeasurementSignature &signature, const style::TextStyle &textStyle, const style::Markdown &st, const std::vector *formulas); std::shared_ptr _renderer; std::map< PreparedFormulaMeasurementSignature, std::shared_ptr, PreparedFormulaMeasurementSignatureLess> _states; }; InlineFormulaSharedState::InlineFormulaSharedState( PreparedFormulaMeasurementSignature signature, std::shared_ptr measuredData, QString displayFallbackText, std::shared_ptr renderer) : _signature(std::move(signature)) , _measuredData(std::move(measuredData)) , _displayFallbackText(std::move(displayFallbackText)) , _renderer(std::move(renderer)) { } int InlineFormulaSharedState::width() const { const auto geometry = InlineFormulaGeometryFrom(measured()); return (measured().success && (geometry.width > 0)) ? geometry.width : 1; } bool InlineFormulaSharedState::failed() const { return !measured().success; } auto InlineFormulaSharedState::vertical(const style::TextStyle &textStyle) const -> std::optional { const auto &formula = measured(); const auto geometry = InlineFormulaGeometryFrom(formula); if (formula.success && (geometry.imageHeight > 0)) { return Ui::Text::CustomEmojiVerticalMetrics{ .ascent = geometry.ascent, .descent = geometry.descent, }; } const auto ascent = std::max(TextLineAscent(textStyle), 0); return Ui::Text::CustomEmojiVerticalMetrics{ .ascent = ascent, .descent = std::max(TextLineHeight(textStyle) - ascent, 0), }; } void InlineFormulaSharedState::paint( QPainter &p, const Ui::Text::CustomEmoji::Context &context, const QString &replacementText, int fallbackWidth) const { const auto rendered = ensureRendered(std::max(style::DevicePixelRatio(), 1)); if (rendered.success) { const auto geometry = InlineFormulaGeometryFrom(rendered); if (const auto image = colorizedImage( context.textColor, std::max(style::DevicePixelRatio(), 1))) { p.drawImage( QPointF(context.position) + QPointF(0., LogicalInlineFormulaMetric( geometry.paintOffsetYScaled)), *image); } return; } const auto fallbackText = replacementText.isEmpty() ? _displayFallbackText : replacementText; if (fallbackText.isEmpty()) { return; } p.save(); p.setPen(context.textColor); p.drawText( QRect( context.position.x(), context.position.y(), std::max(fallbackWidth, 1), p.fontMetrics().height()), Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine, fallbackText); p.restore(); } void InlineFormulaSharedState::setRenderer(std::shared_ptr renderer) { _renderer = std::move(renderer); invalidateRasterCache(); } void InlineFormulaSharedState::invalidatePaletteCache() { _colorized.clear(); } void InlineFormulaSharedState::invalidateRasterCache() { _rendered.clear(); _colorized.clear(); } const MeasuredFormula &InlineFormulaSharedState::measured() const { static const auto kEmpty = MeasuredFormula(); return _measuredData ? *_measuredData : kEmpty; } MathRenderer *InlineFormulaSharedState::renderer() const { if (!_renderer) { _renderer = std::make_shared(); } return _renderer.get(); } RenderedFormula InlineFormulaSharedState::ensureRendered( int devicePixelRatio) const { if (!measured().success) { return MeasuredFallback(measured()); } if (const auto i = _rendered.find(devicePixelRatio); i != end(_rendered)) { return i->second; } auto rendered = renderer()->renderFormula({ .trimmedTex = _signature.trimmedTex, .kind = _signature.kind, .textSize = _signature.textSize, .renderWidthCap = _signature.renderWidthCap, .renderHeightCap = _signature.renderHeightCap, .devicePixelRatio = devicePixelRatio, }); if (rendered.logicalSize.isEmpty()) { rendered.logicalSize = measured().logicalSize; rendered.logicalDepth = measured().logicalDepth; rendered.exact = measured().exact; rendered.fallbackText = measured().fallbackText; rendered.error = measured().error; rendered.overflow = measured().overflow; rendered.tooLarge = measured().tooLarge; NormalizeInlineFormulaRasterMetrics(&rendered); } if (!rendered.success) { rendered = MeasuredFallback(measured()); } const auto i = _rendered.emplace( devicePixelRatio, std::move(rendered)).first; return i->second; } const QImage *InlineFormulaSharedState::colorizedImage( const QColor &color, int devicePixelRatio) const { const auto rendered = ensureRendered(devicePixelRatio); if (!rendered.success) { return nullptr; } const auto key = InlineFormulaColorizedKey{ .color = color.rgba(), .devicePixelRatio = devicePixelRatio, }; if (const auto i = _colorized.find(key); i != end(_colorized)) { return &i->second; } auto colorized = QImage( rendered.image.size(), QImage::Format_ARGB32_Premultiplied); style::colorizeImage( rendered.image, color, &colorized, QRect(), QPoint(), true); const auto i = _colorized.emplace( key, std::move(colorized)).first; return &i->second; } InlineFormulaObject::InlineFormulaObject( QString entityData, QString replacementText, int fallbackWidth, std::shared_ptr state) : _entityData(std::move(entityData)) , _replacementText(std::move(replacementText)) , _fallbackWidth(std::max(fallbackWidth, 1)) , _state(std::move(state)) { } int InlineFormulaObject::width() { if (!_state || _state->failed()) { return _fallbackWidth; } return _state->width(); } QString InlineFormulaObject::entityData() { return _entityData; } auto InlineFormulaObject::vertical(const style::TextStyle &textStyle) -> std::optional { return _state ? _state->vertical(textStyle) : std::nullopt; } QString InlineFormulaObject::replacementText() { return _replacementText; } Ui::Text::CustomEmojiSemantics InlineFormulaObject::semantics() { return { .isEmoji = false, .isRealCustomEmoji = false, .exportEntity = false, .unloadPersistentAnimation = false, .allowCustomEmojiClick = false, }; } void InlineFormulaObject::paint(QPainter &p, const Context &context) { if (_state) { _state->paint(p, context, _replacementText, _fallbackWidth); } } void InlineFormulaObject::unload() { } bool InlineFormulaObject::ready() { return true; } bool InlineFormulaObject::readyInDefaultState() { return true; } void InlineFormulaObjectCache::setRenderer(std::shared_ptr renderer) { _renderer = std::move(renderer); for (const auto &entry : _states) { const auto &state = entry.second; if (state) { state->setRenderer(_renderer); } } } void InlineFormulaObjectCache::clear() { _states.clear(); } void InlineFormulaObjectCache::invalidatePaletteCache() { for (const auto &entry : _states) { const auto &state = entry.second; if (state) { state->invalidatePaletteCache(); } } } void InlineFormulaObjectCache::invalidateRasterCache() { for (const auto &entry : _states) { const auto &state = entry.second; if (state) { state->invalidateRasterCache(); } } } InlineIvImageObject::InlineIvImageObject( QString replacementText, int width, int height, std::shared_ptr image, Fn repaint, Fn repaintRect) : _replacementText(std::move(replacementText)) , _width(std::max(width, 1)) , _height(std::max(height, 1)) , _image(std::move(image)) , _repaint(std::move(repaint)) , _repaintRect(std::move(repaintRect)) { } int InlineIvImageObject::width() { return _width; } QString InlineIvImageObject::entityData() { return QString(); } auto InlineIvImageObject::vertical(const style::TextStyle &textStyle) -> std::optional { if (_height > 0) { const auto line = TextLineHeight(textStyle); const auto above = _height - (_height / 2); const auto ascent = above - (line / 2) + TextLineAscent(textStyle); return Ui::Text::CustomEmojiVerticalMetrics{ .ascent = ascent, .descent = _height - ascent, }; } const auto ascent = std::max(TextLineAscent(textStyle), 0); return Ui::Text::CustomEmojiVerticalMetrics{ .ascent = ascent, .descent = std::max(TextLineHeight(textStyle) - ascent, 0), }; } QString InlineIvImageObject::replacementText() { return _replacementText; } Ui::Text::CustomEmojiSemantics InlineIvImageObject::semantics() { return { .isEmoji = false, .isRealCustomEmoji = false, .exportEntity = false, .unloadPersistentAnimation = false, .allowCustomEmojiClick = false, }; } void InlineIvImageObject::paint(QPainter &p, const Context &context) { *_lastPaintRect = QRect(context.position, QSize(_width, _height)); if (_image) { if (!_subscribed) { _subscribed = true; const auto repaint = _repaint; const auto repaintRect = _repaintRect; const auto lastPaintRect = _lastPaintRect; _image->subscribeToUpdates([=] { if (repaintRect && lastPaintRect && !lastPaintRect->isEmpty()) { repaintRect(*lastPaintRect); } else if (repaint) { repaint(); } }); } if (const auto image = _image->image(std::max(_width, _height)); !image.isNull()) { p.drawImage( QRect(context.position, QSize(_width, _height)), image); return; } } if (_replacementText.isEmpty()) { return; } p.save(); p.setPen(context.textColor); p.drawText( QRect(context.position, QSize(_width, _height)), Qt::AlignCenter | Qt::TextWordWrap, _replacementText); p.restore(); } void InlineIvImageObject::unload() { if (_subscribed && _image) { _subscribed = false; _image->subscribeToUpdates(nullptr); } } bool InlineIvImageObject::ready() { return true; } bool InlineIvImageObject::readyInDefaultState() { return true; } std::unique_ptr InlineFormulaObjectCache::create( const InlineTextObjectFormulaData &data, const style::TextStyle &textStyle, const style::Markdown &st, const std::vector *formulas) { auto replacementText = data.copySource; if (replacementText.isEmpty()) { replacementText = u"$"_q + data.trimmedTex + u"$"_q; } auto state = lookupOrCreate( InlineFormulaSignature(data.trimmedTex, textStyle, st), textStyle, st, formulas); if (!state) { return nullptr; } const auto fallbackWidth = std::max( textStyle.font->width(replacementText), 1); const auto entityData = SerializeInlineTextObjectEntity({ .kind = InlineTextObjectKind::Formula, .data = data, }); return std::make_unique( std::move(entityData), std::move(replacementText), fallbackWidth, std::move(state)); } auto InlineFormulaObjectCache::lookupOrCreate( const PreparedFormulaMeasurementSignature &signature, const style::TextStyle &textStyle, const style::Markdown &st, const std::vector *formulas) -> std::shared_ptr { if (const auto i = _states.find(signature); i != end(_states)) { return i->second; } auto measured = MeasuredFormula(); auto measuredData = FindInlineFormulaMeasuredData( formulas, signature, st, &measured); if (measuredData) { measured = *measuredData; } else { if (!_renderer) { _renderer = std::make_shared(); } measured = _renderer->measureFormula({ .trimmedTex = signature.trimmedTex, .kind = signature.kind, .textSize = signature.textSize, .renderWidthCap = signature.renderWidthCap, .renderHeightCap = signature.renderHeightCap, }); measuredData = std::make_shared(measured); } const auto fallbackText = InlineFormulaDisplayFallbackText( signature, measured); auto state = std::make_shared( signature, std::move(measuredData), fallbackText, _renderer); _states.emplace(signature, state); return state; } std::shared_ptr CreateInlineFormulaObjectCache( std::shared_ptr renderer) { auto result = std::make_shared(); result->setRenderer(std::move(renderer)); return result; } void SetInlineFormulaObjectCacheRenderer( const std::shared_ptr &cache, std::shared_ptr renderer) { if (cache) { cache->setRenderer(std::move(renderer)); } } void ClearInlineFormulaObjectCache( const std::shared_ptr &cache) { if (cache) { cache->clear(); } } void InvalidateInlineFormulaPaletteCache( const std::shared_ptr &cache) { if (cache) { cache->invalidatePaletteCache(); } } void InvalidateInlineFormulaRasterCache( const std::shared_ptr &cache) { if (cache) { cache->invalidateRasterCache(); } } void SetTextLeaf( Ui::Text::String *leaf, const style::TextStyle &textStyle, const style::Markdown &st, const TextWithEntities &text, const std::vector *formulas, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, int minResizeWidth, Fn repaint, Fn repaintRect, Fn spoilerLinkFilter) { *leaf = Ui::Text::String(TextMinResizeWidth(minResizeWidth)); auto context = mediaRuntime ? mediaRuntime->textContext() : Ui::Text::MarkedContext(); context.repaint = repaint; const auto textStylePtr = &textStyle; const auto stPtr = &st; auto originalCustomEmojiFactory = std::move(context.customEmojiFactory); context.customEmojiFactory = [ formulas, inlineFormulaObjects, mediaRuntime, repaintRect = std::move(repaintRect), originalCustomEmojiFactory = std::move(originalCustomEmojiFactory), textStyle = textStylePtr, st = stPtr ]( QStringView data, const Ui::Text::MarkedContext &context ) -> std::unique_ptr { const auto parsed = ParseInlineTextObjectEntity(data); if (!parsed) { return originalCustomEmojiFactory ? originalCustomEmojiFactory(data, context) : std::unique_ptr(); } switch (parsed->kind) { case InlineTextObjectKind::Formula: { if (!inlineFormulaObjects) { return std::unique_ptr(); } const auto formula = std::get_if( &parsed->data); return formula ? inlineFormulaObjects->create( *formula, *textStyle, *st, formulas) : std::unique_ptr(); } break; case InlineTextObjectKind::IvImage: { const auto image = std::get_if( &parsed->data); if (!image) { return std::unique_ptr(); } const auto resolved = mediaRuntime ? mediaRuntime->resolveInlineImage( image->documentId, QSize(image->width, image->height)) : nullptr; return std::make_unique( image->replacementText, image->width, image->height, std::move(resolved), context.repaint, repaintRect); } } return std::unique_ptr(); }; leaf->setMarkedText(textStyle, text, kIvMarkedTextOptions, context); SetTextLeafSpoilerLinkFilter(leaf, std::move(spoilerLinkFilter)); } } // namespace Iv::Markdown