Support filling highlight path in Text.
This commit is contained in:
parent
dc8313f6ae
commit
6dc93b53a1
3 changed files with 221 additions and 83 deletions
|
|
@ -199,6 +199,13 @@ void FillQuotePaint(
|
|||
const style::QuoteStyle &st,
|
||||
SkipBlockPaintParts parts = {});
|
||||
|
||||
struct HighlightInfoRequest {
|
||||
TextSelection range;
|
||||
QRect interpolateTo;
|
||||
float64 interpolateProgress = 0.;
|
||||
QPainterPath *outPath = nullptr;
|
||||
};
|
||||
|
||||
struct PaintContext {
|
||||
QPoint position;
|
||||
int outerWidth = 0; // For automatic RTL Ui inversion.
|
||||
|
|
@ -220,6 +227,8 @@ struct PaintContext {
|
|||
TextSelection selection;
|
||||
bool fullWidthSelection = true;
|
||||
|
||||
HighlightInfoRequest *highlight = nullptr;
|
||||
|
||||
int elisionHeight = 0;
|
||||
int elisionRemoveFromEnd = 0;
|
||||
bool elisionOneLine = false;
|
||||
|
|
|
|||
|
|
@ -197,6 +197,7 @@ void Renderer::draw(QPainter &p, const PaintContext &context) {
|
|||
_breakEverywhere = _geometry.breakEverywhere;
|
||||
_spoilerCache = context.spoiler;
|
||||
_selection = context.selection;
|
||||
_highlight = context.highlight;
|
||||
_fullWidthSelection = context.fullWidthSelection;
|
||||
_align = context.align;
|
||||
_cachedNow = context.now;
|
||||
|
|
@ -245,6 +246,9 @@ void Renderer::enumerate() {
|
|||
if (_p) {
|
||||
paintSpoilerRects();
|
||||
}
|
||||
if (_highlight) {
|
||||
composeHighlightPath();
|
||||
}
|
||||
});
|
||||
|
||||
auto blockIndex = 0;
|
||||
|
|
@ -892,39 +896,39 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato
|
|||
}
|
||||
}
|
||||
return false;
|
||||
} else if (_p && (_type == TextBlockType::Emoji || _type == TextBlockType::CustomEmoji)) {
|
||||
} else if (_p
|
||||
&& (_type == TextBlockType::Emoji
|
||||
|| _type == TextBlockType::CustomEmoji)) {
|
||||
auto glyphX = x;
|
||||
auto spacesWidth = (si.width - currentBlock->f_width());
|
||||
if (rtl) {
|
||||
glyphX += spacesWidth;
|
||||
}
|
||||
FixedRange fillSelect;
|
||||
FixedRange fillSpoiler;
|
||||
if (_background.selectActiveBlock) {
|
||||
fillSelect = { x, x + si.width };
|
||||
} else if (_localFrom + si.position < _selection.to) {
|
||||
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)) {
|
||||
fillSelect = { x, x + si.width };
|
||||
} else { // or with space
|
||||
fillSelect = { glyphX, glyphX + currentBlock->f_width() };
|
||||
}
|
||||
} else if (chTo > chFrom && (chTo - 1)->unicode() == QChar::Space && (chTo - 1 - _str) >= _selection.from) {
|
||||
if (rtl) { // rtl space only
|
||||
fillSelect = { x, glyphX };
|
||||
} else { // ltr space only
|
||||
fillSelect = { x + currentBlock->f_width(), x + si.width };
|
||||
}
|
||||
}
|
||||
const auto fillSelect = _background.selectActiveBlock
|
||||
? FixedRange{ x, x + si.width }
|
||||
: findSelectEmojiRange(
|
||||
si,
|
||||
currentBlock,
|
||||
nextBlock,
|
||||
x,
|
||||
glyphX,
|
||||
_selection);
|
||||
fillSelectRange(fillSelect);
|
||||
if (_highlight) {
|
||||
pushHighlightRange(findSelectEmojiRange(
|
||||
si,
|
||||
currentBlock,
|
||||
nextBlock,
|
||||
x,
|
||||
glyphX,
|
||||
_highlight->range));
|
||||
}
|
||||
|
||||
const auto hasSpoiler = _background.spoiler
|
||||
&& (_spoilerOpacity > 0.);
|
||||
if (hasSpoiler) {
|
||||
fillSpoiler = { x, x + si.width };
|
||||
}
|
||||
fillSelectRange(fillSelect);
|
||||
const auto fillSpoiler = hasSpoiler
|
||||
? FixedRange{ x, x + si.width }
|
||||
: FixedRange();
|
||||
const auto opacity = _p->opacity();
|
||||
if (!hasSpoiler || _spoilerOpacity < 1.) {
|
||||
if (hasSpoiler) {
|
||||
|
|
@ -1058,58 +1062,39 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato
|
|||
gf.justified = false;
|
||||
InitTextItemWithScriptItem(gf, si);
|
||||
|
||||
auto itemRange = FixedRange{ x, x + itemWidth };
|
||||
auto fillSelect = FixedRange();
|
||||
auto hasSelected = false;
|
||||
auto hasNotSelected = true;
|
||||
const auto itemRange = FixedRange{ x, x + itemWidth };
|
||||
auto selectedRect = QRect();
|
||||
if (_background.selectActiveBlock) {
|
||||
fillSelect = itemRange;
|
||||
fillSelectRange(fillSelect);
|
||||
} else if (_localFrom + itemStart < _selection.to && _localFrom + itemEnd > _selection.from) {
|
||||
hasSelected = true;
|
||||
auto selX = x;
|
||||
auto selWidth = itemWidth;
|
||||
if (_localFrom + itemStart >= _selection.from && _localFrom + itemEnd <= _selection.to) {
|
||||
hasNotSelected = false;
|
||||
} else {
|
||||
selWidth = 0;
|
||||
int itemL = itemEnd - itemStart;
|
||||
int selStart = _selection.from - (_localFrom + itemStart), selEnd = _selection.to - (_localFrom + itemStart);
|
||||
if (selStart < 0) selStart = 0;
|
||||
if (selEnd > itemL) selEnd = itemL;
|
||||
for (int ch = 0, g; ch < selEnd;) {
|
||||
g = logClusters[itemStart - si.position + ch];
|
||||
QFixed gwidth = glyphs.effectiveAdvance(g);
|
||||
// ch2 - glyph end, ch - glyph start, (ch2 - ch) - how much chars it takes
|
||||
int ch2 = ch + 1;
|
||||
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);
|
||||
auto fillSelect = itemRange;
|
||||
if (!_background.selectActiveBlock) {
|
||||
fillSelect = findSelectTextRange(
|
||||
si,
|
||||
itemStart,
|
||||
itemEnd,
|
||||
x,
|
||||
itemWidth,
|
||||
gf,
|
||||
_selection);
|
||||
const auto from = fillSelect.from.toInt();
|
||||
selectedRect = QRect(
|
||||
from,
|
||||
_y + _yDelta,
|
||||
fillSelect.till.toInt() - from,
|
||||
_fontHeight);
|
||||
}
|
||||
const auto hasSelected = !fillSelect.empty();
|
||||
const auto hasNotSelected = (fillSelect.from != itemRange.from)
|
||||
|| (fillSelect.till != itemRange.till);
|
||||
fillSelectRange(fillSelect);
|
||||
|
||||
if (_highlight) {
|
||||
pushHighlightRange(findSelectTextRange(
|
||||
si,
|
||||
itemStart,
|
||||
itemEnd,
|
||||
x,
|
||||
itemWidth,
|
||||
gf,
|
||||
_highlight->range));
|
||||
}
|
||||
const auto hasSpoiler = _background.spoiler
|
||||
&& (_spoilerOpacity > 0.);
|
||||
|
|
@ -1200,10 +1185,98 @@ bool Renderer::drawLine(uint16 _lineEnd, const String::TextBlocks::const_iterato
|
|||
|
||||
x += itemWidth;
|
||||
}
|
||||
fillSpoilerRects();
|
||||
fillRectsFromRanges();
|
||||
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) {
|
||||
if (range.empty()) {
|
||||
return;
|
||||
|
|
@ -1213,6 +1286,13 @@ void Renderer::fillSelectRange(FixedRange range) {
|
|||
_p->fillRect(left, _y + _yDelta, width, _fontHeight, _palette->selectBg);
|
||||
}
|
||||
|
||||
void Renderer::pushHighlightRange(FixedRange range) {
|
||||
if (range.empty()) {
|
||||
return;
|
||||
}
|
||||
AppendRange(_highlightRanges, range);
|
||||
}
|
||||
|
||||
void Renderer::pushSpoilerRange(
|
||||
FixedRange range,
|
||||
FixedRange selected,
|
||||
|
|
@ -1233,12 +1313,13 @@ void Renderer::pushSpoilerRange(
|
|||
}
|
||||
}
|
||||
|
||||
void Renderer::fillSpoilerRects() {
|
||||
fillSpoilerRects(_spoilerRects, _spoilerRanges);
|
||||
fillSpoilerRects(_spoilerSelectedRects, _spoilerSelectedRanges);
|
||||
void Renderer::fillRectsFromRanges() {
|
||||
fillRectsFromRanges(_spoilerRects, _spoilerRanges);
|
||||
fillRectsFromRanges(_spoilerSelectedRects, _spoilerSelectedRanges);
|
||||
fillRectsFromRanges(_highlightRects, _highlightRanges);
|
||||
}
|
||||
|
||||
void Renderer::fillSpoilerRects(
|
||||
void Renderer::fillRectsFromRanges(
|
||||
QVarLengthArray<QRect, kSpoilersRectsSize> &rects,
|
||||
QVarLengthArray<FixedRange> &ranges) {
|
||||
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) {
|
||||
if (_elideSavedBlock) {
|
||||
restoreAfterElided();
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@
|
|||
|
||||
#include <private/qtextengine_p.h>
|
||||
|
||||
class QTextItemInt;
|
||||
struct QScriptAnalysis;
|
||||
struct QScriptLine;
|
||||
struct QScriptItem;
|
||||
|
||||
namespace Ui::Text {
|
||||
|
||||
|
|
@ -62,13 +64,29 @@ private:
|
|||
uint16 _lineEnd,
|
||||
const String::TextBlocks::const_iterator &_endBlockIter,
|
||||
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 pushHighlightRange(FixedRange range);
|
||||
void pushSpoilerRange(
|
||||
FixedRange range,
|
||||
FixedRange selected,
|
||||
bool isElidedItem);
|
||||
void fillSpoilerRects();
|
||||
void fillSpoilerRects(
|
||||
void fillRectsFromRanges();
|
||||
void fillRectsFromRanges(
|
||||
QVarLengthArray<QRect, kSpoilersRectsSize> &rects,
|
||||
QVarLengthArray<FixedRange> &ranges);
|
||||
void paintSpoilerRects();
|
||||
|
|
@ -76,6 +94,7 @@ private:
|
|||
const QVarLengthArray<QRect, kSpoilersRectsSize> &rects,
|
||||
const style::color &color,
|
||||
int index);
|
||||
void composeHighlightPath();
|
||||
void elideSaveBlock(
|
||||
int32 blockIndex,
|
||||
const AbstractBlock *&_endBlock,
|
||||
|
|
@ -137,13 +156,16 @@ private:
|
|||
int _yTo = 0;
|
||||
TextSelection _selection = { 0, 0 };
|
||||
bool _fullWidthSelection = true;
|
||||
HighlightInfoRequest *_highlight = nullptr;
|
||||
const QChar *_str = nullptr;
|
||||
mutable crl::time _cachedNow = 0;
|
||||
float64 _spoilerOpacity = 0.;
|
||||
QVarLengthArray<FixedRange> _spoilerRanges;
|
||||
QVarLengthArray<FixedRange> _spoilerSelectedRanges;
|
||||
QVarLengthArray<FixedRange> _highlightRanges;
|
||||
QVarLengthArray<QRect, kSpoilersRectsSize> _spoilerRects;
|
||||
QVarLengthArray<QRect, kSpoilersRectsSize> _spoilerSelectedRects;
|
||||
QVarLengthArray<QRect, kSpoilersRectsSize> _highlightRects;
|
||||
|
||||
std::optional<CustomEmoji::Context> _customEmojiContext;
|
||||
int _customEmojiSize = 0;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue