Support filling highlight path in Text.

This commit is contained in:
John Preston 2023-10-30 21:53:22 +04:00
parent dc8313f6ae
commit 6dc93b53a1
3 changed files with 221 additions and 83 deletions

View file

@ -199,6 +199,13 @@ void FillQuotePaint(
const style::QuoteStyle &st, const style::QuoteStyle &st,
SkipBlockPaintParts parts = {}); SkipBlockPaintParts parts = {});
struct HighlightInfoRequest {
TextSelection range;
QRect interpolateTo;
float64 interpolateProgress = 0.;
QPainterPath *outPath = nullptr;
};
struct PaintContext { struct PaintContext {
QPoint position; QPoint position;
int outerWidth = 0; // For automatic RTL Ui inversion. int outerWidth = 0; // For automatic RTL Ui inversion.
@ -220,6 +227,8 @@ struct PaintContext {
TextSelection selection; TextSelection selection;
bool fullWidthSelection = true; bool fullWidthSelection = true;
HighlightInfoRequest *highlight = nullptr;
int elisionHeight = 0; int elisionHeight = 0;
int elisionRemoveFromEnd = 0; int elisionRemoveFromEnd = 0;
bool elisionOneLine = false; bool elisionOneLine = false;

View file

@ -197,6 +197,7 @@ void Renderer::draw(QPainter &p, const PaintContext &context) {
_breakEverywhere = _geometry.breakEverywhere; _breakEverywhere = _geometry.breakEverywhere;
_spoilerCache = context.spoiler; _spoilerCache = context.spoiler;
_selection = context.selection; _selection = context.selection;
_highlight = context.highlight;
_fullWidthSelection = context.fullWidthSelection; _fullWidthSelection = context.fullWidthSelection;
_align = context.align; _align = context.align;
_cachedNow = context.now; _cachedNow = context.now;
@ -245,6 +246,9 @@ void Renderer::enumerate() {
if (_p) { if (_p) {
paintSpoilerRects(); paintSpoilerRects();
} }
if (_highlight) {
composeHighlightPath();
}
}); });
auto blockIndex = 0; auto blockIndex = 0;
@ -892,39 +896,39 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato
} }
} }
return false; return false;
} else if (_p && (_type == TextBlockType::Emoji || _type == TextBlockType::CustomEmoji)) { } else if (_p
&& (_type == TextBlockType::Emoji
|| _type == TextBlockType::CustomEmoji)) {
auto glyphX = x; auto glyphX = x;
auto spacesWidth = (si.width - currentBlock->f_width()); auto spacesWidth = (si.width - currentBlock->f_width());
if (rtl) { if (rtl) {
glyphX += spacesWidth; glyphX += spacesWidth;
} }
FixedRange fillSelect; const auto fillSelect = _background.selectActiveBlock
FixedRange fillSpoiler; ? FixedRange{ x, x + si.width }
if (_background.selectActiveBlock) { : findSelectEmojiRange(
fillSelect = { x, x + si.width }; si,
} else if (_localFrom + si.position < _selection.to) { currentBlock,
auto chFrom = _str + currentBlock->position(); nextBlock,
auto chTo = chFrom + ((nextBlock ? nextBlock->position() : _t->_text.size()) - currentBlock->position()); x,
if (_localFrom + si.position >= _selection.from) { // could be without space glyphX,
if (chTo == chFrom || (chTo - 1)->unicode() != QChar::Space || _selection.to >= (chTo - _str)) { _selection);
fillSelect = { x, x + si.width }; fillSelectRange(fillSelect);
} else { // or with space if (_highlight) {
fillSelect = { glyphX, glyphX + currentBlock->f_width() }; pushHighlightRange(findSelectEmojiRange(
} si,
} else if (chTo > chFrom && (chTo - 1)->unicode() == QChar::Space && (chTo - 1 - _str) >= _selection.from) { currentBlock,
if (rtl) { // rtl space only nextBlock,
fillSelect = { x, glyphX }; x,
} else { // ltr space only glyphX,
fillSelect = { x + currentBlock->f_width(), x + si.width }; _highlight->range));
}
}
} }
const auto hasSpoiler = _background.spoiler const auto hasSpoiler = _background.spoiler
&& (_spoilerOpacity > 0.); && (_spoilerOpacity > 0.);
if (hasSpoiler) { const auto fillSpoiler = hasSpoiler
fillSpoiler = { x, x + si.width }; ? FixedRange{ x, x + si.width }
} : FixedRange();
fillSelectRange(fillSelect);
const auto opacity = _p->opacity(); const auto opacity = _p->opacity();
if (!hasSpoiler || _spoilerOpacity < 1.) { if (!hasSpoiler || _spoilerOpacity < 1.) {
if (hasSpoiler) { if (hasSpoiler) {
@ -1058,58 +1062,39 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato
gf.justified = false; gf.justified = false;
InitTextItemWithScriptItem(gf, si); InitTextItemWithScriptItem(gf, si);
auto itemRange = FixedRange{ x, x + itemWidth }; const auto itemRange = FixedRange{ x, x + itemWidth };
auto fillSelect = FixedRange();
auto hasSelected = false;
auto hasNotSelected = true;
auto selectedRect = QRect(); auto selectedRect = QRect();
if (_background.selectActiveBlock) { auto fillSelect = itemRange;
fillSelect = itemRange; if (!_background.selectActiveBlock) {
fillSelectRange(fillSelect); fillSelect = findSelectTextRange(
} else if (_localFrom + itemStart < _selection.to && _localFrom + itemEnd > _selection.from) { si,
hasSelected = true; itemStart,
auto selX = x; itemEnd,
auto selWidth = itemWidth; x,
if (_localFrom + itemStart >= _selection.from && _localFrom + itemEnd <= _selection.to) { itemWidth,
hasNotSelected = false; gf,
} else { _selection);
selWidth = 0; const auto from = fillSelect.from.toInt();
int itemL = itemEnd - itemStart; selectedRect = QRect(
int selStart = _selection.from - (_localFrom + itemStart), selEnd = _selection.to - (_localFrom + itemStart); from,
if (selStart < 0) selStart = 0; _y + _yDelta,
if (selEnd > itemL) selEnd = itemL; fillSelect.till.toInt() - from,
for (int ch = 0, g; ch < selEnd;) { _fontHeight);
g = logClusters[itemStart - si.position + ch]; }
QFixed gwidth = glyphs.effectiveAdvance(g); const auto hasSelected = !fillSelect.empty();
// ch2 - glyph end, ch - glyph start, (ch2 - ch) - how much chars it takes const auto hasNotSelected = (fillSelect.from != itemRange.from)
int ch2 = ch + 1; || (fillSelect.till != itemRange.till);
while ((ch2 < itemL) && (g == logClusters[itemStart - si.position + ch2])) {
++ch2;
}
if (ch2 <= selStart) {
selX += gwidth;
} else if (ch >= selStart && ch2 <= selEnd) {
selWidth += gwidth;
} else {
int sStart = ch, sEnd = ch2;
if (ch < selStart) {
sStart = selStart;
selX += QFixed(sStart - ch) * gwidth / QFixed(ch2 - ch);
}
if (ch2 >= selEnd) {
sEnd = selEnd;
selWidth += QFixed(sEnd - sStart) * gwidth / QFixed(ch2 - ch);
break;
}
selWidth += QFixed(sEnd - sStart) * gwidth / QFixed(ch2 - ch);
}
ch = ch2;
}
}
if (rtl) selX = x + itemWidth - (selX - x) - selWidth;
selectedRect = QRect(selX.toInt(), _y + _yDelta, (selX + selWidth).toInt() - selX.toInt(), _fontHeight);
fillSelect = { selX, selX + selWidth };
fillSelectRange(fillSelect); fillSelectRange(fillSelect);
if (_highlight) {
pushHighlightRange(findSelectTextRange(
si,
itemStart,
itemEnd,
x,
itemWidth,
gf,
_highlight->range));
} }
const auto hasSpoiler = _background.spoiler const auto hasSpoiler = _background.spoiler
&& (_spoilerOpacity > 0.); && (_spoilerOpacity > 0.);
@ -1200,10 +1185,98 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato
x += itemWidth; x += itemWidth;
} }
fillSpoilerRects(); fillRectsFromRanges();
return !_elidedLine; return !_elidedLine;
} }
FixedRange Renderer::findSelectEmojiRange(
const QScriptItem &si,
const Ui::Text::AbstractBlock *currentBlock,
const Ui::Text::AbstractBlock *nextBlock,
QFixed x,
QFixed glyphX,
TextSelection selection) const {
if (_localFrom + si.position >= selection.to) {
return {};
}
auto chFrom = _str + currentBlock->position();
auto chTo = chFrom + ((nextBlock ? nextBlock->position() : _t->_text.size()) - currentBlock->position());
if (_localFrom + si.position >= selection.from) { // could be without space
if (chTo == chFrom || (chTo - 1)->unicode() != QChar::Space || selection.to >= (chTo - _str)) {
return { x, x + si.width };
} else { // or with space
return { glyphX, glyphX + currentBlock->f_width() };
}
} else if (chTo > chFrom && (chTo - 1)->unicode() == QChar::Space && (chTo - 1 - _str) >= selection.from) {
const auto rtl = (si.analysis.bidiLevel % 2);
if (rtl) { // rtl space only
return { x, glyphX };
} else { // ltr space only
return { x + currentBlock->f_width(), x + si.width };
}
}
return {};
}
FixedRange Renderer::findSelectTextRange(
const QScriptItem &si,
int itemStart,
int itemEnd,
QFixed x,
QFixed itemWidth,
const QTextItemInt &gf,
TextSelection selection) const {
if (_localFrom + itemStart >= selection.to
|| _localFrom + itemEnd <= selection.from) {
return {};
}
auto selX = x;
auto selWidth = itemWidth;
const auto rtl = (si.analysis.bidiLevel % 2);
if (_localFrom + itemStart < selection.from
|| _localFrom + itemEnd > selection.to) {
selWidth = 0;
const auto itemL = itemEnd - itemStart;
const auto selStart = std::max(
selection.from - (_localFrom + itemStart),
0);
const auto selEnd = std::min(
selection.to - (_localFrom + itemStart),
itemL);
const auto lczero = gf.logClusters[0];
for (int ch = 0, g; ch < selEnd;) {
g = gf.logClusters[ch];
const auto gwidth = gf.glyphs.effectiveAdvance(g - lczero);
// ch2 - glyph end, ch - glyph start, (ch2 - ch) - how much chars it takes
int ch2 = ch + 1;
while ((ch2 < itemL) && (g == gf.logClusters[ch2])) {
++ch2;
}
if (ch2 <= selStart) {
selX += gwidth;
} else if (ch >= selStart && ch2 <= selEnd) {
selWidth += gwidth;
} else {
int sStart = ch, sEnd = ch2;
if (ch < selStart) {
sStart = selStart;
selX += QFixed(sStart - ch) * gwidth / QFixed(ch2 - ch);
}
if (ch2 >= selEnd) {
sEnd = selEnd;
selWidth += QFixed(sEnd - sStart) * gwidth / QFixed(ch2 - ch);
break;
}
selWidth += QFixed(sEnd - sStart) * gwidth / QFixed(ch2 - ch);
}
ch = ch2;
}
}
if (rtl) selX = x + itemWidth - (selX - x) - selWidth;
return { selX, selX + selWidth };
}
void Renderer::fillSelectRange(FixedRange range) { void Renderer::fillSelectRange(FixedRange range) {
if (range.empty()) { if (range.empty()) {
return; return;
@ -1213,6 +1286,13 @@ void Renderer::fillSelectRange(FixedRange range) {
_p->fillRect(left, _y + _yDelta, width, _fontHeight, _palette->selectBg); _p->fillRect(left, _y + _yDelta, width, _fontHeight, _palette->selectBg);
} }
void Renderer::pushHighlightRange(FixedRange range) {
if (range.empty()) {
return;
}
AppendRange(_highlightRanges, range);
}
void Renderer::pushSpoilerRange( void Renderer::pushSpoilerRange(
FixedRange range, FixedRange range,
FixedRange selected, FixedRange selected,
@ -1233,12 +1313,13 @@ void Renderer::pushSpoilerRange(
} }
} }
void Renderer::fillSpoilerRects() { void Renderer::fillRectsFromRanges() {
fillSpoilerRects(_spoilerRects, _spoilerRanges); fillRectsFromRanges(_spoilerRects, _spoilerRanges);
fillSpoilerRects(_spoilerSelectedRects, _spoilerSelectedRanges); fillRectsFromRanges(_spoilerSelectedRects, _spoilerSelectedRanges);
fillRectsFromRanges(_highlightRects, _highlightRanges);
} }
void Renderer::fillSpoilerRects( void Renderer::fillRectsFromRanges(
QVarLengthArray<QRect, kSpoilersRectsSize> &rects, QVarLengthArray<QRect, kSpoilersRectsSize> &rects,
QVarLengthArray<FixedRange> &ranges) { QVarLengthArray<FixedRange> &ranges) {
if (ranges.empty()) { if (ranges.empty()) {
@ -1306,6 +1387,32 @@ void Renderer::paintSpoilerRects(
} }
} }
void Renderer::composeHighlightPath() {
Expects(_highlight != nullptr);
Expects(_highlight->outPath != nullptr);
if (_highlight->interpolateProgress >= 1.) {
_highlight->outPath->addRect(_highlight->interpolateTo);
} else if (_highlight->interpolateProgress <= 0.) {
for (const auto &rect : _highlightRects) {
_highlight->outPath->addRect(rect);
}
} else {
const auto to = _highlight->interpolateTo;
const auto progress = _highlight->interpolateProgress;
const auto lerp = [=](int from, int to) {
return from + (to - from) * progress;
};
for (const auto &rect : _highlightRects) {
_highlight->outPath->addRect(
lerp(rect.x(), to.x()),
lerp(rect.y(), to.y()),
lerp(rect.width(), to.width()),
lerp(rect.height(), to.height()));
}
}
}
void Renderer::elideSaveBlock(int32 blockIndex, const AbstractBlock *&_endBlock, int32 elideStart, int32 elideWidth) { void Renderer::elideSaveBlock(int32 blockIndex, const AbstractBlock *&_endBlock, int32 elideStart, int32 elideWidth) {
if (_elideSavedBlock) { if (_elideSavedBlock) {
restoreAfterElided(); restoreAfterElided();

View file

@ -12,8 +12,10 @@
#include <private/qtextengine_p.h> #include <private/qtextengine_p.h>
class QTextItemInt;
struct QScriptAnalysis; struct QScriptAnalysis;
struct QScriptLine; struct QScriptLine;
struct QScriptItem;
namespace Ui::Text { namespace Ui::Text {
@ -62,13 +64,29 @@ private:
uint16 _lineEnd, uint16 _lineEnd,
const String::TextBlocks::const_iterator &_endBlockIter, const String::TextBlocks::const_iterator &_endBlockIter,
const String::TextBlocks::const_iterator &_end); const String::TextBlocks::const_iterator &_end);
[[nodiscard]] FixedRange findSelectEmojiRange(
const QScriptItem &si,
const Ui::Text::AbstractBlock *currentBlock,
const Ui::Text::AbstractBlock *nextBlock,
QFixed x,
QFixed glyphX,
TextSelection selection) const;
[[nodiscard]] FixedRange findSelectTextRange(
const QScriptItem &si,
int itemStart,
int itemEnd,
QFixed x,
QFixed itemWidth,
const QTextItemInt &gf,
TextSelection selection) const;
void fillSelectRange(FixedRange range); void fillSelectRange(FixedRange range);
void pushHighlightRange(FixedRange range);
void pushSpoilerRange( void pushSpoilerRange(
FixedRange range, FixedRange range,
FixedRange selected, FixedRange selected,
bool isElidedItem); bool isElidedItem);
void fillSpoilerRects(); void fillRectsFromRanges();
void fillSpoilerRects( void fillRectsFromRanges(
QVarLengthArray<QRect, kSpoilersRectsSize> &rects, QVarLengthArray<QRect, kSpoilersRectsSize> &rects,
QVarLengthArray<FixedRange> &ranges); QVarLengthArray<FixedRange> &ranges);
void paintSpoilerRects(); void paintSpoilerRects();
@ -76,6 +94,7 @@ private:
const QVarLengthArray<QRect, kSpoilersRectsSize> &rects, const QVarLengthArray<QRect, kSpoilersRectsSize> &rects,
const style::color &color, const style::color &color,
int index); int index);
void composeHighlightPath();
void elideSaveBlock( void elideSaveBlock(
int32 blockIndex, int32 blockIndex,
const AbstractBlock *&_endBlock, const AbstractBlock *&_endBlock,
@ -137,13 +156,16 @@ private:
int _yTo = 0; int _yTo = 0;
TextSelection _selection = { 0, 0 }; TextSelection _selection = { 0, 0 };
bool _fullWidthSelection = true; bool _fullWidthSelection = true;
HighlightInfoRequest *_highlight = nullptr;
const QChar *_str = nullptr; const QChar *_str = nullptr;
mutable crl::time _cachedNow = 0; mutable crl::time _cachedNow = 0;
float64 _spoilerOpacity = 0.; float64 _spoilerOpacity = 0.;
QVarLengthArray<FixedRange> _spoilerRanges; QVarLengthArray<FixedRange> _spoilerRanges;
QVarLengthArray<FixedRange> _spoilerSelectedRanges; QVarLengthArray<FixedRange> _spoilerSelectedRanges;
QVarLengthArray<FixedRange> _highlightRanges;
QVarLengthArray<QRect, kSpoilersRectsSize> _spoilerRects; QVarLengthArray<QRect, kSpoilersRectsSize> _spoilerRects;
QVarLengthArray<QRect, kSpoilersRectsSize> _spoilerSelectedRects; QVarLengthArray<QRect, kSpoilersRectsSize> _spoilerSelectedRects;
QVarLengthArray<QRect, kSpoilersRectsSize> _highlightRects;
std::optional<CustomEmoji::Context> _customEmojiContext; std::optional<CustomEmoji::Context> _customEmojiContext;
int _customEmojiSize = 0; int _customEmojiSize = 0;