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

View file

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

View file

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