Added initial spoiler support to Ui::Text::String.

This commit is contained in:
23rd 2021-12-16 20:05:13 +03:00
parent e19a4c6544
commit 1819e6e3a5
3 changed files with 192 additions and 22 deletions

View file

@ -253,6 +253,30 @@ const TextParseOptions _textPlainOptions = {
namespace Ui {
namespace Text {
class String::SpoilerClickHandler final : public ClickHandler {
public:
SpoilerClickHandler() = default;
TextEntity getTextEntity() const override {
return { EntityType::Spoiler };
}
void onClick(ClickContext context) const override {
if (!_shown) {
const auto nonconst = const_cast<SpoilerClickHandler*>(this);
nonconst->_shown = true;
}
}
[[nodiscard]] bool shown() const {
return _shown;
}
private:
bool _shown = false;
};
class Parser {
public:
Parser(
@ -273,13 +297,20 @@ private:
class StartedEntity {
public:
explicit StartedEntity(TextBlockFlags flags);
explicit StartedEntity(uint16 lnkIndex);
explicit StartedEntity(uint16 index, bool isLnk = true);
std::optional<TextBlockFlags> flags() const;
std::optional<uint16> lnkIndex() const;
std::optional<uint16> spoilerIndex() const;
private:
int _value = 0;
enum class Type {
Flags,
Link,
Spoiler,
};
const int _value = 0;
const Type _type;
};
@ -334,15 +365,18 @@ private:
const bool _checkTilde = false; // do we need a special text block for tilde symbol
std::vector<EntityLinkData> _links;
std::vector<EntityLinkData> _spoilers;
base::flat_map<
const QChar*,
std::vector<StartedEntity>> _startedEntities;
uint16 _maxLnkIndex = 0;
uint16 _maxSpoilerIndex = 0;
// current state
int32 _flags = 0;
uint16 _lnkIndex = 0;
uint16 _spoilerIndex = 0;
EmojiPtr _emoji = nullptr; // current emoji, if current word is an emoji, or zero
int32 _blockStart = 0; // offset in result, from which current parsed block is started
int32 _diacs = 0; // diac chars skipped without good char
@ -357,23 +391,36 @@ private:
};
Parser::StartedEntity::StartedEntity(TextBlockFlags flags) : _value(flags) {
Parser::StartedEntity::StartedEntity(TextBlockFlags flags)
: _value(flags)
, _type(Type::Flags) {
Expects(_value >= 0 && _value < int(kStringLinkIndexShift));
}
Parser::StartedEntity::StartedEntity(uint16 lnkIndex) : _value(lnkIndex) {
Expects(_value >= kStringLinkIndexShift);
Parser::StartedEntity::StartedEntity(uint16 index, bool isLnk)
: _value(index)
, _type(isLnk ? Type::Link : Type::Spoiler) {
Expects((_type == Type::Link)
? (_value >= kStringLinkIndexShift)
: (_value < kStringLinkIndexShift));
}
std::optional<TextBlockFlags> Parser::StartedEntity::flags() const {
if (_value < int(kStringLinkIndexShift)) {
if (_value < int(kStringLinkIndexShift) && (_type == Type::Flags)) {
return TextBlockFlags(_value);
}
return std::nullopt;
}
std::optional<uint16> Parser::StartedEntity::lnkIndex() const {
if (_value >= int(kStringLinkIndexShift)) {
if (_value >= int(kStringLinkIndexShift) && (_type == Type::Link)) {
return uint16(_value);
}
return std::nullopt;
}
std::optional<uint16> Parser::StartedEntity::spoilerIndex() const {
if (_value < int(kStringLinkIndexShift) && (_type == Type::Spoiler)) {
return uint16(_value);
}
return std::nullopt;
@ -437,6 +484,9 @@ void Parser::createBlock(int32 skipBack) {
if (_lnkIndex < kStringLinkIndexShift && _lnkIndex > _maxLnkIndex) {
_maxLnkIndex = _lnkIndex;
}
if (_spoilerIndex > _maxSpoilerIndex) {
_maxSpoilerIndex = _spoilerIndex;
}
int32 len = int32(_t->_text.size()) + skipBack - _blockStart;
if (len > 0) {
@ -450,13 +500,13 @@ void Parser::createBlock(int32 skipBack) {
}
_lastSkipped = false;
if (_emoji) {
_t->_blocks.push_back(Block::Emoji(_t->_st->font, _t->_text, _blockStart, len, _flags, _lnkIndex, 0/*spoilerIndex*/, _emoji));
_t->_blocks.push_back(Block::Emoji(_t->_st->font, _t->_text, _blockStart, len, _flags, _lnkIndex, _spoilerIndex, _emoji));
_emoji = nullptr;
_lastSkipped = true;
} else if (newline) {
_t->_blocks.push_back(Block::Newline(_t->_st->font, _t->_text, _blockStart, len, _flags, _lnkIndex, 0/*spoilerIndex*/));
_t->_blocks.push_back(Block::Newline(_t->_st->font, _t->_text, _blockStart, len, _flags, _lnkIndex, _spoilerIndex));
} else {
_t->_blocks.push_back(Block::Text(_t->_st->font, _t->_text, _t->_minResizeWidth, _blockStart, len, _flags, _lnkIndex, 0/*spoilerIndex*/));
_t->_blocks.push_back(Block::Text(_t->_st->font, _t->_text, _t->_minResizeWidth, _blockStart, len, _flags, _lnkIndex, _spoilerIndex));
}
_blockStart += len;
blockCreated();
@ -466,7 +516,7 @@ void Parser::createBlock(int32 skipBack) {
void Parser::createSkipBlock(int32 w, int32 h) {
createBlock();
_t->_text.push_back('_');
_t->_blocks.push_back(Block::Skip(_t->_st->font, _t->_text, _blockStart++, w, h, _lnkIndex, 0/*spoilerIndex*/));
_t->_blocks.push_back(Block::Skip(_t->_st->font, _t->_text, _blockStart++, w, h, _lnkIndex, _spoilerIndex));
blockCreated();
}
@ -509,6 +559,11 @@ void Parser::finishEntities() {
createBlock();
_lnkIndex = 0;
}
} else if (const auto spoilerIndex = list.back().spoilerIndex()) {
if (_spoilerIndex == *spoilerIndex && (_spoilerIndex != 0)) {
createBlock();
_spoilerIndex = 0;
}
}
list.pop_back();
}
@ -594,6 +649,16 @@ bool Parser::checkEntities() {
_flags |= flags;
_startedEntities[entityEnd].emplace_back(flags);
}
} else if (entityType == EntityType::Spoiler) {
createBlock();
_spoilers.push_back(EntityLinkData{
.data = QString::number(_spoilers.size() + 1),
.type = entityType,
});
_spoilerIndex = _spoilers.size();
_startedEntities[entityEnd].emplace_back(_spoilerIndex, false);
}
++_waitingEntity;
@ -918,7 +983,16 @@ void Parser::checkForElidedSkipBlock() {
void Parser::finalize(const TextParseOptions &options) {
_t->_links.resize(_maxLnkIndex);
_t->_spoilers.resize(_maxSpoilerIndex);
for (auto &block : _t->_blocks) {
const auto spoilerIndex = block->spoilerIndex();
if (spoilerIndex) {
_t->_spoilers.resize(spoilerIndex);
const auto handler = (options.flags & TextParseLinks)
? std::make_shared<String::SpoilerClickHandler>()
: nullptr;
_t->_spoilers[spoilerIndex - 1] = std::move(handler);
}
const auto shiftedIndex = block->lnkIndex();
if (shiftedIndex <= kStringLinkIndexShift) {
continue;
@ -939,6 +1013,7 @@ void Parser::finalize(const TextParseOptions &options) {
}
}
_t->_links.squeeze();
_t->_spoilers.squeeze();
_t->_blocks.squeeze();
_t->_text.squeeze();
}
@ -1591,8 +1666,14 @@ private:
TextBlockType _type = currentBlock->type();
if (!_p && _lookupX >= x && _lookupX < x + si.width) { // _lookupRequest
if (_lookupLink) {
if (currentBlock->lnkIndex() && _lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) {
_lookupResult.link = _t->_links.at(currentBlock->lnkIndex() - 1);
if (_lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) {
const auto spoilerLink = _t->spoilerLink(currentBlock->spoilerIndex());
const auto resultLink = (spoilerLink || !currentBlock->lnkIndex())
? spoilerLink
: _t->_links.at(currentBlock->lnkIndex() - 1);
if (resultLink) {
_lookupResult.link = resultLink;
}
}
}
if (_type != TextBlockTSkip) {
@ -1664,12 +1745,35 @@ private:
}
}
}
Emoji::Draw(
*_p,
static_cast<const EmojiBlock*>(currentBlock)->_emoji,
Emoji::GetSizeNormal(),
(glyphX + st::emojiPadding).toInt(),
_y + _yDelta + emojiY);
if (_background.brush) {
const auto from = currentBlock->from();
const auto to = currentBlock->from() + currentBlock->width();
if (_localFrom + si.position < to) {
auto chFrom = _str + currentBlock->from();
auto chTo = chFrom + ((nextBlock ? nextBlock->from() : _t->_text.size()) - currentBlock->from());
if (_localFrom + si.position >= from) { // could be without space
if (chTo == chFrom || (chTo - 1)->unicode() != QChar::Space || to >= (chTo - _str)) {
fillSpoilerRange(x, si.width, blockIndex);
} else { // or with space
fillSpoilerRange(glyphX, currentBlock->f_width(), blockIndex);
}
} else if (chTo > chFrom && (chTo - 1)->unicode() == QChar::Space && (chTo - 1 - _str) >= from) {
if (rtl) { // rtl space only
fillSpoilerRange(x, glyphX - x, blockIndex);
} else { // ltr space only
fillSpoilerRange(x + currentBlock->f_width(), si.width, blockIndex);
}
}
}
}
if (!_background.inFront) {
Emoji::Draw(
*_p,
static_cast<const EmojiBlock*>(currentBlock)->_emoji,
Emoji::GetSizeNormal(),
(glyphX + st::emojiPadding).toInt(),
_y + _yDelta + emojiY);
}
// } else if (_p && currentBlock->type() == TextBlockSkip) { // debug
// _p->fillRect(QRect(x.toInt(), _y, currentBlock->width(), static_cast<SkipBlock*>(currentBlock)->height()), QColor(0, 0, 0, 32));
}
@ -1697,8 +1801,14 @@ private:
if (!_p && _lookupX >= x && _lookupX < x + itemWidth) { // _lookupRequest
if (_lookupLink) {
if (currentBlock->lnkIndex() && _lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) {
_lookupResult.link = _t->_links.at(currentBlock->lnkIndex() - 1);
if (_lookupY >= _y + _yDelta && _lookupY < _y + _yDelta + _fontHeight) {
const auto spoilerLink = _t->spoilerLink(currentBlock->spoilerIndex());
const auto resultLink = (spoilerLink || !currentBlock->lnkIndex())
? spoilerLink
: _t->_links.at(currentBlock->lnkIndex() - 1);
if (resultLink) {
_lookupResult.link = resultLink;
}
}
}
_lookupResult.uponSymbol = true;
@ -1752,6 +1862,10 @@ private:
gf.justified = false;
gf.initWithScriptItem(si);
if (!_background.inFront) {
fillSpoilerRange(x, si.width, blockIndex);
}
auto hasSelected = false;
auto hasNotSelected = true;
auto selectedRect = QRect();
@ -1823,6 +1937,10 @@ private:
_p->setPen(*_currentPen);
_p->drawTextItem(QPointF(x.toReal(), textY), gf);
}
if (_background.inFront) {
fillSpoilerRange(x, si.width, blockIndex);
}
}
x += itemWidth;
@ -1835,6 +1953,22 @@ private:
_p->fillRect(left, _y + _yDelta, width, _fontHeight, _textPalette->selectBg);
}
void fillSpoilerRange(QFixed x, QFixed width, int currentBlockIndex) {
if (_background.brush) {
const auto elideOffset =
(_indexOfElidedBlock == currentBlockIndex)
? (_elideRemoveFromEnd + _f->elidew)
: 0;
_p->fillRect(
x.toInt(),
_y + _yDelta,
width.toInt() - elideOffset,
_fontHeight,
*_background.brush);
}
}
void elideSaveBlock(int32 blockIndex, const AbstractBlock *&_endBlock, int32 elideStart, int32 elideWidth) {
if (_elideSavedBlock) {
restoreAfterElided();
@ -1900,6 +2034,7 @@ private:
lineText = lineText.mid(0, currentBlock->from() - _localFrom) + _Elide;
lineLength = currentBlock->from() + _Elide.size() - _lineStart;
_selection.to = qMin(_selection.to, currentBlock->from());
_indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0);
setElideBidi(currentBlock->from(), _Elide.size());
elideSaveBlock(blockIndex - 1, _endBlock, currentBlock->from(), elideWidth);
return;
@ -1932,6 +2067,7 @@ private:
lineText += _Elide;
lineLength = _localFrom + pos + _Elide.size() - _lineStart;
_selection.to = qMin(_selection.to, uint16(_localFrom + pos));
_indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0);
setElideBidi(_localFrom + pos, _Elide.size());
_blocksSize = blockIndex;
_endBlock = nextBlock;
@ -1952,6 +2088,7 @@ private:
int32 elideStart = _localFrom + lineText.size();
_selection.to = qMin(_selection.to, uint16(elideStart));
_indexOfElidedBlock = blockIndex + (nextBlock ? 1 : 0);
setElideBidi(elideStart, _Elide.size());
lineText += _Elide;
@ -2611,6 +2748,18 @@ private:
void applyBlockProperties(const AbstractBlock *block) {
eSetFont(block);
if (_p) {
const auto handler = !block->spoilerIndex()
? nullptr
: _t->_spoilers.at(block->spoilerIndex() - 1);
if (!block->spoilerIndex()) {
_background = {};
} else {
const auto inBack = (handler && handler->shown());
_background.inFront = !inBack;
_background.brush = inBack
? &_textPalette->spoilerActiveBg->b
: &_textPalette->spoilerBg->b;
}
if (block->lnkIndex()) {
_currentPen = &_textPalette->linkFg->p;
_currentPenSelected = &_textPalette->selectLinkFg->p;
@ -2635,6 +2784,10 @@ private:
QPen _originalPenSelected;
const QPen *_currentPen = nullptr;
const QPen *_currentPenSelected = nullptr;
struct {
const QBrush *brush = nullptr;
bool inFront = false;
} _background;
int _yFrom = 0;
int _yTo = 0;
int _yToElide = 0;
@ -2642,6 +2795,8 @@ private:
bool _fullWidthSelection = true;
const QChar *_str = nullptr;
int _indexOfElidedBlock = -1; // For spoilers.
// current paragraph data
String::TextBlocks::const_iterator _parStartBlock;
Qt::LayoutDirection _parDirection;
@ -3248,7 +3403,7 @@ TextForMimeData String::toText(
{ TextBlockFUnderline, EntityType::Underline },
{ TextBlockFStrikeOut, EntityType::StrikeOut },
{ TextBlockFCode, EntityType::Code }, // #TODO entities
{ TextBlockFPre, EntityType::Pre }
{ TextBlockFPre, EntityType::Pre },
} : std::vector<MarkdownTagTracker>();
const auto flagsChangeCallback = [&](int32 oldFlags, int32 newFlags) {
if (!composeEntities) {
@ -3368,10 +3523,19 @@ void String::clear() {
void String::clearFields() {
_blocks.clear();
_links.clear();
_spoilers.clear();
_maxWidth = _minHeight = 0;
_startDir = Qt::LayoutDirectionAuto;
}
ClickHandlerPtr String::spoilerLink(uint16 spoilerIndex) const {
if (spoilerIndex) {
const auto &handler = _spoilers.at(spoilerIndex - 1);
return (handler && !handler->shown()) ? handler : nullptr;
}
return nullptr;
}
bool IsWordSeparator(QChar ch) {
switch (ch.unicode()) {
case QChar::Space:

View file

@ -205,6 +205,8 @@ private:
// it is also called from move constructor / assignment operator
void clearFields();
ClickHandlerPtr spoilerLink(uint16 spoilerIndex) const;
TextForMimeData toText(
TextSelection selection,
bool composeExpanded,
@ -220,6 +222,9 @@ private:
TextBlocks _blocks;
TextLinks _links;
class SpoilerClickHandler;
QVector<std::shared_ptr<SpoilerClickHandler>> _spoilers;
Qt::LayoutDirection _startDir = Qt::LayoutDirectionAuto;
friend class Parser;

View file

@ -33,6 +33,7 @@ enum class EntityType : uchar {
StrikeOut,
Code, // inline
Pre, // block
Spoiler,
};
enum class EntityLinkShown : uchar {