2489 lines
73 KiB
C++
2489 lines
73 KiB
C++
/*
|
|
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 "api/api_bot.h"
|
|
#include "base/weak_ptr.h"
|
|
#include "core/click_handler_types.h"
|
|
#include "iv/markdown/iv_markdown_article_layout_blocks.h"
|
|
#include "iv/markdown/iv_markdown_button_row.h"
|
|
#include "iv/markdown/iv_markdown_prepare_links.h"
|
|
#include "iv/markdown/iv_markdown_prepare_serialize.h"
|
|
#include "lang/lang_keys.h"
|
|
#include "ui/effects/animation_value.h"
|
|
#include "ui/style/style_core.h"
|
|
#include "ui/style/style_core_scale.h"
|
|
#include "ui/text/text_custom_emoji.h"
|
|
#include "ui/text/text_utilities.h"
|
|
#include "ui/basic_click_handlers.h"
|
|
#include "ui/dynamic_image.h"
|
|
#include "ui/emoji_config.h"
|
|
#include "ui/integration.h"
|
|
#include "ui/painter.h"
|
|
|
|
#include "styles/palette.h"
|
|
#include "styles/style_iv.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <utility>
|
|
|
|
namespace Iv::Markdown {
|
|
namespace {
|
|
|
|
constexpr auto kIvMarkedTextOptions = TextParseOptions{
|
|
TextParseMultiline,
|
|
0,
|
|
0,
|
|
Qt::LayoutDirectionAuto,
|
|
};
|
|
|
|
constexpr auto kIvMarkedTextOptionsRtl = TextParseOptions{
|
|
TextParseMultiline,
|
|
0,
|
|
0,
|
|
Qt::RightToLeft,
|
|
};
|
|
|
|
constexpr auto kInlineButtonLabelProbeStart = 32;
|
|
constexpr auto kInlineButtonLabelSpanCutsMax
|
|
= 2 * kInlineButtonLabelProbeStart;
|
|
|
|
using ButtonColor = HistoryMessageMarkupButton::Color;
|
|
|
|
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<PreparedLinkExternalData> 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:
|
|
case PreparedLinkKind::ToggleBlockquote:
|
|
case PreparedLinkKind::RichPageButton:
|
|
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:
|
|
case PreparedLinkKind::ToggleBlockquote:
|
|
case PreparedLinkKind::RichPageButton:
|
|
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 <typename Formula>
|
|
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<const MeasuredFormula> measuredData,
|
|
QString displayFallbackText,
|
|
std::shared_ptr<MathRenderer> renderer);
|
|
|
|
[[nodiscard]] int width() const;
|
|
[[nodiscard]] bool failed() const;
|
|
[[nodiscard]] std::optional<Ui::Text::CustomEmojiVerticalMetrics> 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<MathRenderer> 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<const MeasuredFormula> _measuredData;
|
|
QString _displayFallbackText;
|
|
mutable std::shared_ptr<MathRenderer> _renderer;
|
|
mutable std::map<int, RenderedFormula> _rendered;
|
|
mutable std::map<InlineFormulaColorizedKey, QImage> _colorized;
|
|
|
|
};
|
|
|
|
struct InlineFormulaGeometry {
|
|
int width = 1;
|
|
int imageHeight = 0;
|
|
int imageDescent = 0;
|
|
int ascent = 0;
|
|
int descent = 0;
|
|
int paintOffsetYScaled = 0;
|
|
};
|
|
|
|
template <typename Formula>
|
|
[[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 <typename Formula>
|
|
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<InlineFormulaSharedState> state);
|
|
|
|
int width() override;
|
|
QString entityData() override;
|
|
std::optional<Ui::Text::CustomEmojiVerticalMetrics> 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<InlineFormulaSharedState> _state;
|
|
|
|
};
|
|
|
|
class InlineIvImageObject final : public Ui::Text::CustomEmoji {
|
|
public:
|
|
InlineIvImageObject(
|
|
QString replacementText,
|
|
int width,
|
|
int height,
|
|
std::shared_ptr<Ui::DynamicImage> image,
|
|
Fn<void()> repaint,
|
|
Fn<void(QRect)> repaintRect);
|
|
|
|
int width() override;
|
|
QString entityData() override;
|
|
std::optional<Ui::Text::CustomEmojiVerticalMetrics> 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<Ui::DynamicImage> _image;
|
|
const Fn<void()> _repaint;
|
|
const Fn<void(QRect)> _repaintRect;
|
|
const std::shared_ptr<QRect> _lastPaintRect = std::make_shared<QRect>();
|
|
bool _subscribed = false;
|
|
|
|
};
|
|
|
|
class InlineButtonPlainEmoji final : public Ui::Text::CustomEmoji {
|
|
public:
|
|
InlineButtonPlainEmoji(EmojiPtr emoji, int size);
|
|
|
|
int width() override;
|
|
QString entityData() override;
|
|
std::optional<Ui::Text::CustomEmojiVerticalMetrics> vertical(
|
|
const style::TextStyle &textStyle) override;
|
|
void paint(QPainter &p, const Context &context) override;
|
|
void unload() override;
|
|
bool ready() override;
|
|
bool readyInDefaultState() override;
|
|
|
|
private:
|
|
const EmojiPtr _emoji = nullptr;
|
|
QImage _frame;
|
|
int _size = 1;
|
|
|
|
};
|
|
|
|
class InlineButtonScaledEmoji final : public Ui::Text::CustomEmoji {
|
|
public:
|
|
InlineButtonScaledEmoji(
|
|
std::unique_ptr<Ui::Text::CustomEmoji> wrapped,
|
|
int size);
|
|
|
|
int width() override;
|
|
QString entityData() override;
|
|
std::optional<Ui::Text::CustomEmojiVerticalMetrics> vertical(
|
|
const style::TextStyle &textStyle) override;
|
|
void paint(QPainter &p, const Context &context) override;
|
|
void unload() override;
|
|
bool ready() override;
|
|
bool readyInDefaultState() override;
|
|
|
|
private:
|
|
const std::unique_ptr<Ui::Text::CustomEmoji> _wrapped;
|
|
QImage _frame;
|
|
int _size = 1;
|
|
|
|
};
|
|
|
|
class InlineButtonObject final : public Ui::Text::CustomEmoji {
|
|
public:
|
|
InlineButtonObject(
|
|
const InlineTextObjectButtonData &data,
|
|
const style::TextStyle &textStyle,
|
|
const style::Markdown &st,
|
|
const Ui::Text::MarkedContext &context,
|
|
std::shared_ptr<InlineButtonPaintState> paintState,
|
|
int widthCap);
|
|
|
|
int width() override;
|
|
QString entityData() override;
|
|
std::optional<Ui::Text::CustomEmojiVerticalMetrics> vertical(
|
|
const style::TextStyle &textStyle) override;
|
|
QString replacementText() override;
|
|
EntitiesInText replacementEntities() override;
|
|
Ui::Text::CustomEmojiSemantics semantics() override;
|
|
void paint(QPainter &p, const Context &context) override;
|
|
void unload() override;
|
|
bool ready() override;
|
|
bool readyInDefaultState() override;
|
|
|
|
private:
|
|
[[nodiscard]] const style::Markdown &resolvedStyle() const;
|
|
[[nodiscard]] bool labelWouldOverflowIcon(
|
|
const style::Markdown &st) const;
|
|
void paintContent(
|
|
QPainter &p,
|
|
QPoint position,
|
|
QColor color,
|
|
const style::Markdown &st,
|
|
const Context &context) const;
|
|
|
|
const QString _entityData;
|
|
const QString _replacementText;
|
|
const EntitiesInText _labelEntities;
|
|
const QByteArray _loadingKey;
|
|
const style::Markdown *_st = nullptr;
|
|
const style::icon *_icon = nullptr;
|
|
const std::shared_ptr<InlineButtonPaintState> _paintState;
|
|
Ui::Text::String _label;
|
|
Ui::Text::CustomEmojiVerticalMetrics _vertical;
|
|
int _width = 1;
|
|
int _height = 1;
|
|
int _labelWidth = 0;
|
|
int _labelLeft = 0;
|
|
int _labelTop = 0;
|
|
int _iconLeft = 0;
|
|
int _lineTopSkip = 0;
|
|
int _lineHeight = 0;
|
|
ButtonColor _color = ButtonColor::Normal;
|
|
bool _disabled = false;
|
|
bool _labelHasParagraphBreak = 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<const MeasuredFormula>
|
|
FindInlineFormulaMeasuredData(
|
|
const std::vector<PreparedFormulaSlot> *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<MeasuredFormula>(slot.measured);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
[[nodiscard]] Ui::Text::CustomEmojiVerticalMetrics CenteredVerticalMetrics(
|
|
const style::TextStyle &textStyle,
|
|
int height) {
|
|
const auto top = std::max((TextLineHeight(textStyle) - height) / 2, 0);
|
|
const auto ascent = TextLineAscent(textStyle) - top;
|
|
return {
|
|
.ascent = ascent,
|
|
.descent = height - ascent,
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] int InlineButtonPillHeight(
|
|
const style::TextStyle &textStyle,
|
|
const style::MarkdownInlineButton &st) {
|
|
return std::min(
|
|
textStyle.font->height,
|
|
st.labelStyle.font->height + 2 * st.verticalPadding);
|
|
}
|
|
|
|
[[nodiscard]] int InlineButtonEmojiSize(
|
|
const style::TextStyle &textStyle,
|
|
const style::MarkdownInlineButton &st) {
|
|
return std::max(
|
|
InlineButtonPillHeight(textStyle, st) - 2 * st.verticalPadding,
|
|
1);
|
|
}
|
|
|
|
[[nodiscard]] int InlineButtonLabelWidthCap(
|
|
const style::MarkdownInlineButton &st,
|
|
int buttonWidthCap,
|
|
int trailing) {
|
|
return std::max(buttonWidthCap - st.padding - trailing, 0);
|
|
}
|
|
|
|
[[nodiscard]] int InlineButtonWidthCap(
|
|
const style::MarkdownInlineButton &st,
|
|
const std::shared_ptr<InlineButtonPaintState> &paintState,
|
|
int bandWidthCap) {
|
|
const auto published = (bandWidthCap > 0)
|
|
? bandWidthCap
|
|
: (paintState ? paintState->widthCap : 0);
|
|
return (published > 0) ? std::min(st.maxWidth, published) : st.maxWidth;
|
|
}
|
|
|
|
[[nodiscard]] RichButtonPillColors ResolveInlineButtonColors(
|
|
ButtonColor color,
|
|
const style::Markdown &st) {
|
|
const auto &inlineSt = st.inlineButton;
|
|
const auto tint = [&](const style::color &fg) {
|
|
return RichButtonPillColors{
|
|
.bg = anim::with_alpha(fg->c, inlineSt.tintBgOpacity),
|
|
.ripple = anim::with_alpha(
|
|
fg->c,
|
|
st.buttonRow.tintRippleOpacity),
|
|
.fg = fg->c,
|
|
};
|
|
};
|
|
switch (color) {
|
|
case ButtonColor::Primary:
|
|
return PrimaryPillColors(
|
|
st,
|
|
inlineSt.primaryBg->c,
|
|
st.buttonRow.primaryRipple->c);
|
|
case ButtonColor::Success:
|
|
return tint(inlineSt.successFg);
|
|
case ButtonColor::Danger:
|
|
return tint(inlineSt.dangerFg);
|
|
}
|
|
return tint(inlineSt.defaultFg);
|
|
}
|
|
|
|
[[nodiscard]] std::optional<InlineTextObjectButtonData> InlineButtonDataFor(
|
|
QStringView data) {
|
|
const auto parsed = ParseInlineTextObjectEntity(data);
|
|
if (!parsed || parsed->kind != InlineTextObjectKind::Button) {
|
|
return std::nullopt;
|
|
}
|
|
const auto button = std::get_if<InlineTextObjectButtonData>(&parsed->data);
|
|
return button
|
|
? std::make_optional(*button)
|
|
: std::nullopt;
|
|
}
|
|
|
|
[[nodiscard]] auto ActionableInlineButtonDataFor(QStringView data)
|
|
-> std::optional<InlineTextObjectButtonData> {
|
|
auto result = InlineButtonDataFor(data);
|
|
if (result
|
|
&& (result->type == HistoryMessageMarkupButton::Type::Disabled)) {
|
|
return std::nullopt;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] bool InlineButtonActionable(QStringView data) {
|
|
return ActionableInlineButtonDataFor(data).has_value();
|
|
}
|
|
|
|
[[nodiscard]] HistoryMessageMarkupButton InlineButtonRecord(
|
|
const InlineTextObjectButtonData &button) {
|
|
auto result = HistoryMessageMarkupButton(
|
|
button.type,
|
|
button.label.text,
|
|
HistoryMessageMarkupButton::Visual{ .color = button.color },
|
|
button.data,
|
|
QString(),
|
|
button.buttonId);
|
|
result.peerTypes = button.peerTypes;
|
|
return result;
|
|
}
|
|
|
|
void ActivateInlineButton(QStringView data, ClickContext context) {
|
|
const auto button = ActionableInlineButtonDataFor(data);
|
|
if (!button) {
|
|
return;
|
|
}
|
|
const auto record = InlineButtonRecord(*button);
|
|
const auto my = context.other.value<ClickHandlerContext>();
|
|
Api::ActivateRichPageBotButton(my, record);
|
|
}
|
|
|
|
class InlineButtonClickHandler final : public ClickHandler {
|
|
public:
|
|
explicit InlineButtonClickHandler(QString data);
|
|
|
|
void onClick(ClickContext context) const override;
|
|
|
|
QString tooltip() const override;
|
|
|
|
private:
|
|
const QString _data;
|
|
|
|
};
|
|
|
|
InlineButtonClickHandler::InlineButtonClickHandler(QString data)
|
|
: _data(std::move(data)) {
|
|
}
|
|
|
|
void InlineButtonClickHandler::onClick(ClickContext context) const {
|
|
ActivateInlineButton(_data, std::move(context));
|
|
}
|
|
|
|
QString InlineButtonClickHandler::tooltip() const {
|
|
return InlineButtonTooltip(_data);
|
|
}
|
|
|
|
[[nodiscard]] QString InlineButtonPlainEmojiPrefix() {
|
|
return u"iv-markdown:inline-button-emoji:"_q;
|
|
}
|
|
|
|
// MarkInlineButtonPlainEmoji marks the plain emoji of a label, and
|
|
// InlineButtonLabelEmojiCrosses answers whether a planned removal can change
|
|
// what it marks; the guard is sound only while it visits exactly the positions
|
|
// the marking walk lands on, so the two share one traversal rather than two
|
|
// look-alike loops that can drift. Both details below are load-bearing: `ch`
|
|
// moves past the whole match before the callback runs, so a callback that
|
|
// declines the match still leaves the walk past it, and a miss advances by one
|
|
// code unit rather than by one code point.
|
|
template <typename Callback>
|
|
void EnumerateInlineButtonLabelEmoji(
|
|
const QString &text,
|
|
Callback &&callback) {
|
|
const auto start = text.constData();
|
|
const auto finish = start + text.size();
|
|
auto ch = start;
|
|
while (ch != finish) {
|
|
auto length = 0;
|
|
const auto emoji = Ui::Emoji::Find(ch, finish, &length);
|
|
if (!emoji) {
|
|
++ch;
|
|
continue;
|
|
}
|
|
const auto from = int(ch - start);
|
|
ch += length;
|
|
if (!callback(from, length, emoji)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] TextWithEntities MarkInlineButtonPlainEmoji(
|
|
TextWithEntities label) {
|
|
const auto till = [](const EntityInText &entity) {
|
|
return entity.offset() + entity.length();
|
|
};
|
|
auto i = label.entities.begin();
|
|
EnumerateInlineButtonLabelEmoji(label.text, [&](
|
|
int from,
|
|
int length,
|
|
EmojiPtr emoji) {
|
|
while (i != label.entities.end() && till(*i) <= from) {
|
|
++i;
|
|
}
|
|
if (i != label.entities.end() && i->offset() < from + length) {
|
|
return true;
|
|
}
|
|
i = label.entities.insert(i, EntityInText(
|
|
EntityType::CustomEmoji,
|
|
from,
|
|
length,
|
|
InlineButtonPlainEmojiPrefix() + emoji->text()));
|
|
return true;
|
|
});
|
|
return label;
|
|
}
|
|
|
|
struct InlineButtonLabelCodePoint {
|
|
uint ucs4 = 0;
|
|
int length = 1;
|
|
};
|
|
|
|
// InlineButtonLabelStrongCut and InlineButtonLabelSpanKeep have to agree on
|
|
// which character is the first strong one: the scan keeps a span's prefix
|
|
// through its own first strong character precisely so that StringDirection
|
|
// and the strong cut still find that same character over the shortened label.
|
|
// Two look-alike decode loops can drift apart; one shared pair cannot.
|
|
[[nodiscard]] InlineButtonLabelCodePoint InlineButtonLabelCodePointAt(
|
|
QStringView text,
|
|
int i) {
|
|
auto result = InlineButtonLabelCodePoint{
|
|
.ucs4 = uint(text.at(i).unicode()),
|
|
};
|
|
if (QChar::isHighSurrogate(result.ucs4) && (i + 1 < int(text.size()))) {
|
|
const auto low = text.at(i + 1).unicode();
|
|
if (QChar::isLowSurrogate(low)) {
|
|
result.ucs4 = QChar::surrogateToUcs4(result.ucs4, low);
|
|
result.length = 2;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] Qt::LayoutDirection InlineButtonLabelStrongDirection(
|
|
uint ucs4) {
|
|
const auto direction = QChar::direction(ucs4);
|
|
return (direction == QChar::DirL)
|
|
? Qt::LeftToRight
|
|
: (direction == QChar::DirR || direction == QChar::DirAL)
|
|
? Qt::RightToLeft
|
|
: Qt::LayoutDirectionAuto;
|
|
}
|
|
|
|
[[nodiscard]] bool InlineButtonLabelCharKept(
|
|
QChar ch,
|
|
InlineButtonLabelCodePoint point) {
|
|
return !Ui::Text::IsBad(ch)
|
|
&& !Ui::Text::IsDiacritic(ch)
|
|
&& ((point.length == 2)
|
|
? (point.ucs4 < 0xE0000)
|
|
: !QChar::isSurrogate(point.ucs4));
|
|
}
|
|
|
|
[[nodiscard]] int InlineButtonLabelStrongCut(
|
|
const TextWithEntities &label,
|
|
int cut) {
|
|
// A Ui::Text::String takes its first paragraph's direction from the first
|
|
// character of strong direction in that paragraph and falls back to the
|
|
// interface direction when there is none, and a line whose paragraph
|
|
// direction opposes the alignment is drawn shifted by the width it did
|
|
// not use. A prefix that dropped the label's only strong character would
|
|
// therefore draw the same glyphs at a different x — but only when that
|
|
// character resolves to the direction the fallback would not have picked.
|
|
const auto &text = label.text;
|
|
const auto size = int(text.size());
|
|
auto i = 0;
|
|
while (i < size) {
|
|
const auto ch = text.at(i);
|
|
if (ch.unicode() == QChar::LineFeed) {
|
|
return cut;
|
|
}
|
|
const auto point = InlineButtonLabelCodePointAt(text, i);
|
|
const auto strong = InlineButtonLabelStrongDirection(point.ucs4);
|
|
if (strong != Qt::LayoutDirectionAuto) {
|
|
return (strong == style::LayoutDirection())
|
|
? cut
|
|
: std::max(cut, i + point.length);
|
|
}
|
|
i += point.length;
|
|
}
|
|
return cut;
|
|
}
|
|
|
|
[[nodiscard]] int InlineButtonLabelCut(
|
|
const TextWithEntities &label,
|
|
int position) {
|
|
const auto size = int(label.text.size());
|
|
if (position >= size) {
|
|
return size;
|
|
}
|
|
auto result = InlineButtonLabelStrongCut(label, position);
|
|
// Ui::Text::Mid truncates an entity that straddles the range end instead
|
|
// of dropping it, so a CustomEmoji entity that starts inside the drawn
|
|
// region and ends past the cut would be rewritten and would change
|
|
// visible glyphs; the cut moves past its end instead. The list is not
|
|
// ordered by offset and this one forward pass does not need it to be.
|
|
// What it needs is that no two entities partially overlap, and the
|
|
// producers nest them: AddEntity in iv_rich_page.cpp pushes a container
|
|
// after the entities it encloses, so an entry with an earlier offset
|
|
// than one before it is one that contains it and reaches at least as
|
|
// far. Moving the cut can therefore never expose an entity the pass
|
|
// already walked past.
|
|
for (const auto &entity : label.entities) {
|
|
const auto till = entity.offset() + entity.length();
|
|
if ((entity.offset() < result) && (till > result)) {
|
|
result = till;
|
|
}
|
|
}
|
|
return std::min(result, size);
|
|
}
|
|
|
|
[[nodiscard]] Ui::Text::MarkedContext InlineButtonLabelContext(
|
|
const Ui::Text::MarkedContext &context,
|
|
int emojiSize) {
|
|
auto result = context;
|
|
result.customEmojiFactory = [
|
|
parent = context.customEmojiFactory,
|
|
emojiSize
|
|
](
|
|
QStringView data,
|
|
const Ui::Text::MarkedContext &context
|
|
) -> std::unique_ptr<Ui::Text::CustomEmoji> {
|
|
const auto prefix = InlineButtonPlainEmojiPrefix();
|
|
if (data.startsWith(prefix)) {
|
|
const auto text = data.mid(prefix.size());
|
|
const auto emoji = Ui::Emoji::Find(text);
|
|
return emoji
|
|
? std::make_unique<InlineButtonPlainEmoji>(emoji, emojiSize)
|
|
: nullptr;
|
|
}
|
|
return Ui::Text::MakeWrappedEmoji<InlineButtonScaledEmoji>(
|
|
parent ? parent(data, context) : nullptr,
|
|
emojiSize);
|
|
};
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] Ui::Text::String MakeInlineButtonLabel(
|
|
const TextWithEntities &label,
|
|
const style::TextStyle &labelStyle,
|
|
const Ui::Text::MarkedContext &context) {
|
|
auto result = Ui::Text::String();
|
|
result.setMarkedText(
|
|
labelStyle,
|
|
MarkInlineButtonPlainEmoji(label),
|
|
kIvMarkedTextOptions,
|
|
context);
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] int InlineButtonLabelSpanKeep(
|
|
QStringView span,
|
|
bool trimmableEnd) {
|
|
// The text engine draws a CustomEmoji entity as one object of one width
|
|
// whatever it covers, and BidiAlgorithm::infoAt reads every covered
|
|
// character as an object replacement character, so the covered text
|
|
// contributes no glyph, no advance and no direction. It does decide two
|
|
// other things: BlockParser::createBlock emits the block only if it kept
|
|
// at least one of those characters, and StringDirection reads them raw,
|
|
// so the first one of strong direction still resolves the paragraph
|
|
// direction. Keep the span's own text through both, and keep all of it
|
|
// when a character of strong direction is one the parser may skip.
|
|
// A kept prefix may end on a trimmable parsed character only when the
|
|
// caller has proven the parser's trailing trim cannot reach it — a
|
|
// character the parser keeps and does not trim survives after the span
|
|
// in the spliced label, and BlockParser::trimSourceRange moves only the
|
|
// two ends of the string it is handed — which is what `trimmableEnd`
|
|
// asserts. A span with no parsed character at all still yields 0
|
|
// whatever the caller proved: keeping a character the parser skips
|
|
// would invent an object where none exists.
|
|
const auto size = int(span.size());
|
|
auto keepable = 0;
|
|
auto trimmable = 0;
|
|
auto i = 0;
|
|
while (i != size) {
|
|
const auto ch = span.at(i);
|
|
const auto point = InlineButtonLabelCodePointAt(span, i);
|
|
const auto kept = InlineButtonLabelCharKept(ch, point);
|
|
const auto strong = InlineButtonLabelStrongDirection(point.ucs4);
|
|
if (strong != Qt::LayoutDirectionAuto) {
|
|
return kept ? (i + point.length) : 0;
|
|
} else if (!keepable && kept && !Ui::Text::IsTrimmed(ch)) {
|
|
keepable = i + point.length;
|
|
} else if (!trimmable && kept) {
|
|
trimmable = i + point.length;
|
|
}
|
|
i += point.length;
|
|
}
|
|
return keepable
|
|
? keepable
|
|
: trimmableEnd
|
|
? trimmable
|
|
: 0;
|
|
}
|
|
|
|
[[nodiscard]] bool InlineButtonLabelEmojiCrosses(
|
|
const QString &text,
|
|
const std::vector<int> &borders) {
|
|
// MarkInlineButtonPlainEmoji walks the label left to right and asks
|
|
// Ui::Emoji::Find at every position it lands on, so removing text can
|
|
// change what it marks only by carrying that walk over one of the
|
|
// positions the removal joins: a match starting before such a position
|
|
// and ending after it hides the emoji behind it from the walk, or stops
|
|
// hiding one. Running the very same walk here — over the label before the
|
|
// removal against both ends of every planned cut, and over the spliced
|
|
// text against every seam — answers that at every position the real walk
|
|
// can reach, so no match crosses undetected, including one that starts
|
|
// before the span. `borders` is ascending.
|
|
auto border = borders.begin();
|
|
auto crosses = false;
|
|
EnumerateInlineButtonLabelEmoji(text, [&](
|
|
int from,
|
|
int length,
|
|
EmojiPtr) {
|
|
while ((border != borders.end()) && (*border <= from)) {
|
|
++border;
|
|
}
|
|
if ((border != borders.end()) && (*border < from + length)) {
|
|
crosses = true;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
return crosses;
|
|
}
|
|
|
|
[[nodiscard]] bool InlineButtonLabelSpanCandidate(
|
|
const EntityInText &entity) {
|
|
return (entity.type() == EntityType::CustomEmoji)
|
|
&& (entity.length() > kInlineButtonLabelProbeStart)
|
|
&& !entity.data().isEmpty();
|
|
}
|
|
|
|
[[nodiscard]] std::vector<int> InlineButtonLabelEntityOrder(
|
|
const EntitiesInText &entities) {
|
|
auto result = std::vector<int>(entities.size());
|
|
for (auto i = 0, count = int(result.size()); i != count; ++i) {
|
|
result[i] = i;
|
|
}
|
|
ranges::sort(result, ranges::less(), [&](int i) {
|
|
return entities[i].offset();
|
|
});
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] std::vector<bool> InlineButtonLabelIntersected(
|
|
const EntitiesInText &entities,
|
|
const std::vector<int> &order) {
|
|
// A candidate may be shortened only when no other entity overlaps it, and
|
|
// the list is not ordered by offset (see InlineButtonLabelCut), so the
|
|
// test cannot look at neighbours only. Asking it one candidate at a time
|
|
// is quadratic in a label the sender sizes; two sweeps in offset order
|
|
// answer it for the whole list at once. An entity is overlapped when one
|
|
// before it in that order reaches past its start, or one after it starts
|
|
// before its end.
|
|
const auto count = int(order.size());
|
|
auto result = std::vector<bool>(count, false);
|
|
auto reached = std::numeric_limits<int>::min();
|
|
for (auto i = 0; i != count; ++i) {
|
|
const auto &entity = entities[order[i]];
|
|
if (entity.offset() < reached) {
|
|
result[order[i]] = true;
|
|
}
|
|
reached = std::max(reached, entity.offset() + entity.length());
|
|
}
|
|
auto nearest = std::numeric_limits<int>::max();
|
|
for (auto i = count; i != 0;) {
|
|
const auto &entity = entities[order[--i]];
|
|
if (nearest < entity.offset() + entity.length()) {
|
|
result[order[i]] = true;
|
|
}
|
|
nearest = std::min(nearest, entity.offset());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] int InlineButtonLabelLastSolid(
|
|
const TextWithEntities &label,
|
|
const std::vector<int> &order) {
|
|
// A kept trimmable span end is safe only while the parser's trailing
|
|
// trim cannot reach it, and trimSourceRange moves only the two ends of
|
|
// the string it is handed — so the end is safe exactly when a character
|
|
// the parser keeps and does not trim survives after the span in the
|
|
// spliced label. The witness is searched only in text no planned cut
|
|
// can remove: outside every candidate span, because a later candidate's
|
|
// own cut may drop any character inside it. Text between spans and in
|
|
// non-candidate entities survives the splice verbatim, which is what
|
|
// makes the answer valid for the spliced label while it is computed
|
|
// over the original one. `order` is the entity indices in offset order.
|
|
const auto text = QStringView(label.text);
|
|
const auto size = int(text.size());
|
|
auto entity = order.begin();
|
|
const auto end = order.end();
|
|
auto skipTill = 0;
|
|
auto last = -1;
|
|
auto i = 0;
|
|
while (i < size) {
|
|
while ((entity != end)
|
|
&& (label.entities[*entity].offset() <= i)) {
|
|
const auto &excluded = label.entities[*entity];
|
|
if (InlineButtonLabelSpanCandidate(excluded)) {
|
|
skipTill = std::max(
|
|
skipTill,
|
|
excluded.offset() + excluded.length());
|
|
}
|
|
++entity;
|
|
}
|
|
if (i < skipTill) {
|
|
i = skipTill;
|
|
continue;
|
|
}
|
|
const auto ch = text.at(i);
|
|
const auto point = InlineButtonLabelCodePointAt(text, i);
|
|
if (InlineButtonLabelCharKept(ch, point)
|
|
&& !Ui::Text::IsTrimmed(ch)) {
|
|
last = i;
|
|
}
|
|
i += point.length;
|
|
}
|
|
return last;
|
|
}
|
|
|
|
struct InlineButtonLabelSpanCut {
|
|
int index = 0;
|
|
int offset = 0;
|
|
int keep = 0;
|
|
int length = 0;
|
|
};
|
|
|
|
[[nodiscard]] TextWithEntities ShortenInlineButtonLabelSpans(
|
|
TextWithEntities label,
|
|
const Ui::Text::MarkedContext &context) {
|
|
// This walk runs for every label, before any search, so the candidate
|
|
// count is what keeps an ordinary one out of it. A span of at most
|
|
// kInlineButtonLabelProbeStart characters is never a candidate: it cannot
|
|
// be what leaves the prefix search unbounded, because an accepted prefix
|
|
// carries at most `available / emojiSize` objects, so a label whose every
|
|
// span is that short is bounded by width exactly as in the shipped build.
|
|
// The rewrite is all or nothing over the candidates themselves and not
|
|
// only over how many there are: the whole label is declined as soon as
|
|
// one candidate cannot be brought under kInlineButtonLabelProbeStart
|
|
// characters, and declined again past kInlineButtonLabelSpanCutsMax of
|
|
// them. A span left whole absorbs the search that the shortened ones
|
|
// before it no longer trip — every character removed ahead of it pulls it
|
|
// under a probe that used to land in front of it, and the straddle snap
|
|
// then jumps to its end. So either every CustomEmoji entity carrying data
|
|
// in the label the search is handed covers at most
|
|
// kInlineButtonLabelProbeStart characters, or that label reaches the
|
|
// search exactly as the shipped build hands it and no input is made worse
|
|
// than it is today. The count cap is 2 * kInlineButtonLabelProbeStart
|
|
// because a shortened span draws as one object, so a prefix holding k of
|
|
// them measures at least k * emojiSize, kInlineButtonLabelProbeStart *
|
|
// emojiSize is already more than the pill can ever draw, and probe
|
|
// lengths start at kInlineButtonLabelProbeStart and double while the
|
|
// search accepts the second prefix wider than the cap, so a prefix made
|
|
// of shortened spans is accepted at twice that many of them at the
|
|
// latest. That inequality is checked per scale rather than guaranteed by
|
|
// construction — the cap is a .style pixel while emojiSize follows the
|
|
// label font's height, so they do not scale together: 448 > 408 at 100 %
|
|
// and 640 > 612 at 150 %. Where it does not hold, under a small markdown
|
|
// text style, the only effect is that a label is declined which the
|
|
// search could have bounded, which is the shipped behaviour. A span
|
|
// whose every parsed character is trimmable is admitted with its first
|
|
// parsed character kept only when a witness — a character the parser
|
|
// keeps and does not trim, in text no planned cut can remove — survives
|
|
// after the span in the spliced label (see InlineButtonLabelLastSolid).
|
|
const auto &entities = label.entities;
|
|
const auto candidates = ranges::count_if(
|
|
entities,
|
|
InlineButtonLabelSpanCandidate);
|
|
if (!candidates || (candidates > kInlineButtonLabelSpanCutsMax)) {
|
|
return label;
|
|
}
|
|
const auto order = InlineButtonLabelEntityOrder(entities);
|
|
const auto intersected = InlineButtonLabelIntersected(entities, order);
|
|
const auto lastSolid = InlineButtonLabelLastSolid(label, order);
|
|
auto cuts = std::vector<InlineButtonLabelSpanCut>();
|
|
for (const auto i : order) {
|
|
const auto &entity = entities[i];
|
|
if (!InlineButtonLabelSpanCandidate(entity)) {
|
|
continue;
|
|
} else if (intersected[i]) {
|
|
return label;
|
|
}
|
|
const auto offset = entity.offset();
|
|
const auto length = entity.length();
|
|
const auto keep = InlineButtonLabelSpanKeep(
|
|
QStringView(label.text).mid(offset, length),
|
|
lastSolid >= offset + length);
|
|
if (!keep
|
|
|| (keep > kInlineButtonLabelProbeStart)
|
|
|| !Ui::Text::MakeCustomEmoji(entity.data(), context)) {
|
|
return label;
|
|
}
|
|
cuts.push_back({
|
|
.index = i,
|
|
.offset = offset,
|
|
.keep = keep,
|
|
.length = length,
|
|
});
|
|
}
|
|
auto borders = std::vector<int>();
|
|
borders.reserve(2 * cuts.size());
|
|
for (const auto &cut : cuts) {
|
|
borders.push_back(cut.offset + cut.keep);
|
|
borders.push_back(cut.offset + cut.length);
|
|
}
|
|
if (InlineButtonLabelEmojiCrosses(label.text, borders)) {
|
|
return label;
|
|
}
|
|
auto text = QString();
|
|
auto seams = std::vector<int>();
|
|
text.reserve(label.text.size());
|
|
seams.reserve(cuts.size());
|
|
auto copied = 0;
|
|
for (const auto &cut : cuts) {
|
|
const auto kept = cut.offset + cut.keep;
|
|
text.append(QStringView(label.text).mid(copied, kept - copied));
|
|
seams.push_back(int(text.size()));
|
|
copied = cut.offset + cut.length;
|
|
}
|
|
text.append(QStringView(label.text).mid(copied));
|
|
if (InlineButtonLabelEmojiCrosses(text, seams)) {
|
|
return label;
|
|
}
|
|
auto removed = 0;
|
|
auto cut = cuts.begin();
|
|
for (const auto i : order) {
|
|
auto &entity = label.entities[i];
|
|
while ((cut != cuts.end())
|
|
&& (cut->offset + cut->length <= entity.offset())) {
|
|
removed += cut->length - cut->keep;
|
|
++cut;
|
|
}
|
|
if (removed) {
|
|
entity.shiftRight(-removed);
|
|
}
|
|
if ((cut != cuts.end()) && (cut->index == i)) {
|
|
entity.shrinkFromRight(cut->length - cut->keep);
|
|
}
|
|
}
|
|
label.text = std::move(text);
|
|
return label;
|
|
}
|
|
|
|
[[nodiscard]] Ui::Text::String MakeBoundedInlineButtonLabel(
|
|
const TextWithEntities &label,
|
|
const style::TextStyle &labelStyle,
|
|
const Ui::Text::MarkedContext &context,
|
|
int emojiSize,
|
|
int widthCap) {
|
|
// The renderer lays the whole string out on every paint even though the
|
|
// pill can only ever draw `available` pixels of it, so the label is built
|
|
// from a prefix instead. Probe lengths double until one measures wider
|
|
// than the pill can draw, and the prefix after that one is accepted, so
|
|
// everything the renderer reaches — the line break, the elision cut and
|
|
// the ellipsis — sits inside the half the shorter probe already covered,
|
|
// with more than a pill-width of identical content behind it. A prefix
|
|
// alone cannot bound a custom-emoji span, which draws as one object of
|
|
// one width however many characters it covers: the search has to step
|
|
// past the whole span before its width test can trip, so the span is what
|
|
// makes the accepted prefix long. The spans are therefore shortened
|
|
// first, once, for every label. That walk asks Ui::Text::MakeCustomEmoji
|
|
// at most once per candidate span, and that question registers a session
|
|
// instance and can post messages.getCustomEmojiDocuments, so the walk's
|
|
// candidate cap is what bounds the fetching — and the accepted prefix may
|
|
// still drop a span the walk resolved. The one input this does not
|
|
// render like the shipped build, on either exit, is a span running into
|
|
// the parser's 32 768-character cap: the text behind it that the capped
|
|
// parse never reached becomes visible, and the pill grows to fit it.
|
|
// A probe cut snapped to a shortened span's end can leave the span's
|
|
// kept trimmable character last in the probe's string, where the
|
|
// parser's trailing trim eats it and drops the object from that probe.
|
|
// That only lowers the probe's measured width, so the search runs a
|
|
// probe longer, never shorter, and the accepted prefix stays
|
|
// pixel-identical to the whole-label build: the dropped object lies
|
|
// past everything the renderer reaches, behind the first half that
|
|
// already measured wider than the pill can draw.
|
|
const auto resolved = ResolveRichButtonLabelDates(
|
|
label,
|
|
context.formattedDateFactory);
|
|
const auto nested = InlineButtonLabelContext(context, emojiSize);
|
|
const auto shortened = ShortenInlineButtonLabelSpans(resolved, nested);
|
|
const auto available = std::max(widthCap, 1);
|
|
const auto size = int(shortened.text.size());
|
|
auto exceeded = false;
|
|
auto length = kInlineButtonLabelProbeStart;
|
|
while (true) {
|
|
const auto cut = InlineButtonLabelCut(shortened, length);
|
|
if (cut >= size) {
|
|
return MakeInlineButtonLabel(shortened, labelStyle, nested);
|
|
}
|
|
auto result = MakeInlineButtonLabel(
|
|
Ui::Text::Mid(shortened, 0, cut),
|
|
labelStyle,
|
|
nested);
|
|
if (result.maxWidth() > available) {
|
|
if (exceeded) {
|
|
return result;
|
|
}
|
|
exceeded = true;
|
|
}
|
|
length = 2 * cut;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ClickHandlerPtr CreatePreparedLinkHandler(PreparedLink link) {
|
|
// A rich-page button link is deliberately not a PreparedLinkClickHandler:
|
|
// ExtractPreparedLink therefore refuses it, a hit carries only
|
|
// state.link, and both hosts activate that through the generic
|
|
// ActivateClickHandler arm the inline pill already uses. That is why this
|
|
// one kind gets no arm in the three prepared-link routers.
|
|
if (link.kind == PreparedLinkKind::RichPageButton) {
|
|
return std::make_shared<InlineButtonClickHandler>(
|
|
std::move(link.target));
|
|
}
|
|
return std::make_shared<PreparedLinkClickHandler>(std::move(link));
|
|
}
|
|
|
|
std::optional<PreparedLink> ExtractPreparedLink(const ClickHandlerPtr &link) {
|
|
if (const auto prepared = std::dynamic_pointer_cast<PreparedLinkClickHandler>(
|
|
link)) {
|
|
return prepared->link();
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
void BindLinks(
|
|
Ui::Text::String *leaf,
|
|
const std::vector<PreparedLink> &links) {
|
|
for (const auto &link : links) {
|
|
leaf->setLink(
|
|
link.index,
|
|
CreatePreparedLinkHandler(link));
|
|
}
|
|
}
|
|
|
|
void SetTextLeafSpoilerLinkFilter(
|
|
Ui::Text::String *leaf,
|
|
Fn<bool(const ClickContext&)> 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<PreparedFormulaSlot> &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<PreparedFormulaSlot> *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<RenderedFormula> *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<MathRenderer>();
|
|
if (!renderer) {
|
|
ownedRenderer = std::make_shared<MathRenderer>();
|
|
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<MathRenderer> renderer);
|
|
void clear();
|
|
void invalidatePaletteCache();
|
|
void invalidateRasterCache();
|
|
[[nodiscard]] std::unique_ptr<Ui::Text::CustomEmoji> create(
|
|
const InlineTextObjectFormulaData &data,
|
|
const style::TextStyle &textStyle,
|
|
const style::Markdown &st,
|
|
const std::vector<PreparedFormulaSlot> *formulas);
|
|
|
|
private:
|
|
[[nodiscard]] std::shared_ptr<InlineFormulaSharedState> lookupOrCreate(
|
|
const PreparedFormulaMeasurementSignature &signature,
|
|
const style::TextStyle &textStyle,
|
|
const style::Markdown &st,
|
|
const std::vector<PreparedFormulaSlot> *formulas);
|
|
|
|
std::shared_ptr<MathRenderer> _renderer;
|
|
std::map<
|
|
PreparedFormulaMeasurementSignature,
|
|
std::shared_ptr<InlineFormulaSharedState>,
|
|
PreparedFormulaMeasurementSignatureLess> _states;
|
|
|
|
};
|
|
|
|
InlineFormulaSharedState::InlineFormulaSharedState(
|
|
PreparedFormulaMeasurementSignature signature,
|
|
std::shared_ptr<const MeasuredFormula> measuredData,
|
|
QString displayFallbackText,
|
|
std::shared_ptr<MathRenderer> 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<Ui::Text::CustomEmojiVerticalMetrics> {
|
|
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<MathRenderer> 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<MathRenderer>();
|
|
}
|
|
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<InlineFormulaSharedState> 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<Ui::Text::CustomEmojiVerticalMetrics> {
|
|
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<MathRenderer> 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<Ui::DynamicImage> image,
|
|
Fn<void()> repaint,
|
|
Fn<void(QRect)> 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<Ui::Text::CustomEmojiVerticalMetrics> {
|
|
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;
|
|
}
|
|
|
|
InlineButtonPlainEmoji::InlineButtonPlainEmoji(EmojiPtr emoji, int size)
|
|
: _emoji(emoji)
|
|
, _size(std::max(size, 1)) {
|
|
}
|
|
|
|
int InlineButtonPlainEmoji::width() {
|
|
return _size;
|
|
}
|
|
|
|
QString InlineButtonPlainEmoji::entityData() {
|
|
return InlineButtonPlainEmojiPrefix() + _emoji->text();
|
|
}
|
|
|
|
auto InlineButtonPlainEmoji::vertical(const style::TextStyle &textStyle)
|
|
-> std::optional<Ui::Text::CustomEmojiVerticalMetrics> {
|
|
return CenteredVerticalMetrics(textStyle, _size);
|
|
}
|
|
|
|
void InlineButtonPlainEmoji::paint(QPainter &p, const Context &context) {
|
|
if (_frame.isNull()) {
|
|
const auto ratio = style::DevicePixelRatio();
|
|
const auto large = Ui::Emoji::GetSizeLarge();
|
|
_frame = QImage(
|
|
QSize(large, large),
|
|
QImage::Format_ARGB32_Premultiplied);
|
|
_frame.setDevicePixelRatio(ratio);
|
|
_frame.fill(Qt::transparent);
|
|
{
|
|
auto q = QPainter(&_frame);
|
|
Ui::Emoji::Draw(q, _emoji, large, 0, 0);
|
|
}
|
|
_frame = _frame.scaled(
|
|
QSize(_size, _size) * ratio,
|
|
Qt::IgnoreAspectRatio,
|
|
Qt::SmoothTransformation);
|
|
}
|
|
p.drawImage(context.position, _frame);
|
|
}
|
|
|
|
void InlineButtonPlainEmoji::unload() {
|
|
_frame = QImage();
|
|
}
|
|
|
|
bool InlineButtonPlainEmoji::ready() {
|
|
return true;
|
|
}
|
|
|
|
bool InlineButtonPlainEmoji::readyInDefaultState() {
|
|
return true;
|
|
}
|
|
|
|
InlineButtonScaledEmoji::InlineButtonScaledEmoji(
|
|
std::unique_ptr<Ui::Text::CustomEmoji> wrapped,
|
|
int size)
|
|
: _wrapped(std::move(wrapped))
|
|
, _size(std::max(size, 1)) {
|
|
}
|
|
|
|
int InlineButtonScaledEmoji::width() {
|
|
return _size;
|
|
}
|
|
|
|
QString InlineButtonScaledEmoji::entityData() {
|
|
return _wrapped->entityData();
|
|
}
|
|
|
|
auto InlineButtonScaledEmoji::vertical(const style::TextStyle &textStyle)
|
|
-> std::optional<Ui::Text::CustomEmojiVerticalMetrics> {
|
|
return CenteredVerticalMetrics(textStyle, _size);
|
|
}
|
|
|
|
void InlineButtonScaledEmoji::paint(QPainter &p, const Context &context) {
|
|
const auto ratio = style::DevicePixelRatio();
|
|
|
|
// A normal-size custom emoji advances by st::emojiSize plus twice
|
|
// st::emojiPadding, but it draws AdjustCustomEmojiSize(st::emojiSize)
|
|
// with its top-left exactly at the position it is handed, so its drawn
|
|
// box is not its advance and a frame sized from the advance would wrap
|
|
// the glyph in a transparent border. This wrapper reports both width()
|
|
// and vertical()->height() as _size, so the renderer paints the wrapped
|
|
// object at the advance box's own top-left instead of at the padded
|
|
// position, and rasterizing exactly the drawn box and stretching it
|
|
// across that whole box is what lets neighbouring pieces tile.
|
|
const auto drawn = Ui::Text::AdjustCustomEmojiSize(st::emojiSize);
|
|
const auto full = QSize(drawn, drawn) * ratio;
|
|
if (_frame.size() != full) {
|
|
_frame = QImage(full, QImage::Format_ARGB32_Premultiplied);
|
|
_frame.setDevicePixelRatio(ratio);
|
|
}
|
|
_frame.fill(Qt::transparent);
|
|
{
|
|
auto q = QPainter(&_frame);
|
|
q.translate(-context.position);
|
|
_wrapped->paint(q, context);
|
|
}
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.drawImage(QRect(context.position, QSize(_size, _size)), _frame);
|
|
}
|
|
|
|
void InlineButtonScaledEmoji::unload() {
|
|
_frame = QImage();
|
|
_wrapped->unload();
|
|
}
|
|
|
|
bool InlineButtonScaledEmoji::ready() {
|
|
return _wrapped->ready();
|
|
}
|
|
|
|
bool InlineButtonScaledEmoji::readyInDefaultState() {
|
|
return _wrapped->readyInDefaultState();
|
|
}
|
|
|
|
InlineButtonObject::InlineButtonObject(
|
|
const InlineTextObjectButtonData &data,
|
|
const style::TextStyle &textStyle,
|
|
const style::Markdown &st,
|
|
const Ui::Text::MarkedContext &context,
|
|
std::shared_ptr<InlineButtonPaintState> paintState,
|
|
int widthCap)
|
|
: _entityData(SerializeInlineTextObjectEntity({
|
|
.kind = InlineTextObjectKind::Button,
|
|
.data = data,
|
|
}))
|
|
, _replacementText(data.label.text)
|
|
, _labelEntities(Ui::Text::Filtered(
|
|
data.label,
|
|
{ EntityType::CustomEmoji }).entities)
|
|
, _loadingKey(RichButtonLoadingKey(InlineButtonRecord(data)))
|
|
, _st(&st)
|
|
, _icon(RichButtonIcon(data.type))
|
|
, _paintState(std::move(paintState))
|
|
, _label(MakeBoundedInlineButtonLabel(
|
|
data.label,
|
|
st.inlineButton.labelStyle,
|
|
context,
|
|
InlineButtonEmojiSize(textStyle, st.inlineButton),
|
|
InlineButtonLabelWidthCap(
|
|
st.inlineButton,
|
|
st.inlineButton.maxWidth,
|
|
st.inlineButton.padding)))
|
|
, _color(data.color)
|
|
, _disabled(data.type == HistoryMessageMarkupButton::Type::Disabled)
|
|
, _labelHasParagraphBreak(
|
|
ranges::any_of(data.label.text, Ui::Text::IsNewline)) {
|
|
const auto &inlineSt = st.inlineButton;
|
|
const auto padding = inlineSt.padding;
|
|
_height = InlineButtonPillHeight(textStyle, inlineSt);
|
|
const auto inset = _icon
|
|
? std::max((_height - _icon->height()) / 2, 0)
|
|
: 0;
|
|
const auto trailing = _icon
|
|
? (2 * inset + _icon->width())
|
|
: padding;
|
|
const auto buttonWidthCap = InlineButtonWidthCap(
|
|
inlineSt,
|
|
_paintState,
|
|
widthCap);
|
|
_labelWidth = std::min(
|
|
_label.maxWidth(),
|
|
InlineButtonLabelWidthCap(inlineSt, buttonWidthCap, trailing));
|
|
_width = std::max(_height, padding + _labelWidth + trailing);
|
|
_vertical = CenteredVerticalMetrics(textStyle, _height);
|
|
_lineTopSkip = TextLineAscent(textStyle) - _vertical.ascent;
|
|
_lineHeight = TextLineHeight(textStyle);
|
|
_labelLeft = padding;
|
|
_labelTop = std::clamp(
|
|
_vertical.ascent - inlineSt.labelStyle.font->ascent,
|
|
0,
|
|
std::max(_height - inlineSt.labelStyle.font->height, 0));
|
|
_iconLeft = _icon ? (_width - inset - _icon->width()) : 0;
|
|
}
|
|
|
|
int InlineButtonObject::width() {
|
|
return _width;
|
|
}
|
|
|
|
QString InlineButtonObject::entityData() {
|
|
return _entityData;
|
|
}
|
|
|
|
auto InlineButtonObject::vertical(const style::TextStyle &)
|
|
-> std::optional<Ui::Text::CustomEmojiVerticalMetrics> {
|
|
return _vertical;
|
|
}
|
|
|
|
QString InlineButtonObject::replacementText() {
|
|
return _replacementText;
|
|
}
|
|
|
|
EntitiesInText InlineButtonObject::replacementEntities() {
|
|
return _labelEntities;
|
|
}
|
|
|
|
Ui::Text::CustomEmojiSemantics InlineButtonObject::semantics() {
|
|
return {
|
|
.isEmoji = false,
|
|
.isRealCustomEmoji = false,
|
|
.exportEntity = false,
|
|
.unloadPersistentAnimation = true,
|
|
.allowCustomEmojiClick = !_disabled,
|
|
};
|
|
}
|
|
|
|
const style::Markdown &InlineButtonObject::resolvedStyle() const {
|
|
return (_paintState && _paintState->st) ? *_paintState->st : *_st;
|
|
}
|
|
|
|
bool InlineButtonObject::labelWouldOverflowIcon(
|
|
const style::Markdown &st) const {
|
|
return _icon
|
|
&& ((_labelWidth < _label.maxWidth()) || _labelHasParagraphBreak)
|
|
&& (_labelWidth < st.inlineButton.labelStyle.font->elidew);
|
|
}
|
|
|
|
void InlineButtonObject::paintContent(
|
|
QPainter &p,
|
|
QPoint position,
|
|
QColor color,
|
|
const style::Markdown &st,
|
|
const Context &context) const {
|
|
if (_icon) {
|
|
_icon->paintInCenter(
|
|
p,
|
|
QRect(
|
|
position.x() + _iconLeft,
|
|
position.y(),
|
|
_icon->width(),
|
|
_height),
|
|
color);
|
|
}
|
|
if (_label.isEmpty() || labelWouldOverflowIcon(st)) {
|
|
return;
|
|
}
|
|
const auto available = std::max(_labelWidth, 1);
|
|
p.setPen(color);
|
|
_label.draw(p, {
|
|
.position = position + QPoint(_labelLeft, _labelTop),
|
|
.availableWidth = available,
|
|
.geometry = Ui::Text::SimpleGeometry(available, 1, 0, false),
|
|
.palette = &st.textPalette,
|
|
.now = context.now,
|
|
.paused = context.paused,
|
|
.elisionLines = 1,
|
|
});
|
|
}
|
|
|
|
void InlineButtonObject::paint(QPainter &p, const Context &context) {
|
|
const auto &markdownSt = resolvedStyle();
|
|
const auto &st = markdownSt.inlineButton;
|
|
const auto position = context.position;
|
|
const auto rect = QRect(position, QSize(_width, _height));
|
|
const auto radius = _height / 2;
|
|
const auto state = _paintState.get();
|
|
const auto lineRect = QRect(
|
|
rect.x(),
|
|
rect.y() - _lineTopSkip,
|
|
_width,
|
|
_lineHeight);
|
|
if (state
|
|
&& state->pressPending
|
|
&& lineRect.contains(state->pressPoint)) {
|
|
state->pressPending = false;
|
|
if (!_disabled) {
|
|
state->rippleRect = rect;
|
|
AddPillRipple(
|
|
&state->ripple,
|
|
&state->rippleSize,
|
|
rect.size(),
|
|
state->pressPoint - rect.topLeft(),
|
|
state->repaint);
|
|
}
|
|
}
|
|
const auto ripple = (state
|
|
&& state->ripple
|
|
&& (state->rippleRect == rect))
|
|
? state->ripple.get()
|
|
: nullptr;
|
|
const auto loading = state
|
|
? RichButtonLoadingActive(state->buttonLoading, _loadingKey)
|
|
: nullptr;
|
|
const auto fillPill = [&](
|
|
QPainter &q,
|
|
const RichButtonPillColors &colors,
|
|
bool eraseRipple) {
|
|
auto hq = PainterHighQualityEnabler(q);
|
|
q.setPen(Qt::NoPen);
|
|
q.setBrush(colors.bg);
|
|
q.drawRoundedRect(rect, radius, radius);
|
|
if (ripple) {
|
|
const auto mode = q.compositionMode();
|
|
if (eraseRipple) {
|
|
q.setCompositionMode(
|
|
QPainter::CompositionMode_DestinationOut);
|
|
}
|
|
ripple->paint(
|
|
q,
|
|
rect.x(),
|
|
rect.y(),
|
|
rect.x() * 2 + rect.width(),
|
|
&colors.ripple);
|
|
q.setCompositionMode(mode);
|
|
}
|
|
};
|
|
p.save();
|
|
const auto colors = (state && state->bubbleGradient)
|
|
? BubbleGradientPillColors(
|
|
markdownSt,
|
|
st.tintBgOpacity,
|
|
(_color == ButtonColor::Primary))
|
|
: ResolveInlineButtonColors(_color, markdownSt);
|
|
if (colors.punchOut) {
|
|
PaintPunchedOutPill(
|
|
p,
|
|
rect,
|
|
_disabled ? st.disabledPrimaryOpacity : 1.,
|
|
[&](QPainter &q) {
|
|
fillPill(q, colors, colors.punchOut);
|
|
if (loading) {
|
|
PaintRichButtonLoading(q, loading, colors, rect, radius);
|
|
}
|
|
},
|
|
[&](QPainter &q, QColor fg) {
|
|
paintContent(q, position, fg, markdownSt, context);
|
|
});
|
|
} else {
|
|
const auto primary = (_color == ButtonColor::Primary);
|
|
fillPill(p, colors, false);
|
|
if (loading) {
|
|
PaintRichButtonLoading(p, loading, colors, rect, radius);
|
|
}
|
|
if (_disabled) {
|
|
p.setOpacity(p.opacity() * (primary
|
|
? st.disabledPrimaryOpacity
|
|
: st.disabledOpacity));
|
|
}
|
|
paintContent(p, position, colors.fg, markdownSt, context);
|
|
}
|
|
p.restore();
|
|
}
|
|
|
|
void InlineButtonObject::unload() {
|
|
_label.unloadPersistentAnimation();
|
|
}
|
|
|
|
bool InlineButtonObject::ready() {
|
|
return true;
|
|
}
|
|
|
|
bool InlineButtonObject::readyInDefaultState() {
|
|
return true;
|
|
}
|
|
|
|
std::unique_ptr<Ui::Text::CustomEmoji> InlineFormulaObjectCache::create(
|
|
const InlineTextObjectFormulaData &data,
|
|
const style::TextStyle &textStyle,
|
|
const style::Markdown &st,
|
|
const std::vector<PreparedFormulaSlot> *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<InlineFormulaObject>(
|
|
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<PreparedFormulaSlot> *formulas)
|
|
-> std::shared_ptr<InlineFormulaSharedState> {
|
|
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<MathRenderer>();
|
|
}
|
|
measured = _renderer->measureFormula({
|
|
.trimmedTex = signature.trimmedTex,
|
|
.kind = signature.kind,
|
|
.textSize = signature.textSize,
|
|
.renderWidthCap = signature.renderWidthCap,
|
|
.renderHeightCap = signature.renderHeightCap,
|
|
});
|
|
measuredData = std::make_shared<MeasuredFormula>(measured);
|
|
}
|
|
const auto fallbackText = InlineFormulaDisplayFallbackText(
|
|
signature,
|
|
measured);
|
|
auto state = std::make_shared<InlineFormulaSharedState>(
|
|
signature,
|
|
std::move(measuredData),
|
|
fallbackText,
|
|
_renderer);
|
|
_states.emplace(signature, state);
|
|
return state;
|
|
}
|
|
|
|
std::shared_ptr<InlineFormulaObjectCache> CreateInlineFormulaObjectCache(
|
|
std::shared_ptr<MathRenderer> renderer) {
|
|
auto result = std::make_shared<InlineFormulaObjectCache>();
|
|
result->setRenderer(std::move(renderer));
|
|
return result;
|
|
}
|
|
|
|
void SetInlineFormulaObjectCacheRenderer(
|
|
const std::shared_ptr<InlineFormulaObjectCache> &cache,
|
|
std::shared_ptr<MathRenderer> renderer) {
|
|
if (cache) {
|
|
cache->setRenderer(std::move(renderer));
|
|
}
|
|
}
|
|
|
|
void ClearInlineFormulaObjectCache(
|
|
const std::shared_ptr<InlineFormulaObjectCache> &cache) {
|
|
if (cache) {
|
|
cache->clear();
|
|
}
|
|
}
|
|
|
|
void InvalidateInlineFormulaPaletteCache(
|
|
const std::shared_ptr<InlineFormulaObjectCache> &cache) {
|
|
if (cache) {
|
|
cache->invalidatePaletteCache();
|
|
}
|
|
}
|
|
|
|
void InvalidateInlineFormulaRasterCache(
|
|
const std::shared_ptr<InlineFormulaObjectCache> &cache) {
|
|
if (cache) {
|
|
cache->invalidateRasterCache();
|
|
}
|
|
}
|
|
|
|
bool TextHasInlineButton(const TextWithEntities &text) {
|
|
return ranges::any_of(text.entities, [](const EntityInText &entity) {
|
|
return (entity.type() == EntityType::CustomEmoji)
|
|
&& InlineButtonDataFor(entity.data()).has_value();
|
|
});
|
|
}
|
|
|
|
QString InlineButtonTooltip(QStringView data) {
|
|
const auto button = ActionableInlineButtonDataFor(data);
|
|
return button
|
|
? RichButtonTooltip(button->type, button->data, QString())
|
|
: QString();
|
|
}
|
|
|
|
void SetTextLeaf(
|
|
Ui::Text::String *leaf,
|
|
const style::TextStyle &textStyle,
|
|
const style::Markdown &st,
|
|
const TextWithEntities &text,
|
|
const std::vector<PreparedFormulaSlot> *formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<InlineButtonPaintState> &inlineButtonPaintState,
|
|
int inlineButtonWidthCap,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
int minResizeWidth,
|
|
bool rtl,
|
|
Fn<void()> repaint,
|
|
Fn<void(QRect)> repaintRect,
|
|
Fn<bool(const ClickContext&)> spoilerLinkFilter,
|
|
bool richButtonLabel) {
|
|
*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,
|
|
inlineButtonPaintState,
|
|
inlineButtonWidthCap,
|
|
mediaRuntime,
|
|
repaintRect = std::move(repaintRect),
|
|
originalCustomEmojiFactory = std::move(originalCustomEmojiFactory),
|
|
textStyle = textStylePtr,
|
|
st = stPtr
|
|
](
|
|
QStringView data,
|
|
const Ui::Text::MarkedContext &context
|
|
) -> std::unique_ptr<Ui::Text::CustomEmoji> {
|
|
const auto parsed = ParseInlineTextObjectEntity(data);
|
|
if (!parsed) {
|
|
return originalCustomEmojiFactory
|
|
? originalCustomEmojiFactory(data, context)
|
|
: std::unique_ptr<Ui::Text::CustomEmoji>();
|
|
}
|
|
switch (parsed->kind) {
|
|
case InlineTextObjectKind::Formula: {
|
|
if (!inlineFormulaObjects) {
|
|
return std::unique_ptr<Ui::Text::CustomEmoji>();
|
|
}
|
|
const auto formula = std::get_if<InlineTextObjectFormulaData>(
|
|
&parsed->data);
|
|
return formula
|
|
? inlineFormulaObjects->create(
|
|
*formula,
|
|
*textStyle,
|
|
*st,
|
|
formulas)
|
|
: std::unique_ptr<Ui::Text::CustomEmoji>();
|
|
} break;
|
|
case InlineTextObjectKind::IvImage: {
|
|
const auto image = std::get_if<InlineTextObjectIvImageData>(
|
|
&parsed->data);
|
|
if (!image) {
|
|
return std::unique_ptr<Ui::Text::CustomEmoji>();
|
|
}
|
|
const auto resolved = mediaRuntime
|
|
? mediaRuntime->resolveInlineImage(
|
|
image->documentId,
|
|
QSize(image->width, image->height))
|
|
: nullptr;
|
|
return std::make_unique<InlineIvImageObject>(
|
|
image->replacementText,
|
|
image->width,
|
|
image->height,
|
|
std::move(resolved),
|
|
context.repaint,
|
|
repaintRect);
|
|
}
|
|
case InlineTextObjectKind::Button: {
|
|
const auto button = std::get_if<InlineTextObjectButtonData>(
|
|
&parsed->data);
|
|
if (!button) {
|
|
return std::unique_ptr<Ui::Text::CustomEmoji>();
|
|
}
|
|
return std::make_unique<InlineButtonObject>(
|
|
*button,
|
|
*textStyle,
|
|
*st,
|
|
context,
|
|
inlineButtonPaintState,
|
|
inlineButtonWidthCap);
|
|
}
|
|
}
|
|
return std::unique_ptr<Ui::Text::CustomEmoji>();
|
|
};
|
|
const auto resolved = richButtonLabel
|
|
? ResolveRichButtonLabelDates(text, context.formattedDateFactory)
|
|
: TextWithEntities();
|
|
leaf->setMarkedText(
|
|
textStyle,
|
|
richButtonLabel ? resolved : text,
|
|
rtl ? kIvMarkedTextOptionsRtl : kIvMarkedTextOptions,
|
|
context);
|
|
SetTextLeafSpoilerLinkFilter(leaf, std::move(spoilerLinkFilter));
|
|
if (inlineButtonPaintState
|
|
&& !inlineButtonPaintState->editMode
|
|
&& ranges::any_of(text.entities, [](const EntityInText &entity) {
|
|
return (entity.type() == EntityType::CustomEmoji)
|
|
&& InlineButtonActionable(entity.data());
|
|
})) {
|
|
leaf->setCustomEmojiClickHandler([
|
|
state = inlineButtonPaintState
|
|
](QStringView data) {
|
|
if (!InlineButtonActionable(data)) {
|
|
return false;
|
|
}
|
|
state->lookedUpButton = data.toString();
|
|
return true;
|
|
}, ActivateInlineButton);
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<Ui::Text::CustomEmoji> MakeInlineButtonObject(
|
|
QStringView data,
|
|
const style::TextStyle &textStyle,
|
|
const style::Markdown &st,
|
|
const Ui::Text::MarkedContext &context) {
|
|
const auto button = InlineButtonDataFor(data);
|
|
if (!button) {
|
|
return nullptr;
|
|
}
|
|
return std::make_unique<InlineButtonObject>(
|
|
*button,
|
|
textStyle,
|
|
st,
|
|
context,
|
|
nullptr,
|
|
0);
|
|
}
|
|
|
|
} // namespace Iv::Markdown
|