Support IV-specific tags in the editor.

This commit is contained in:
John Preston
2026-06-02 13:07:55 +04:00
parent 7118a7032e
commit 2d372c29cc
14 changed files with 918 additions and 59 deletions
@@ -403,7 +403,8 @@ Fn<bool(
EditLinkAction action)> DefaultEditLinkCallback(
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::InputField*> field,
const style::InputField *fieldStyle) {
const style::InputField *fieldStyle,
Fn<QString(QString)> linkValidator) {
const auto weak = base::make_weak(field);
return [=](
EditLinkSelection selection,
@@ -413,7 +414,8 @@ Fn<bool(
if (action == EditLinkAction::Check) {
return (Ui::InputField::IsValidMarkdownLink(link)
&& !TextUtilities::IsMentionLink(link))
|| Ui::InputField::IsCustomDateLink(link);
|| Ui::InputField::IsCustomDateLink(link)
|| (linkValidator && !linkValidator(link).isEmpty());
}
if (Ui::InputField::IsCustomDateLink(link)) {
const auto dateStr = link.mid(
@@ -479,6 +481,9 @@ Fn<bool(
strong->commitMarkdownLinkEdit(selection, text, link);
}
};
const auto validateLink = linkValidator
? linkValidator
: Fn<QString(QString)>(qthelp::validate_url);
show->showBox(Box(
EditLinkBox,
show,
@@ -486,7 +491,7 @@ Fn<bool(
link,
std::move(callback),
fieldStyle,
qthelp::validate_url));
validateLink));
return true;
};
}
@@ -523,7 +528,11 @@ auto InitMessageFieldHandlers(MessageFieldHandlersArgs &&args)
}));
if (const auto &show = args.show) {
field->setEditLinkCallback(
DefaultEditLinkCallback(show, field, args.fieldStyle));
DefaultEditLinkCallback(
show,
field,
args.fieldStyle,
args.linkValidator));
field->setEditLanguageCallback(DefaultEditLanguageCallback(show));
InitSpellchecker(show, field, args.fieldStyle != nullptr);
}
@@ -66,7 +66,8 @@ Fn<bool(
Ui::InputField::EditLinkAction action)> DefaultEditLinkCallback(
std::shared_ptr<Main::SessionShow> show,
not_null<Ui::InputField*> field,
const style::InputField *fieldStyle = nullptr);
const style::InputField *fieldStyle = nullptr,
Fn<QString(QString)> linkValidator = nullptr);
Fn<void(QString now, Fn<void(QString)> save)> DefaultEditLanguageCallback(
std::shared_ptr<Ui::Show> show);
@@ -77,6 +78,7 @@ struct MessageFieldHandlersArgs {
Fn<bool()> customEmojiPaused;
Fn<bool(not_null<DocumentData*>)> allowPremiumEmoji;
const style::InputField *fieldStyle = nullptr;
Fn<QString(QString)> linkValidator;
base::flat_set<QString> allowMarkdownTags;
};
auto InitMessageFieldHandlers(MessageFieldHandlersArgs &&args)
@@ -2137,8 +2137,8 @@ void State::rebuildTextNodes(
})
: std::nullopt);
}
appendBlockTextNode(path, LeafKind::BlockCaption);
rebuildTextNodes(block.blocks, BlockChildrenContainer(path));
appendBlockTextNode(path, LeafKind::BlockCaption);
break;
case BlockKind::List:
for (auto j = 0, itemCount = int(block.listItems.size());
@@ -0,0 +1,722 @@
/*
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/editor/iv_editor_text_entities.h"
#include "iv/markdown/iv_markdown_prepare_serialize.h"
#include "iv/markdown/iv_markdown_prepare_links.h"
#include "ui/widgets/fields/input_field.h"
#include <algorithm>
#include <optional>
#include <vector>
namespace Iv::Editor {
namespace {
struct FormulaReplacement {
int offset = 0;
int length = 0;
QString source;
};
struct TextRange {
int offset = 0;
int length = 0;
};
[[nodiscard]] bool RangeInsideText(
const QString &text,
int offset,
int length) {
return (offset >= 0)
&& (length >= 0)
&& (offset <= text.size())
&& (length <= text.size() - offset);
}
[[nodiscard]] bool IsFormulaObjectSpan(
const QString &text,
const EntityInText &entity) {
return (entity.length() == 1)
&& RangeInsideText(text, entity.offset(), entity.length())
&& (text[entity.offset()] == QChar::ObjectReplacementCharacter);
}
[[nodiscard]] std::optional<Markdown::InlineTextObjectFormulaData>
FormulaDataFromEntity(const EntityInText &entity) {
if (entity.type() != EntityType::CustomEmoji) {
return std::nullopt;
}
const auto parsed = Markdown::ParseInlineTextObjectEntity(entity.data());
if (!parsed || parsed->kind != Markdown::InlineTextObjectKind::Formula) {
return std::nullopt;
}
const auto formula = std::get_if<Markdown::InlineTextObjectFormulaData>(
&parsed->data);
return formula ? std::make_optional(*formula) : std::nullopt;
}
[[nodiscard]] QString EditorSourceForFormula(
const Markdown::InlineTextObjectFormulaData &formula) {
const auto stripDelimiters = [](QString result) {
result = result.trimmed();
if (result.size() >= 2
&& result.front() == QChar('$')
&& result.back() == QChar('$')) {
result = result.mid(1, result.size() - 2).trimmed();
}
return result;
};
if (!formula.trimmedTex.isEmpty()) {
return Markdown::InlineFormulaCopySource(
stripDelimiters(formula.trimmedTex));
}
return Markdown::InlineFormulaCopySource(
stripDelimiters(formula.copySource));
}
[[nodiscard]] std::optional<EntityInText> AdjustEntityForReplacement(
const EntityInText &entity,
int from,
int oldLength,
int newLength) {
const auto till = from + oldLength;
const auto delta = newLength - oldLength;
const auto begin = entity.offset();
const auto end = begin + entity.length();
if (end <= from) {
return entity;
} else if (begin >= till) {
return EntityInText(
entity.type(),
begin + delta,
entity.length(),
entity.data());
} else if (begin <= from && end >= till) {
const auto length = entity.length() + delta;
if (length <= 0) {
return std::nullopt;
}
return EntityInText(entity.type(), begin, length, entity.data());
}
return std::nullopt;
}
[[nodiscard]] std::optional<TextWithTags::Tag> AdjustTagForReplacement(
const TextWithTags::Tag &tag,
int from,
int oldLength,
int newLength) {
const auto till = from + oldLength;
const auto delta = newLength - oldLength;
const auto begin = tag.offset;
const auto end = begin + tag.length;
if (end <= from) {
return tag;
} else if (begin >= till) {
return TextWithTags::Tag{
.offset = begin + delta,
.length = tag.length,
.id = tag.id,
};
} else if (begin <= from && end >= till) {
const auto length = tag.length + delta;
if (length <= 0) {
return std::nullopt;
}
return TextWithTags::Tag{
.offset = begin,
.length = length,
.id = tag.id,
};
}
return std::nullopt;
}
[[nodiscard]] QString TagsWithoutIvEditorTags(QStringView tags) {
auto result = QList<QStringView>();
for (const auto &tag : TextUtilities::SplitTags(tags)) {
if (!Ui::InputField::IsInstantViewEditorTag(tag)
&& !tag.startsWith(QChar('#'))) {
result.push_back(tag);
}
}
return TextUtilities::JoinTag(result);
}
[[nodiscard]] TextWithTags::Tags GenericTagsWithoutIvEditorTags(
const TextWithTags::Tags &tags) {
auto result = TextWithTags::Tags();
result.reserve(tags.size());
for (const auto &tag : tags) {
auto filtered = TagsWithoutIvEditorTags(tag.id);
if (!filtered.isEmpty()) {
result.push_back({
.offset = tag.offset,
.length = tag.length,
.id = filtered,
});
}
}
return result;
}
[[nodiscard]] bool IsValidAnchorEntity(const EntityInText &entity) {
if (entity.type() != EntityType::CustomUrl
|| !Ui::InputField::IsInstantViewAnchorLink(entity.data())) {
return false;
}
return !Markdown::NormalizeFragmentId(entity.data().mid(1)).isEmpty();
}
void SortTags(TextWithTags::Tags *tags);
void AppendIvEntityTags(
TextWithTags::Tags *tags,
const EntitiesInText &entities) {
for (const auto &entity : entities) {
if (entity.length() <= 0) {
continue;
}
switch (entity.type()) {
case EntityType::Subscript:
tags->push_back({
.offset = entity.offset(),
.length = entity.length(),
.id = Ui::InputField::kTagIvSubscript,
});
break;
case EntityType::Superscript:
tags->push_back({
.offset = entity.offset(),
.length = entity.length(),
.id = Ui::InputField::kTagIvSuperscript,
});
break;
case EntityType::Marked:
tags->push_back({
.offset = entity.offset(),
.length = entity.length(),
.id = Ui::InputField::kTagIvMarked,
});
break;
case EntityType::CustomUrl:
if (IsValidAnchorEntity(entity)) {
tags->push_back({
.offset = entity.offset(),
.length = entity.length(),
.id = entity.data(),
});
}
break;
default:
break;
}
}
}
void OverlayTag(
TextWithTags::Tags *tags,
const TextWithTags::Tag &overlay,
const QString &text) {
if (overlay.id.isEmpty()
|| overlay.length <= 0
|| !RangeInsideText(text, overlay.offset, overlay.length)) {
return;
}
const auto from = overlay.offset;
const auto till = from + overlay.length;
auto coveredTill = from;
auto result = TextWithTags::Tags();
result.reserve(tags->size() + 3);
for (const auto &tag : *tags) {
const auto tagFrom = tag.offset;
const auto tagTill = tag.offset + tag.length;
if (tagTill <= from) {
result.push_back(tag);
continue;
} else if (tagFrom >= till) {
if (coveredTill < till) {
result.push_back({
.offset = coveredTill,
.length = till - coveredTill,
.id = overlay.id,
});
coveredTill = till;
}
result.push_back(tag);
continue;
}
if (tagFrom > coveredTill) {
result.push_back({
.offset = coveredTill,
.length = tagFrom - coveredTill,
.id = overlay.id,
});
coveredTill = tagFrom;
}
if (tagFrom < from) {
result.push_back({
.offset = tagFrom,
.length = from - tagFrom,
.id = tag.id,
});
}
const auto middleFrom = std::max(tagFrom, from);
const auto middleTill = std::min(tagTill, till);
if (middleFrom < middleTill) {
result.push_back({
.offset = middleFrom,
.length = middleTill - middleFrom,
.id = TextUtilities::TagWithAdded(tag.id, overlay.id),
});
coveredTill = middleTill;
}
if (tagTill > till) {
result.push_back({
.offset = till,
.length = tagTill - till,
.id = tag.id,
});
}
}
if (coveredTill < till) {
result.push_back({
.offset = coveredTill,
.length = till - coveredTill,
.id = overlay.id,
});
}
SortTags(&result);
*tags = TextUtilities::SimplifyTags(std::move(result));
}
void OverlayTags(
TextWithTags::Tags *tags,
const TextWithTags::Tags &overlays,
const QString &text) {
for (const auto &overlay : overlays) {
OverlayTag(tags, overlay, text);
}
}
void SubtractRange(
std::vector<TextRange> *ranges,
int from,
int till) {
auto result = std::vector<TextRange>();
for (const auto &range : *ranges) {
const auto rangeFrom = range.offset;
const auto rangeTill = range.offset + range.length;
if (rangeTill <= from || rangeFrom >= till) {
result.push_back(range);
continue;
}
if (rangeFrom < from) {
result.push_back({
.offset = rangeFrom,
.length = from - rangeFrom,
});
}
if (rangeTill > till) {
result.push_back({
.offset = till,
.length = rangeTill - till,
});
}
}
*ranges = std::move(result);
}
void RemoveRangesFromTags(
TextWithTags::Tags *tags,
const TextWithTags::Tags &removed) {
auto result = TextWithTags::Tags();
for (const auto &tag : *tags) {
auto ranges = std::vector<TextRange>{ {
.offset = tag.offset,
.length = tag.length,
} };
for (const auto &remove : removed) {
SubtractRange(
&ranges,
remove.offset,
remove.offset + remove.length);
}
for (const auto &range : ranges) {
if (range.length > 0) {
result.push_back({
.offset = range.offset,
.length = range.length,
.id = tag.id,
});
}
}
}
SortTags(&result);
*tags = TextUtilities::SimplifyTags(std::move(result));
}
void SortTags(TextWithTags::Tags *tags) {
std::sort(tags->begin(), tags->end(), [](const auto &a, const auto &b) {
if (a.offset != b.offset) {
return a.offset < b.offset;
} else if (a.length != b.length) {
return a.length < b.length;
}
return a.id < b.id;
});
}
[[nodiscard]] bool TagContains(QStringView tags, QStringView tagId) {
return TextUtilities::SplitTags(tags).contains(tagId);
}
[[nodiscard]] bool TagContainsOtherThan(QStringView tags, QStringView tagId) {
for (const auto &tag : TextUtilities::SplitTags(tags)) {
if (tag != tagId) {
return true;
}
}
return false;
}
void MergeRanges(std::vector<TextRange> *ranges);
[[nodiscard]] std::vector<TextRange> RangesContainingTagId(
const TextWithTags::Tags &tags,
QStringView tagId,
const QString &text) {
auto result = std::vector<TextRange>();
result.reserve(tags.size());
for (const auto &tag : tags) {
if (tag.length <= 0
|| !RangeInsideText(text, tag.offset, tag.length)
|| !TagContains(tag.id, tagId)) {
continue;
}
result.push_back({
.offset = tag.offset,
.length = tag.length,
});
}
return result;
}
[[nodiscard]] EntitiesInText ValidAnchorEntitiesFromTags(
const TextWithTags::Tags &tags,
const QString &text) {
auto result = EntitiesInText();
result.reserve(tags.size());
for (const auto &tag : tags) {
if (tag.length <= 0 || !RangeInsideText(text, tag.offset, tag.length)) {
continue;
}
for (const auto &single : TextUtilities::SplitTags(tag.id)) {
if (!Ui::InputField::IsInstantViewAnchorLink(single)) {
continue;
}
auto link = single.toString();
if (Markdown::NormalizeFragmentId(link).isEmpty()) {
continue;
}
result.push_back(EntityInText(
EntityType::CustomUrl,
tag.offset,
tag.length,
link));
}
}
return result;
}
[[nodiscard]] std::vector<TextRange> MathRangesWithoutIntersections(
const TextWithTags::Tags &tags,
const QString &text) {
auto result = std::vector<TextRange>();
for (const auto &math : tags) {
if (math.length <= 0
|| !RangeInsideText(text, math.offset, math.length)
|| !TagContains(math.id, Ui::InputField::kTagIvMath)) {
continue;
}
auto ranges = std::vector<TextRange>{ {
.offset = math.offset,
.length = math.length,
} };
for (const auto &other : tags) {
if (other.length <= 0
|| !RangeInsideText(text, other.offset, other.length)
|| !TagContainsOtherThan(
other.id,
Ui::InputField::kTagIvMath)) {
continue;
}
SubtractRange(
&ranges,
other.offset,
other.offset + other.length);
}
result.insert(result.end(), ranges.begin(), ranges.end());
}
MergeRanges(&result);
return result;
}
void AppendEntitiesForTagId(
EntitiesInText *entities,
const TextWithTags::Tags &tags,
QStringView tagId,
EntityType type,
const QString &text) {
for (const auto &range : RangesContainingTagId(tags, tagId, text)) {
entities->push_back(EntityInText(type, range.offset, range.length));
}
}
void MergeRanges(std::vector<TextRange> *ranges) {
std::sort(ranges->begin(), ranges->end(), [](const auto &a, const auto &b) {
if (a.offset != b.offset) {
return a.offset < b.offset;
}
return a.length < b.length;
});
auto result = std::vector<TextRange>();
result.reserve(ranges->size());
for (const auto &range : *ranges) {
if (result.empty()) {
result.push_back(range);
continue;
}
auto &last = result.back();
const auto lastEnd = last.offset + last.length;
const auto rangeEnd = range.offset + range.length;
if (range.offset > lastEnd) {
result.push_back(range);
} else if (rangeEnd > lastEnd) {
last.length = rangeEnd - last.offset;
}
}
*ranges = std::move(result);
}
void SortEntities(EntitiesInText *entities) {
std::sort(
entities->begin(),
entities->end(),
[](const auto &a, const auto &b) {
if (a.offset() != b.offset()) {
return a.offset() < b.offset();
} else if (a.length() != b.length()) {
return a.length() < b.length();
} else if (a.type() != b.type()) {
return int(a.type()) < int(b.type());
}
return a.data() < b.data();
});
}
} // namespace
RichTextEditorConversion ConvertRichTextToEditorTags(TextWithEntities text) {
auto formulas = std::vector<FormulaReplacement>();
auto entities = EntitiesInText();
entities.reserve(text.entities.size());
for (const auto &entity : text.entities) {
const auto formula = FormulaDataFromEntity(entity);
if (formula && IsFormulaObjectSpan(text.text, entity)) {
formulas.push_back({
.offset = entity.offset(),
.length = entity.length(),
.source = EditorSourceForFormula(*formula),
});
} else {
entities.push_back(entity);
}
}
std::sort(
formulas.begin(),
formulas.end(),
[](const auto &a, const auto &b) {
return a.offset > b.offset;
});
auto mathTags = TextWithTags::Tags();
auto replacements = std::vector<RichTextEditorOffsetReplacement>();
replacements.reserve(formulas.size());
for (const auto &formula : formulas) {
const auto newLength = formula.source.size();
for (auto i = entities.begin(); i != entities.end();) {
if (const auto adjusted = AdjustEntityForReplacement(
*i,
formula.offset,
formula.length,
newLength)) {
*i++ = *adjusted;
} else {
i = entities.erase(i);
}
}
for (auto i = mathTags.begin(); i != mathTags.end();) {
if (const auto adjusted = AdjustTagForReplacement(
*i,
formula.offset,
formula.length,
newLength)) {
*i++ = *adjusted;
} else {
i = mathTags.erase(i);
}
}
text.text.replace(formula.offset, formula.length, formula.source);
if (newLength > 0) {
mathTags.push_back({
.offset = formula.offset,
.length = newLength,
.id = Ui::InputField::kTagIvMath,
});
}
replacements.push_back({
.richOffset = formula.offset,
.richLength = formula.length,
.editorLength = newLength,
});
}
auto tags = TextUtilities::ConvertEntitiesToTextTags(entities);
auto ivTags = TextWithTags::Tags();
AppendIvEntityTags(&ivTags, entities);
RemoveRangesFromTags(&tags, mathTags);
RemoveRangesFromTags(&ivTags, mathTags);
OverlayTags(&tags, ivTags, text.text);
OverlayTags(&tags, mathTags, text.text);
SortTags(&tags);
tags = TextUtilities::SimplifyTags(tags);
std::sort(
replacements.begin(),
replacements.end(),
[](const auto &a, const auto &b) {
return a.richOffset < b.richOffset;
});
return {
.text = { text.text, tags },
.replacements = replacements,
};
}
int MapRichTextOffsetToEditorOffset(
const std::vector<RichTextEditorOffsetReplacement> &replacements,
int offset) {
auto delta = 0;
for (const auto &replacement : replacements) {
if (replacement.richLength <= 0) {
continue;
}
const auto richStart = replacement.richOffset;
const auto richEnd = richStart + replacement.richLength;
const auto editorStart = richStart + delta;
if (offset < richStart) {
break;
} else if (offset <= richEnd) {
return editorStart
+ ((offset == richEnd) ? replacement.editorLength : 0);
}
delta += replacement.editorLength - replacement.richLength;
}
return offset + delta;
}
TextWithEntities ConvertEditorTagsToRichText(TextWithTags text) {
auto entities = TextUtilities::ConvertTextTagsToEntities(
GenericTagsWithoutIvEditorTags(text.tags));
AppendEntitiesForTagId(
&entities,
text.tags,
Ui::InputField::kTagIvMarked,
EntityType::Marked,
text.text);
AppendEntitiesForTagId(
&entities,
text.tags,
Ui::InputField::kTagIvSubscript,
EntityType::Subscript,
text.text);
AppendEntitiesForTagId(
&entities,
text.tags,
Ui::InputField::kTagIvSuperscript,
EntityType::Superscript,
text.text);
for (const auto &entity : ValidAnchorEntitiesFromTags(
text.tags,
text.text)) {
entities.push_back(entity);
}
auto mathRanges = MathRangesWithoutIntersections(text.tags, text.text);
for (auto i = mathRanges.rbegin(); i != mathRanges.rend(); ++i) {
const auto source = text.text.mid(i->offset, i->length);
auto trimmedSource = source.trimmed();
if (trimmedSource.size() >= 2
&& trimmedSource.front() == QChar('$')
&& trimmedSource.back() == QChar('$')) {
trimmedSource = trimmedSource
.mid(1, trimmedSource.size() - 2)
.trimmed();
}
if (trimmedSource.isEmpty()) {
continue;
}
const auto entityData = Markdown::SerializeInlineTextObjectEntity({
.kind = Markdown::InlineTextObjectKind::Formula,
.data = Markdown::InlineTextObjectFormulaData{
.copySource = Markdown::InlineFormulaCopySource(trimmedSource),
.trimmedTex = trimmedSource,
},
});
for (auto j = entities.begin(); j != entities.end();) {
if (const auto adjusted = AdjustEntityForReplacement(
*j,
i->offset,
i->length,
1)) {
*j++ = *adjusted;
} else {
j = entities.erase(j);
}
}
text.text.replace(
i->offset,
i->length,
QString(QChar::ObjectReplacementCharacter));
entities.push_back(EntityInText(
EntityType::CustomEmoji,
i->offset,
1,
entityData));
}
SortEntities(&entities);
return {
.text = text.text,
.entities = entities,
};
}
} // namespace Iv::Editor
@@ -0,0 +1,34 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/text/text_entity.h"
#include <vector>
namespace Iv::Editor {
struct RichTextEditorOffsetReplacement {
int richOffset = 0;
int richLength = 0;
int editorLength = 0;
};
struct RichTextEditorConversion {
TextWithTags text;
std::vector<RichTextEditorOffsetReplacement> replacements;
};
[[nodiscard]] RichTextEditorConversion ConvertRichTextToEditorTags(
TextWithEntities text);
[[nodiscard]] int MapRichTextOffsetToEditorOffset(
const std::vector<RichTextEditorOffsetReplacement> &replacements,
int offset);
[[nodiscard]] TextWithEntities ConvertEditorTagsToRichText(TextWithTags text);
} // namespace Iv::Editor
@@ -7,12 +7,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "iv/editor/iv_editor_widget.h"
#include "base/qthelp_url.h"
#include "base/qt/qt_common_adapters.h"
#include "chat_helpers/message_field.h"
#include "data/data_msg_id.h"
#include "data/data_types.h"
#include "data/stickers/data_custom_emoji.h"
#include "iv/editor/iv_editor_text_entities.h"
#include "iv/markdown/iv_markdown_article_paint.h"
#include "iv/markdown/iv_markdown_prepare_links.h"
#include "iv/markdown/iv_markdown_prepare_native_richtext.h"
#include "spellcheck/spellcheck_highlight_syntax.h"
#include "ui/chat/chat_style.h"
@@ -156,6 +159,25 @@ void EnableQTextEditLineMetrics(style::Markdown &style) {
return false;
}
[[nodiscard]] QString ValidateInstantViewEditorLink(QString link) {
const auto normal = qthelp::validate_url(link);
if (!normal.isEmpty()) {
return normal;
}
link = link.trimmed();
const auto hasPayload = [&](const QString &prefix) {
return link.startsWith(prefix)
&& !link.mid(prefix.size()).trimmed().isEmpty();
};
if (hasPayload(u"mailto:"_q)
|| hasPayload(u"tel:"_q)
|| (link.startsWith(u"#"_q)
&& !Markdown::NormalizeFragmentId(link).isEmpty())) {
return link;
}
return QString();
}
[[nodiscard]] bool ImeEventProducesInput(
const QInputMethodEvent &e,
const QTextCursor &cursor) {
@@ -1479,6 +1501,9 @@ const Widget::CachedInlineFieldStyle &Widget::inlineFieldStyleFor(
auto key = inlineFieldStyleKey(data);
auto textFg = data.textFg;
auto ownedTextFg = std::shared_ptr<style::owned_color>();
auto ownedTextMarkBg = std::make_shared<style::owned_color>(
data.textMarkBg);
auto textMarkBg = ownedTextMarkBg->color();
if (_inlineFieldTextColorOverride
&& data.textFg.get() == _inlineFieldTextColorOverride->color().get()) {
ownedTextFg = std::make_shared<style::owned_color>(data.textFg->c);
@@ -1498,6 +1523,7 @@ const Widget::CachedInlineFieldStyle &Widget::inlineFieldStyleFor(
: data.textStyle->font;
fieldStyle->style.lineHeight = data.lineHeight;
fieldStyle->textFg = textFg;
fieldStyle->textMarkBg = textMarkBg;
fieldStyle->textAlign = data.align;
fieldStyle->placeholderFont = fieldStyle->style.font;
fieldStyle->placeholderAlign = data.align;
@@ -1505,6 +1531,7 @@ const Widget::CachedInlineFieldStyle &Widget::inlineFieldStyleFor(
.key = key,
.style = std::move(fieldStyle),
.ownedTextFg = std::move(ownedTextFg),
.ownedTextMarkBg = std::move(ownedTextMarkBg),
});
return _fieldStyles.back();
}
@@ -1558,6 +1585,9 @@ Widget::InlineFieldStyleData Widget::normalizedInlineFieldStyle(
.textFg = _inlineFieldTextColorOverride
? _inlineFieldTextColorOverride->color()
: (valid ? leafStyle.textColor : _articleStyle->textColor),
.textMarkBg = valid
? leafStyle.markBg
: _articleStyle->textPalette.markBg->c,
.align = valid ? leafStyle.align : style::al_left,
.italic = valid ? leafStyle.italic : false,
};
@@ -1574,6 +1604,7 @@ Widget::InlineFieldStyleKey Widget::inlineFieldStyleKey(
: textStyle->font,
.lineHeight = data.lineHeight,
.textFg = data.textFg,
.textMarkBg = data.textMarkBg,
.align = data.align,
};
}
@@ -1601,6 +1632,7 @@ void Widget::setupInlineField() {
not_null<DocumentData*> emoji) {
return Data::AllowEmojiWithoutPremium(peer, emoji);
};
_field->setInstantViewEditorTagsEnabled(true);
InitMessageFieldHandlers({
.session = &_controller->session(),
.show = _controller->uiShow(),
@@ -1611,11 +1643,13 @@ void Widget::setupInlineField() {
},
.allowPremiumEmoji = allowPremiumEmoji,
.fieldStyle = &_field->st(),
.linkValidator = ValidateInstantViewEditorLink,
});
_field->setMimeDataHook(WrappedMessageFieldMimeHook(
Ui::InputField::MimeDataHook(),
_field.get()));
} else {
_field->setInstantViewEditorTagsEnabled(false);
_field->setInstantReplacesEnabled(
rpl::single(false),
rpl::single(false));
@@ -1737,24 +1771,30 @@ void Widget::activateTextOrdinal(
ensureInlineFieldForSegment(segmentIndex);
refreshInlineFieldPlaceholder();
_settingField = true;
auto cursorSelectionFrom = selectionFrom;
auto cursorSelectionTo = selectionTo;
if (_state->activeFieldMode() == State::FieldMode::Raw) {
_field->setTextWithTags(
{ _state->activeRawText(), {} },
Ui::InputField::HistoryAction::Clear);
_article->clearTextLeafHeightOverride();
} else {
const auto activeText = _state->activeText();
const auto activeText = ConvertRichTextToEditorTags(
_state->activeText());
_field->setTextWithTags(
{
activeText.text,
TextUtilities::ConvertEntitiesToTextTags(activeText.entities),
},
activeText.text,
Ui::InputField::HistoryAction::Clear);
cursorSelectionFrom = MapRichTextOffsetToEditorOffset(
activeText.replacements,
selectionFrom);
cursorSelectionTo = MapRichTextOffsetToEditorOffset(
activeText.replacements,
selectionTo);
}
auto cursor = _field->textCursor();
const auto size = int(_field->getLastText().size());
const auto from = std::clamp(selectionFrom, 0, size);
const auto to = std::clamp(selectionTo, 0, size);
const auto from = std::clamp(cursorSelectionFrom, 0, size);
const auto to = std::clamp(cursorSelectionTo, 0, size);
cursor.setPosition(from);
if (to != from) {
cursor.setPosition(to, QTextCursor::KeepAnchor);
@@ -1784,10 +1824,7 @@ void Widget::applyFieldTextToState() {
return;
}
const auto text = _field->getTextWithAppliedMarkdown();
_state->applyActiveText({
.text = text.text,
.entities = TextUtilities::ConvertTextTagsToEntities(text.tags),
});
_state->applyActiveText(ConvertEditorTagsToRichText(text));
}
void Widget::hideInlineField() {
@@ -85,6 +85,7 @@ private:
const style::TextStyle *textStyle = nullptr;
int lineHeight = 0;
style::color textFg;
QColor textMarkBg;
style::align align = style::al_left;
bool italic = false;
};
@@ -93,6 +94,7 @@ private:
style::font font;
int lineHeight = 0;
style::color textFg;
QColor textMarkBg;
style::align align = style::al_left;
friend inline bool operator==(
@@ -101,6 +103,7 @@ private:
return (a.font == b.font)
&& (a.lineHeight == b.lineHeight)
&& (a.textFg == b.textFg)
&& (a.textMarkBg == b.textMarkBg)
&& (a.align == b.align);
}
@@ -115,6 +118,7 @@ private:
InlineFieldStyleKey key;
std::shared_ptr<style::InputField> style;
std::shared_ptr<style::owned_color> ownedTextFg;
std::shared_ptr<style::owned_color> ownedTextMarkBg;
};
enum class DragSelectionMode {
@@ -38,6 +38,17 @@ using TaskState = RichPage::TaskState;
constexpr auto kDefaultMapWidth = 400;
constexpr auto kDefaultMapHeight = 200;
constexpr auto kNoEntityIndex = -1;
[[nodiscard]] QString FormulaTexFromSource(QString source) {
source = source.trimmed();
if (source.size() >= 2
&& source.front() == QChar('$')
&& source.back() == QChar('$')) {
source = source.mid(1, source.size() - 2).trimmed();
}
return source;
}
struct SerializeContext {
not_null<Main::Session*> session;
@@ -250,24 +261,42 @@ struct SerializeContext {
return result;
}
[[nodiscard]] const EntityInText *FindOuterEntityAt(
[[nodiscard]] bool SkipEntityForRange(
const std::vector<EntityInText> &entities,
int index,
int skipIndex) {
if (index == skipIndex) {
return true;
} else if (skipIndex == kNoEntityIndex) {
return false;
}
const auto &entity = entities[index];
const auto &skip = entities[skipIndex];
return (index < skipIndex)
&& (entity.offset() == skip.offset())
&& (entity.length() == skip.length());
}
[[nodiscard]] int FindOuterEntityAt(
const std::vector<EntityInText> &entities,
int position,
int till,
const EntityInText *skip) {
for (const auto &entity : entities) {
if (&entity == skip) {
int skipIndex) {
const auto count = int(entities.size());
for (auto index = 0; index != count; ++index) {
const auto &entity = entities[index];
if (SkipEntityForRange(entities, index, skipIndex)) {
continue;
}
if (entity.offset() == position
&& entity.offset() + entity.length() <= till) {
return &entity;
return index;
}
if (entity.offset() > position) {
break;
}
}
return nullptr;
return kNoEntityIndex;
}
[[nodiscard]] std::optional<MTPRichText> SerializeRichTextRange(
@@ -276,13 +305,14 @@ struct SerializeContext {
int from,
int till,
SerializeContext *context,
const EntityInText *skip);
int skipIndex);
[[nodiscard]] std::optional<MTPRichText> SerializeRichTextEntity(
const QString &text,
const std::vector<EntityInText> &entities,
const EntityInText &entity,
int entityIndex,
SerializeContext *context) {
const auto &entity = entities[entityIndex];
const auto from = entity.offset();
const auto length = entity.length();
const auto segment = text.mid(from, length);
@@ -292,7 +322,7 @@ struct SerializeContext {
from,
from + length,
context,
&entity);
entityIndex);
if (!inner) {
return std::nullopt;
}
@@ -362,9 +392,9 @@ struct SerializeContext {
if (!formula) {
return std::nullopt;
}
const auto source = !formula->copySource.isEmpty()
? formula->copySource
: formula->trimmedTex;
const auto source = !formula->trimmedTex.isEmpty()
? formula->trimmedTex
: FormulaTexFromSource(formula->copySource);
return source.isEmpty()
? std::optional<MTPRichText>(
MakePlainRichText(segment))
@@ -446,13 +476,15 @@ struct SerializeContext {
int from,
int till,
SerializeContext *context,
const EntityInText *skip) {
int skipIndex) {
auto parts = QVector<MTPRichText>();
auto position = from;
while (position < till) {
auto nextEntityStart = till;
for (const auto &entity : entities) {
if (&entity == skip) {
const auto count = int(entities.size());
for (auto index = 0; index != count; ++index) {
const auto &entity = entities[index];
if (SkipEntityForRange(entities, index, skipIndex)) {
continue;
}
if (entity.offset() >= position
@@ -471,8 +503,8 @@ struct SerializeContext {
entities,
position,
till,
skip);
if (!entity) {
skipIndex);
if (entity == kNoEntityIndex) {
parts.push_back(MakePlainRichText(text.mid(position, 1)));
++position;
continue;
@@ -480,13 +512,14 @@ struct SerializeContext {
const auto wrapped = SerializeRichTextEntity(
text,
entities,
*entity,
entity,
context);
if (!wrapped) {
return std::nullopt;
}
parts.push_back(*wrapped);
position = entity->offset() + entity->length();
const auto &wrappedEntity = entities[entity];
position = wrappedEntity.offset() + wrappedEntity.length();
}
return JoinRichTextParts(std::move(parts));
}
@@ -502,7 +535,7 @@ struct SerializeContext {
0,
text.text.text.size(),
context,
nullptr);
kNoEntityIndex);
if (!result) {
return std::nullopt;
}
+12 -2
View File
@@ -43,6 +43,16 @@ using TableRow = RichPage::TableRow;
using TableVerticalAlignment = RichPage::TableVerticalAlignment;
using TaskState = RichPage::TaskState;
[[nodiscard]] QString FormulaTexFromSource(QString source) {
source = source.trimmed();
if (source.size() >= 2
&& source.front() == QChar('$')
&& source.back() == QChar('$')) {
source = source.mid(1, source.size() - 2).trimmed();
}
return source;
}
const auto PhotoLargeLevels = u"ydxcwmbsa"_q;
constexpr auto kDefaultMapWidth = 400;
constexpr auto kDefaultMapHeight = 200;
@@ -423,12 +433,12 @@ void RememberWebPageMedia(
entityData));
return true;
}, [&](const MTPDtextMath &data) {
const auto source = qs(data.vsource());
const auto source = FormulaTexFromSource(qs(data.vsource()));
const auto entityData = Markdown::SerializeInlineTextObjectEntity({
.kind = Markdown::InlineTextObjectKind::Formula,
.data = Markdown::InlineTextObjectFormulaData{
.copySource = Markdown::InlineFormulaCopySource(source),
.trimmedTex = source.trimmed(),
.trimmedTex = source,
},
});
if (entityData.isEmpty()) {
@@ -180,6 +180,15 @@ void RestoreRelatedArticleImageStates(
return segment.isTextLeaf() || IsDisplayMathSegment(segment);
}
[[nodiscard]] QColor MarkBgColorForStyle(const style::Markdown &st) {
auto result = st.textPalette.markBg->c;
result.setAlphaF(result.alphaF() * std::clamp(
st.markBgOpacity,
0.,
1.));
return result;
}
template <typename T>
[[nodiscard]] int CompareValues(const T &a, const T &b) {
return (a < b) ? -1 : (b < a) ? 1 : 0;
@@ -2084,11 +2093,7 @@ void MarkdownArticle::Impl::paint(
local.selectionState.segments = &_segments;
const auto &paintSt = local.paintMarkdownStyle(st);
auto textPalette = paintSt.textPalette;
auto markBg = textPalette.markBg->c;
markBg.setAlphaF(markBg.alphaF() * std::clamp(
paintSt.markBgOpacity,
0.,
1.));
auto markBg = MarkBgColorForStyle(paintSt);
const auto ownedMarkBg = style::internal::OwnedColor(markBg);
textPalette.markBg = ownedMarkBg.color();
const auto &previousTextPalette = p.textPalette();
@@ -2305,6 +2310,7 @@ MarkdownArticleTextLeafStyle MarkdownArticle::Impl::textLeafStyleForSegment(
return {
.textStyle = &textStyle,
.textColor = TextColorForSegment(*segment, st),
.markBg = MarkBgColorForStyle(st),
.lineHeight = TextLineHeight(textStyle),
.align = segment->align,
.italic = segment->block && segment->block->pullquote,
@@ -2325,6 +2331,7 @@ MarkdownArticleTextLeafStyle MarkdownArticle::Impl::editableStyleForSegment(
return {
.textStyle = &st.displayMath.fallbackStyle,
.textColor = st.displayMath.fg,
.markBg = MarkBgColorForStyle(st),
.lineHeight = TextLineHeight(st.displayMath.fallbackStyle),
.align = segment->align,
};
@@ -20,6 +20,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/click_handler.h"
#include "ui/painter.h"
#include <QtGui/QColor>
#include <memory>
#include <optional>
#include <span>
@@ -250,6 +252,7 @@ inline bool operator!=(
struct MarkdownArticleTextLeafStyle {
const style::TextStyle *textStyle = nullptr;
style::color textColor;
QColor markBg;
int lineHeight = 0;
style::align align = style::al_left;
bool italic = false;
@@ -1094,20 +1094,16 @@ auto InlineFormulaObjectCache::lookupOrCreate(
if (measuredData) {
measured = *measuredData;
} else {
const auto fallbackAscent = std::max(TextLineAscent(textStyle), 0);
const auto fallbackSize = QSize(
std::max(textStyle.font->width(signature.trimmedTex), 1),
std::max(TextLineHeight(textStyle), 1));
measured.logicalSize = fallbackSize;
measured.logicalDepth = std::max(
fallbackSize.height() - fallbackAscent,
0);
measured.exact = InlineFormulaExactMetricsFromLogical(
fallbackSize,
fallbackAscent);
measured.fallbackText = signature.trimmedTex;
measured.success = false;
NormalizeInlineFormulaRasterMetrics(&measured);
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(
+2
View File
@@ -15,6 +15,8 @@ PRIVATE
iv/editor/iv_editor_box.h
iv/editor/iv_editor_state.cpp
iv/editor/iv_editor_state.h
iv/editor/iv_editor_text_entities.cpp
iv/editor/iv_editor_text_entities.h
iv/editor/iv_editor_widget.cpp
iv/editor/iv_editor_widget.h